source: trunk/gtk/main.c @ 12669

Last change on this file since 12669 was 12669, checked in by jordan, 11 years ago

(trunk gtk) simplify the implementation of get_details_dialog_key()

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