1/******************************************************************************
2 * $Id: main.c 13477 2012-09-07 17:18:17Z jordan $
3 *
4 * Copyright (c) Transmission authors and contributors
5 *
6 * Permission is hereby granted, free of charge, to any person obtaining a
7 * copy of this software and associated documentation files (the "Software"),
8 * to deal in the Software without restriction, including without limitation
9 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
10 * and/or sell copies of the Software, and to permit persons to whom the
11 * Software is furnished to do so, subject to the following conditions:
12 *
13 * The above copyright notice and this permission notice shall be included in
14 * all copies or substantial portions of the Software.
15 *
16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
22 * DEALINGS IN THE SOFTWARE.
23 *****************************************************************************/
24
25#include <locale.h>
26#include <signal.h>
27#include <string.h>
28#include <stdio.h>
29#include <stdlib.h> /* exit() */
30#include <time.h>
31
32#include <glib/gi18n.h>
33#include <glib/gstdio.h>
34#include <gio/gio.h>
35#include <gtk/gtk.h>
36
37#include <libtransmission/transmission.h>
38#include <libtransmission/rpcimpl.h>
39#include <libtransmission/utils.h>
40#include <libtransmission/version.h>
41
42#include "actions.h"
43#include "conf.h"
44#include "details.h"
45#include "dialogs.h"
46#include "hig.h"
47#include "makemeta-ui.h"
48#include "msgwin.h"
49#include "notify.h"
50#include "open-dialog.h"
51#include "relocate.h"
52#include "stats.h"
53#include "tr-core.h"
54#include "tr-icon.h"
55#include "tr-prefs.h"
56#include "tr-window.h"
57#include "util.h"
58
59#define MY_CONFIG_NAME "transmission"
60#define MY_READABLE_NAME "transmission-gtk"
61
62#define TR_RESOURCE_PATH "/com/transmissionbt/transmission/"
63
64#define SHOW_LICENSE
65static const char * LICENSE =
66"The OS X client, CLI client, and parts of libtransmission are licensed under the terms of the MIT license.\n\n"
67"The Transmission daemon, GTK+ client, Qt client, Web client, and most of libtransmission are licensed under the terms of the GNU GPL version 2, with two special exceptions:\n\n"
68"1. The MIT-licensed portions of Transmission listed above are exempt from GPLv2 clause 2(b) and may retain their MIT license.\n\n"
69"2. Permission is granted to link the code in this release with the OpenSSL project's 'OpenSSL' library and to distribute the linked executables. Works derived from Transmission may, at their authors' discretion, keep or delete this exception.";
70
71struct cbdata
72{
73    char                      * config_dir;
74    gboolean                    start_paused;
75    gboolean                    is_iconified;
76
77    guint                       activation_count;
78    guint                       timer;
79    guint                       update_model_soon_tag;
80    guint                       refresh_actions_tag;
81    gpointer                    icon;
82    GtkWindow                 * wind;
83    TrCore                    * core;
84    GtkWidget                 * msgwin;
85    GtkWidget                 * prefs;
86    GSList                    * error_list;
87    GSList                    * duplicates_list;
88    GSList                    * details;
89    GtkTreeSelection          * sel;
90    gpointer                    quit_dialog;
91};
92
93static void
94gtr_window_present( GtkWindow * window )
95{
96    gtk_window_present_with_time( window, gtk_get_current_event_time( ) );
97}
98
99/***
100****
101****  DETAILS DIALOGS MANAGEMENT
102****
103***/
104
105static int
106compare_integers( gconstpointer a, gconstpointer b )
107{
108    return GPOINTER_TO_INT(a) - GPOINTER_TO_INT(b);
109}
110
111static char*
112get_details_dialog_key( GSList * id_list )
113{
114    GSList * l;
115    GSList * tmp = g_slist_sort( g_slist_copy( id_list ), compare_integers );
116    GString * gstr = g_string_new( NULL );
117
118    for( l=tmp; l!=NULL; l=l->next )
119        g_string_append_printf( gstr, "%d ", GPOINTER_TO_INT(l->data) );
120
121    g_slist_free( tmp );
122    return g_string_free( gstr, FALSE );
123}
124
125static void
126get_selected_torrent_ids_foreach( GtkTreeModel  * model,
127                                  GtkTreePath   * p UNUSED,
128                                  GtkTreeIter   * iter,
129                                  gpointer        gdata )
130{
131    int id;
132    GSList ** ids = gdata;
133    gtk_tree_model_get( model, iter, MC_TORRENT_ID, &id, -1 );
134    *ids = g_slist_append( *ids, GINT_TO_POINTER( id ) );
135}
136static GSList*
137get_selected_torrent_ids( struct cbdata * data )
138{
139    GSList * ids = NULL;
140    gtk_tree_selection_selected_foreach( data->sel,
141                                         get_selected_torrent_ids_foreach,
142                                         &ids );
143    return ids;
144}
145
146static void
147on_details_dialog_closed( gpointer gdata, GObject * dead )
148{
149    struct cbdata * data = gdata;
150
151    data->details = g_slist_remove( data->details, dead );
152}
153
154static void
155show_details_dialog_for_selected_torrents( struct cbdata * data )
156{
157    GtkWidget * dialog = NULL;
158    GSList * l;
159    GSList * ids = get_selected_torrent_ids( data );
160    char * key = get_details_dialog_key( ids );
161
162    for( l=data->details; dialog==NULL && l!=NULL; l=l->next )
163        if( !strcmp( key, g_object_get_data( l->data, "key" ) ) )
164            dialog = l->data;
165
166    if( dialog == NULL )
167    {
168        dialog = gtr_torrent_details_dialog_new( GTK_WINDOW( data->wind ), data->core );
169        gtr_torrent_details_dialog_set_torrents( dialog, ids );
170        g_object_set_data_full( G_OBJECT( dialog ), "key", g_strdup( key ), g_free );
171        g_object_weak_ref( G_OBJECT( dialog ), on_details_dialog_closed, data );
172        data->details = g_slist_append( data->details, dialog );
173        gtk_widget_show( dialog );
174    }
175
176    gtr_window_present( GTK_WINDOW( dialog ) );
177    g_free( key );
178    g_slist_free( ids );
179}
180
181/****
182*****
183*****  ON SELECTION CHANGED
184*****
185****/
186
187struct counts_data
188{
189    int total_count;
190    int queued_count;
191    int stopped_count;
192};
193
194static void
195get_selected_torrent_counts_foreach( GtkTreeModel * model, GtkTreePath * path UNUSED,
196                                     GtkTreeIter * iter, gpointer user_data )
197{
198    int activity = 0;
199    struct counts_data * counts = user_data;
200
201    ++counts->total_count;
202
203    gtk_tree_model_get( model, iter, MC_ACTIVITY, &activity, -1 );
204
205    if( ( activity == TR_STATUS_DOWNLOAD_WAIT ) || ( activity == TR_STATUS_SEED_WAIT ) )
206        ++counts->queued_count;
207
208    if( activity == TR_STATUS_STOPPED )
209        ++counts->stopped_count;
210}
211
212static void
213get_selected_torrent_counts( struct cbdata * data, struct counts_data * counts )
214{
215    counts->total_count = 0;
216    counts->queued_count = 0;
217    counts->stopped_count = 0;
218
219    gtk_tree_selection_selected_foreach( data->sel, get_selected_torrent_counts_foreach, counts );
220}
221
222static void
223count_updatable_foreach( GtkTreeModel * model, GtkTreePath * path UNUSED,
224                         GtkTreeIter * iter, gpointer accumulated_status )
225{
226    tr_torrent * tor;
227    gtk_tree_model_get( model, iter, MC_TORRENT, &tor, -1 );
228    *(int*)accumulated_status |= tr_torrentCanManualUpdate( tor );
229}
230
231static gboolean
232refresh_actions( gpointer gdata )
233{
234    int canUpdate;
235    struct counts_data sel_counts;
236    struct cbdata * data = gdata;
237    const size_t total = gtr_core_get_torrent_count( data->core );
238    const size_t active = gtr_core_get_active_torrent_count( data->core );
239    const int torrent_count = gtk_tree_model_iter_n_children( gtr_core_model( data->core ), NULL );
240    bool has_selection;
241
242    get_selected_torrent_counts( data, &sel_counts );
243    has_selection = sel_counts.total_count > 0;
244
245    gtr_action_set_sensitive( "select-all", torrent_count != 0 );
246    gtr_action_set_sensitive( "deselect-all", torrent_count != 0 );
247    gtr_action_set_sensitive( "pause-all-torrents", active != 0 );
248    gtr_action_set_sensitive( "start-all-torrents", active != total );
249
250    gtr_action_set_sensitive( "torrent-stop", ( sel_counts.stopped_count < sel_counts.total_count ) );
251    gtr_action_set_sensitive( "torrent-start", ( sel_counts.stopped_count ) > 0 );
252    gtr_action_set_sensitive( "torrent-start-now", ( sel_counts.stopped_count + sel_counts.queued_count ) > 0 );
253    gtr_action_set_sensitive( "torrent-verify",          has_selection );
254    gtr_action_set_sensitive( "remove-torrent",          has_selection );
255    gtr_action_set_sensitive( "delete-torrent",          has_selection );
256    gtr_action_set_sensitive( "relocate-torrent",        has_selection );
257    gtr_action_set_sensitive( "queue-move-top",          has_selection );
258    gtr_action_set_sensitive( "queue-move-up",           has_selection );
259    gtr_action_set_sensitive( "queue-move-down",         has_selection );
260    gtr_action_set_sensitive( "queue-move-bottom",       has_selection );
261    gtr_action_set_sensitive( "show-torrent-properties", has_selection );
262    gtr_action_set_sensitive( "open-torrent-folder", sel_counts.total_count == 1 );
263    gtr_action_set_sensitive( "copy-magnet-link-to-clipboard", sel_counts.total_count == 1 );
264
265    canUpdate = 0;
266    gtk_tree_selection_selected_foreach( data->sel, count_updatable_foreach, &canUpdate );
267    gtr_action_set_sensitive( "torrent-reannounce", canUpdate != 0 );
268
269    data->refresh_actions_tag = 0;
270    return FALSE;
271}
272
273static void
274refresh_actions_soon( gpointer gdata )
275{
276    struct cbdata * data = gdata;
277
278    if( data->refresh_actions_tag == 0 )
279        data->refresh_actions_tag = gdk_threads_add_idle( refresh_actions, data );
280}
281
282static void
283on_selection_changed( GtkTreeSelection * s UNUSED, gpointer gdata )
284{
285    refresh_actions_soon( gdata );
286}
287
288/***
289****
290***/
291
292static void
293register_magnet_link_handler( void )
294{
295    GAppInfo * app_info = g_app_info_get_default_for_uri_scheme( "magnet" );
296    if( app_info == NULL )
297    {
298        /* there's no default magnet handler, so register ourselves for the job... */
299        GError * error = NULL;
300        app_info = g_app_info_create_from_commandline( "transmission-gtk", "transmission-gtk", G_APP_INFO_CREATE_SUPPORTS_URIS, NULL );
301        g_app_info_set_as_default_for_type( app_info, "x-scheme-handler/magnet", &error );
302        if( error != NULL )
303        {
304            g_warning( _( "Error registering Transmission as x-scheme-handler/magnet handler: %s" ), error->message );
305            g_clear_error( &error );
306        }
307    }
308}
309
310static void
311on_main_window_size_allocated( GtkWidget      * gtk_window,
312                               GtkAllocation  * alloc UNUSED,
313                               gpointer         gdata UNUSED )
314{
315    GdkWindow * gdk_window = gtk_widget_get_window( gtk_window );
316    const gboolean isMaximized = ( gdk_window != NULL )
317                              && ( gdk_window_get_state( gdk_window ) & GDK_WINDOW_STATE_MAXIMIZED );
318
319    gtr_pref_int_set( PREF_KEY_MAIN_WINDOW_IS_MAXIMIZED, isMaximized );
320
321    if( !isMaximized )
322    {
323        int x, y, w, h;
324        gtk_window_get_position( GTK_WINDOW( gtk_window ), &x, &y );
325        gtk_window_get_size( GTK_WINDOW( gtk_window ), &w, &h );
326        gtr_pref_int_set( PREF_KEY_MAIN_WINDOW_X, x );
327        gtr_pref_int_set( PREF_KEY_MAIN_WINDOW_Y, y );
328        gtr_pref_int_set( PREF_KEY_MAIN_WINDOW_WIDTH, w );
329        gtr_pref_int_set( PREF_KEY_MAIN_WINDOW_HEIGHT, h );
330    }
331}
332
333/***
334**** listen to changes that come from RPC
335***/
336
337struct rpc_idle_data
338{
339    TrCore * core;
340    int id;
341    gboolean delete_files;
342};
343
344static gboolean
345rpc_torrent_remove_idle( gpointer gdata )
346{
347    struct rpc_idle_data * data = gdata;
348
349    gtr_core_remove_torrent( data->core, data->id, data->delete_files );
350
351    g_free( data );
352    return FALSE; /* tell g_idle not to call this func twice */
353}
354
355static gboolean
356rpc_torrent_add_idle( gpointer gdata )
357{
358    tr_torrent * tor;
359    struct rpc_idle_data * data = gdata;
360
361    if(( tor = gtr_core_find_torrent( data->core, data->id )))
362        gtr_core_add_torrent( data->core, tor, TRUE );
363
364    g_free( data );
365    return FALSE; /* tell g_idle not to call this func twice */
366}
367
368static tr_rpc_callback_status
369on_rpc_changed( tr_session            * session,
370                tr_rpc_callback_type    type,
371                struct tr_torrent     * tor,
372                void                  * gdata )
373{
374    tr_rpc_callback_status status = TR_RPC_OK;
375    struct cbdata * cbdata = gdata;
376    gdk_threads_enter( );
377
378    switch( type )
379    {
380        case TR_RPC_SESSION_CLOSE:
381            gtr_action_activate( "quit" );
382            break;
383
384        case TR_RPC_TORRENT_ADDED: {
385            struct rpc_idle_data * data = g_new0( struct rpc_idle_data, 1 );
386            data->id = tr_torrentId( tor );
387            data->core = cbdata->core;
388            gdk_threads_add_idle( rpc_torrent_add_idle, data );
389            break;
390        }
391
392        case TR_RPC_TORRENT_REMOVING:
393        case TR_RPC_TORRENT_TRASHING: {
394            struct rpc_idle_data * data = g_new0( struct rpc_idle_data, 1 );
395            data->id = tr_torrentId( tor );
396            data->core = cbdata->core;
397            data->delete_files = type == TR_RPC_TORRENT_TRASHING;
398            gdk_threads_add_idle( rpc_torrent_remove_idle, data );
399            status = TR_RPC_NOREMOVE;
400            break;
401        }
402
403        case TR_RPC_SESSION_CHANGED: {
404            int i;
405            tr_benc tmp;
406            tr_benc * newval;
407            tr_benc * oldvals = gtr_pref_get_all( );
408            const char * key;
409            GSList * l;
410            GSList * changed_keys = NULL;
411            tr_bencInitDict( &tmp, 100 );
412            tr_sessionGetSettings( session, &tmp );
413            for( i=0; tr_bencDictChild( &tmp, i, &key, &newval ); ++i )
414            {
415                bool changed;
416                tr_benc * oldval = tr_bencDictFind( oldvals, key );
417                if( !oldval )
418                    changed = true;
419                else {
420                    char * a = tr_bencToStr( oldval, TR_FMT_BENC, NULL );
421                    char * b = tr_bencToStr( newval, TR_FMT_BENC, NULL );
422                    changed = strcmp( a, b ) != 0;
423                    tr_free( b );
424                    tr_free( a );
425                }
426
427                if( changed )
428                    changed_keys = g_slist_append( changed_keys, (gpointer)key );
429            }
430            tr_sessionGetSettings( session, oldvals );
431
432            for( l=changed_keys; l!=NULL; l=l->next )
433                gtr_core_pref_changed( cbdata->core, l->data );
434
435            g_slist_free( changed_keys );
436            tr_bencFree( &tmp );
437            break;
438        }
439
440        case TR_RPC_TORRENT_CHANGED:
441        case TR_RPC_TORRENT_MOVED:
442        case TR_RPC_TORRENT_STARTED:
443        case TR_RPC_TORRENT_STOPPED:
444        case TR_RPC_SESSION_QUEUE_POSITIONS_CHANGED:
445            /* nothing interesting to do here */
446            break;
447    }
448
449    gdk_threads_leave( );
450    return status;
451}
452
453/***
454****  signal handling
455***/
456
457static sig_atomic_t global_sigcount = 0;
458static struct cbdata * sighandler_cbdata = NULL;
459
460static void
461signal_handler( int sig )
462{
463    if( ++global_sigcount > 1 )
464    {
465        signal( sig, SIG_DFL );
466        raise( sig );
467    }
468    else if(( sig == SIGINT ) || ( sig == SIGTERM ))
469    {
470        g_message( _( "Got signal %d; trying to shut down cleanly. Do it again if it gets stuck." ), sig );
471        gtr_actions_handler( "quit", sighandler_cbdata );
472    }
473}
474
475/****
476*****
477*****
478****/
479
480static void app_setup( GtkWindow * wind, struct cbdata  * cbdata );
481
482static void
483on_startup( GApplication * application, gpointer user_data )
484{
485    GError * error;
486    const char * str;
487    GtkWindow * win;
488    GtkUIManager * ui_manager;
489    tr_session * session;
490    struct cbdata * cbdata = user_data;
491
492    signal( SIGINT, signal_handler );
493    signal( SIGTERM, signal_handler );
494
495    sighandler_cbdata = cbdata;
496
497    /* ensure the directories are created */
498    if(( str = gtr_pref_string_get( TR_PREFS_KEY_DOWNLOAD_DIR )))
499      g_mkdir_with_parents( str, 0777 );
500    if(( str = gtr_pref_string_get( TR_PREFS_KEY_INCOMPLETE_DIR )))
501      g_mkdir_with_parents( str, 0777 );
502
503    /* initialize the libtransmission session */
504    session = tr_sessionInit( "gtk", cbdata->config_dir, TRUE, gtr_pref_get_all( ) );
505
506    gtr_pref_flag_set( TR_PREFS_KEY_ALT_SPEED_ENABLED, tr_sessionUsesAltSpeed( session ) );
507    gtr_pref_int_set( TR_PREFS_KEY_PEER_PORT, tr_sessionGetPeerPort( session ) );
508    cbdata->core = gtr_core_new( session );
509
510    /* init the ui manager */
511    error = NULL;
512    ui_manager = gtk_ui_manager_new ( );
513    gtr_actions_init ( ui_manager, cbdata );
514    gtk_ui_manager_add_ui_from_resource ( ui_manager, TR_RESOURCE_PATH "transmission-ui.xml", &error );
515    g_assert_no_error (error);
516    gtk_ui_manager_ensure_update ( ui_manager );
517
518    /* create main window now to be a parent to any error dialogs */
519    win = GTK_WINDOW( gtr_window_new( GTK_APPLICATION( application ), ui_manager, cbdata->core ) );
520    g_signal_connect( win, "size-allocate", G_CALLBACK( on_main_window_size_allocated ), cbdata );
521    g_application_hold( application );
522    g_object_weak_ref( G_OBJECT( win ), (GWeakNotify)g_application_release, application );
523    app_setup( win, cbdata );
524    tr_sessionSetRPCCallback( session, on_rpc_changed, cbdata );
525
526    /* check & see if it's time to update the blocklist */
527    if( gtr_pref_flag_get( TR_PREFS_KEY_BLOCKLIST_ENABLED ) ) {
528      if( gtr_pref_flag_get( PREF_KEY_BLOCKLIST_UPDATES_ENABLED ) ) {
529        const int64_t last_time = gtr_pref_int_get( "blocklist-date" );
530        const int SECONDS_IN_A_WEEK = 7 * 24 * 60 * 60;
531        const time_t now = time( NULL );
532        if( last_time + SECONDS_IN_A_WEEK < now )
533          gtr_core_blocklist_update( cbdata->core );
534      }
535    }
536
537    /* if there's no magnet link handler registered, register us */
538    register_magnet_link_handler( );
539}
540
541static void
542on_activate( GApplication * app UNUSED, struct cbdata * cbdata )
543{
544    cbdata->activation_count++;
545
546    /* GApplication emits an 'activate' signal when bootstrapping the primary.
547     * Ordinarily we handle that by presenting the main window, but if the user
548     * user started Transmission minimized, ignore that initial signal... */
549    if( cbdata->is_iconified && ( cbdata->activation_count == 1 ) )
550        return;
551
552    gtr_action_activate( "present-main-window" );
553}
554
555static void
556open_files( GSList * files, gpointer gdata )
557{
558    struct cbdata * cbdata = gdata;
559    const gboolean do_start = gtr_pref_flag_get( TR_PREFS_KEY_START ) && !cbdata->start_paused;
560    const gboolean do_prompt = gtr_pref_flag_get( PREF_KEY_OPTIONS_PROMPT );
561    const gboolean do_notify = TRUE;
562
563    gtr_core_add_files( cbdata->core, files, do_start, do_prompt, do_notify );
564}
565
566static void
567on_open (GApplication  * application UNUSED,
568         GFile        ** f,
569         gint            file_count,
570         gchar         * hint UNUSED,
571         gpointer        gdata )
572{
573    int i;
574    GSList * files = NULL;
575
576    for( i=0; i<file_count; ++i )
577        files = g_slist_prepend( files, f[i] );
578
579    open_files( files, gdata );
580
581    g_slist_free( files );
582}
583
584/***
585****
586***/
587
588int
589main( int argc, char ** argv )
590{
591    int ret;
592    struct stat sb;
593    char * application_id;
594    GtkApplication * app;
595    GOptionContext * option_context;
596    bool show_version = false;
597    GError * error = NULL;
598    struct cbdata cbdata;
599
600    GOptionEntry option_entries[] = {
601        { "config-dir", 'g', 0, G_OPTION_ARG_FILENAME, &cbdata.config_dir, _( "Where to look for configuration files" ), NULL },
602        { "paused",     'p', 0, G_OPTION_ARG_NONE, &cbdata.start_paused, _( "Start with all torrents paused" ), NULL },
603        { "minimized",  'm', 0, G_OPTION_ARG_NONE, &cbdata.is_iconified, _( "Start minimized in notification area" ), NULL },
604        { "version",    'v', 0, G_OPTION_ARG_NONE, &show_version, _( "Show version number and exit" ), NULL },
605        { NULL, 0,   0, 0, NULL, NULL, NULL }
606    };
607
608    /* default settings */
609    memset( &cbdata, 0, sizeof( struct cbdata ) );
610    cbdata.config_dir = (char*) tr_getDefaultConfigDir( MY_CONFIG_NAME );
611
612    /* init i18n */
613    setlocale( LC_ALL, "" );
614    bindtextdomain( MY_READABLE_NAME, TRANSMISSIONLOCALEDIR );
615    bind_textdomain_codeset( MY_READABLE_NAME, "UTF-8" );
616    textdomain( MY_READABLE_NAME );
617
618    /* init glib/gtk */
619    g_type_init ();
620    gtk_init (&argc, &argv);
621    g_set_application_name (_( "Transmission" ));
622    gtk_window_set_default_icon_name (MY_CONFIG_NAME);
623
624    /* parse the command line */
625    option_context = g_option_context_new( _( "[torrent files or urls]" ) );
626    g_option_context_add_main_entries( option_context, option_entries, GETTEXT_PACKAGE );
627    g_option_context_set_translation_domain( option_context, GETTEXT_PACKAGE );
628    if( !g_option_context_parse( option_context, &argc, &argv, &error ) ) {
629        g_print (_("%s\nRun '%s --help' to see a full list of available command line options.\n"), error->message, argv[0]);
630        g_error_free (error);
631        g_option_context_free (option_context);
632        return 1;
633    }
634    g_option_context_free (option_context);
635
636    /* handle the trivial "version" option */
637    if( show_version ) {
638        fprintf( stderr, "%s %s\n", MY_READABLE_NAME, LONG_VERSION_STRING );
639        return 0;
640    }
641
642    /* init the unit formatters */
643    tr_formatter_mem_init( mem_K, _(mem_K_str), _(mem_M_str), _(mem_G_str), _(mem_T_str) );
644    tr_formatter_size_init( disk_K, _(disk_K_str), _(disk_M_str), _(disk_G_str), _(disk_T_str) );
645    tr_formatter_speed_init( speed_K, _(speed_K_str), _(speed_M_str), _(speed_G_str), _(speed_T_str) );
646
647    /* set up the config dir */
648    gtr_pref_init( cbdata.config_dir );
649    g_mkdir_with_parents( cbdata.config_dir, 0755 );
650
651    /* init notifications */
652    gtr_notify_init( );
653
654    /* init the application for the specified config dir */
655    stat( cbdata.config_dir, &sb );
656    application_id = g_strdup_printf( "com.transmissionbt.transmission_%lu_%lu", (unsigned long)sb.st_dev, (unsigned long)sb.st_ino );
657    app = gtk_application_new( application_id, G_APPLICATION_HANDLES_OPEN );
658    g_signal_connect( app, "open", G_CALLBACK(on_open), &cbdata );
659    g_signal_connect( app, "startup", G_CALLBACK(on_startup), &cbdata );
660    g_signal_connect( app, "activate", G_CALLBACK(on_activate), &cbdata );
661    ret = g_application_run( G_APPLICATION( app ), argc, argv);
662    g_object_unref( app );
663    g_free( application_id );
664    return ret;
665}
666
667static void
668on_core_busy( TrCore * core UNUSED, gboolean busy, struct cbdata * c )
669{
670    gtr_window_set_busy( c->wind, busy );
671}
672
673static void on_core_error( TrCore *, guint, const char *, struct cbdata * );
674static void on_add_torrent( TrCore *, tr_ctor *, gpointer );
675static void on_prefs_changed( TrCore * core, const char * key, gpointer );
676static void main_window_setup( struct cbdata * cbdata, GtkWindow * wind );
677static gboolean update_model_loop( gpointer gdata );
678static gboolean update_model_once( gpointer gdata );
679
680static void
681app_setup( GtkWindow * wind, struct cbdata * cbdata )
682{
683    if( cbdata->is_iconified )
684        gtr_pref_flag_set( PREF_KEY_SHOW_TRAY_ICON, TRUE );
685
686    gtr_actions_set_core( cbdata->core );
687
688    /* set up core handlers */
689    g_signal_connect( cbdata->core, "busy", G_CALLBACK( on_core_busy ), cbdata );
690    g_signal_connect( cbdata->core, "add-error", G_CALLBACK( on_core_error ), cbdata );
691    g_signal_connect( cbdata->core, "add-prompt", G_CALLBACK( on_add_torrent ), cbdata );
692    g_signal_connect( cbdata->core, "prefs-changed", G_CALLBACK( on_prefs_changed ), cbdata );
693
694    /* add torrents from command-line and saved state */
695    gtr_core_load( cbdata->core, cbdata->start_paused );
696    gtr_core_torrents_added( cbdata->core );
697
698    /* set up main window */
699    main_window_setup( cbdata, wind );
700
701    /* set up the icon */
702    on_prefs_changed( cbdata->core, PREF_KEY_SHOW_TRAY_ICON, cbdata );
703
704    /* start model update timer */
705    cbdata->timer = gdk_threads_add_timeout_seconds( MAIN_WINDOW_REFRESH_INTERVAL_SECONDS, update_model_loop, cbdata );
706    update_model_once( cbdata );
707
708    /* either show the window or iconify it */
709    if( !cbdata->is_iconified )
710        gtk_widget_show( GTK_WIDGET( wind ) );
711    else
712    {
713        gtk_window_set_skip_taskbar_hint( cbdata->wind,
714                                          cbdata->icon != NULL );
715        cbdata->is_iconified = FALSE; // ensure that the next toggle iconifies
716        gtr_action_set_toggled( "toggle-main-window", FALSE );
717    }
718
719    if( !gtr_pref_flag_get( PREF_KEY_USER_HAS_GIVEN_INFORMED_CONSENT ) )
720    {
721        GtkWidget * w = gtk_message_dialog_new( GTK_WINDOW( wind ),
722                                                GTK_DIALOG_DESTROY_WITH_PARENT,
723                                                GTK_MESSAGE_INFO,
724                                                GTK_BUTTONS_NONE,
725                                                "%s",
726             _( "Transmission is a file-sharing program. When you run a torrent, its data will be made available to others by means of upload. You and you alone are fully responsible for exercising proper judgement and abiding by your local laws." ) );
727        gtk_dialog_add_button( GTK_DIALOG( w ), GTK_STOCK_QUIT, GTK_RESPONSE_REJECT );
728        gtk_dialog_add_button( GTK_DIALOG( w ), _( "I _Accept" ), GTK_RESPONSE_ACCEPT );
729        gtk_dialog_set_default_response( GTK_DIALOG( w ), GTK_RESPONSE_ACCEPT );
730        switch( gtk_dialog_run( GTK_DIALOG( w ) ) ) {
731            case GTK_RESPONSE_ACCEPT:
732                /* only show it once */
733                gtr_pref_flag_set( PREF_KEY_USER_HAS_GIVEN_INFORMED_CONSENT, TRUE );
734                gtk_widget_destroy( w );
735                break;
736            default:
737                exit( 0 );
738        }
739    }
740}
741
742static void
743presentMainWindow( struct cbdata * cbdata )
744{
745    GtkWindow * window = cbdata->wind;
746
747    if( cbdata->is_iconified )
748    {
749        cbdata->is_iconified = false;
750
751        gtk_window_set_skip_taskbar_hint( window, FALSE );
752    }
753
754    if( !gtk_widget_get_visible( GTK_WIDGET( window ) ) )
755    {
756        gtk_window_resize( window, gtr_pref_int_get( PREF_KEY_MAIN_WINDOW_WIDTH ),
757                                   gtr_pref_int_get( PREF_KEY_MAIN_WINDOW_HEIGHT ) );
758        gtk_window_move( window, gtr_pref_int_get( PREF_KEY_MAIN_WINDOW_X ),
759                                 gtr_pref_int_get( PREF_KEY_MAIN_WINDOW_Y ) );
760        gtr_widget_set_visible( GTK_WIDGET( window ), TRUE );
761    }
762    gtr_window_present( window );
763}
764
765static void
766hideMainWindow( struct cbdata * cbdata )
767{
768    GtkWindow * window = cbdata->wind;
769    gtk_window_set_skip_taskbar_hint( window, TRUE );
770    gtr_widget_set_visible( GTK_WIDGET( window ), FALSE );
771    cbdata->is_iconified = true;
772}
773
774static void
775toggleMainWindow( struct cbdata * cbdata )
776{
777    if( cbdata->is_iconified )
778        presentMainWindow( cbdata );
779    else
780        hideMainWindow( cbdata );
781}
782
783static void on_app_exit( gpointer vdata );
784
785static gboolean
786winclose( GtkWidget * w    UNUSED,
787          GdkEvent * event UNUSED,
788          gpointer         gdata )
789{
790    struct cbdata * cbdata = gdata;
791
792    if( cbdata->icon != NULL )
793        gtr_action_activate ( "toggle-main-window" );
794    else
795        on_app_exit( cbdata );
796
797    return TRUE; /* don't propagate event further */
798}
799
800static void
801rowChangedCB( GtkTreeModel  * model UNUSED,
802              GtkTreePath   * path,
803              GtkTreeIter   * iter  UNUSED,
804              gpointer        gdata )
805{
806    struct cbdata * data = gdata;
807
808    if( gtk_tree_selection_path_is_selected ( data->sel, path ) )
809        refresh_actions_soon( data );
810}
811
812static void
813on_drag_data_received( GtkWidget         * widget          UNUSED,
814                       GdkDragContext    * drag_context,
815                       gint                x               UNUSED,
816                       gint                y               UNUSED,
817                       GtkSelectionData  * selection_data,
818                       guint               info            UNUSED,
819                       guint               time_,
820                       gpointer            gdata )
821{
822    guint i;
823    char ** uris = gtk_selection_data_get_uris( selection_data );
824    const guint file_count = g_strv_length( uris );
825    GSList * files = NULL;
826
827    for( i=0; i<file_count; ++i )
828        files = g_slist_prepend( files, g_file_new_for_uri( uris[i] ) );
829
830    open_files( files, gdata );
831
832    /* cleanup */
833    g_slist_foreach( files, (GFunc)g_object_unref, NULL );
834    g_slist_free( files );
835    g_strfreev( uris );
836
837    gtk_drag_finish( drag_context, true, FALSE, time_ );
838}
839
840static void
841main_window_setup( struct cbdata * cbdata, GtkWindow * wind )
842{
843    GtkWidget * w;
844    GtkTreeModel * model;
845    GtkTreeSelection * sel;
846
847    g_assert( NULL == cbdata->wind );
848    cbdata->wind = wind;
849    cbdata->sel = sel = GTK_TREE_SELECTION( gtr_window_get_selection( cbdata->wind ) );
850
851    g_signal_connect( sel, "changed", G_CALLBACK( on_selection_changed ), cbdata );
852    on_selection_changed( sel, cbdata );
853    model = gtr_core_model( cbdata->core );
854    g_signal_connect( model, "row-changed", G_CALLBACK( rowChangedCB ), cbdata );
855    g_signal_connect( wind, "delete-event", G_CALLBACK( winclose ), cbdata );
856    refresh_actions( cbdata );
857
858    /* register to handle URIs that get dragged onto our main window */
859    w = GTK_WIDGET( wind );
860    gtk_drag_dest_set( w, GTK_DEST_DEFAULT_ALL, NULL, 0, GDK_ACTION_COPY );
861    gtk_drag_dest_add_uri_targets( w );
862    g_signal_connect( w, "drag-data-received", G_CALLBACK(on_drag_data_received), cbdata );
863}
864
865static gboolean
866on_session_closed( gpointer gdata )
867{
868    GSList * tmp;
869    struct cbdata * cbdata = gdata;
870
871    tmp = g_slist_copy( cbdata->details );
872    g_slist_foreach( tmp, (GFunc)gtk_widget_destroy, NULL );
873    g_slist_free( tmp );
874
875    if( cbdata->prefs )
876        gtk_widget_destroy( GTK_WIDGET( cbdata->prefs ) );
877    if( cbdata->wind )
878        gtk_widget_destroy( GTK_WIDGET( cbdata->wind ) );
879    g_object_unref( cbdata->core );
880    if( cbdata->icon )
881        g_object_unref( cbdata->icon );
882    g_slist_foreach( cbdata->error_list, (GFunc)g_free, NULL );
883    g_slist_free( cbdata->error_list );
884    g_slist_foreach( cbdata->duplicates_list, (GFunc)g_free, NULL );
885    g_slist_free( cbdata->duplicates_list );
886
887    return FALSE;
888}
889
890static gpointer
891session_close_threadfunc( gpointer gdata )
892{
893    /* since tr_sessionClose() is a blocking function,
894     * call it from another thread... when it's done,
895     * punt the GUI teardown back to the GTK+ thread */
896    struct cbdata * cbdata = gdata;
897    gdk_threads_enter( );
898    gtr_core_close( cbdata->core );
899    gdk_threads_add_idle( on_session_closed, gdata );
900    gdk_threads_leave( );
901    return NULL;
902}
903
904static void
905exit_now_cb( GtkWidget *w UNUSED, gpointer data UNUSED )
906{
907    exit( 0 );
908}
909
910static void
911on_app_exit( gpointer vdata )
912{
913    GtkWidget *r, *p, *b, *w, *c;
914    struct cbdata *cbdata = vdata;
915
916    /* stop the update timer */
917    if( cbdata->timer )
918    {
919        g_source_remove( cbdata->timer );
920        cbdata->timer = 0;
921    }
922
923    c = GTK_WIDGET( cbdata->wind );
924    gtk_container_remove( GTK_CONTAINER( c ), gtk_bin_get_child( GTK_BIN( c ) ) );
925
926    r = gtk_alignment_new( 0.5, 0.5, 0.01, 0.01 );
927    gtk_container_add( GTK_CONTAINER( c ), r );
928
929    p = gtk_grid_new( );
930    gtk_grid_set_column_spacing( GTK_GRID( p ), GUI_PAD_BIG );
931    gtk_container_add( GTK_CONTAINER( r ), p );
932
933    w = gtk_image_new_from_stock( GTK_STOCK_NETWORK, GTK_ICON_SIZE_DIALOG );
934    gtk_grid_attach( GTK_GRID( p ), w, 0, 0, 1, 2 );
935
936    w = gtk_label_new( NULL );
937    gtk_label_set_markup( GTK_LABEL( w ), _( "<b>Closing Connections</b>" ) );
938    gtk_misc_set_alignment( GTK_MISC( w ), 0.0, 0.5 );
939    gtk_grid_attach( GTK_GRID( p ), w, 1, 0, 1, 1 );
940
941    w = gtk_label_new( _( "Sending upload/download totals to tracker���" ) );
942    gtk_misc_set_alignment( GTK_MISC( w ), 0.0, 0.5 );
943    gtk_grid_attach( GTK_GRID( p ), w, 1, 1, 1, 1 );
944
945    b = gtk_alignment_new( 0.0, 1.0, 0.01, 0.01 );
946    w = gtk_button_new_with_mnemonic( _( "_Quit Now" ) );
947    g_signal_connect( w, "clicked", G_CALLBACK( exit_now_cb ), NULL );
948    gtk_container_add( GTK_CONTAINER( b ), w );
949    gtk_grid_attach( GTK_GRID( p ), b, 1, 2, 1, 1 );
950
951    gtk_widget_show_all( r );
952    gtk_widget_grab_focus( w );
953
954    /* clear the UI */
955    gtr_core_clear( cbdata->core );
956
957    /* ensure the window is in its previous position & size.
958     * this seems to be necessary because changing the main window's
959     * child seems to unset the size */
960    gtk_window_resize( cbdata->wind, gtr_pref_int_get( PREF_KEY_MAIN_WINDOW_WIDTH ),
961                                     gtr_pref_int_get( PREF_KEY_MAIN_WINDOW_HEIGHT ) );
962    gtk_window_move( cbdata->wind, gtr_pref_int_get( PREF_KEY_MAIN_WINDOW_X ),
963                                   gtr_pref_int_get( PREF_KEY_MAIN_WINDOW_Y ) );
964
965    /* shut down libT */
966    g_thread_new( "shutdown-thread", session_close_threadfunc, vdata );
967}
968
969static void
970show_torrent_errors( GtkWindow * window, const char * primary, GSList ** files )
971{
972    GSList * l;
973    GtkWidget * w;
974    GString * s = g_string_new( NULL );
975    const char * leader = g_slist_length( *files ) > 1
976                        ? gtr_get_unicode_string( GTR_UNICODE_BULLET )
977                        : "";
978
979    for( l=*files; l!=NULL; l=l->next )
980        g_string_append_printf( s, "%s %s\n", leader, (const char*)l->data );
981
982    w = gtk_message_dialog_new( window,
983                                GTK_DIALOG_DESTROY_WITH_PARENT,
984                                GTK_MESSAGE_ERROR,
985                                GTK_BUTTONS_CLOSE,
986                                "%s", primary );
987    gtk_message_dialog_format_secondary_text( GTK_MESSAGE_DIALOG( w ),
988                                              "%s", s->str );
989    g_signal_connect_swapped( w, "response",
990                              G_CALLBACK( gtk_widget_destroy ), w );
991    gtk_widget_show( w );
992    g_string_free( s, TRUE );
993
994    g_slist_foreach( *files, (GFunc)g_free, NULL );
995    g_slist_free( *files );
996    *files = NULL;
997}
998
999static void
1000flush_torrent_errors( struct cbdata * cbdata )
1001{
1002    if( cbdata->error_list )
1003        show_torrent_errors( cbdata->wind,
1004                              ngettext( "Couldn't add corrupt torrent",
1005                                        "Couldn't add corrupt torrents",
1006                                        g_slist_length( cbdata->error_list ) ),
1007                              &cbdata->error_list );
1008
1009    if( cbdata->duplicates_list )
1010        show_torrent_errors( cbdata->wind,
1011                              ngettext( "Couldn't add duplicate torrent",
1012                                        "Couldn't add duplicate torrents",
1013                                        g_slist_length( cbdata->duplicates_list ) ),
1014                              &cbdata->duplicates_list );
1015}
1016
1017static void
1018on_core_error( TrCore * core UNUSED, guint code, const char * msg, struct cbdata * c )
1019{
1020    switch( code )
1021    {
1022        case TR_PARSE_ERR:
1023            c->error_list =
1024                g_slist_append( c->error_list, g_path_get_basename( msg ) );
1025            break;
1026
1027        case TR_PARSE_DUPLICATE:
1028            c->duplicates_list = g_slist_append( c->duplicates_list, g_strdup( msg ) );
1029            break;
1030
1031        case TR_CORE_ERR_NO_MORE_TORRENTS:
1032            flush_torrent_errors( c );
1033            break;
1034
1035        default:
1036            g_assert_not_reached( );
1037            break;
1038    }
1039}
1040
1041static gboolean
1042on_main_window_focus_in( GtkWidget      * widget UNUSED,
1043                         GdkEventFocus  * event  UNUSED,
1044                         gpointer                gdata )
1045{
1046    struct cbdata * cbdata = gdata;
1047
1048    if( cbdata->wind )
1049        gtk_window_set_urgency_hint( cbdata->wind, FALSE );
1050    return FALSE;
1051}
1052
1053static void
1054on_add_torrent( TrCore * core, tr_ctor * ctor, gpointer gdata )
1055{
1056    struct cbdata * cbdata = gdata;
1057    GtkWidget * w = gtr_torrent_options_dialog_new( cbdata->wind, core, ctor );
1058
1059    g_signal_connect( w, "focus-in-event",
1060                      G_CALLBACK( on_main_window_focus_in ),  cbdata );
1061    if( cbdata->wind )
1062        gtk_window_set_urgency_hint( cbdata->wind, TRUE );
1063
1064    gtk_widget_show( w );
1065}
1066
1067static void
1068on_prefs_changed( TrCore * core UNUSED, const char * key, gpointer data )
1069{
1070    struct cbdata * cbdata = data;
1071    tr_session * tr = gtr_core_session( cbdata->core );
1072
1073    if( !strcmp( key, TR_PREFS_KEY_ENCRYPTION ) )
1074    {
1075        tr_sessionSetEncryption( tr, gtr_pref_int_get( key ) );
1076    }
1077    else if( !strcmp( key, TR_PREFS_KEY_DOWNLOAD_DIR ) )
1078    {
1079        tr_sessionSetDownloadDir( tr, gtr_pref_string_get( key ) );
1080    }
1081    else if( !strcmp( key, TR_PREFS_KEY_MSGLEVEL ) )
1082    {
1083        tr_setMessageLevel( gtr_pref_int_get( key ) );
1084    }
1085    else if( !strcmp( key, TR_PREFS_KEY_PEER_PORT ) )
1086    {
1087        tr_sessionSetPeerPort( tr, gtr_pref_int_get( key ) );
1088    }
1089    else if( !strcmp( key, TR_PREFS_KEY_BLOCKLIST_ENABLED ) )
1090    {
1091        tr_blocklistSetEnabled( tr, gtr_pref_flag_get( key ) );
1092    }
1093    else if( !strcmp( key, TR_PREFS_KEY_BLOCKLIST_URL ) )
1094    {
1095        tr_blocklistSetURL( tr, gtr_pref_string_get( key ) );
1096    }
1097    else if( !strcmp( key, PREF_KEY_SHOW_TRAY_ICON ) )
1098    {
1099        const int show = gtr_pref_flag_get( key );
1100        if( show && !cbdata->icon )
1101            cbdata->icon = gtr_icon_new( cbdata->core );
1102        else if( !show && cbdata->icon ) {
1103            g_object_unref( cbdata->icon );
1104            cbdata->icon = NULL;
1105        }
1106    }
1107    else if( !strcmp( key, TR_PREFS_KEY_DSPEED_ENABLED ) )
1108    {
1109        tr_sessionLimitSpeed( tr, TR_DOWN, gtr_pref_flag_get( key ) );
1110    }
1111    else if( !strcmp( key, TR_PREFS_KEY_DSPEED_KBps ) )
1112    {
1113        tr_sessionSetSpeedLimit_KBps( tr, TR_DOWN, gtr_pref_int_get( key ) );
1114    }
1115    else if( !strcmp( key, TR_PREFS_KEY_USPEED_ENABLED ) )
1116    {
1117        tr_sessionLimitSpeed( tr, TR_UP, gtr_pref_flag_get( key ) );
1118    }
1119    else if( !strcmp( key, TR_PREFS_KEY_USPEED_KBps ) )
1120    {
1121        tr_sessionSetSpeedLimit_KBps( tr, TR_UP, gtr_pref_int_get( key ) );
1122    }
1123    else if( !strcmp( key, TR_PREFS_KEY_RATIO_ENABLED ) )
1124    {
1125        tr_sessionSetRatioLimited( tr, gtr_pref_flag_get( key ) );
1126    }
1127    else if( !strcmp( key, TR_PREFS_KEY_RATIO ) )
1128    {
1129        tr_sessionSetRatioLimit( tr, gtr_pref_double_get( key ) );
1130    }
1131    else if( !strcmp( key, TR_PREFS_KEY_IDLE_LIMIT ) )
1132    {
1133        tr_sessionSetIdleLimit( tr, gtr_pref_int_get( key ) );
1134    }
1135    else if( !strcmp( key, TR_PREFS_KEY_IDLE_LIMIT_ENABLED ) )
1136    {
1137        tr_sessionSetIdleLimited( tr, gtr_pref_flag_get( key ) );
1138    }
1139    else if( !strcmp( key, TR_PREFS_KEY_PORT_FORWARDING ) )
1140    {
1141        tr_sessionSetPortForwardingEnabled( tr, gtr_pref_flag_get( key ) );
1142    }
1143    else if( !strcmp( key, TR_PREFS_KEY_PEX_ENABLED ) )
1144    {
1145        tr_sessionSetPexEnabled( tr, gtr_pref_flag_get( key ) );
1146    }
1147    else if( !strcmp( key, TR_PREFS_KEY_RENAME_PARTIAL_FILES ) )
1148    {
1149        tr_sessionSetIncompleteFileNamingEnabled( tr, gtr_pref_flag_get( key ) );
1150    }
1151    else if( !strcmp( key, TR_PREFS_KEY_DOWNLOAD_QUEUE_SIZE ) )
1152    {
1153        tr_sessionSetQueueSize( tr, TR_DOWN, gtr_pref_int_get( key ) );
1154    }
1155    else if( !strcmp( key, TR_PREFS_KEY_QUEUE_STALLED_MINUTES ) )
1156    {
1157        tr_sessionSetQueueStalledMinutes( tr, gtr_pref_int_get( key ) );
1158    }
1159    else if( !strcmp( key, TR_PREFS_KEY_DHT_ENABLED ) )
1160    {
1161        tr_sessionSetDHTEnabled( tr, gtr_pref_flag_get( key ) );
1162    }
1163    else if( !strcmp( key, TR_PREFS_KEY_UTP_ENABLED ) )
1164    {
1165        tr_sessionSetUTPEnabled( tr, gtr_pref_flag_get( key ) );
1166    }
1167    else if( !strcmp( key, TR_PREFS_KEY_LPD_ENABLED ) )
1168    {
1169        tr_sessionSetLPDEnabled( tr, gtr_pref_flag_get( key ) );
1170    }
1171    else if( !strcmp( key, TR_PREFS_KEY_RPC_PORT ) )
1172    {
1173        tr_sessionSetRPCPort( tr, gtr_pref_int_get( key ) );
1174    }
1175    else if( !strcmp( key, TR_PREFS_KEY_RPC_ENABLED ) )
1176    {
1177        tr_sessionSetRPCEnabled( tr, gtr_pref_flag_get( key ) );
1178    }
1179    else if( !strcmp( key, TR_PREFS_KEY_RPC_WHITELIST ) )
1180    {
1181        tr_sessionSetRPCWhitelist( tr, gtr_pref_string_get( key ) );
1182    }
1183    else if( !strcmp( key, TR_PREFS_KEY_RPC_WHITELIST_ENABLED ) )
1184    {
1185        tr_sessionSetRPCWhitelistEnabled( tr, gtr_pref_flag_get( key ) );
1186    }
1187    else if( !strcmp( key, TR_PREFS_KEY_RPC_USERNAME ) )
1188    {
1189        tr_sessionSetRPCUsername( tr, gtr_pref_string_get( key ) );
1190    }
1191    else if( !strcmp( key, TR_PREFS_KEY_RPC_PASSWORD ) )
1192    {
1193        tr_sessionSetRPCPassword( tr, gtr_pref_string_get( key ) );
1194    }
1195    else if( !strcmp( key, TR_PREFS_KEY_RPC_AUTH_REQUIRED ) )
1196    {
1197        tr_sessionSetRPCPasswordEnabled( tr, gtr_pref_flag_get( key ) );
1198    }
1199    else if( !strcmp( key, TR_PREFS_KEY_ALT_SPEED_UP_KBps ) )
1200    {
1201        tr_sessionSetAltSpeed_KBps( tr, TR_UP, gtr_pref_int_get( key ) );
1202    }
1203    else if( !strcmp( key, TR_PREFS_KEY_ALT_SPEED_DOWN_KBps ) )
1204    {
1205        tr_sessionSetAltSpeed_KBps( tr, TR_DOWN, gtr_pref_int_get( key ) );
1206    }
1207    else if( !strcmp( key, TR_PREFS_KEY_ALT_SPEED_ENABLED ) )
1208    {
1209        const gboolean b = gtr_pref_flag_get( key );
1210        tr_sessionUseAltSpeed( tr, b );
1211        gtr_action_set_toggled( key, b );
1212    }
1213    else if( !strcmp( key, TR_PREFS_KEY_ALT_SPEED_TIME_BEGIN ) )
1214    {
1215        tr_sessionSetAltSpeedBegin( tr, gtr_pref_int_get( key ) );
1216    }
1217    else if( !strcmp( key, TR_PREFS_KEY_ALT_SPEED_TIME_END ) )
1218    {
1219        tr_sessionSetAltSpeedEnd( tr, gtr_pref_int_get( key ) );
1220    }
1221    else if( !strcmp( key, TR_PREFS_KEY_ALT_SPEED_TIME_ENABLED ) )
1222    {
1223        tr_sessionUseAltSpeedTime( tr, gtr_pref_flag_get( key ) );
1224    }
1225    else if( !strcmp( key, TR_PREFS_KEY_ALT_SPEED_TIME_DAY ) )
1226    {
1227        tr_sessionSetAltSpeedDay( tr, gtr_pref_int_get( key ) );
1228    }
1229    else if( !strcmp( key, TR_PREFS_KEY_PEER_PORT_RANDOM_ON_START ) )
1230    {
1231        tr_sessionSetPeerPortRandomOnStart( tr, gtr_pref_flag_get( key ) );
1232    }
1233    else if( !strcmp( key, TR_PREFS_KEY_INCOMPLETE_DIR ) )
1234    {
1235        tr_sessionSetIncompleteDir( tr, gtr_pref_string_get( key ) );
1236    }
1237    else if( !strcmp( key, TR_PREFS_KEY_INCOMPLETE_DIR_ENABLED ) )
1238    {
1239        tr_sessionSetIncompleteDirEnabled( tr, gtr_pref_flag_get( key ) );
1240    }
1241    else if( !strcmp( key, TR_PREFS_KEY_SCRIPT_TORRENT_DONE_ENABLED ) )
1242    {
1243        tr_sessionSetTorrentDoneScriptEnabled( tr, gtr_pref_flag_get( key ) );
1244    }
1245    else if( !strcmp( key, TR_PREFS_KEY_SCRIPT_TORRENT_DONE_FILENAME ) )
1246    {
1247        tr_sessionSetTorrentDoneScript( tr, gtr_pref_string_get( key ) );
1248    }
1249    else if( !strcmp( key, TR_PREFS_KEY_START) )
1250    {
1251        tr_sessionSetPaused( tr, !gtr_pref_flag_get( key ) );
1252    }
1253    else if( !strcmp( key, TR_PREFS_KEY_TRASH_ORIGINAL ) )
1254    {
1255        tr_sessionSetDeleteSource( tr, gtr_pref_flag_get( key ) );
1256    }
1257}
1258
1259static gboolean
1260update_model_once( gpointer gdata )
1261{
1262    struct cbdata *data = gdata;
1263
1264    /* update the torrent data in the model */
1265    gtr_core_update( data->core );
1266
1267    /* refresh the main window's statusbar and toolbar buttons */
1268    if( data->wind != NULL )
1269        gtr_window_refresh( data->wind );
1270
1271    /* update the actions */
1272        refresh_actions( data );
1273
1274    /* update the status tray icon */
1275    if( data->icon != NULL )
1276        gtr_icon_refresh( data->icon );
1277
1278    data->update_model_soon_tag = 0;
1279    return FALSE;
1280}
1281
1282static void
1283update_model_soon( gpointer gdata )
1284{
1285    struct cbdata *data = gdata;
1286
1287    if( data->update_model_soon_tag == 0 )
1288        data->update_model_soon_tag = gdk_threads_add_idle( update_model_once, data );
1289}
1290
1291static gboolean
1292update_model_loop( gpointer gdata )
1293{
1294    const gboolean done = global_sigcount;
1295
1296    if( !done )
1297        update_model_once( gdata );
1298
1299    return !done;
1300}
1301
1302static void
1303show_about_dialog( GtkWindow * parent )
1304{
1305    const char * uri = "http://www.transmissionbt.com/";
1306    const char * authors[] = { "Jordan Lee (Backend; GTK+)",
1307                               "Mitchell Livingston (Backend; OS X)",
1308                               NULL };
1309
1310    gtk_show_about_dialog( parent,
1311        "authors", authors,
1312        "comments", _( "A fast and easy BitTorrent client" ),
1313        "copyright", _( "Copyright (c) The Transmission Project" ),
1314        "logo-icon-name", MY_CONFIG_NAME,
1315        "name", g_get_application_name( ),
1316        /* Translators: translate "translator-credits" as your name
1317           to have it appear in the credits in the "About"
1318           dialog */
1319        "translator-credits", _( "translator-credits" ),
1320        "version", LONG_VERSION_STRING,
1321        "website", uri,
1322        "website-label", uri,
1323#ifdef SHOW_LICENSE
1324        "license", LICENSE,
1325        "wrap-license", TRUE,
1326#endif
1327        NULL );
1328}
1329
1330static void
1331append_id_to_benc_list( GtkTreeModel * m, GtkTreePath * path UNUSED,
1332                        GtkTreeIter * iter, gpointer list )
1333{
1334    tr_torrent * tor = NULL;
1335    gtk_tree_model_get( m, iter, MC_TORRENT, &tor, -1 );
1336    tr_bencListAddInt( list, tr_torrentId( tor ) );
1337}
1338
1339static gboolean
1340call_rpc_for_selected_torrents( struct cbdata * data, const char * method )
1341{
1342    tr_benc top, *args, *ids;
1343    gboolean invoked = FALSE;
1344    GtkTreeSelection * s = data->sel;
1345    tr_session * session = gtr_core_session( data->core );
1346
1347    tr_bencInitDict( &top, 2 );
1348    tr_bencDictAddStr( &top, "method", method );
1349    args = tr_bencDictAddDict( &top, "arguments", 1 );
1350    ids = tr_bencDictAddList( args, "ids", 0 );
1351    gtk_tree_selection_selected_foreach( s, append_id_to_benc_list, ids );
1352
1353    if( tr_bencListSize( ids ) != 0 )
1354    {
1355        int json_len;
1356        char * json = tr_bencToStr( &top, TR_FMT_JSON_LEAN, &json_len );
1357        tr_rpc_request_exec_json( session, json, json_len, NULL, NULL );
1358        g_free( json );
1359        invoked = TRUE;
1360    }
1361
1362    tr_bencFree( &top );
1363    return invoked;
1364}
1365
1366static void
1367open_folder_foreach( GtkTreeModel * model, GtkTreePath * path UNUSED,
1368                     GtkTreeIter * iter, gpointer core )
1369{
1370    int id;
1371    gtk_tree_model_get( model, iter, MC_TORRENT_ID, &id, -1 );
1372    gtr_core_open_folder( core, id );
1373}
1374
1375static gboolean
1376on_message_window_closed( void )
1377{
1378    gtr_action_set_toggled( "toggle-message-log", FALSE );
1379    return FALSE;
1380}
1381
1382static void
1383accumulate_selected_torrents( GtkTreeModel  * model, GtkTreePath   * path UNUSED,
1384                              GtkTreeIter   * iter, gpointer        gdata )
1385{
1386    int id;
1387    GSList ** data = gdata;
1388
1389    gtk_tree_model_get( model, iter, MC_TORRENT_ID, &id, -1 );
1390    *data = g_slist_append( *data, GINT_TO_POINTER( id ) );
1391}
1392
1393static void
1394remove_selected( struct cbdata * data, gboolean delete_files )
1395{
1396    GSList * l = NULL;
1397
1398    gtk_tree_selection_selected_foreach( data->sel, accumulate_selected_torrents, &l );
1399
1400    if( l != NULL )
1401        gtr_confirm_remove( data->wind, data->core, l, delete_files );
1402}
1403
1404static void
1405start_all_torrents( struct cbdata * data )
1406{
1407    tr_session * session = gtr_core_session( data->core );
1408    const char * cmd = "{ \"method\": \"torrent-start\" }";
1409    tr_rpc_request_exec_json( session, cmd, strlen( cmd ), NULL, NULL );
1410}
1411
1412static void
1413pause_all_torrents( struct cbdata * data )
1414{
1415    tr_session * session = gtr_core_session( data->core );
1416    const char * cmd = "{ \"method\": \"torrent-stop\" }";
1417    tr_rpc_request_exec_json( session, cmd, strlen( cmd ), NULL, NULL );
1418}
1419
1420static tr_torrent*
1421get_first_selected_torrent( struct cbdata * data )
1422{
1423    tr_torrent * tor = NULL;
1424    GtkTreeModel * m;
1425    GList * l = gtk_tree_selection_get_selected_rows( data->sel, &m );
1426    if( l != NULL ) {
1427        GtkTreePath * p = l->data;
1428        GtkTreeIter i;
1429        if( gtk_tree_model_get_iter( m, &i, p ) )
1430            gtk_tree_model_get( m, &i, MC_TORRENT, &tor, -1 );
1431    }
1432    g_list_foreach( l, (GFunc)gtk_tree_path_free, NULL );
1433    g_list_free( l );
1434    return tor;
1435}
1436
1437static void
1438copy_magnet_link_to_clipboard( GtkWidget * w, tr_torrent * tor )
1439{
1440    char * magnet = tr_torrentGetMagnetLink( tor );
1441    GdkDisplay * display = gtk_widget_get_display( w );
1442    GdkAtom selection;
1443    GtkClipboard * clipboard;
1444
1445    /* this is The Right Thing for copy/paste... */
1446    selection = GDK_SELECTION_CLIPBOARD;
1447    clipboard = gtk_clipboard_get_for_display( display, selection );
1448    gtk_clipboard_set_text( clipboard, magnet, -1 );
1449
1450    /* ...but people using plain ol' X need this instead */
1451    selection = GDK_SELECTION_PRIMARY;
1452    clipboard = gtk_clipboard_get_for_display( display, selection );
1453    gtk_clipboard_set_text( clipboard, magnet, -1 );
1454
1455    /* cleanup */
1456    tr_free( magnet );
1457}
1458
1459void
1460gtr_actions_handler( const char * action_name, gpointer user_data )
1461{
1462    struct cbdata * data = user_data;
1463    gboolean        changed = FALSE;
1464
1465    if( !strcmp( action_name, "open-torrent-from-url" ) )
1466    {
1467        GtkWidget * w = gtr_torrent_open_from_url_dialog_new( data->wind, data->core );
1468        gtk_widget_show( w );
1469    }
1470    else if(  !strcmp( action_name, "open-torrent-menu" )
1471      || !strcmp( action_name, "open-torrent-toolbar" ) )
1472    {
1473        GtkWidget * w = gtr_torrent_open_from_file_dialog_new( data->wind, data->core );
1474        gtk_widget_show( w );
1475    }
1476    else if( !strcmp( action_name, "show-stats" ) )
1477    {
1478        GtkWidget * dialog = gtr_stats_dialog_new( data->wind, data->core );
1479        gtk_widget_show( dialog );
1480    }
1481    else if( !strcmp( action_name, "donate" ) )
1482    {
1483        gtr_open_uri( "http://www.transmissionbt.com/donate.php" );
1484    }
1485    else if( !strcmp( action_name, "pause-all-torrents" ) )
1486    {
1487        pause_all_torrents( data );
1488    }
1489    else if( !strcmp( action_name, "start-all-torrents" ) )
1490    {
1491        start_all_torrents( data );
1492    }
1493    else if( !strcmp( action_name, "copy-magnet-link-to-clipboard" ) )
1494    {
1495        tr_torrent * tor = get_first_selected_torrent( data );
1496        if( tor != NULL )
1497        {
1498            copy_magnet_link_to_clipboard( GTK_WIDGET( data->wind ), tor );
1499        }
1500    }
1501    else if( !strcmp( action_name, "relocate-torrent" ) )
1502    {
1503        GSList * ids = get_selected_torrent_ids( data );
1504        if( ids != NULL )
1505        {
1506            GtkWindow * parent = data->wind;
1507            GtkWidget * w = gtr_relocate_dialog_new( parent, data->core, ids );
1508            gtk_widget_show( w );
1509        }
1510    }
1511    else if( !strcmp( action_name, "torrent-start" )
1512          || !strcmp( action_name, "torrent-start-now" )
1513          || !strcmp( action_name, "torrent-stop" )
1514          || !strcmp( action_name, "torrent-reannounce" )
1515          || !strcmp( action_name, "torrent-verify" )
1516          || !strcmp( action_name, "queue-move-top" )
1517          || !strcmp( action_name, "queue-move-up" )
1518          || !strcmp( action_name, "queue-move-down" )
1519          || !strcmp( action_name, "queue-move-bottom" ) )
1520    {
1521        changed |= call_rpc_for_selected_torrents( data, action_name );
1522    }
1523    else if( !strcmp( action_name, "open-torrent-folder" ) )
1524    {
1525        gtk_tree_selection_selected_foreach( data->sel, open_folder_foreach, data->core );
1526    }
1527    else if( !strcmp( action_name, "show-torrent-properties" ) )
1528    {
1529        show_details_dialog_for_selected_torrents( data );
1530    }
1531    else if( !strcmp( action_name, "new-torrent" ) )
1532    {
1533        GtkWidget * w = gtr_torrent_creation_dialog_new( data->wind, data->core );
1534        gtk_widget_show( w );
1535    }
1536    else if( !strcmp( action_name, "remove-torrent" ) )
1537    {
1538        remove_selected( data, FALSE );
1539    }
1540    else if( !strcmp( action_name, "delete-torrent" ) )
1541    {
1542        remove_selected( data, TRUE );
1543    }
1544    else if( !strcmp( action_name, "quit" ) )
1545    {
1546        on_app_exit( data );
1547    }
1548    else if( !strcmp( action_name, "select-all" ) )
1549    {
1550        gtk_tree_selection_select_all( data->sel );
1551    }
1552    else if( !strcmp( action_name, "deselect-all" ) )
1553    {
1554        gtk_tree_selection_unselect_all( data->sel );
1555    }
1556    else if( !strcmp( action_name, "edit-preferences" ) )
1557    {
1558        if( NULL == data->prefs )
1559        {
1560            data->prefs = gtr_prefs_dialog_new( data->wind, G_OBJECT( data->core ) );
1561            g_signal_connect( data->prefs, "destroy",
1562                              G_CALLBACK( gtk_widget_destroyed ), &data->prefs );
1563        }
1564        gtr_window_present( GTK_WINDOW( data->prefs ) );
1565    }
1566    else if( !strcmp( action_name, "toggle-message-log" ) )
1567    {
1568        if( !data->msgwin )
1569        {
1570            GtkWidget * win = gtr_message_log_window_new( data->wind, data->core );
1571            g_signal_connect( win, "destroy", G_CALLBACK( on_message_window_closed ), NULL );
1572            data->msgwin = win;
1573        }
1574        else
1575        {
1576            gtr_action_set_toggled( "toggle-message-log", FALSE );
1577            gtk_widget_destroy( data->msgwin );
1578            data->msgwin = NULL;
1579        }
1580    }
1581    else if( !strcmp( action_name, "show-about-dialog" ) )
1582    {
1583        show_about_dialog( data->wind );
1584    }
1585    else if( !strcmp ( action_name, "help" ) )
1586    {
1587        gtr_open_uri( gtr_get_help_uri( ) );
1588    }
1589    else if( !strcmp( action_name, "toggle-main-window" ) )
1590    {
1591        toggleMainWindow( data );
1592    }
1593    else if( !strcmp( action_name, "present-main-window" ) )
1594    {
1595        presentMainWindow( data );
1596    }
1597    else g_error ( "Unhandled action: %s", action_name );
1598
1599    if( changed )
1600        update_model_soon( data );
1601}
1602