source: trunk/gtk/main.c @ 12671

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

(trunk gtk) simplify the details dialog manager

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