source: trunk/gtk/main.c @ 12657

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

(trunk gtk) fix minor memory leak introduced in r12657

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