source: trunk/gtk/main.c @ 10638

Last change on this file since 10638 was 10638, checked in by charles, 12 years ago

(trunk) #1796 "run script after torrent completion" -- implemented for libT, RPC, and the GTK+ and Qt clients

  • Property svn:keywords set to Date Rev Author Id
File size: 54.3 KB
Line 
1/******************************************************************************
2 * $Id: main.c 10638 2010-05-08 22:42:28Z charles $
3 *
4 * Copyright (c) 2005-2008 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 <sys/param.h>
27#include <signal.h>
28#include <string.h>
29#include <stdio.h>
30#include <stdlib.h>
31#include <time.h>
32#include <unistd.h>
33
34#include <gtk/gtk.h>
35#include <glib/gi18n.h>
36#include <glib/gstdio.h>
37
38#include <libtransmission/transmission.h>
39#include <libtransmission/rpcimpl.h>
40#include <libtransmission/utils.h>
41#include <libtransmission/version.h>
42
43#include "actions.h"
44#include "add-dialog.h"
45#include "conf.h"
46#include "details.h"
47#include "dialogs.h"
48#include "hig.h"
49#include "makemeta-ui.h"
50#include "msgwin.h"
51#include "notify.h"
52#include "relocate.h"
53#include "stats.h"
54#include "tr-core.h"
55#include "tr-icon.h"
56#include "tr-prefs.h"
57#include "tr-torrent.h"
58#include "tr-window.h"
59#include "util.h"
60#include "ui.h"
61
62#define MY_NAME "transmission"
63
64#if GTK_CHECK_VERSION( 2, 8, 0 )
65 #define SHOW_LICENSE
66static const char * LICENSE =
67"The OS X client, CLI client, and parts of libtransmission are licensed under the terms of the MIT license.\n\n"
68"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"
69"1. The MIT-licensed portions of Transmission listed above are exempt from GPLv2 clause 2(b) and may retain their MIT license.\n\n"
70"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.";
71#endif
72
73struct cbdata
74{
75    gboolean            isIconified;
76    gboolean            isClosing;
77    guint               timer;
78    gpointer            icon;
79    GtkWindow         * wind;
80    TrCore            * core;
81    GtkWidget         * msgwin;
82    GtkWidget         * prefs;
83    GSList            * errqueue;
84    GSList            * dupqueue;
85    GSList            * details;
86    GtkTreeSelection  * sel;
87};
88
89/**
90***
91**/
92
93static int
94compareInts( const void * a, const void * b )
95{
96    return *(int*)a - *(int*)b;
97}
98
99static char*
100getDetailsDialogKey( GSList * id_list )
101{
102    int i;
103    int n;
104    int * ids;
105    GSList * l;
106    GString * gstr = g_string_new( NULL );
107
108    n = g_slist_length( id_list );
109    ids = g_new( int, n );
110    i = 0;
111    for( l=id_list; l!=NULL; l=l->next )
112        ids[i++] = GPOINTER_TO_INT( l->data );
113    g_assert( i == n );
114    qsort( ids, n, sizeof(int), compareInts );
115
116    for( i=0; i<n; ++i )
117        g_string_append_printf( gstr, "%d ", ids[i] );
118
119    g_free( ids );
120    return g_string_free( gstr, FALSE );
121}
122
123struct DetailsDialogHandle
124{
125    char * key;
126    GtkWidget * dialog;
127};
128
129static GSList*
130getSelectedTorrentIds( struct cbdata * data )
131{
132    GtkTreeSelection * s;
133    GtkTreeModel * model;
134    GSList * ids = NULL;
135    GList * paths = NULL;
136    GList * l;
137
138    /* build a list of the selected torrents' ids */
139    s = tr_window_get_selection( data->wind );
140    for( paths=l=gtk_tree_selection_get_selected_rows(s,&model); l; l=l->next ) {
141        GtkTreeIter iter;
142        if( gtk_tree_model_get_iter( model, &iter, l->data ) ) {
143            tr_torrent * tor;
144            gtk_tree_model_get( model, &iter, MC_TORRENT_RAW, &tor, -1 );
145            ids = g_slist_append( ids, GINT_TO_POINTER( tr_torrentId( tor ) ) );
146        }
147    }
148
149    /* cleanup */
150    g_list_foreach( paths, (GFunc)gtk_tree_path_free, NULL );
151    g_list_free( paths );
152    return ids;
153}
154
155static struct DetailsDialogHandle*
156findDetailsDialogFromIds( struct cbdata * cbdata, GSList * ids )
157{
158    GSList * l;
159    struct DetailsDialogHandle * ret = NULL;
160    char * key = getDetailsDialogKey( ids );
161
162    for( l=cbdata->details; l!=NULL && ret==NULL; l=l->next ) {
163        struct DetailsDialogHandle * h = l->data;
164        if( !strcmp( h->key, key ) )
165            ret = h;
166    }
167
168    g_free( key );
169    return ret;
170}
171
172static struct DetailsDialogHandle*
173findDetailsDialogFromWidget( struct cbdata * cbdata, gpointer w )
174{
175    GSList * l;
176    struct DetailsDialogHandle * ret = NULL;
177
178    for( l=cbdata->details; l!=NULL && ret==NULL; l=l->next ) {
179        struct DetailsDialogHandle * h = l->data;
180        if( h->dialog == w )
181            ret = h;
182    }
183
184    return ret;
185}
186
187/***
188****
189***/
190
191static void           appsetup( TrWindow * wind,
192                                GSList *   args,
193                                struct     cbdata *,
194                                gboolean   paused,
195                                gboolean   minimized );
196
197static void           winsetup( struct cbdata * cbdata,
198                                TrWindow *      wind );
199
200static void           wannaquit( gpointer vdata );
201
202static void           setupdrag( GtkWidget *    widget,
203                                 struct cbdata *data );
204
205static void           gotdrag( GtkWidget *       widget,
206                               GdkDragContext *  dc,
207                               gint              x,
208                               gint              y,
209                               GtkSelectionData *sel,
210                               guint             info,
211                               guint             time,
212                               gpointer          gdata );
213
214static void coreerr( TrCore *, guint, const char *, struct cbdata * );
215
216static void           onAddTorrent( TrCore *,
217                                    tr_ctor *,
218                                    gpointer );
219
220static void           prefschanged( TrCore *     core,
221                                    const char * key,
222                                    gpointer     data );
223
224static gboolean       updatemodel( gpointer gdata );
225
226/***
227****
228***/
229
230#ifdef HAVE_GCONF2
231 #include <gconf/gconf.h>
232 #include <gconf/gconf-client.h>
233#endif
234
235static void
236registerMagnetLinkHandler( void )
237{
238#ifdef HAVE_GCONF2
239    GError * err;
240    GConfValue * value;
241    GConfClient * client = gconf_client_get_default( );
242    const char * key = "/desktop/gnome/url-handlers/magnet/command";
243
244    /* if there's already a manget handler registered, don't do anything */
245    value = gconf_client_get( client, key, NULL );
246    if( value != NULL )
247    {
248        gconf_value_free( value );
249        return;
250    }
251
252    err = NULL;
253    if( !gconf_client_set_string( client, key, "transmission '%s'", &err ) )
254    {
255        tr_inf( "Unable to register Transmission as default magnet link handler: \"%s\"", err->message );
256        g_clear_error( &err );
257    }
258    else
259    {
260        gconf_client_set_bool( client, "/desktop/gnome/url-handlers/magnet/needs_terminal", FALSE, NULL );
261        gconf_client_set_bool( client, "/desktop/gnome/url-handlers/magnet/enabled", TRUE, NULL );
262        tr_inf( "Transmission registered as default magnet link handler" );
263    }
264#endif
265}
266
267/***
268****
269***/
270
271struct counts_data
272{
273    int    totalCount;
274    int    activeCount;
275    int    inactiveCount;
276};
277
278static void
279accumulateStatusForeach( GtkTreeModel *      model,
280                         GtkTreePath  * path UNUSED,
281                         GtkTreeIter *       iter,
282                         gpointer            user_data )
283{
284    int                  activity = 0;
285    struct counts_data * counts = user_data;
286
287    ++counts->totalCount;
288
289    gtk_tree_model_get( model, iter, MC_ACTIVITY, &activity, -1 );
290
291    if( activity == TR_STATUS_STOPPED )
292        ++counts->inactiveCount;
293    else
294        ++counts->activeCount;
295}
296
297static void
298accumulateCanUpdateForeach( GtkTreeModel *      model,
299                            GtkTreePath  * path UNUSED,
300                            GtkTreeIter *       iter,
301                            gpointer            accumulated_status )
302{
303    tr_torrent * tor;
304    gtk_tree_model_get( model, iter, MC_TORRENT_RAW, &tor, -1 );
305    *(int*)accumulated_status |= tr_torrentCanManualUpdate( tor );
306}
307
308static void
309refreshActions( struct cbdata * data )
310{
311    int canUpdate;
312    struct counts_data counts;
313    GtkTreeSelection * s = data->sel;
314
315    counts.activeCount = 0;
316    counts.inactiveCount = 0;
317    counts.totalCount = 0;
318    gtk_tree_selection_selected_foreach( s, accumulateStatusForeach, &counts );
319    action_sensitize( "pause-torrent", counts.activeCount != 0 );
320    action_sensitize( "start-torrent", counts.inactiveCount != 0 );
321    action_sensitize( "remove-torrent", counts.totalCount != 0 );
322    action_sensitize( "delete-torrent", counts.totalCount != 0 );
323    action_sensitize( "verify-torrent", counts.totalCount != 0 );
324    action_sensitize( "relocate-torrent", counts.totalCount != 0 );
325    action_sensitize( "show-torrent-properties", counts.totalCount != 0 );
326    action_sensitize( "open-torrent-folder", counts.totalCount == 1 );
327    action_sensitize( "copy-magnet-link-to-clipboard", counts.totalCount == 1 );
328
329    canUpdate = 0;
330    gtk_tree_selection_selected_foreach( s, accumulateCanUpdateForeach, &canUpdate );
331    action_sensitize( "update-tracker", canUpdate != 0 );
332
333    {
334        GtkTreeView *  view = gtk_tree_selection_get_tree_view( s );
335        GtkTreeModel * model = gtk_tree_view_get_model( view );
336        const int torrentCount = gtk_tree_model_iter_n_children( model, NULL ) != 0;
337        action_sensitize( "select-all", torrentCount != 0 );
338        action_sensitize( "deselect-all", torrentCount != 0 );
339    }
340
341    {
342        const int total = tr_core_get_torrent_count( data->core );
343        const int active = tr_core_get_active_torrent_count( data->core );
344        action_sensitize( "pause-all-torrents", active != 0 );
345        action_sensitize( "start-all-torrents", active != total );
346    }
347}
348
349static void
350selectionChangedCB( GtkTreeSelection * s UNUSED, gpointer data )
351{
352    refreshActions( data );
353}
354
355static void
356onMainWindowSizeAllocated( GtkWidget *            window,
357                           GtkAllocation  * alloc UNUSED,
358                           gpointer         gdata UNUSED )
359{
360    const gboolean isMaximized = window->window
361                            && ( gdk_window_get_state( window->window )
362                                 & GDK_WINDOW_STATE_MAXIMIZED );
363
364    pref_int_set( PREF_KEY_MAIN_WINDOW_IS_MAXIMIZED, isMaximized );
365
366    if( !isMaximized )
367    {
368        int x, y, w, h;
369        gtk_window_get_position( GTK_WINDOW( window ), &x, &y );
370        gtk_window_get_size( GTK_WINDOW( window ), &w, &h );
371        pref_int_set( PREF_KEY_MAIN_WINDOW_X, x );
372        pref_int_set( PREF_KEY_MAIN_WINDOW_Y, y );
373        pref_int_set( PREF_KEY_MAIN_WINDOW_WIDTH, w );
374        pref_int_set( PREF_KEY_MAIN_WINDOW_HEIGHT, h );
375    }
376}
377
378static sig_atomic_t global_sigcount = 0;
379static struct cbdata * sighandler_cbdata = NULL;
380
381static void
382signal_handler( int sig )
383{
384    if( ++global_sigcount > 1 )
385    {
386        signal( sig, SIG_DFL );
387        raise( sig );
388    }
389    else switch( sig )
390    {
391        case SIGINT:
392        case SIGTERM:
393            g_message( _( "Got signal %d; trying to shut down cleanly.  Do it again if it gets stuck." ), sig );
394            doAction( "quit", sighandler_cbdata );
395            break;
396
397        default:
398            g_message( "unhandled signal" );
399            break;
400    }
401}
402
403struct remove_torrent_idle_data
404{
405    TrCore * core;
406    int id;
407};
408
409static gboolean
410remove_torrent_idle( gpointer gdata )
411{
412    struct remove_torrent_idle_data * data = gdata;
413    tr_core_remove_torrent_from_id( data->core, data->id, FALSE );
414    g_free( data );
415    return FALSE; /* tell g_idle not to call this func twice */
416}
417
418static void
419setupsighandlers( void )
420{
421    signal( SIGINT, signal_handler );
422    signal( SIGKILL, signal_handler );
423}
424
425static tr_rpc_callback_status
426onRPCChanged( tr_session            * session,
427              tr_rpc_callback_type    type,
428              struct tr_torrent     * tor,
429              void                  * gdata )
430{
431    tr_rpc_callback_status status = TR_RPC_OK;
432    struct cbdata * cbdata = gdata;
433    gdk_threads_enter( );
434
435    switch( type )
436    {
437        case TR_RPC_TORRENT_ADDED:
438            tr_core_add_torrent( cbdata->core, tr_torrent_new_preexisting( tor ), TRUE );
439            break;
440
441        case TR_RPC_TORRENT_REMOVING: {
442            struct remove_torrent_idle_data * data = g_new0( struct remove_torrent_idle_data, 1 );
443            data->id = tr_torrentId( tor );
444            data->core = cbdata->core;
445            gtr_idle_add( remove_torrent_idle, data );
446            status = TR_RPC_NOREMOVE;
447            break;
448        }
449
450        case TR_RPC_SESSION_CHANGED:
451            tr_sessionGetSettings( session, pref_get_all( ) );
452            break;
453
454        case TR_RPC_TORRENT_CHANGED:
455        case TR_RPC_TORRENT_MOVED:
456        case TR_RPC_TORRENT_STARTED:
457        case TR_RPC_TORRENT_STOPPED:
458            /* nothing interesting to do here */
459            break;
460    }
461
462    gdk_threads_leave( );
463    return status;
464}
465
466static GSList *
467checkfilenames( int argc, char **argv )
468{
469    int i;
470    GSList * ret = NULL;
471    char * pwd = g_get_current_dir( );
472
473    for( i=0; i<argc; ++i )
474    {
475        if( gtr_is_supported_url( argv[i] ) || gtr_is_magnet_link( argv[i] ) )
476        {
477            ret = g_slist_prepend( ret, g_strdup( argv[i] ) );
478        }
479        else /* local file */
480        {
481            char * filename = g_path_is_absolute( argv[i] )
482                            ? g_strdup ( argv[i] )
483                            : g_build_filename( pwd, argv[i], NULL );
484
485            if( g_file_test( filename, G_FILE_TEST_EXISTS ) )
486                ret = g_slist_prepend( ret, filename );
487            else {
488                if( gtr_is_hex_hashcode( argv[i] ) )
489                    ret = g_slist_prepend( ret, g_strdup_printf( "magnet:?xt=urn:btih:%s", argv[i] ) );
490                g_free( filename );
491            }
492        }
493    }
494
495    g_free( pwd );
496    return g_slist_reverse( ret );
497}
498
499int
500main( int argc, char ** argv )
501{
502    char * err = NULL;
503    GSList * argfiles;
504    GError * gerr;
505    gboolean didinit = FALSE;
506    gboolean didlock = FALSE;
507    gboolean showversion = FALSE;
508    gboolean startpaused = FALSE;
509    gboolean startminimized = FALSE;
510    const char * domain = MY_NAME;
511    char * configDir = NULL;
512    gtr_lockfile_state_t tr_state;
513
514    GOptionEntry entries[] = {
515        { "paused",     'p', 0, G_OPTION_ARG_NONE,
516          &startpaused, _( "Start with all torrents paused" ), NULL },
517        { "version",    '\0', 0, G_OPTION_ARG_NONE,
518          &showversion, _( "Show version number and exit" ), NULL },
519#ifdef STATUS_ICON_SUPPORTED
520        { "minimized",  'm', 0, G_OPTION_ARG_NONE,
521          &startminimized,
522          _( "Start minimized in notification area" ), NULL },
523#endif
524        { "config-dir", 'g', 0, G_OPTION_ARG_FILENAME, &configDir,
525          _( "Where to look for configuration files" ), NULL },
526        { NULL, 0,   0, 0, NULL, NULL, NULL }
527    };
528
529    /* bind the gettext domain */
530    setlocale( LC_ALL, "" );
531    bindtextdomain( domain, TRANSMISSIONLOCALEDIR );
532    bind_textdomain_codeset( domain, "UTF-8" );
533    textdomain( domain );
534    g_set_application_name( _( "Transmission" ) );
535
536    /* initialize gtk */
537    if( !g_thread_supported( ) )
538        g_thread_init( NULL );
539
540    gerr = NULL;
541    if( !gtk_init_with_args( &argc, &argv, (char*)_( "[torrent files or urls]" ), entries,
542                             (char*)domain, &gerr ) )
543    {
544        fprintf( stderr, "%s\n", gerr->message );
545        g_clear_error( &gerr );
546        return 0;
547    }
548
549    if( showversion )
550    {
551        fprintf( stderr, "%s %s\n", g_get_application_name( ), LONG_VERSION_STRING );
552        return 0;
553    }
554
555    if( configDir == NULL )
556        configDir = (char*) tr_getDefaultConfigDir( MY_NAME );
557
558    tr_notify_init( );
559    didinit = cf_init( configDir, NULL ); /* must come before actions_init */
560
561    setupsighandlers( ); /* set up handlers for fatal signals */
562
563    didlock = cf_lock( &tr_state, &err );
564    argfiles = checkfilenames( argc - 1, argv + 1 );
565
566    if( !didlock && argfiles )
567    {
568        /* We have torrents to add but there's another copy of Transmsision
569         * running... chances are we've been invoked from a browser, etc.
570         * So send the files over to the "real" copy of Transmission, and
571         * if that goes well, then our work is done. */
572        GSList * l;
573        gboolean delegated = FALSE;
574
575        for( l = argfiles; l; l = l->next )
576            delegated |= gtr_dbus_add_torrent( l->data );
577
578        if( delegated ) {
579            g_slist_foreach( argfiles, (GFunc)g_free, NULL );
580            g_slist_free( argfiles );
581            argfiles = NULL;
582
583            if( err ) {
584                g_free( err );
585                err = NULL;
586            }
587        }
588    }
589    else if( ( !didlock ) && ( tr_state == GTR_LOCKFILE_ELOCK ) )
590    {
591        /* There's already another copy of Transmission running,
592         * so tell it to present its window to the user */
593        err = NULL;
594        if( !gtr_dbus_present_window( ) )
595            err = g_strdup( _( "Transmission is already running, but is not responding.  To start a new session, you must first close the existing Transmission process." ) );
596    }
597
598    if( didlock && ( didinit || cf_init( configDir, &err ) ) )
599    {
600        /* No other copy of Transmission running...
601         * so we're going to be the primary. */
602
603        const char * str;
604        GtkWindow * win;
605        GtkUIManager * myUIManager;
606        tr_session * session;
607        struct cbdata * cbdata = g_new0( struct cbdata, 1 );
608
609        sighandler_cbdata = cbdata;
610
611        /* ensure the directories are created */
612        if(( str = pref_string_get( TR_PREFS_KEY_DOWNLOAD_DIR )))
613            gtr_mkdir_with_parents( str, 0777 );
614        if(( str = pref_string_get( TR_PREFS_KEY_INCOMPLETE_DIR )))
615            gtr_mkdir_with_parents( str, 0777 );
616
617        /* initialize the libtransmission session */
618        session = tr_sessionInit( "gtk", configDir, TRUE, pref_get_all( ) );
619        pref_flag_set( TR_PREFS_KEY_ALT_SPEED_ENABLED, tr_sessionUsesAltSpeed( session ) );
620        pref_int_set( TR_PREFS_KEY_PEER_PORT, tr_sessionGetPeerPort( session ) );
621        cbdata->core = tr_core_new( session );
622
623        /* init the ui manager */
624        myUIManager = gtk_ui_manager_new ( );
625        actions_init ( myUIManager, cbdata );
626        gtk_ui_manager_add_ui_from_string ( myUIManager, fallback_ui_file, -1, NULL );
627        gtk_ui_manager_ensure_update ( myUIManager );
628        gtk_window_set_default_icon_name ( MY_NAME );
629
630        /* create main window now to be a parent to any error dialogs */
631        win = GTK_WINDOW( tr_window_new( myUIManager, cbdata->core ) );
632        g_signal_connect( win, "size-allocate", G_CALLBACK( onMainWindowSizeAllocated ), cbdata );
633
634        appsetup( win, argfiles, cbdata, startpaused, startminimized );
635        tr_sessionSetRPCCallback( session, onRPCChanged, cbdata );
636
637        /* on startup, check & see if it's time to update the blocklist */
638        if( pref_flag_get( PREF_KEY_BLOCKLIST_UPDATES_ENABLED )
639            && ( time( NULL ) - pref_int_get( "blocklist-date" ) > ( 60 * 60 * 24 * 7 ) ) )
640                tr_core_blocklist_update( cbdata->core );
641
642        /* if there's no magnet link handler registered, register us */
643        registerMagnetLinkHandler( );
644
645        gtk_main( );
646    }
647    else if( err )
648    {
649        const char * primary_text = _( "Transmission cannot be started." );
650        GtkWidget * w = gtk_message_dialog_new( NULL, 0, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, primary_text, NULL );
651        gtk_message_dialog_format_secondary_text( GTK_MESSAGE_DIALOG( w ), "%s", err );
652        g_signal_connect( w, "response", G_CALLBACK(gtk_main_quit), NULL );
653        gtk_widget_show( w );
654        g_free( err );
655        gtk_main( );
656    }
657
658    return 0;
659}
660
661static void
662appsetup( TrWindow *      wind,
663          GSList *        torrentFiles,
664          struct cbdata * cbdata,
665          gboolean        forcepause,
666          gboolean        isIconified )
667{
668    const pref_flag_t start =
669        forcepause ? PREF_FLAG_FALSE : PREF_FLAG_DEFAULT;
670    const pref_flag_t prompt = PREF_FLAG_DEFAULT;
671
672    /* fill out cbdata */
673    cbdata->wind         = NULL;
674    cbdata->icon         = NULL;
675    cbdata->msgwin       = NULL;
676    cbdata->prefs        = NULL;
677    cbdata->timer        = 0;
678    cbdata->isClosing    = 0;
679    cbdata->errqueue     = NULL;
680    cbdata->dupqueue     = NULL;
681    cbdata->isIconified  = isIconified;
682
683    if( isIconified )
684        pref_flag_set( PREF_KEY_SHOW_TRAY_ICON, TRUE );
685
686    actions_set_core( cbdata->core );
687
688    /* set up core handlers */
689    g_signal_connect( cbdata->core, "error", G_CALLBACK( coreerr ), cbdata );
690    g_signal_connect( cbdata->core, "add-torrent-prompt",
691                      G_CALLBACK( onAddTorrent ), cbdata );
692    g_signal_connect_swapped( cbdata->core, "quit",
693                              G_CALLBACK( wannaquit ), cbdata );
694    g_signal_connect( cbdata->core, "prefs-changed",
695                      G_CALLBACK( prefschanged ), cbdata );
696
697    /* add torrents from command-line and saved state */
698    tr_core_load( cbdata->core, forcepause );
699    tr_core_add_list( cbdata->core, torrentFiles, start, prompt, TRUE );
700    torrentFiles = NULL;
701    tr_core_torrents_added( cbdata->core );
702
703    /* set up main window */
704    winsetup( cbdata, wind );
705
706    /* set up the icon */
707    prefschanged( cbdata->core, PREF_KEY_SHOW_TRAY_ICON, cbdata );
708
709    /* start model update timer */
710    cbdata->timer = gtr_timeout_add_seconds( MAIN_WINDOW_REFRESH_INTERVAL_SECONDS, updatemodel, cbdata );
711    updatemodel( cbdata );
712
713    /* either show the window or iconify it */
714    if( !isIconified )
715        gtk_widget_show( GTK_WIDGET( wind ) );
716    else
717    {
718        gtk_window_set_skip_taskbar_hint( cbdata->wind,
719                                          cbdata->icon != NULL );
720        cbdata->isIconified = FALSE; // ensure that the next toggle iconifies
721        action_toggle( "toggle-main-window", FALSE );
722    }
723
724    if( !pref_flag_get( PREF_KEY_USER_HAS_GIVEN_INFORMED_CONSENT ) )
725    {
726        GtkWidget * w = gtk_message_dialog_new( GTK_WINDOW( wind ),
727                                                GTK_DIALOG_DESTROY_WITH_PARENT,
728                                                GTK_MESSAGE_INFO,
729                                                GTK_BUTTONS_NONE,
730                                                "%s",
731             _( "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." ) );
732        gtk_dialog_add_button( GTK_DIALOG( w ), GTK_STOCK_QUIT, GTK_RESPONSE_REJECT );
733        gtk_dialog_add_button( GTK_DIALOG( w ), _( "I _Accept" ), GTK_RESPONSE_ACCEPT );
734        gtk_dialog_set_default_response( GTK_DIALOG( w ), GTK_RESPONSE_ACCEPT );
735        switch( gtk_dialog_run( GTK_DIALOG( w ) ) ) {
736            case GTK_RESPONSE_ACCEPT:
737                /* only show it once */
738                pref_flag_set( PREF_KEY_USER_HAS_GIVEN_INFORMED_CONSENT, TRUE );
739                gtk_widget_destroy( w );
740                break;
741            default:
742                exit( 0 );
743        }
744    }
745}
746
747static void
748tr_window_present( GtkWindow * window )
749{
750#if GTK_CHECK_VERSION( 2, 8, 0 )
751    gtk_window_present_with_time( window, gtk_get_current_event_time( ) );
752#else
753    gtk_window_present( window );
754#endif
755}
756
757static void
758toggleMainWindow( struct cbdata * cbdata )
759{
760    GtkWindow * window = GTK_WINDOW( cbdata->wind );
761    const int   doShow = cbdata->isIconified;
762    static int  x = 0;
763    static int  y = 0;
764
765    if( doShow )
766    {
767        cbdata->isIconified = 0;
768        gtk_window_set_skip_taskbar_hint( window, FALSE );
769        gtk_window_move( window, x, y );
770        gtk_widget_show( GTK_WIDGET( window ) );
771        tr_window_present( window );
772    }
773    else
774    {
775        gtk_window_get_position( window, &x, &y );
776        gtk_window_set_skip_taskbar_hint( window, TRUE );
777        gtk_widget_hide( GTK_WIDGET( window ) );
778        cbdata->isIconified = 1;
779    }
780}
781
782static gboolean
783winclose( GtkWidget * w    UNUSED,
784          GdkEvent * event UNUSED,
785          gpointer         gdata )
786{
787    struct cbdata * cbdata = gdata;
788
789    if( cbdata->icon != NULL )
790        action_activate ( "toggle-main-window" );
791    else
792        askquit( cbdata->core, cbdata->wind, wannaquit, cbdata );
793
794    return TRUE; /* don't propagate event further */
795}
796
797static void
798rowChangedCB( GtkTreeModel  * model UNUSED,
799              GtkTreePath   * path,
800              GtkTreeIter   * iter  UNUSED,
801              gpointer        gdata )
802{
803    struct cbdata * data = gdata;
804    if( gtk_tree_selection_path_is_selected ( data->sel, path ) )
805        refreshActions( gdata );
806}
807
808static void
809winsetup( struct cbdata * cbdata,
810          TrWindow *      wind )
811{
812    GtkTreeModel *     model;
813    GtkTreeSelection * sel;
814
815    g_assert( NULL == cbdata->wind );
816    cbdata->wind = GTK_WINDOW( wind );
817    cbdata->sel = sel = GTK_TREE_SELECTION( tr_window_get_selection( cbdata->wind ) );
818
819    g_signal_connect( sel, "changed", G_CALLBACK( selectionChangedCB ), cbdata );
820    selectionChangedCB( sel, cbdata );
821    model = tr_core_model( cbdata->core );
822    g_signal_connect( model, "row-changed", G_CALLBACK( rowChangedCB ), cbdata );
823    g_signal_connect( wind, "delete-event", G_CALLBACK( winclose ), cbdata );
824    refreshActions( cbdata );
825
826    setupdrag( GTK_WIDGET( wind ), cbdata );
827}
828
829static gboolean
830onSessionClosed( gpointer gdata )
831{
832    struct cbdata * cbdata = gdata;
833
834    /* shutdown the gui */
835    while( cbdata->details != NULL ) {
836        struct DetailsDialogHandle * h = cbdata->details->data;
837        gtk_widget_destroy( h->dialog );
838    }
839
840    if( cbdata->prefs )
841        gtk_widget_destroy( GTK_WIDGET( cbdata->prefs ) );
842    if( cbdata->wind )
843        gtk_widget_destroy( GTK_WIDGET( cbdata->wind ) );
844    g_object_unref( cbdata->core );
845    if( cbdata->icon )
846        g_object_unref( cbdata->icon );
847    if( cbdata->errqueue ) {
848        g_slist_foreach( cbdata->errqueue, (GFunc)g_free, NULL );
849        g_slist_free( cbdata->errqueue );
850    }
851    if( cbdata->dupqueue ) {
852        g_slist_foreach( cbdata->dupqueue, (GFunc)g_free, NULL );
853        g_slist_free( cbdata->dupqueue );
854    }
855    g_free( cbdata );
856
857    gtk_main_quit( );
858
859    return FALSE;
860}
861
862static gpointer
863sessionCloseThreadFunc( gpointer gdata )
864{
865    /* since tr_sessionClose() is a blocking function,
866     * call it from another thread... when it's done,
867     * punt the GUI teardown back to the GTK+ thread */
868    struct cbdata * cbdata = gdata;
869    gdk_threads_enter( );
870    tr_core_close( cbdata->core );
871    gtr_idle_add( onSessionClosed, gdata );
872    gdk_threads_leave( );
873    return NULL;
874}
875
876static void
877do_exit_cb( GtkWidget *w  UNUSED,
878            gpointer data UNUSED )
879{
880    exit( 0 );
881}
882
883static void
884wannaquit( gpointer vdata )
885{
886    GtkWidget *r, *p, *b, *w, *c;
887    struct cbdata *cbdata = vdata;
888
889    /* stop the update timer */
890    if( cbdata->timer )
891    {
892        g_source_remove( cbdata->timer );
893        cbdata->timer = 0;
894    }
895
896    c = GTK_WIDGET( cbdata->wind );
897    gtk_container_remove( GTK_CONTAINER( c ), gtk_bin_get_child( GTK_BIN( c ) ) );
898
899    r = gtk_alignment_new( 0.5, 0.5, 0.01, 0.01 );
900    gtk_container_add( GTK_CONTAINER( c ), r );
901
902    p = gtk_table_new( 3, 2, FALSE );
903    gtk_table_set_col_spacings( GTK_TABLE( p ), GUI_PAD_BIG );
904    gtk_container_add( GTK_CONTAINER( r ), p );
905
906    w = gtk_image_new_from_stock( GTK_STOCK_NETWORK, GTK_ICON_SIZE_DIALOG );
907    gtk_table_attach_defaults( GTK_TABLE( p ), w, 0, 1, 0, 2 );
908
909    w = gtk_label_new( NULL );
910    gtk_label_set_markup( GTK_LABEL( w ), _( "<b>Closing Connections</b>" ) );
911    gtk_misc_set_alignment( GTK_MISC( w ), 0.0, 0.5 );
912    gtk_table_attach_defaults( GTK_TABLE( p ), w, 1, 2, 0, 1 );
913
914    w = gtk_label_new( _( "Sending upload/download totals to tracker..." ) );
915    gtk_misc_set_alignment( GTK_MISC( w ), 0.0, 0.5 );
916    gtk_table_attach_defaults( GTK_TABLE( p ), w, 1, 2, 1, 2 );
917
918    b = gtk_alignment_new( 0.0, 1.0, 0.01, 0.01 );
919    w = gtr_button_new_from_stock( GTK_STOCK_QUIT, _( "_Quit Now" ) );
920    g_signal_connect( w, "clicked", G_CALLBACK( do_exit_cb ), NULL );
921    gtk_container_add( GTK_CONTAINER( b ), w );
922    gtk_table_attach( GTK_TABLE( p ), b, 1, 2, 2, 3, GTK_FILL, GTK_FILL, 0, 10 );
923
924    gtk_widget_show_all( r );
925    gtk_widget_grab_focus( w );
926
927    /* clear the UI */
928    gtk_list_store_clear( GTK_LIST_STORE( tr_core_model( cbdata->core ) ) );
929
930    /* ensure the window is in its previous position & size.
931     * this seems to be necessary because changing the main window's
932     * child seems to unset the size */
933    gtk_window_resize( cbdata->wind, pref_int_get( PREF_KEY_MAIN_WINDOW_WIDTH ),
934                                     pref_int_get( PREF_KEY_MAIN_WINDOW_HEIGHT ) );
935    gtk_window_move( cbdata->wind, pref_int_get( PREF_KEY_MAIN_WINDOW_X ),
936                                   pref_int_get( PREF_KEY_MAIN_WINDOW_Y ) );
937
938    /* shut down libT */
939    g_thread_create( sessionCloseThreadFunc, vdata, TRUE, NULL );
940}
941
942static void
943gotdrag( GtkWidget         * widget UNUSED,
944         GdkDragContext *           dc,
945         gint                x      UNUSED,
946         gint                y      UNUSED,
947         GtkSelectionData *         sel,
948         guint               info   UNUSED,
949         guint                      time,
950         gpointer                   gdata )
951{
952    struct cbdata * data = gdata;
953    GSList *        paths = NULL;
954    GSList *        freeme = NULL;
955
956#if 0
957    int             i;
958    char *          sele = gdk_atom_name( sel->selection );
959    char *          targ = gdk_atom_name( sel->target );
960    char *          type = gdk_atom_name( sel->type );
961
962    g_message( "dropped file: sel=%s targ=%s type=%s fmt=%i len=%i",
963               sele, targ, type, sel->format, sel->length );
964    g_free( sele );
965    g_free( targ );
966    g_free( type );
967    if( sel->format == 8 )
968    {
969        for( i = 0; i < sel->length; ++i )
970            fprintf( stderr, "%02X ", sel->data[i] );
971        fprintf( stderr, "\n" );
972    }
973#endif
974
975    if( ( sel->format == 8 )
976      && ( sel->selection == gdk_atom_intern( "XdndSelection", FALSE ) ) )
977    {
978        int      i;
979        char *   str = g_strndup( (char*)sel->data, sel->length );
980        gchar ** files = g_strsplit_set( str, "\r\n", -1 );
981        for( i = 0; files && files[i]; ++i )
982        {
983            char * filename;
984            if( !*files[i] ) /* empty filename... */
985                continue;
986
987            /* decode the filename */
988            filename = decode_uri( files[i] );
989            freeme = g_slist_prepend( freeme, filename );
990            if( !g_utf8_validate( filename, -1, NULL ) )
991                continue;
992
993            /* walk past "file://", if present */
994            if( g_str_has_prefix( filename, "file:" ) ) {
995                filename += 5;
996                while( g_str_has_prefix( filename, "//" ) )
997                    ++filename;
998            }
999
1000            g_debug( "got from drag: [%s]", filename );
1001
1002            if( g_file_test( filename, G_FILE_TEST_EXISTS ) )
1003                paths = g_slist_prepend( paths, g_strdup( filename ) );
1004            else
1005                tr_core_add_from_url( data->core, filename );
1006        }
1007
1008        /* try to add any torrents we found */
1009        if( paths )
1010        {
1011            paths = g_slist_reverse( paths );
1012            tr_core_add_list_defaults( data->core, paths, TRUE );
1013            tr_core_torrents_added( data->core );
1014        }
1015
1016        freestrlist( freeme );
1017        g_strfreev( files );
1018        g_free( str );
1019    }
1020
1021    gtk_drag_finish( dc, ( NULL != paths ), FALSE, time );
1022}
1023
1024static void
1025setupdrag( GtkWidget *    widget,
1026           struct cbdata *data )
1027{
1028    GtkTargetEntry targets[] = {
1029        { (char*)"STRING",          0, 0 },
1030        { (char*)"text/plain",      0, 0 },
1031        { (char*)"text/uri-list",   0, 0 },
1032    };
1033
1034    g_signal_connect( widget, "drag_data_received", G_CALLBACK(
1035                          gotdrag ), data );
1036
1037    gtk_drag_dest_set( widget, GTK_DEST_DEFAULT_ALL, targets,
1038                       G_N_ELEMENTS( targets ), GDK_ACTION_COPY | GDK_ACTION_MOVE );
1039}
1040
1041static void
1042flushAddTorrentErrors( GtkWindow *  window,
1043                       const char * primary,
1044                       GSList **    files )
1045{
1046    GString *   s = g_string_new( NULL );
1047    GSList *    l;
1048    GtkWidget * w;
1049
1050    if( g_slist_length( *files ) > 1 ) {
1051        for( l=*files; l!=NULL; l=l->next )
1052            g_string_append_printf( s, "\xE2\x88\x99 %s\n", (const char*)l->data );
1053    } else {
1054        for( l=*files; l!=NULL; l=l->next )
1055            g_string_append_printf( s, "%s\n", (const char*)l->data );
1056    }
1057    w = gtk_message_dialog_new( window,
1058                                GTK_DIALOG_DESTROY_WITH_PARENT,
1059                                GTK_MESSAGE_ERROR,
1060                                GTK_BUTTONS_CLOSE,
1061                                "%s", primary );
1062    gtk_message_dialog_format_secondary_text( GTK_MESSAGE_DIALOG( w ),
1063                                              "%s", s->str );
1064    g_signal_connect_swapped( w, "response",
1065                              G_CALLBACK( gtk_widget_destroy ), w );
1066    gtk_widget_show_all( w );
1067    g_string_free( s, TRUE );
1068
1069    g_slist_foreach( *files, (GFunc)g_free, NULL );
1070    g_slist_free( *files );
1071    *files = NULL;
1072}
1073
1074static void
1075showTorrentErrors( struct cbdata * cbdata )
1076{
1077    if( cbdata->errqueue )
1078        flushAddTorrentErrors( GTK_WINDOW( cbdata->wind ),
1079                               ngettext( "Couldn't add corrupt torrent",
1080                                         "Couldn't add corrupt torrents",
1081                                         g_slist_length( cbdata->errqueue ) ),
1082                               &cbdata->errqueue );
1083
1084    if( cbdata->dupqueue )
1085        flushAddTorrentErrors( GTK_WINDOW( cbdata->wind ),
1086                               ngettext( "Couldn't add duplicate torrent",
1087                                         "Couldn't add duplicate torrents",
1088                                         g_slist_length( cbdata->dupqueue ) ),
1089                               &cbdata->dupqueue );
1090}
1091
1092static void
1093coreerr( TrCore * core UNUSED, guint code, const char * msg, struct cbdata * c )
1094{
1095    switch( code )
1096    {
1097        case TR_PARSE_ERR:
1098            c->errqueue =
1099                g_slist_append( c->errqueue, g_path_get_basename( msg ) );
1100            break;
1101
1102        case TR_PARSE_DUPLICATE:
1103            c->dupqueue = g_slist_append( c->dupqueue, g_strdup( msg ) );
1104            break;
1105
1106        case TR_CORE_ERR_NO_MORE_TORRENTS:
1107            showTorrentErrors( c );
1108            break;
1109
1110        default:
1111            g_assert_not_reached( );
1112            break;
1113    }
1114}
1115
1116#if GTK_CHECK_VERSION( 2, 8, 0 )
1117static void
1118on_main_window_focus_in( GtkWidget      * widget UNUSED,
1119                         GdkEventFocus  * event  UNUSED,
1120                         gpointer                gdata )
1121{
1122    struct cbdata * cbdata = gdata;
1123
1124    if( cbdata->wind )
1125        gtk_window_set_urgency_hint( GTK_WINDOW( cbdata->wind ), FALSE );
1126}
1127
1128#endif
1129
1130static void
1131onAddTorrent( TrCore *  core,
1132              tr_ctor * ctor,
1133              gpointer  gdata )
1134{
1135    struct cbdata * cbdata = gdata;
1136    GtkWidget *     w = addSingleTorrentDialog( cbdata->wind, core, ctor );
1137
1138#if GTK_CHECK_VERSION( 2, 8, 0 )
1139    g_signal_connect( w, "focus-in-event",
1140                      G_CALLBACK( on_main_window_focus_in ),  cbdata );
1141    if( cbdata->wind )
1142        gtk_window_set_urgency_hint( cbdata->wind, TRUE );
1143#endif
1144}
1145
1146static void
1147prefschanged( TrCore * core UNUSED, const char * key, gpointer data )
1148{
1149    struct cbdata * cbdata = data;
1150    tr_session * tr = tr_core_session( cbdata->core );
1151
1152    if( !strcmp( key, TR_PREFS_KEY_ENCRYPTION ) )
1153    {
1154        tr_sessionSetEncryption( tr, pref_int_get( key ) );
1155    }
1156    else if( !strcmp( key, TR_PREFS_KEY_DOWNLOAD_DIR ) )
1157    {
1158        tr_sessionSetDownloadDir( tr, pref_string_get( key ) );
1159    }
1160    else if( !strcmp( key, TR_PREFS_KEY_MSGLEVEL ) )
1161    {
1162        tr_setMessageLevel( pref_int_get( key ) );
1163    }
1164    else if( !strcmp( key, TR_PREFS_KEY_PEER_PORT ) )
1165    {
1166        tr_sessionSetPeerPort( tr, pref_int_get( key ) );
1167    }
1168    else if( !strcmp( key, TR_PREFS_KEY_BLOCKLIST_ENABLED ) )
1169    {
1170        tr_blocklistSetEnabled( tr, pref_flag_get( key ) );
1171    }
1172    else if( !strcmp( key, PREF_KEY_SHOW_TRAY_ICON ) )
1173    {
1174        const int show = pref_flag_get( key );
1175        if( show && !cbdata->icon )
1176            cbdata->icon = tr_icon_new( cbdata->core );
1177        else if( !show && cbdata->icon ) {
1178            g_object_unref( cbdata->icon );
1179            cbdata->icon = NULL;
1180        }
1181    }
1182    else if( !strcmp( key, TR_PREFS_KEY_DSPEED_ENABLED ) )
1183    {
1184        tr_sessionLimitSpeed( tr, TR_DOWN, pref_flag_get( key ) );
1185    }
1186    else if( !strcmp( key, TR_PREFS_KEY_DSPEED ) )
1187    {
1188        tr_sessionSetSpeedLimit( tr, TR_DOWN, pref_int_get( key ) );
1189    }
1190    else if( !strcmp( key, TR_PREFS_KEY_USPEED_ENABLED ) )
1191    {
1192        tr_sessionLimitSpeed( tr, TR_UP, pref_flag_get( key ) );
1193    }
1194    else if( !strcmp( key, TR_PREFS_KEY_USPEED ) )
1195    {
1196        tr_sessionSetSpeedLimit( tr, TR_UP, pref_int_get( key ) );
1197    }
1198    else if( !strcmp( key, TR_PREFS_KEY_RATIO_ENABLED ) )
1199    {
1200        tr_sessionSetRatioLimited( tr, pref_flag_get( key ) );
1201    }
1202    else if( !strcmp( key, TR_PREFS_KEY_RATIO ) )
1203    {
1204        tr_sessionSetRatioLimit( tr, pref_double_get( key ) );
1205    }
1206    else if( !strcmp( key, TR_PREFS_KEY_PORT_FORWARDING ) )
1207    {
1208        tr_sessionSetPortForwardingEnabled( tr, pref_flag_get( key ) );
1209    }
1210    else if( !strcmp( key, TR_PREFS_KEY_PEX_ENABLED ) )
1211    {
1212        tr_sessionSetPexEnabled( tr, pref_flag_get( key ) );
1213    }
1214    else if( !strcmp( key, TR_PREFS_KEY_RENAME_PARTIAL_FILES ) )
1215    {
1216        tr_sessionSetIncompleteFileNamingEnabled( tr, pref_flag_get( key ) );
1217    }
1218    else if( !strcmp( key, TR_PREFS_KEY_DHT_ENABLED ) )
1219    {
1220        tr_sessionSetDHTEnabled( tr, pref_flag_get( key ) );
1221    }
1222    else if( !strcmp( key, TR_PREFS_KEY_LPD_ENABLED ) )
1223    {
1224        tr_sessionSetLPDEnabled( tr, pref_flag_get( key ) );
1225    }
1226    else if( !strcmp( key, TR_PREFS_KEY_RPC_PORT ) )
1227    {
1228        tr_sessionSetRPCPort( tr, pref_int_get( key ) );
1229    }
1230    else if( !strcmp( key, TR_PREFS_KEY_RPC_ENABLED ) )
1231    {
1232        tr_sessionSetRPCEnabled( tr, pref_flag_get( key ) );
1233    }
1234    else if( !strcmp( key, TR_PREFS_KEY_RPC_WHITELIST ) )
1235    {
1236        tr_sessionSetRPCWhitelist( tr, pref_string_get( key ) );
1237    }
1238    else if( !strcmp( key, TR_PREFS_KEY_RPC_WHITELIST_ENABLED ) )
1239    {
1240        tr_sessionSetRPCWhitelistEnabled( tr, pref_flag_get( key ) );
1241    }
1242    else if( !strcmp( key, TR_PREFS_KEY_RPC_USERNAME ) )
1243    {
1244        tr_sessionSetRPCUsername( tr, pref_string_get( key ) );
1245    }
1246    else if( !strcmp( key, TR_PREFS_KEY_RPC_PASSWORD ) )
1247    {
1248        tr_sessionSetRPCPassword( tr, pref_string_get( key ) );
1249    }
1250    else if( !strcmp( key, TR_PREFS_KEY_RPC_AUTH_REQUIRED ) )
1251    {
1252        tr_sessionSetRPCPasswordEnabled( tr, pref_flag_get( key ) );
1253    }
1254    else if( !strcmp( key, TR_PREFS_KEY_PROXY ) )
1255    {
1256        tr_sessionSetProxy( tr, pref_string_get( key ) );
1257    }
1258    else if( !strcmp( key, TR_PREFS_KEY_PROXY_TYPE ) )
1259    {
1260        tr_sessionSetProxyType( tr, pref_int_get( key ) );
1261    }
1262    else if( !strcmp( key, TR_PREFS_KEY_PROXY_ENABLED ) )
1263    {
1264        tr_sessionSetProxyEnabled( tr, pref_flag_get( key ) );
1265    }
1266    else if( !strcmp( key, TR_PREFS_KEY_PROXY_AUTH_ENABLED ) )
1267    {
1268        tr_sessionSetProxyAuthEnabled( tr, pref_flag_get( key ) );
1269    }
1270    else if( !strcmp( key, TR_PREFS_KEY_PROXY_USERNAME ) )
1271    {
1272        tr_sessionSetProxyUsername( tr, pref_string_get( key ) );
1273    }
1274    else if( !strcmp( key, TR_PREFS_KEY_PROXY_PASSWORD ) )
1275    {
1276        tr_sessionSetProxyPassword( tr, pref_string_get( key ) );
1277    }
1278    else if( !strcmp( key, TR_PREFS_KEY_PROXY_PORT ) )
1279    {
1280        tr_sessionSetProxyPort( tr, pref_int_get( key ) );
1281    }
1282    else if( !strcmp( key, TR_PREFS_KEY_ALT_SPEED_UP ) )
1283    {
1284        tr_sessionSetAltSpeed( tr, TR_UP, pref_int_get( key ) );
1285    }
1286    else if( !strcmp( key, TR_PREFS_KEY_ALT_SPEED_DOWN ) )
1287    {
1288        tr_sessionSetAltSpeed( tr, TR_DOWN, pref_int_get( key ) );
1289    }
1290    else if( !strcmp( key, TR_PREFS_KEY_ALT_SPEED_ENABLED ) )
1291    {
1292        const gboolean b = pref_flag_get( key );
1293        tr_sessionUseAltSpeed( tr, b );
1294        action_toggle( key, b );
1295    }
1296    else if( !strcmp( key, TR_PREFS_KEY_ALT_SPEED_TIME_BEGIN ) )
1297    {
1298        tr_sessionSetAltSpeedBegin( tr, pref_int_get( key ) );
1299    }
1300    else if( !strcmp( key, TR_PREFS_KEY_ALT_SPEED_TIME_END ) )
1301    {
1302        tr_sessionSetAltSpeedEnd( tr, pref_int_get( key ) );
1303    }
1304    else if( !strcmp( key, TR_PREFS_KEY_ALT_SPEED_TIME_ENABLED ) )
1305    {
1306        tr_sessionUseAltSpeedTime( tr, pref_flag_get( key ) );
1307    }
1308    else if( !strcmp( key, TR_PREFS_KEY_ALT_SPEED_TIME_DAY ) )
1309    {
1310        tr_sessionSetAltSpeedDay( tr, pref_int_get( key ) );
1311    }
1312    else if( !strcmp( key, TR_PREFS_KEY_PEER_PORT_RANDOM_ON_START ) )
1313    {
1314        tr_sessionSetPeerPortRandomOnStart( tr, pref_flag_get( key ) );
1315    }
1316    else if( !strcmp( key, TR_PREFS_KEY_INCOMPLETE_DIR ) )
1317    {
1318        tr_sessionSetIncompleteDir( tr, pref_string_get( key ) );
1319    }
1320    else if( !strcmp( key, TR_PREFS_KEY_INCOMPLETE_DIR_ENABLED ) )
1321    {
1322        tr_sessionSetIncompleteDirEnabled( tr, pref_flag_get( key ) );
1323    }
1324    else if( !strcmp( key, TR_PREFS_KEY_SCRIPT_TORRENT_DONE_ENABLED ) )
1325    {
1326        tr_sessionSetTorrentDoneScriptEnabled( tr, pref_flag_get( key ) );
1327    }
1328    else if( !strcmp( key, TR_PREFS_KEY_SCRIPT_TORRENT_DONE_FILENAME ) )
1329    {
1330        tr_sessionSetTorrentDoneScript( tr, pref_string_get( key ) );
1331    }
1332    else if( !strcmp( key, TR_PREFS_KEY_START) )
1333    {
1334        tr_sessionSetPaused( tr, !pref_flag_get( key ) );
1335    }
1336    else if( !strcmp( key, TR_PREFS_KEY_TRASH_ORIGINAL ) )
1337    {
1338        tr_sessionSetDeleteSource( tr, pref_flag_get( key ) );
1339    }
1340}
1341
1342static gboolean
1343updatemodel( gpointer gdata )
1344{
1345    struct cbdata *data = gdata;
1346    const gboolean done = data->isClosing || global_sigcount;
1347
1348    if( !done )
1349    {
1350        /* update the torrent data in the model */
1351        tr_core_update( data->core );
1352
1353        /* update the main window's statusbar and toolbar buttons */
1354        if( data->wind != NULL )
1355            tr_window_update( data->wind );
1356
1357        /* update the actions */
1358        refreshActions( data );
1359
1360        /* update the status tray icon */
1361        if( data->icon != NULL )
1362            tr_icon_refresh( data->icon );
1363    }
1364
1365    return !done;
1366}
1367
1368static void
1369aboutDialogActivateLink( GtkAboutDialog * dialog    UNUSED,
1370                         const gchar *              link_,
1371                         gpointer         user_data UNUSED )
1372{
1373    gtr_open_file( link_ );
1374}
1375
1376static void
1377about( GtkWindow * parent )
1378{
1379    const char *authors[] =
1380    {
1381        "Charles Kerr (Backend; GTK+)",
1382        "Mitchell Livingston (Backend; OS X)",
1383        "Kevin Glowacz (Web client)",
1384        NULL
1385    };
1386
1387    const char *website_url = "http://www.transmissionbt.com/";
1388
1389    gtk_about_dialog_set_url_hook( aboutDialogActivateLink, NULL, NULL );
1390
1391    gtk_show_about_dialog( parent,
1392                           "name", g_get_application_name( ),
1393                           "comments",
1394                           _( "A fast and easy BitTorrent client" ),
1395                           "version", LONG_VERSION_STRING,
1396                           "website", website_url,
1397                           "website-label", website_url,
1398                           "copyright",
1399                           _( "Copyright 2005-2009 The Transmission Project" ),
1400                           "logo-icon-name", MY_NAME,
1401#ifdef SHOW_LICENSE
1402                           "license", LICENSE,
1403                           "wrap-license", TRUE,
1404#endif
1405                           "authors", authors,
1406                           /* Translators: translate "translator-credits" as
1407                              your name
1408                              to have it appear in the credits in the "About"
1409                              dialog */
1410                           "translator-credits", _( "translator-credits" ),
1411                           NULL );
1412}
1413
1414static void
1415appendIdToBencList( GtkTreeModel * m, GtkTreePath * path UNUSED,
1416                    GtkTreeIter * iter, gpointer list )
1417{
1418    tr_torrent * tor = NULL;
1419    gtk_tree_model_get( m, iter, MC_TORRENT_RAW, &tor, -1 );
1420    tr_bencListAddInt( list, tr_torrentId( tor ) );
1421}
1422
1423static gboolean
1424rpcOnSelectedTorrents( struct cbdata * data, const char * method )
1425{
1426    tr_benc top, *args, *ids;
1427    gboolean invoked = FALSE;
1428    tr_session * session = tr_core_session( data->core );
1429    GtkTreeSelection * s = tr_window_get_selection( data->wind );
1430
1431    tr_bencInitDict( &top, 2 );
1432    tr_bencDictAddStr( &top, "method", method );
1433    args = tr_bencDictAddDict( &top, "arguments", 1 );
1434    ids = tr_bencDictAddList( args, "ids", 0 );
1435    gtk_tree_selection_selected_foreach( s, appendIdToBencList, ids );
1436
1437    if( tr_bencListSize( ids ) != 0 )
1438    {
1439        int json_len;
1440        char * json = tr_bencToStr( &top, TR_FMT_JSON_LEAN, &json_len );
1441        tr_rpc_request_exec_json( session, json, json_len, NULL, NULL );
1442        g_free( json );
1443        invoked = TRUE;
1444    }
1445
1446    tr_bencFree( &top );
1447    return invoked;
1448}
1449
1450static void
1451openFolderForeach( GtkTreeModel *           model,
1452                   GtkTreePath  * path      UNUSED,
1453                   GtkTreeIter *            iter,
1454                   gpointer       user_data UNUSED )
1455{
1456    TrTorrent * gtor = NULL;
1457
1458    gtk_tree_model_get( model, iter, MC_TORRENT, &gtor, -1 );
1459    tr_torrent_open_folder( gtor );
1460    g_object_unref( G_OBJECT( gtor ) );
1461}
1462
1463static gboolean
1464msgwinclosed( void )
1465{
1466    action_toggle( "toggle-message-log", FALSE );
1467    return FALSE;
1468}
1469
1470static void
1471accumulateSelectedTorrents( GtkTreeModel * model,
1472                            GtkTreePath  * path UNUSED,
1473                            GtkTreeIter  * iter,
1474                            gpointer       gdata )
1475{
1476    GSList ** data = ( GSList** ) gdata;
1477    TrTorrent * gtor = NULL;
1478
1479    gtk_tree_model_get( model, iter, MC_TORRENT, &gtor, -1 );
1480    *data = g_slist_prepend( *data, gtor );
1481    g_object_unref( G_OBJECT( gtor ) );
1482}
1483
1484static void
1485removeSelected( struct cbdata * data, gboolean delete_files )
1486{
1487    GSList *           l = NULL;
1488    GtkTreeSelection * s = tr_window_get_selection( data->wind );
1489
1490    gtk_tree_selection_selected_foreach( s, accumulateSelectedTorrents, &l );
1491    gtk_tree_selection_unselect_all( s );
1492    if( l )
1493    {
1494        l = g_slist_reverse( l );
1495        confirmRemove( data->wind, data->core, l, delete_files );
1496    }
1497}
1498
1499static void
1500startAllTorrents( struct cbdata * data )
1501{
1502    tr_session * session = tr_core_session( data->core );
1503    const char * cmd = "{ \"method\": \"torrent-start\" }";
1504    tr_rpc_request_exec_json( session, cmd, strlen( cmd ), NULL, NULL );
1505}
1506
1507static void
1508pauseAllTorrents( struct cbdata * data )
1509{
1510    tr_session * session = tr_core_session( data->core );
1511    const char * cmd = "{ \"method\": \"torrent-stop\" }";
1512    tr_rpc_request_exec_json( session, cmd, strlen( cmd ), NULL, NULL );
1513}
1514
1515static tr_torrent*
1516getFirstSelectedTorrent( struct cbdata * data )
1517{
1518    tr_torrent * tor = NULL;
1519    GtkTreeSelection * s = tr_window_get_selection( data->wind );
1520    GtkTreeModel * m;
1521    GList * l = gtk_tree_selection_get_selected_rows( s, &m );
1522    if( l != NULL ) {
1523        GtkTreePath * p = l->data;
1524        GtkTreeIter i;
1525        if( gtk_tree_model_get_iter( m, &i, p ) )
1526            gtk_tree_model_get( m, &i, MC_TORRENT_RAW, &tor, -1 );
1527    }
1528    g_list_foreach( l, (GFunc)gtk_tree_path_free, NULL );
1529    g_list_free( l );
1530    return tor;
1531}
1532
1533static void
1534detailsClosed( gpointer gdata, GObject * dead )
1535{
1536    struct cbdata * data = gdata;
1537    struct DetailsDialogHandle * h = findDetailsDialogFromWidget( data, dead );
1538
1539    if( h != NULL )
1540    {
1541        data->details = g_slist_remove( data->details, h );
1542        g_free( h->key );
1543        g_free( h );
1544    }
1545}
1546
1547static void
1548copyMagnetLinkToClipboard( GtkWidget * w, tr_torrent * tor )
1549{
1550    char * magnet = tr_torrentGetMagnetLink( tor );
1551    GdkDisplay * display = gtk_widget_get_display( w );
1552    GdkAtom selection;
1553    GtkClipboard * clipboard;
1554
1555    /* this is The Right Thing for copy/paste... */
1556    selection = GDK_SELECTION_CLIPBOARD;
1557    clipboard = gtk_clipboard_get_for_display( display, selection );
1558    gtk_clipboard_set_text( clipboard, magnet, -1 );
1559
1560    /* ...but people using plain ol' X need this instead */
1561    selection = GDK_SELECTION_PRIMARY;
1562    clipboard = gtk_clipboard_get_for_display( display, selection );
1563    gtk_clipboard_set_text( clipboard, magnet, -1 );
1564
1565    /* cleanup */
1566    tr_free( magnet );
1567}
1568
1569void
1570doAction( const char * action_name, gpointer user_data )
1571{
1572    struct cbdata * data = user_data;
1573    gboolean        changed = FALSE;
1574
1575    if( !strcmp( action_name, "add-torrent-from-url" ) )
1576    {
1577        addURLDialog( data->wind, data->core );
1578    }
1579    else if(  !strcmp( action_name, "add-torrent-menu" )
1580      || !strcmp( action_name, "add-torrent-toolbar" ) )
1581    {
1582        addDialog( data->wind, data->core );
1583    }
1584    else if( !strcmp( action_name, "show-stats" ) )
1585    {
1586        GtkWidget * dialog = stats_dialog_create( data->wind, data->core );
1587        gtk_widget_show( dialog );
1588    }
1589    else if( !strcmp( action_name, "donate" ) )
1590    {
1591        gtr_open_file( "http://www.transmissionbt.com/donate.php" );
1592    }
1593    else if( !strcmp( action_name, "pause-all-torrents" ) )
1594    {
1595        pauseAllTorrents( data );
1596    }
1597    else if( !strcmp( action_name, "start-all-torrents" ) )
1598    {
1599        startAllTorrents( data );
1600    }
1601    else if( !strcmp( action_name, "copy-magnet-link-to-clipboard" ) )
1602    {
1603        tr_torrent * tor = getFirstSelectedTorrent( data );
1604        if( tor != NULL )
1605        {
1606            copyMagnetLinkToClipboard( GTK_WIDGET( data->wind ), tor );
1607        }
1608    }
1609    else if( !strcmp( action_name, "relocate-torrent" ) )
1610    {
1611        GSList * ids = getSelectedTorrentIds( data );
1612        if( ids != NULL )
1613        {
1614            GtkWindow * parent = GTK_WINDOW( data->wind );
1615            GtkWidget * w = gtr_relocate_dialog_new( parent, data->core, ids );
1616            gtk_widget_show( w );
1617        }
1618    }
1619    else if( !strcmp( action_name, "start-torrent" ) )
1620    {
1621        changed |= rpcOnSelectedTorrents( data, "torrent-start" );
1622    }
1623    else if( !strcmp( action_name, "pause-torrent" ) )
1624    {
1625        changed |= rpcOnSelectedTorrents( data, "torrent-stop" );
1626    }
1627    else if( !strcmp( action_name, "verify-torrent" ) )
1628    {
1629        changed |= rpcOnSelectedTorrents( data, "torrent-verify" );
1630    }
1631    else if( !strcmp( action_name, "update-tracker" ) )
1632    {
1633        changed |= rpcOnSelectedTorrents( data, "torrent-reannounce" );
1634    }
1635    else if( !strcmp( action_name, "open-torrent-folder" ) )
1636    {
1637        GtkTreeSelection * s = tr_window_get_selection( data->wind );
1638        gtk_tree_selection_selected_foreach( s, openFolderForeach, data );
1639    }
1640    else if( !strcmp( action_name, "show-torrent-properties" ) )
1641    {
1642        GtkWidget * w;
1643        GSList * ids = getSelectedTorrentIds( data );
1644        struct DetailsDialogHandle * h = findDetailsDialogFromIds( data, ids );
1645        if( h != NULL )
1646            w = h->dialog;
1647        else {
1648            h = g_new( struct DetailsDialogHandle, 1 );
1649            h->key = getDetailsDialogKey( ids );
1650            h->dialog = w = torrent_inspector_new( GTK_WINDOW( data->wind ), data->core );
1651            torrent_inspector_set_torrents( w, ids );
1652            data->details = g_slist_append( data->details, h );
1653            g_object_weak_ref( G_OBJECT( w ), detailsClosed, data );
1654        }
1655        gtk_window_present( GTK_WINDOW( w ) );
1656        g_slist_free( ids );
1657    }
1658    else if( !strcmp( action_name, "new-torrent" ) )
1659    {
1660        GtkWidget * w = make_meta_ui( GTK_WINDOW( data->wind ), data->core );
1661        gtk_widget_show_all( w );
1662    }
1663    else if( !strcmp( action_name, "remove-torrent" ) )
1664    {
1665        removeSelected( data, FALSE );
1666    }
1667    else if( !strcmp( action_name, "delete-torrent" ) )
1668    {
1669        removeSelected( data, TRUE );
1670    }
1671    else if( !strcmp( action_name, "quit" ) )
1672    {
1673        askquit( data->core, data->wind, wannaquit, data );
1674    }
1675    else if( !strcmp( action_name, "select-all" ) )
1676    {
1677        GtkTreeSelection * s = tr_window_get_selection( data->wind );
1678        gtk_tree_selection_select_all( s );
1679    }
1680    else if( !strcmp( action_name, "deselect-all" ) )
1681    {
1682        GtkTreeSelection * s = tr_window_get_selection( data->wind );
1683        gtk_tree_selection_unselect_all( s );
1684    }
1685    else if( !strcmp( action_name, "edit-preferences" ) )
1686    {
1687        if( NULL == data->prefs )
1688        {
1689            data->prefs = tr_prefs_dialog_new( G_OBJECT( data->core ),
1690                                               data->wind );
1691            g_signal_connect( data->prefs, "destroy",
1692                              G_CALLBACK( gtk_widget_destroyed ), &data->prefs );
1693            gtk_widget_show( GTK_WIDGET( data->prefs ) );
1694        }
1695    }
1696    else if( !strcmp( action_name, "toggle-message-log" ) )
1697    {
1698        if( !data->msgwin )
1699        {
1700            GtkWidget * win = msgwin_new( data->core );
1701            g_signal_connect( win, "destroy", G_CALLBACK( msgwinclosed ),
1702                              NULL );
1703            data->msgwin = win;
1704        }
1705        else
1706        {
1707            action_toggle( "toggle-message-log", FALSE );
1708            gtk_widget_destroy( data->msgwin );
1709            data->msgwin = NULL;
1710        }
1711    }
1712    else if( !strcmp( action_name, "show-about-dialog" ) )
1713    {
1714        about( data->wind );
1715    }
1716    else if( !strcmp ( action_name, "help" ) )
1717    {
1718        char * url = gtr_get_help_url( );
1719        gtr_open_file( url );
1720        g_free( url );
1721    }
1722    else if( !strcmp( action_name, "toggle-main-window" ) )
1723    {
1724        toggleMainWindow( data );
1725    }
1726    else g_error ( "Unhandled action: %s", action_name );
1727
1728    if( changed )
1729        updatemodel( data );
1730}
Note: See TracBrowser for help on using the repository browser.