source: trunk/gtk/main.c @ 12656

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

(trunk gtk) first cut at using GApplication. This lets glib replace hundreds of lines of homegrown code. Whee!

  • Property svn:keywords set to Date Rev Author Id
File size: 53.4 KB
Line 
1/******************************************************************************
2 * $Id: main.c 12656 2011-08-09 02:30:31Z 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    return ret;
737}
738
739static void
740on_core_busy( TrCore * core UNUSED, gboolean busy, struct cbdata * c )
741{
742    gtr_window_set_busy( c->wind, busy );
743}
744
745static void
746app_setup( TrWindow * wind, struct cbdata * cbdata )
747{
748    if( cbdata->is_iconified )
749        gtr_pref_flag_set( PREF_KEY_SHOW_TRAY_ICON, TRUE );
750
751    gtr_actions_set_core( cbdata->core );
752
753    /* set up core handlers */
754    g_signal_connect( cbdata->core, "busy", G_CALLBACK( on_core_busy ), cbdata );
755    g_signal_connect( cbdata->core, "add-error", G_CALLBACK( on_core_error ), cbdata );
756    g_signal_connect( cbdata->core, "add-prompt", G_CALLBACK( on_add_torrent ), cbdata );
757    g_signal_connect( cbdata->core, "prefs-changed", G_CALLBACK( on_prefs_changed ), cbdata );
758
759    /* add torrents from command-line and saved state */
760    gtr_core_load( cbdata->core, cbdata->start_paused );
761    gtr_core_torrents_added( cbdata->core );
762
763    /* set up main window */
764    main_window_setup( cbdata, wind );
765
766    /* set up the icon */
767    on_prefs_changed( cbdata->core, PREF_KEY_SHOW_TRAY_ICON, cbdata );
768
769    /* start model update timer */
770    cbdata->timer = gdk_threads_add_timeout_seconds( MAIN_WINDOW_REFRESH_INTERVAL_SECONDS, update_model_loop, cbdata );
771    update_model_once( cbdata );
772
773    /* either show the window or iconify it */
774    if( !cbdata->is_iconified )
775        gtk_widget_show( GTK_WIDGET( wind ) );
776    else
777    {
778        gtk_window_set_skip_taskbar_hint( cbdata->wind,
779                                          cbdata->icon != NULL );
780        cbdata->is_iconified = FALSE; // ensure that the next toggle iconifies
781        gtr_action_set_toggled( "toggle-main-window", FALSE );
782    }
783
784    if( !gtr_pref_flag_get( PREF_KEY_USER_HAS_GIVEN_INFORMED_CONSENT ) )
785    {
786        GtkWidget * w = gtk_message_dialog_new( GTK_WINDOW( wind ),
787                                                GTK_DIALOG_DESTROY_WITH_PARENT,
788                                                GTK_MESSAGE_INFO,
789                                                GTK_BUTTONS_NONE,
790                                                "%s",
791             _( "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." ) );
792        gtk_dialog_add_button( GTK_DIALOG( w ), GTK_STOCK_QUIT, GTK_RESPONSE_REJECT );
793        gtk_dialog_add_button( GTK_DIALOG( w ), _( "I _Accept" ), GTK_RESPONSE_ACCEPT );
794        gtk_dialog_set_default_response( GTK_DIALOG( w ), GTK_RESPONSE_ACCEPT );
795        switch( gtk_dialog_run( GTK_DIALOG( w ) ) ) {
796            case GTK_RESPONSE_ACCEPT:
797                /* only show it once */
798                gtr_pref_flag_set( PREF_KEY_USER_HAS_GIVEN_INFORMED_CONSENT, TRUE );
799                gtk_widget_destroy( w );
800                break;
801            default:
802                exit( 0 );
803        }
804    }
805}
806
807static void
808presentMainWindow( struct cbdata * cbdata )
809{
810    GtkWindow * window = cbdata->wind;
811
812    if( cbdata->is_iconified )
813    {
814        cbdata->is_iconified = false;
815
816        gtk_window_set_skip_taskbar_hint( window, FALSE );
817    }
818
819    if( !gtk_widget_get_visible( GTK_WIDGET( window ) ) )
820    {
821        gtk_window_resize( window, gtr_pref_int_get( PREF_KEY_MAIN_WINDOW_WIDTH ),
822                                   gtr_pref_int_get( PREF_KEY_MAIN_WINDOW_HEIGHT ) );
823        gtk_window_move( window, gtr_pref_int_get( PREF_KEY_MAIN_WINDOW_X ),
824                                 gtr_pref_int_get( PREF_KEY_MAIN_WINDOW_Y ) );
825        gtr_widget_set_visible( GTK_WIDGET( window ), TRUE );
826    }
827    gtr_window_present( window );
828}
829
830static void
831hideMainWindow( struct cbdata * cbdata )
832{
833    GtkWindow * window = cbdata->wind;
834    gtk_window_set_skip_taskbar_hint( window, TRUE );
835    gtr_widget_set_visible( GTK_WIDGET( window ), FALSE );
836    cbdata->is_iconified = true;
837}
838
839static void
840toggleMainWindow( struct cbdata * cbdata )
841{
842    if( cbdata->is_iconified )
843        presentMainWindow( cbdata );
844    else
845        hideMainWindow( cbdata );
846}
847
848static gboolean
849winclose( GtkWidget * w    UNUSED,
850          GdkEvent * event UNUSED,
851          gpointer         gdata )
852{
853    struct cbdata * cbdata = gdata;
854
855    if( cbdata->icon != NULL )
856        gtr_action_activate ( "toggle-main-window" );
857    else
858        on_app_exit( cbdata );
859
860    return TRUE; /* don't propagate event further */
861}
862
863static void
864rowChangedCB( GtkTreeModel  * model UNUSED,
865              GtkTreePath   * path,
866              GtkTreeIter   * iter  UNUSED,
867              gpointer        gdata )
868{
869    struct cbdata * data = gdata;
870
871    if( gtk_tree_selection_path_is_selected ( data->sel, path ) )
872        refresh_actions_soon( data );
873}
874
875static void
876on_drag_data_received( GtkWidget         * widget          UNUSED,
877                       GdkDragContext    * drag_context,
878                       gint                x               UNUSED,
879                       gint                y               UNUSED,
880                       GtkSelectionData  * selection_data,
881                       guint               info            UNUSED,
882                       guint               time_,
883                       gpointer            gdata )
884{
885    guint i;
886    char ** uris = gtk_selection_data_get_uris( selection_data );
887    const guint file_count = g_strv_length( uris );
888    GSList * files = NULL;
889
890    for( i=0; i<file_count; ++i )
891        files = g_slist_append( files, g_file_new_for_uri( uris[i] ) );
892
893    open_files( files, gdata );
894
895    /* cleanup */
896    g_slist_foreach( files, (GFunc)g_object_unref, NULL );
897    g_slist_free( files );
898    g_strfreev( uris );
899
900    gtk_drag_finish( drag_context, true, FALSE, time_ );
901}
902
903static void
904main_window_setup( struct cbdata * cbdata, TrWindow * wind )
905{
906    GtkWidget * w;
907    GtkTreeModel * model;
908    GtkTreeSelection * sel;
909
910    g_assert( NULL == cbdata->wind );
911    cbdata->wind = GTK_WINDOW( wind );
912    cbdata->sel = sel = GTK_TREE_SELECTION( gtr_window_get_selection( cbdata->wind ) );
913
914    g_signal_connect( sel, "changed", G_CALLBACK( on_selection_changed ), cbdata );
915    on_selection_changed( sel, cbdata );
916    model = gtr_core_model( cbdata->core );
917    g_signal_connect( model, "row-changed", G_CALLBACK( rowChangedCB ), cbdata );
918    g_signal_connect( wind, "delete-event", G_CALLBACK( winclose ), cbdata );
919    refresh_actions( cbdata );
920
921    /* register to handle URIs that get dragged onto our main window */
922    w = GTK_WIDGET( wind );
923    gtk_drag_dest_set( w, GTK_DEST_DEFAULT_ALL, NULL, 0, GDK_ACTION_COPY );
924    gtk_drag_dest_add_uri_targets( w );
925    g_signal_connect( w, "drag-data-received", G_CALLBACK(on_drag_data_received), cbdata );
926}
927
928static gboolean
929on_session_closed( gpointer gdata )
930{
931    struct cbdata * cbdata = gdata;
932
933    /* shutdown the gui */
934    while( cbdata->details != NULL ) {
935        struct DetailsDialogHandle * h = cbdata->details->data;
936        gtk_widget_destroy( h->dialog );
937    }
938
939    if( cbdata->prefs )
940        gtk_widget_destroy( GTK_WIDGET( cbdata->prefs ) );
941    if( cbdata->wind )
942        gtk_widget_destroy( GTK_WIDGET( cbdata->wind ) );
943    g_object_unref( cbdata->core );
944    if( cbdata->icon )
945        g_object_unref( cbdata->icon );
946    g_slist_foreach( cbdata->error_list, (GFunc)g_free, NULL );
947    g_slist_free( cbdata->error_list );
948    g_slist_foreach( cbdata->duplicates_list, (GFunc)g_free, NULL );
949    g_slist_free( cbdata->duplicates_list );
950
951    return FALSE;
952}
953
954static gpointer
955session_close_threadfunc( gpointer gdata )
956{
957    /* since tr_sessionClose() is a blocking function,
958     * call it from another thread... when it's done,
959     * punt the GUI teardown back to the GTK+ thread */
960    struct cbdata * cbdata = gdata;
961    gdk_threads_enter( );
962    gtr_core_close( cbdata->core );
963    gdk_threads_add_idle( on_session_closed, gdata );
964    gdk_threads_leave( );
965    return NULL;
966}
967
968static void
969exit_now_cb( GtkWidget *w UNUSED, gpointer data UNUSED )
970{
971    exit( 0 );
972}
973
974static void
975on_app_exit( gpointer vdata )
976{
977    GtkWidget *r, *p, *b, *w, *c;
978    struct cbdata *cbdata = vdata;
979
980    /* stop the update timer */
981    if( cbdata->timer )
982    {
983        g_source_remove( cbdata->timer );
984        cbdata->timer = 0;
985    }
986
987    c = GTK_WIDGET( cbdata->wind );
988    gtk_container_remove( GTK_CONTAINER( c ), gtk_bin_get_child( GTK_BIN( c ) ) );
989
990    r = gtk_alignment_new( 0.5, 0.5, 0.01, 0.01 );
991    gtk_container_add( GTK_CONTAINER( c ), r );
992
993    p = gtk_table_new( 3, 2, FALSE );
994    gtk_table_set_col_spacings( GTK_TABLE( p ), GUI_PAD_BIG );
995    gtk_container_add( GTK_CONTAINER( r ), p );
996
997    w = gtk_image_new_from_stock( GTK_STOCK_NETWORK, GTK_ICON_SIZE_DIALOG );
998    gtk_table_attach_defaults( GTK_TABLE( p ), w, 0, 1, 0, 2 );
999
1000    w = gtk_label_new( NULL );
1001    gtk_label_set_markup( GTK_LABEL( w ), _( "<b>Closing Connections</b>" ) );
1002    gtk_misc_set_alignment( GTK_MISC( w ), 0.0, 0.5 );
1003    gtk_table_attach_defaults( GTK_TABLE( p ), w, 1, 2, 0, 1 );
1004
1005    w = gtk_label_new( _( "Sending upload/download totals to tracker..." ) );
1006    gtk_misc_set_alignment( GTK_MISC( w ), 0.0, 0.5 );
1007    gtk_table_attach_defaults( GTK_TABLE( p ), w, 1, 2, 1, 2 );
1008
1009    b = gtk_alignment_new( 0.0, 1.0, 0.01, 0.01 );
1010    w = gtk_button_new_with_mnemonic( _( "_Quit Now" ) );
1011    g_signal_connect( w, "clicked", G_CALLBACK( exit_now_cb ), NULL );
1012    gtk_container_add( GTK_CONTAINER( b ), w );
1013    gtk_table_attach( GTK_TABLE( p ), b, 1, 2, 2, 3, GTK_FILL, GTK_FILL, 0, 10 );
1014
1015    gtk_widget_show_all( r );
1016    gtk_widget_grab_focus( w );
1017
1018    /* clear the UI */
1019    gtr_core_clear( cbdata->core );
1020
1021    /* ensure the window is in its previous position & size.
1022     * this seems to be necessary because changing the main window's
1023     * child seems to unset the size */
1024    gtk_window_resize( cbdata->wind, gtr_pref_int_get( PREF_KEY_MAIN_WINDOW_WIDTH ),
1025                                     gtr_pref_int_get( PREF_KEY_MAIN_WINDOW_HEIGHT ) );
1026    gtk_window_move( cbdata->wind, gtr_pref_int_get( PREF_KEY_MAIN_WINDOW_X ),
1027                                   gtr_pref_int_get( PREF_KEY_MAIN_WINDOW_Y ) );
1028
1029    /* shut down libT */
1030    g_thread_create( session_close_threadfunc, vdata, TRUE, NULL );
1031}
1032
1033static void
1034show_torrent_errors( GtkWindow * window, const char * primary, GSList ** files )
1035{
1036    GSList * l;
1037    GtkWidget * w;
1038    GString * s = g_string_new( NULL );
1039    const char * leader = g_slist_length( *files ) > 1
1040                        ? gtr_get_unicode_string( GTR_UNICODE_BULLET )
1041                        : "";
1042
1043    for( l=*files; l!=NULL; l=l->next )
1044        g_string_append_printf( s, "%s %s\n", leader, (const char*)l->data );
1045
1046    w = gtk_message_dialog_new( window,
1047                                GTK_DIALOG_DESTROY_WITH_PARENT,
1048                                GTK_MESSAGE_ERROR,
1049                                GTK_BUTTONS_CLOSE,
1050                                "%s", primary );
1051    gtk_message_dialog_format_secondary_text( GTK_MESSAGE_DIALOG( w ),
1052                                              "%s", s->str );
1053    g_signal_connect_swapped( w, "response",
1054                              G_CALLBACK( gtk_widget_destroy ), w );
1055    gtk_widget_show( w );
1056    g_string_free( s, TRUE );
1057
1058    g_slist_foreach( *files, (GFunc)g_free, NULL );
1059    g_slist_free( *files );
1060    *files = NULL;
1061}
1062
1063static void
1064flush_torrent_errors( struct cbdata * cbdata )
1065{
1066    if( cbdata->error_list )
1067        show_torrent_errors( cbdata->wind,
1068                              ngettext( "Couldn't add corrupt torrent",
1069                                        "Couldn't add corrupt torrents",
1070                                        g_slist_length( cbdata->error_list ) ),
1071                              &cbdata->error_list );
1072
1073    if( cbdata->duplicates_list )
1074        show_torrent_errors( cbdata->wind,
1075                              ngettext( "Couldn't add duplicate torrent",
1076                                        "Couldn't add duplicate torrents",
1077                                        g_slist_length( cbdata->duplicates_list ) ),
1078                              &cbdata->duplicates_list );
1079}
1080
1081static void
1082on_core_error( TrCore * core UNUSED, guint code, const char * msg, struct cbdata * c )
1083{
1084    switch( code )
1085    {
1086        case TR_PARSE_ERR:
1087            c->error_list =
1088                g_slist_append( c->error_list, g_path_get_basename( msg ) );
1089            break;
1090
1091        case TR_PARSE_DUPLICATE:
1092            c->duplicates_list = g_slist_append( c->duplicates_list, g_strdup( msg ) );
1093            break;
1094
1095        case TR_CORE_ERR_NO_MORE_TORRENTS:
1096            flush_torrent_errors( c );
1097            break;
1098
1099        default:
1100            g_assert_not_reached( );
1101            break;
1102    }
1103}
1104
1105static gboolean
1106on_main_window_focus_in( GtkWidget      * widget UNUSED,
1107                         GdkEventFocus  * event  UNUSED,
1108                         gpointer                gdata )
1109{
1110    struct cbdata * cbdata = gdata;
1111
1112    if( cbdata->wind )
1113        gtk_window_set_urgency_hint( cbdata->wind, FALSE );
1114    return FALSE;
1115}
1116
1117static void
1118on_add_torrent( TrCore * core, tr_ctor * ctor, gpointer gdata )
1119{
1120    struct cbdata * cbdata = gdata;
1121    GtkWidget * w = gtr_torrent_options_dialog_new( cbdata->wind, core, ctor );
1122
1123    g_signal_connect( w, "focus-in-event",
1124                      G_CALLBACK( on_main_window_focus_in ),  cbdata );
1125    if( cbdata->wind )
1126        gtk_window_set_urgency_hint( cbdata->wind, TRUE );
1127
1128    gtk_widget_show( w );
1129}
1130
1131static void
1132on_prefs_changed( TrCore * core UNUSED, const char * key, gpointer data )
1133{
1134    struct cbdata * cbdata = data;
1135    tr_session * tr = gtr_core_session( cbdata->core );
1136
1137    if( !strcmp( key, TR_PREFS_KEY_ENCRYPTION ) )
1138    {
1139        tr_sessionSetEncryption( tr, gtr_pref_int_get( key ) );
1140    }
1141    else if( !strcmp( key, TR_PREFS_KEY_DOWNLOAD_DIR ) )
1142    {
1143        tr_sessionSetDownloadDir( tr, gtr_pref_string_get( key ) );
1144    }
1145    else if( !strcmp( key, TR_PREFS_KEY_MSGLEVEL ) )
1146    {
1147        tr_setMessageLevel( gtr_pref_int_get( key ) );
1148    }
1149    else if( !strcmp( key, TR_PREFS_KEY_PEER_PORT ) )
1150    {
1151        tr_sessionSetPeerPort( tr, gtr_pref_int_get( key ) );
1152    }
1153    else if( !strcmp( key, TR_PREFS_KEY_BLOCKLIST_ENABLED ) )
1154    {
1155        tr_blocklistSetEnabled( tr, gtr_pref_flag_get( key ) );
1156    }
1157    else if( !strcmp( key, TR_PREFS_KEY_BLOCKLIST_URL ) )
1158    {
1159        tr_blocklistSetURL( tr, gtr_pref_string_get( key ) );
1160    }
1161    else if( !strcmp( key, PREF_KEY_SHOW_TRAY_ICON ) )
1162    {
1163        const int show = gtr_pref_flag_get( key );
1164        if( show && !cbdata->icon )
1165            cbdata->icon = gtr_icon_new( cbdata->core );
1166        else if( !show && cbdata->icon ) {
1167            g_object_unref( cbdata->icon );
1168            cbdata->icon = NULL;
1169        }
1170    }
1171    else if( !strcmp( key, TR_PREFS_KEY_DSPEED_ENABLED ) )
1172    {
1173        tr_sessionLimitSpeed( tr, TR_DOWN, gtr_pref_flag_get( key ) );
1174    }
1175    else if( !strcmp( key, TR_PREFS_KEY_DSPEED_KBps ) )
1176    {
1177        tr_sessionSetSpeedLimit_KBps( tr, TR_DOWN, gtr_pref_int_get( key ) );
1178    }
1179    else if( !strcmp( key, TR_PREFS_KEY_USPEED_ENABLED ) )
1180    {
1181        tr_sessionLimitSpeed( tr, TR_UP, gtr_pref_flag_get( key ) );
1182    }
1183    else if( !strcmp( key, TR_PREFS_KEY_USPEED_KBps ) )
1184    {
1185        tr_sessionSetSpeedLimit_KBps( tr, TR_UP, gtr_pref_int_get( key ) );
1186    }
1187    else if( !strcmp( key, TR_PREFS_KEY_RATIO_ENABLED ) )
1188    {
1189        tr_sessionSetRatioLimited( tr, gtr_pref_flag_get( key ) );
1190    }
1191    else if( !strcmp( key, TR_PREFS_KEY_RATIO ) )
1192    {
1193        tr_sessionSetRatioLimit( tr, gtr_pref_double_get( key ) );
1194    }
1195    else if( !strcmp( key, TR_PREFS_KEY_IDLE_LIMIT ) )
1196    {
1197        tr_sessionSetIdleLimit( tr, gtr_pref_int_get( key ) );
1198    }
1199    else if( !strcmp( key, TR_PREFS_KEY_IDLE_LIMIT_ENABLED ) )
1200    {
1201        tr_sessionSetIdleLimited( tr, gtr_pref_flag_get( key ) );
1202    }
1203    else if( !strcmp( key, TR_PREFS_KEY_PORT_FORWARDING ) )
1204    {
1205        tr_sessionSetPortForwardingEnabled( tr, gtr_pref_flag_get( key ) );
1206    }
1207    else if( !strcmp( key, TR_PREFS_KEY_PEX_ENABLED ) )
1208    {
1209        tr_sessionSetPexEnabled( tr, gtr_pref_flag_get( key ) );
1210    }
1211    else if( !strcmp( key, TR_PREFS_KEY_RENAME_PARTIAL_FILES ) )
1212    {
1213        tr_sessionSetIncompleteFileNamingEnabled( tr, gtr_pref_flag_get( key ) );
1214    }
1215    else if( !strcmp( key, TR_PREFS_KEY_DOWNLOAD_QUEUE_SIZE ) )
1216    {
1217        tr_sessionSetQueueSize( tr, TR_DOWN, gtr_pref_int_get( key ) );
1218    }
1219    else if( !strcmp( key, TR_PREFS_KEY_QUEUE_STALLED_MINUTES ) )
1220    {
1221        tr_sessionSetQueueStalledMinutes( tr, gtr_pref_int_get( key ) );
1222    }
1223    else if( !strcmp( key, TR_PREFS_KEY_DHT_ENABLED ) )
1224    {
1225        tr_sessionSetDHTEnabled( tr, gtr_pref_flag_get( key ) );
1226    }
1227    else if( !strcmp( key, TR_PREFS_KEY_UTP_ENABLED ) )
1228    {
1229        tr_sessionSetUTPEnabled( tr, gtr_pref_flag_get( key ) );
1230    }
1231    else if( !strcmp( key, TR_PREFS_KEY_LPD_ENABLED ) )
1232    {
1233        tr_sessionSetLPDEnabled( tr, gtr_pref_flag_get( key ) );
1234    }
1235    else if( !strcmp( key, TR_PREFS_KEY_RPC_PORT ) )
1236    {
1237        tr_sessionSetRPCPort( tr, gtr_pref_int_get( key ) );
1238    }
1239    else if( !strcmp( key, TR_PREFS_KEY_RPC_ENABLED ) )
1240    {
1241        tr_sessionSetRPCEnabled( tr, gtr_pref_flag_get( key ) );
1242    }
1243    else if( !strcmp( key, TR_PREFS_KEY_RPC_WHITELIST ) )
1244    {
1245        tr_sessionSetRPCWhitelist( tr, gtr_pref_string_get( key ) );
1246    }
1247    else if( !strcmp( key, TR_PREFS_KEY_RPC_WHITELIST_ENABLED ) )
1248    {
1249        tr_sessionSetRPCWhitelistEnabled( tr, gtr_pref_flag_get( key ) );
1250    }
1251    else if( !strcmp( key, TR_PREFS_KEY_RPC_USERNAME ) )
1252    {
1253        tr_sessionSetRPCUsername( tr, gtr_pref_string_get( key ) );
1254    }
1255    else if( !strcmp( key, TR_PREFS_KEY_RPC_PASSWORD ) )
1256    {
1257        tr_sessionSetRPCPassword( tr, gtr_pref_string_get( key ) );
1258    }
1259    else if( !strcmp( key, TR_PREFS_KEY_RPC_AUTH_REQUIRED ) )
1260    {
1261        tr_sessionSetRPCPasswordEnabled( tr, gtr_pref_flag_get( key ) );
1262    }
1263    else if( !strcmp( key, TR_PREFS_KEY_ALT_SPEED_UP_KBps ) )
1264    {
1265        tr_sessionSetAltSpeed_KBps( tr, TR_UP, gtr_pref_int_get( key ) );
1266    }
1267    else if( !strcmp( key, TR_PREFS_KEY_ALT_SPEED_DOWN_KBps ) )
1268    {
1269        tr_sessionSetAltSpeed_KBps( tr, TR_DOWN, gtr_pref_int_get( key ) );
1270    }
1271    else if( !strcmp( key, TR_PREFS_KEY_ALT_SPEED_ENABLED ) )
1272    {
1273        const gboolean b = gtr_pref_flag_get( key );
1274        tr_sessionUseAltSpeed( tr, b );
1275        gtr_action_set_toggled( key, b );
1276    }
1277    else if( !strcmp( key, TR_PREFS_KEY_ALT_SPEED_TIME_BEGIN ) )
1278    {
1279        tr_sessionSetAltSpeedBegin( tr, gtr_pref_int_get( key ) );
1280    }
1281    else if( !strcmp( key, TR_PREFS_KEY_ALT_SPEED_TIME_END ) )
1282    {
1283        tr_sessionSetAltSpeedEnd( tr, gtr_pref_int_get( key ) );
1284    }
1285    else if( !strcmp( key, TR_PREFS_KEY_ALT_SPEED_TIME_ENABLED ) )
1286    {
1287        tr_sessionUseAltSpeedTime( tr, gtr_pref_flag_get( key ) );
1288    }
1289    else if( !strcmp( key, TR_PREFS_KEY_ALT_SPEED_TIME_DAY ) )
1290    {
1291        tr_sessionSetAltSpeedDay( tr, gtr_pref_int_get( key ) );
1292    }
1293    else if( !strcmp( key, TR_PREFS_KEY_PEER_PORT_RANDOM_ON_START ) )
1294    {
1295        tr_sessionSetPeerPortRandomOnStart( tr, gtr_pref_flag_get( key ) );
1296    }
1297    else if( !strcmp( key, TR_PREFS_KEY_INCOMPLETE_DIR ) )
1298    {
1299        tr_sessionSetIncompleteDir( tr, gtr_pref_string_get( key ) );
1300    }
1301    else if( !strcmp( key, TR_PREFS_KEY_INCOMPLETE_DIR_ENABLED ) )
1302    {
1303        tr_sessionSetIncompleteDirEnabled( tr, gtr_pref_flag_get( key ) );
1304    }
1305    else if( !strcmp( key, TR_PREFS_KEY_SCRIPT_TORRENT_DONE_ENABLED ) )
1306    {
1307        tr_sessionSetTorrentDoneScriptEnabled( tr, gtr_pref_flag_get( key ) );
1308    }
1309    else if( !strcmp( key, TR_PREFS_KEY_SCRIPT_TORRENT_DONE_FILENAME ) )
1310    {
1311        tr_sessionSetTorrentDoneScript( tr, gtr_pref_string_get( key ) );
1312    }
1313    else if( !strcmp( key, TR_PREFS_KEY_START) )
1314    {
1315        tr_sessionSetPaused( tr, !gtr_pref_flag_get( key ) );
1316    }
1317    else if( !strcmp( key, TR_PREFS_KEY_TRASH_ORIGINAL ) )
1318    {
1319        tr_sessionSetDeleteSource( tr, gtr_pref_flag_get( key ) );
1320    }
1321}
1322
1323static gboolean
1324update_model_once( gpointer gdata )
1325{
1326    struct cbdata *data = gdata;
1327
1328    /* update the torrent data in the model */
1329    gtr_core_update( data->core );
1330
1331    /* refresh the main window's statusbar and toolbar buttons */
1332    if( data->wind != NULL )
1333        gtr_window_refresh( data->wind );
1334
1335    /* update the actions */
1336        refresh_actions( data );
1337
1338    /* update the status tray icon */
1339    if( data->icon != NULL )
1340        gtr_icon_refresh( data->icon );
1341
1342    data->update_model_soon_tag = 0;
1343    return FALSE;
1344}
1345
1346static void
1347update_model_soon( gpointer gdata )
1348{
1349    struct cbdata *data = gdata;
1350
1351    if( data->update_model_soon_tag == 0 )
1352        data->update_model_soon_tag = gdk_threads_add_idle( update_model_once, data );
1353}
1354
1355static gboolean
1356update_model_loop( gpointer gdata )
1357{
1358    const gboolean done = global_sigcount;
1359
1360    if( !done )
1361        update_model_once( gdata );
1362
1363    return !done;
1364}
1365
1366static void
1367show_about_dialog( GtkWindow * parent )
1368{
1369    GtkWidget * d;
1370    const char * website_uri = "http://www.transmissionbt.com/";
1371    const char * authors[] = {
1372        "Jordan Lee (Backend; GTK+)",
1373        "Mitchell Livingston (Backend; OS X)",
1374        NULL
1375    };
1376
1377    d = g_object_new( GTK_TYPE_ABOUT_DIALOG,
1378                      "authors", authors,
1379                      "comments", _( "A fast and easy BitTorrent client" ),
1380                      "copyright", _( "Copyright (c) The Transmission Project" ),
1381                      "logo-icon-name", MY_CONFIG_NAME,
1382                      "name", g_get_application_name( ),
1383                      /* Translators: translate "translator-credits" as your name
1384                         to have it appear in the credits in the "About"
1385                         dialog */
1386                      "translator-credits", _( "translator-credits" ),
1387                      "version", LONG_VERSION_STRING,
1388                      "website", website_uri,
1389                      "website-label", website_uri,
1390#ifdef SHOW_LICENSE
1391                      "license", LICENSE,
1392                      "wrap-license", TRUE,
1393#endif
1394                      NULL );
1395    gtk_window_set_transient_for( GTK_WINDOW( d ), parent );
1396    g_signal_connect_swapped( d, "response", G_CALLBACK (gtk_widget_destroy), d );
1397    gtk_widget_show( d );
1398}
1399
1400static void
1401append_id_to_benc_list( GtkTreeModel * m, GtkTreePath * path UNUSED,
1402                        GtkTreeIter * iter, gpointer list )
1403{
1404    tr_torrent * tor = NULL;
1405    gtk_tree_model_get( m, iter, MC_TORRENT, &tor, -1 );
1406    tr_bencListAddInt( list, tr_torrentId( tor ) );
1407}
1408
1409static gboolean
1410call_rpc_for_selected_torrents( struct cbdata * data, const char * method )
1411{
1412    tr_benc top, *args, *ids;
1413    gboolean invoked = FALSE;
1414    GtkTreeSelection * s = data->sel;
1415    tr_session * session = gtr_core_session( data->core );
1416
1417    tr_bencInitDict( &top, 2 );
1418    tr_bencDictAddStr( &top, "method", method );
1419    args = tr_bencDictAddDict( &top, "arguments", 1 );
1420    ids = tr_bencDictAddList( args, "ids", 0 );
1421    gtk_tree_selection_selected_foreach( s, append_id_to_benc_list, ids );
1422
1423    if( tr_bencListSize( ids ) != 0 )
1424    {
1425        int json_len;
1426        char * json = tr_bencToStr( &top, TR_FMT_JSON_LEAN, &json_len );
1427        tr_rpc_request_exec_json( session, json, json_len, NULL, NULL );
1428        g_free( json );
1429        invoked = TRUE;
1430    }
1431
1432    tr_bencFree( &top );
1433    return invoked;
1434}
1435
1436static void
1437open_folder_foreach( GtkTreeModel * model, GtkTreePath * path UNUSED,
1438                     GtkTreeIter * iter, gpointer core )
1439{
1440    int id;
1441    gtk_tree_model_get( model, iter, MC_TORRENT_ID, &id, -1 );
1442    gtr_core_open_folder( core, id );
1443}
1444
1445static gboolean
1446on_message_window_closed( void )
1447{
1448    gtr_action_set_toggled( "toggle-message-log", FALSE );
1449    return FALSE;
1450}
1451
1452static void
1453accumulate_selected_torrents( GtkTreeModel  * model, GtkTreePath   * path UNUSED,
1454                              GtkTreeIter   * iter, gpointer        gdata )
1455{
1456    int id;
1457    GSList ** data = gdata;
1458
1459    gtk_tree_model_get( model, iter, MC_TORRENT_ID, &id, -1 );
1460    *data = g_slist_append( *data, GINT_TO_POINTER( id ) );
1461}
1462
1463static void
1464remove_selected( struct cbdata * data, gboolean delete_files )
1465{
1466    GSList * l = NULL;
1467
1468    gtk_tree_selection_selected_foreach( data->sel, accumulate_selected_torrents, &l );
1469
1470    if( l != NULL )
1471        gtr_confirm_remove( data->wind, data->core, l, delete_files );
1472}
1473
1474static void
1475start_all_torrents( struct cbdata * data )
1476{
1477    tr_session * session = gtr_core_session( data->core );
1478    const char * cmd = "{ \"method\": \"torrent-start\" }";
1479    tr_rpc_request_exec_json( session, cmd, strlen( cmd ), NULL, NULL );
1480}
1481
1482static void
1483pause_all_torrents( struct cbdata * data )
1484{
1485    tr_session * session = gtr_core_session( data->core );
1486    const char * cmd = "{ \"method\": \"torrent-stop\" }";
1487    tr_rpc_request_exec_json( session, cmd, strlen( cmd ), NULL, NULL );
1488}
1489
1490static tr_torrent*
1491get_first_selected_torrent( struct cbdata * data )
1492{
1493    tr_torrent * tor = NULL;
1494    GtkTreeModel * m;
1495    GList * l = gtk_tree_selection_get_selected_rows( data->sel, &m );
1496    if( l != NULL ) {
1497        GtkTreePath * p = l->data;
1498        GtkTreeIter i;
1499        if( gtk_tree_model_get_iter( m, &i, p ) )
1500            gtk_tree_model_get( m, &i, MC_TORRENT, &tor, -1 );
1501    }
1502    g_list_foreach( l, (GFunc)gtk_tree_path_free, NULL );
1503    g_list_free( l );
1504    return tor;
1505}
1506
1507static void
1508copy_magnet_link_to_clipboard( GtkWidget * w, tr_torrent * tor )
1509{
1510    char * magnet = tr_torrentGetMagnetLink( tor );
1511    GdkDisplay * display = gtk_widget_get_display( w );
1512    GdkAtom selection;
1513    GtkClipboard * clipboard;
1514
1515    /* this is The Right Thing for copy/paste... */
1516    selection = GDK_SELECTION_CLIPBOARD;
1517    clipboard = gtk_clipboard_get_for_display( display, selection );
1518    gtk_clipboard_set_text( clipboard, magnet, -1 );
1519
1520    /* ...but people using plain ol' X need this instead */
1521    selection = GDK_SELECTION_PRIMARY;
1522    clipboard = gtk_clipboard_get_for_display( display, selection );
1523    gtk_clipboard_set_text( clipboard, magnet, -1 );
1524
1525    /* cleanup */
1526    tr_free( magnet );
1527}
1528
1529void
1530gtr_actions_handler( const char * action_name, gpointer user_data )
1531{
1532    struct cbdata * data = user_data;
1533    gboolean        changed = FALSE;
1534
1535    if( !strcmp( action_name, "open-torrent-from-url" ) )
1536    {
1537        GtkWidget * w = gtr_torrent_open_from_url_dialog_new( data->wind, data->core );
1538        gtk_widget_show( w );
1539    }
1540    else if(  !strcmp( action_name, "open-torrent-menu" )
1541      || !strcmp( action_name, "open-torrent-toolbar" ) )
1542    {
1543        GtkWidget * w = gtr_torrent_open_from_file_dialog_new( data->wind, data->core );
1544        gtk_widget_show( w );
1545    }
1546    else if( !strcmp( action_name, "show-stats" ) )
1547    {
1548        GtkWidget * dialog = gtr_stats_dialog_new( data->wind, data->core );
1549        gtk_widget_show( dialog );
1550    }
1551    else if( !strcmp( action_name, "donate" ) )
1552    {
1553        gtr_open_uri( "http://www.transmissionbt.com/donate.php" );
1554    }
1555    else if( !strcmp( action_name, "pause-all-torrents" ) )
1556    {
1557        pause_all_torrents( data );
1558    }
1559    else if( !strcmp( action_name, "start-all-torrents" ) )
1560    {
1561        start_all_torrents( data );
1562    }
1563    else if( !strcmp( action_name, "copy-magnet-link-to-clipboard" ) )
1564    {
1565        tr_torrent * tor = get_first_selected_torrent( data );
1566        if( tor != NULL )
1567        {
1568            copy_magnet_link_to_clipboard( GTK_WIDGET( data->wind ), tor );
1569        }
1570    }
1571    else if( !strcmp( action_name, "relocate-torrent" ) )
1572    {
1573        GSList * ids = getSelectedTorrentIds( data );
1574        if( ids != NULL )
1575        {
1576            GtkWindow * parent = data->wind;
1577            GtkWidget * w = gtr_relocate_dialog_new( parent, data->core, ids );
1578            gtk_widget_show( w );
1579        }
1580    }
1581    else if( !strcmp( action_name, "torrent-start" )
1582          || !strcmp( action_name, "torrent-start-now" )
1583          || !strcmp( action_name, "torrent-stop" )
1584          || !strcmp( action_name, "torrent-reannounce" )
1585          || !strcmp( action_name, "torrent-verify" )
1586          || !strcmp( action_name, "queue-move-top" )
1587          || !strcmp( action_name, "queue-move-up" )
1588          || !strcmp( action_name, "queue-move-down" )
1589          || !strcmp( action_name, "queue-move-bottom" ) )
1590    {
1591        changed |= call_rpc_for_selected_torrents( data, action_name );
1592    }
1593    else if( !strcmp( action_name, "open-torrent-folder" ) )
1594    {
1595        gtk_tree_selection_selected_foreach( data->sel, open_folder_foreach, data->core );
1596    }
1597    else if( !strcmp( action_name, "show-torrent-properties" ) )
1598    {
1599        show_details_dialog_for_selected_torrents( data );
1600    }
1601    else if( !strcmp( action_name, "new-torrent" ) )
1602    {
1603        GtkWidget * w = gtr_torrent_creation_dialog_new( data->wind, data->core );
1604        gtk_widget_show( w );
1605    }
1606    else if( !strcmp( action_name, "remove-torrent" ) )
1607    {
1608        remove_selected( data, FALSE );
1609    }
1610    else if( !strcmp( action_name, "delete-torrent" ) )
1611    {
1612        remove_selected( data, TRUE );
1613    }
1614    else if( !strcmp( action_name, "quit" ) )
1615    {
1616        on_app_exit( data );
1617    }
1618    else if( !strcmp( action_name, "select-all" ) )
1619    {
1620        gtk_tree_selection_select_all( data->sel );
1621    }
1622    else if( !strcmp( action_name, "deselect-all" ) )
1623    {
1624        gtk_tree_selection_unselect_all( data->sel );
1625    }
1626    else if( !strcmp( action_name, "edit-preferences" ) )
1627    {
1628        if( NULL == data->prefs )
1629        {
1630            data->prefs = gtr_prefs_dialog_new( data->wind, G_OBJECT( data->core ) );
1631            g_signal_connect( data->prefs, "destroy",
1632                              G_CALLBACK( gtk_widget_destroyed ), &data->prefs );
1633        }
1634        gtr_window_present( GTK_WINDOW( data->prefs ) );
1635    }
1636    else if( !strcmp( action_name, "toggle-message-log" ) )
1637    {
1638        if( !data->msgwin )
1639        {
1640            GtkWidget * win = gtr_message_log_window_new( data->wind, data->core );
1641            g_signal_connect( win, "destroy", G_CALLBACK( on_message_window_closed ), NULL );
1642            data->msgwin = win;
1643        }
1644        else
1645        {
1646            gtr_action_set_toggled( "toggle-message-log", FALSE );
1647            gtk_widget_destroy( data->msgwin );
1648            data->msgwin = NULL;
1649        }
1650    }
1651    else if( !strcmp( action_name, "show-about-dialog" ) )
1652    {
1653        show_about_dialog( data->wind );
1654    }
1655    else if( !strcmp ( action_name, "help" ) )
1656    {
1657        gtr_open_uri( gtr_get_help_uri( ) );
1658    }
1659    else if( !strcmp( action_name, "toggle-main-window" ) )
1660    {
1661        toggleMainWindow( data );
1662    }
1663    else if( !strcmp( action_name, "present-main-window" ) )
1664    {
1665        presentMainWindow( data );
1666    }
1667    else g_error ( "Unhandled action: %s", action_name );
1668
1669    if( changed )
1670        update_model_soon( data );
1671}
Note: See TracBrowser for help on using the repository browser.