source: trunk/gtk/main.c @ 13107

Last change on this file since 13107 was 13107, checked in by jordan, 10 years ago

(trunk libT) #4372 "make notification and system sounds configurable" -- instead of using notify-send, use GDBus on the org.freedesktop.Notifications API. Patch by fmuellner.

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