source: trunk/gtk/main.c @ 5584

Last change on this file since 5584 was 5584, checked in by charles, 14 years ago

(gtk) #826: gtk client's "--minimized" command-line argument doesn't work

  • Property svn:keywords set to Date Rev Author Id
File size: 35.3 KB
Line 
1/******************************************************************************
2 * $Id: main.c 5584 2008-04-11 14:36:29Z 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 <sys/param.h>
26#include <signal.h>
27#include <string.h>
28#include <stdio.h>
29#include <stdlib.h>
30#include <time.h>
31#include <unistd.h>
32
33#include <gtk/gtk.h>
34#include <glib/gi18n.h>
35#include <glib/gstdio.h>
36
37#include <gdk/gdk.h>
38#ifdef GDK_WINDOWING_X11
39#include <X11/Xatom.h>
40#include <gdk/gdkx.h>
41#endif
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 "ipc.h"
50#include "makemeta-ui.h"
51#include "msgwin.h"
52#include "notify.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#include <libtransmission/transmission.h>
63#include <libtransmission/version.h>
64
65/* interval in milliseconds to update the torrent list display */
66#define UPDATE_INTERVAL         1666
67
68/* interval in milliseconds to check for stopped torrents and update display */
69#define EXIT_CHECK_INTERVAL     500
70
71#if GTK_CHECK_VERSION(2,8,0)
72#define SHOW_LICENSE
73static const char * LICENSE = 
74"The Transmission binaries and most of its source code is distributed "
75"license. "
76"\n\n"
77"Some files are copyrighted by Charles Kerr and are covered by "
78"the GPL version 2.  Works owned by the Transmission project "
79"are granted a special exemption to clause 2(b) so that the bulk "
80"of its code can remain under the MIT license.  This exemption does "
81"not extend to original or derived works not owned by the "
82"Transmission project. "
83"\n\n"
84"Permission is hereby granted, free of charge, to any person obtaining "
85"a copy of this software and associated documentation files (the "
86"'Software'), to deal in the Software without restriction, including "
87"without limitation the rights to use, copy, modify, merge, publish, "
88"distribute, sublicense, and/or sell copies of the Software, and to "
89"permit persons to whom the Software is furnished to do so, subject to "
90"the following conditions: "
91"\n\n"
92"The above copyright notice and this permission notice shall be included "
93"in all copies or substantial portions of the Software. "
94"\n\n"
95"THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, "
96"EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF "
97"MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. "
98"IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY "
99"CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, "
100"TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE "
101"SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.";
102#endif
103
104struct cbdata
105{
106    gboolean       minimized;
107    gboolean       closing;
108    guint          timer;
109    guint          idle_hide_mainwindow_tag;
110    gpointer       icon;
111    GtkWindow    * wind;
112    TrCore       * core;
113    GtkWidget    * msgwin;
114    GtkWidget    * prefs;
115    GSList       * errqueue;
116    GHashTable   * tor2details;
117    GHashTable   * details2tor;
118};
119
120#define CBDATA_PTR "callback-data-pointer"
121
122static GtkUIManager * myUIManager = NULL;
123
124static gboolean
125sendremote( GSList * files, gboolean sendquit );
126static void
127appsetup( TrWindow * wind, GSList * args,
128          struct cbdata *,
129          gboolean paused, gboolean minimized );
130static void
131winsetup( struct cbdata * cbdata, TrWindow * wind );
132static void
133wannaquit( void * vdata );
134static void
135setupdrag(GtkWidget *widget, struct cbdata *data);
136static void
137gotdrag(GtkWidget *widget, GdkDragContext *dc, gint x, gint y,
138        GtkSelectionData *sel, guint info, guint time, gpointer gdata);
139
140static void
141coreerr( TrCore * core, enum tr_core_err code, const char * msg,
142         gpointer gdata );
143static void
144onAddTorrent( TrCore *, tr_ctor *, gpointer );
145static void
146prefschanged( TrCore * core, const char * key, gpointer data );
147static gboolean
148updatemodel(gpointer gdata);
149
150struct counts_data
151{
152    int totalCount;
153    int activeCount;
154    int inactiveCount;
155};
156
157static void
158accumulateStatusForeach( GtkTreeModel * model,
159                         GtkTreePath  * path UNUSED,
160                         GtkTreeIter  * iter,
161                         gpointer       user_data )
162{
163    int status = 0;
164    struct counts_data * counts = user_data;
165
166    ++counts->totalCount;
167
168    gtk_tree_model_get( model, iter, MC_STATUS, &status, -1 );
169
170    if( TR_STATUS_IS_ACTIVE( status ) )
171        ++counts->activeCount;
172    else
173        ++counts->inactiveCount;
174}
175
176static void
177accumulateCanUpdateForeach (GtkTreeModel * model,
178                            GtkTreePath  * path UNUSED,
179                            GtkTreeIter  * iter,
180                            gpointer       accumulated_status)
181{
182    tr_torrent * tor;
183    gtk_tree_model_get( model, iter, MC_TORRENT_RAW, &tor, -1 );
184    *(int*)accumulated_status |= tr_torrentCanManualUpdate( tor );
185}
186
187static void
188refreshTorrentActions( GtkTreeSelection * s )
189{
190    int canUpdate;
191    struct counts_data counts;
192
193    counts.activeCount = 0;
194    counts.inactiveCount = 0;
195    counts.totalCount = 0;
196    gtk_tree_selection_selected_foreach( s, accumulateStatusForeach, &counts );
197    action_sensitize( "pause-torrent", counts.activeCount!=0 );
198    action_sensitize( "start-torrent", counts.inactiveCount!=0 );
199    action_sensitize( "remove-torrent", counts.totalCount!=0 );
200    action_sensitize( "delete-torrent", counts.totalCount!=0 );
201    action_sensitize( "verify-torrent", counts.totalCount!=0 );
202    action_sensitize( "open-torrent-folder", counts.totalCount==1 );
203    action_sensitize( "show-torrent-details", counts.totalCount==1 );
204
205    canUpdate = 0;
206    gtk_tree_selection_selected_foreach( s, accumulateCanUpdateForeach, &canUpdate );
207    action_sensitize( "update-tracker", canUpdate!=0 );
208
209    {
210        GtkTreeView * view = gtk_tree_selection_get_tree_view( s );
211        GtkTreeModel * model = gtk_tree_view_get_model( view );
212        const int torrentCount = gtk_tree_model_iter_n_children( model, NULL ) != 0;
213        action_sensitize( "select-all", torrentCount!=0 );
214        action_sensitize( "deselect-all", torrentCount!=0 );
215    }
216}
217
218static void
219selectionChangedCB( GtkTreeSelection * s, gpointer unused UNUSED )
220{
221    refreshTorrentActions( s );
222}
223
224static void
225windowStateChanged( GtkWidget * widget UNUSED, GdkEventWindowState * event, gpointer gdata )
226{
227    if( event->changed_mask & GDK_WINDOW_STATE_ICONIFIED )
228    {
229        struct cbdata * cbdata = gdata;
230        cbdata->minimized = ( event->new_window_state & GDK_WINDOW_STATE_ICONIFIED ) ? 1 : 0;
231    }
232}
233
234static sig_atomic_t global_sigcount = 0;
235
236static void
237fatalsig( int sig )
238{
239    static const int SIGCOUNT_MAX = 3; /* revert to default handler after this many */
240
241    if( ++global_sigcount >= SIGCOUNT_MAX )
242    {
243        signal( sig, SIG_DFL );
244        raise( sig );
245    }
246}
247
248
249static void
250setupsighandlers( void )
251{
252#ifdef G_OS_WIN32
253  const int sigs[] = { SIGINT, SIGTERM };
254#else
255  const int sigs[] = { SIGHUP, SIGINT, SIGQUIT, SIGTERM };
256#endif
257  guint i;
258
259  for( i=0; i<G_N_ELEMENTS(sigs); ++i )
260      signal( sigs[i], fatalsig );
261}
262
263int
264main( int argc, char ** argv )
265{
266    char * err;
267    struct cbdata * cbdata;
268    GSList * argfiles;
269    GError * gerr;
270    gboolean didinit = FALSE;
271    gboolean didlock = FALSE;
272    gboolean sendquit = FALSE;
273    gboolean startpaused = FALSE;
274    gboolean startminimized = FALSE;
275    char * domain = "transmission";
276    const char * configDir = tr_getDefaultConfigDir( );
277
278    GOptionEntry entries[] = {
279        { "paused", 'p', 0, G_OPTION_ARG_NONE, &startpaused,
280          _("Start with all torrents paused"), NULL },
281        { "quit", 'q', 0, G_OPTION_ARG_NONE, &sendquit,
282          _( "Ask the running instance to quit"), NULL },
283#ifdef STATUS_ICON_SUPPORTED
284        { "minimized", 'm', 0, G_OPTION_ARG_NONE, &startminimized,
285          _( "Start minimized in system tray"), NULL },
286#endif
287        { NULL, 0, 0, 0, NULL, NULL, NULL }
288    };
289
290    cbdata = g_new0( struct cbdata, 1 );
291    cbdata->tor2details = g_hash_table_new( g_str_hash, g_str_equal );
292    cbdata->details2tor = g_hash_table_new( g_direct_hash, g_direct_equal );
293
294    /* bind the gettext domain */
295    bindtextdomain( domain, TRANSMISSIONLOCALEDIR );
296    bind_textdomain_codeset( domain, "UTF-8" );
297    textdomain( domain );
298    g_set_application_name( _( "Transmission" ) );
299
300    /* initialize gtk */
301    g_thread_init( NULL );
302    gerr = NULL;
303    if( !gtk_init_with_args( &argc, &argv, _("[torrent files]"), entries, domain, &gerr ) ) {
304        g_message( "%s", gerr->message );
305        g_clear_error( &gerr );
306        return 0;
307    }
308
309    tr_notify_init( );
310
311    didinit = cf_init( configDir, NULL ); /* must come before actions_init */
312    tr_prefs_init_global( );
313    myUIManager = gtk_ui_manager_new ();
314    actions_init ( myUIManager, cbdata );
315    gtk_ui_manager_add_ui_from_string (myUIManager, fallback_ui_file, -1, NULL);
316    gtk_ui_manager_ensure_update (myUIManager);
317    gtk_window_set_default_icon_name ( "transmission" );
318
319    argfiles = checkfilenames( argc-1, argv+1 );
320    didlock = didinit && sendremote( argfiles, sendquit );
321    setupsighandlers( ); /* set up handlers for fatal signals */
322
323    if( ( didinit || cf_init( configDir, &err ) ) &&
324        ( didlock || cf_lock( &err ) ) )
325    {
326        cbdata->core = tr_core_new( );
327
328        /* create main window now to be a parent to any error dialogs */
329        GtkWindow * mainwind = GTK_WINDOW( tr_window_new( myUIManager, cbdata->core ) );
330        g_signal_connect( mainwind, "window-state-event", G_CALLBACK(windowStateChanged), cbdata );
331
332        appsetup( mainwind, argfiles, cbdata, startpaused, startminimized );
333    }
334    else
335    {
336        gtk_widget_show( errmsg_full( NULL, (callbackfunc_t)gtk_main_quit,
337                                      NULL, "%s", err ) );
338        g_free( err );
339    }
340
341    gtk_main();
342
343    return 0;
344}
345
346static gboolean
347sendremote( GSList * files, gboolean sendquit )
348{
349    const gboolean didlock = cf_lock( NULL );
350
351    /* send files if there's another instance, otherwise start normally */
352    if( !didlock && files )
353        exit( ipc_sendfiles_blocking( files ) ? 0 : 1 );
354
355    /* either send a quit message or exit if no other instance */
356    if( sendquit )
357        exit( didlock ? 0 : !ipc_sendquit_blocking() );
358
359    return didlock;
360}
361
362static void
363appsetup( TrWindow * wind, GSList * torrentFiles,
364          struct cbdata * cbdata,
365          gboolean forcepause, gboolean minimized )
366{
367    const pref_flag_t start = forcepause ? PREF_FLAG_FALSE : PREF_FLAG_DEFAULT;
368    const pref_flag_t prompt = PREF_FLAG_DEFAULT;
369
370    /* fill out cbdata */
371    cbdata->wind       = NULL;
372    cbdata->icon       = NULL;
373    cbdata->msgwin     = NULL;
374    cbdata->prefs      = NULL;
375    cbdata->timer      = 0;
376    cbdata->closing    = FALSE;
377    cbdata->errqueue   = NULL;
378    cbdata->minimized  = minimized;
379
380    actions_set_core( cbdata->core );
381
382    /* set up core handlers */
383    g_signal_connect( cbdata->core, "error", G_CALLBACK( coreerr ), cbdata );
384    g_signal_connect( cbdata->core, "add-torrent-prompt",
385                      G_CALLBACK( onAddTorrent ), cbdata );
386    g_signal_connect_swapped( cbdata->core, "quit",
387                              G_CALLBACK( wannaquit ), cbdata );
388    g_signal_connect( cbdata->core, "prefs-changed",
389                      G_CALLBACK( prefschanged ), cbdata );
390
391    /* add torrents from command-line and saved state */
392    tr_core_load( cbdata->core, forcepause );
393    tr_core_add_list( cbdata->core, torrentFiles, start, prompt );
394    torrentFiles = NULL;
395    tr_core_torrents_added( cbdata->core );
396
397    /* set up the ipc socket */
398    ipc_socket_setup( GTK_WINDOW( wind ), cbdata->core );
399
400    /* set up main window */
401    winsetup( cbdata, wind );
402
403    /* set up the system tray */
404    cbdata->icon = tr_icon_new( cbdata->core );
405
406    /* start model update timer */
407    cbdata->timer = g_timeout_add( UPDATE_INTERVAL, updatemodel, cbdata );
408    updatemodel( cbdata );
409
410    /* either show the window or iconify it */
411    if( !minimized )
412        gtk_widget_show( GTK_WIDGET( wind ) );
413    else {
414        gtk_window_iconify( wind );
415        gtk_window_set_skip_taskbar_hint( cbdata->wind, cbdata->icon != NULL );
416    }
417}
418
419
420/**
421 * hideMainWindow, and the timeout hack in toggleMainWindow,
422 * are loosely cribbed from Colin Walters' tr-shell.c in Rhythmbox
423 */
424static gboolean
425idle_hide_mainwindow( gpointer window )
426{
427    gtk_widget_hide( window );
428    return FALSE;
429}
430static void
431hideMainWindow( struct cbdata * cbdata )
432{
433#if defined(STATUS_ICON_SUPPORTED) && defined(GDK_WINDOWING_X11)
434    GdkRectangle  bounds;
435    gulong        data[4];
436    Display      *dpy;
437    GdkWindow    *gdk_window;
438
439    gtk_status_icon_get_geometry( GTK_STATUS_ICON( cbdata->icon ), NULL, &bounds, NULL );
440    gdk_window = GTK_WIDGET (cbdata->wind)->window;
441    dpy = gdk_x11_drawable_get_xdisplay (gdk_window);
442
443    data[0] = bounds.x;
444    data[1] = bounds.y;
445    data[2] = bounds.width;
446    data[3] = bounds.height;
447
448    XChangeProperty (dpy,
449                     GDK_WINDOW_XID (gdk_window),
450                     gdk_x11_get_xatom_by_name_for_display (gdk_drawable_get_display (gdk_window),
451                     "_NET_WM_ICON_GEOMETRY"),
452                     XA_CARDINAL, 32, PropModeReplace,
453                     (guchar*)&data, 4);
454
455    gtk_window_set_skip_taskbar_hint( cbdata->wind, TRUE );
456#endif
457    gtk_window_iconify( cbdata->wind );
458}
459
460static void
461clearTag( guint * tag )
462{
463    if( *tag )
464        g_source_remove( *tag );
465    *tag = 0;
466}
467
468static void
469toggleMainWindow( struct cbdata * cbdata )
470{
471    GtkWindow * window = GTK_WINDOW( cbdata->wind );
472    const int hide = !cbdata->minimized;
473    static int x=0, y=0;
474
475    if( hide )
476    {
477        gtk_window_get_position( window, &x, &y );
478        clearTag( &cbdata->idle_hide_mainwindow_tag );
479        hideMainWindow( cbdata );
480        cbdata->idle_hide_mainwindow_tag = g_timeout_add( 250, idle_hide_mainwindow, window );
481    }
482    else
483    {
484        gtk_window_set_skip_taskbar_hint( window, FALSE );
485        gtk_window_move( window, x, y );
486        gtk_widget_show( GTK_WIDGET( window ) );
487        gtk_window_deiconify( window );
488#if GTK_CHECK_VERSION(2,8,0)
489        gtk_window_present_with_time( window, gtk_get_current_event_time( ) );
490#else
491        gtk_window_present( window );
492#endif
493    }
494}
495
496static gboolean
497winclose( GtkWidget * w UNUSED, GdkEvent * event UNUSED, gpointer gdata )
498{
499    struct cbdata * cbdata = gdata;
500
501    if( cbdata->icon != NULL )
502        action_activate ("toggle-main-window");
503    else
504        askquit( cbdata->core, cbdata->wind, wannaquit, cbdata );
505
506    return TRUE; /* don't propagate event further */
507}
508
509static void
510rowChangedCB( GtkTreeModel  * model UNUSED,
511              GtkTreePath   * path,
512              GtkTreeIter   * iter UNUSED,
513              gpointer        sel)
514{
515    if( gtk_tree_selection_path_is_selected ( sel, path ) )
516        refreshTorrentActions( GTK_TREE_SELECTION(sel) );
517}
518
519static void
520winsetup( struct cbdata * cbdata, TrWindow * wind )
521{
522    GtkTreeModel * model;
523    GtkTreeSelection * sel;
524
525    g_assert( NULL == cbdata->wind );
526    cbdata->wind = GTK_WINDOW( wind );
527
528    sel = tr_window_get_selection( cbdata->wind );
529    g_signal_connect( sel, "changed", G_CALLBACK(selectionChangedCB), NULL );
530    selectionChangedCB( sel, NULL );
531    model = tr_core_model( cbdata->core );
532    g_signal_connect( model, "row-changed", G_CALLBACK(rowChangedCB), sel );
533    g_signal_connect( wind, "delete-event", G_CALLBACK( winclose ), cbdata );
534    refreshTorrentActions( sel );
535   
536    setupdrag( GTK_WIDGET(wind), cbdata );
537}
538
539static gpointer
540quitThreadFunc( gpointer gdata )
541{
542    struct cbdata * cbdata = gdata;
543
544    tr_close( tr_core_handle( cbdata->core ) );
545
546    /* shutdown the gui */
547    if( cbdata->prefs )
548        gtk_widget_destroy( GTK_WIDGET( cbdata->prefs ) );
549    if( cbdata->wind )
550        gtk_widget_destroy( GTK_WIDGET( cbdata->wind ) );
551    g_object_unref( cbdata->core );
552    if( cbdata->icon )
553        g_object_unref( cbdata->icon );
554    if( cbdata->errqueue ) {
555        g_slist_foreach( cbdata->errqueue, (GFunc)g_free, NULL );
556        g_slist_free( cbdata->errqueue );
557    }
558
559    g_hash_table_destroy( cbdata->details2tor );
560    g_hash_table_destroy( cbdata->tor2details );
561    g_free( cbdata );
562
563    /* exit the gtk main loop */
564    gtk_main_quit( );
565    return NULL;
566}
567
568/* since there are no buttons in the dialog, gtk tries to
569 * select one of the labels, which looks ugly... so force
570 * the dialog's primary and secondary labels to be unselectable */
571static void
572deselectLabels( GtkWidget * w, gpointer unused UNUSED )
573{
574    if( GTK_IS_LABEL( w ) )
575        gtk_label_set_selectable( GTK_LABEL(w), FALSE );
576    else if( GTK_IS_CONTAINER( w ) )
577        gtk_container_foreach( GTK_CONTAINER(w), deselectLabels, NULL );
578}
579
580static void
581do_exit_cb( GtkWidget *w UNUSED, gpointer data UNUSED )
582{
583    exit( 0 );
584}
585
586static void
587wannaquit( void * vdata )
588{
589    GtkWidget * r, * p, * b, * w, *c;
590    struct cbdata * cbdata = vdata;
591
592    /* stop the update timer */
593    if( cbdata->timer ) {
594        g_source_remove( cbdata->timer );
595        cbdata->timer = 0;
596    }
597
598    c = GTK_WIDGET( cbdata->wind );
599    gtk_container_remove( GTK_CONTAINER( c ), gtk_bin_get_child( GTK_BIN( c ) ) );
600
601    r = gtk_alignment_new(0.5, 0.5, 0.01, 0.01);
602    gtk_container_add(GTK_CONTAINER(c), r);
603
604    p = gtk_table_new(3, 2, FALSE);
605    gtk_table_set_col_spacings( GTK_TABLE( p ), GUI_PAD_BIG );
606    gtk_container_add( GTK_CONTAINER( r ), p );
607
608    w = gtk_image_new_from_stock( GTK_STOCK_NETWORK, GTK_ICON_SIZE_DIALOG );
609    gtk_table_attach_defaults(GTK_TABLE(p), w, 0, 1, 0, 2 );
610
611    w = gtk_label_new( NULL );
612    gtk_label_set_markup( GTK_LABEL( w ), _( "<b>Closing Connections</b>" ) );
613    gtk_misc_set_alignment( GTK_MISC( w ), 0.0, 0.5 );
614    gtk_table_attach_defaults( GTK_TABLE( p ), w, 1, 2, 0, 1 );
615
616    w = gtk_label_new( _( "Sending upload/download totals to tracker..." ) );
617    gtk_misc_set_alignment( GTK_MISC( w ), 0.0, 0.5 );
618    gtk_table_attach_defaults( GTK_TABLE( p ), w, 1, 2, 1, 2 );
619
620    b = gtk_alignment_new(0.0, 1.0, 0.01, 0.01);
621    w = gtk_button_new_with_label( _( "_Quit Immediately" ) );
622    gtk_button_set_image( GTK_BUTTON(w), gtk_image_new_from_stock( GTK_STOCK_QUIT, GTK_ICON_SIZE_BUTTON ) );
623    g_signal_connect(w, "clicked", G_CALLBACK(do_exit_cb), NULL);
624    gtk_container_add(GTK_CONTAINER(b), w);
625    gtk_table_attach(GTK_TABLE(p), b, 1, 2, 2, 3, GTK_FILL, GTK_FILL, 0, 10 );
626
627    gtk_widget_show_all(r);
628
629    /* clear the UI */
630    gtk_list_store_clear( GTK_LIST_STORE( tr_core_model( cbdata->core ) ) );
631
632    /* shut down libT */
633    g_thread_create( quitThreadFunc, vdata, TRUE, NULL );
634}
635
636static void
637gotdrag( GtkWidget         * widget UNUSED,
638         GdkDragContext    * dc,
639         gint                x UNUSED,
640         gint                y UNUSED,
641         GtkSelectionData  * sel,
642         guint               info UNUSED,
643         guint               time,
644         gpointer            gdata )
645{
646    struct cbdata * data = gdata;
647    GSList * paths = NULL;
648    GSList * freeme = NULL;
649
650#if 0
651    int i;
652    char *sele = gdk_atom_name(sel->selection);
653    char *targ = gdk_atom_name(sel->target);
654    char *type = gdk_atom_name(sel->type);
655
656    g_message( "dropped file: sel=%s targ=%s type=%s fmt=%i len=%i",
657               sele, targ, type, sel->format, sel->length );
658    g_free(sele);
659    g_free(targ);
660    g_free(type);
661    if( sel->format == 8 ) {
662        for( i=0; i<sel->length; ++i )
663            fprintf(stderr, "%02X ", sel->data[i]);
664        fprintf(stderr, "\n");
665    }
666#endif
667
668    if( ( sel->format == 8 ) &&
669        ( sel->selection == gdk_atom_intern( "XdndSelection", FALSE ) ) )
670    {
671        int i;
672        char * str = g_strndup( (char*)sel->data, sel->length );
673        gchar ** files = g_strsplit_set( str, "\r\n", -1 );
674        for( i=0; files && files[i]; ++i )
675        {
676            char * filename;
677            if( !*files[i] ) /* empty filename... */
678                continue;
679
680            /* decode the filename */
681            filename = decode_uri( files[i] );
682            freeme = g_slist_prepend( freeme, filename );
683            if( !g_utf8_validate( filename, -1, NULL ) )
684                continue;
685
686            /* walk past "file://", if present */
687            if( g_str_has_prefix( filename, "file:" ) ) {
688                filename += 5;
689                while( g_str_has_prefix( filename, "//" ) )
690                    ++filename;
691            }
692
693            /* if the file doesn't exist, the first part
694               might be a hostname ... walk past it. */
695            if( !g_file_test( filename, G_FILE_TEST_EXISTS ) ) {
696                char * pch = strchr( filename + 1, '/' );
697                if( pch != NULL )
698                    filename = pch;
699            }
700
701            /* finally, add it to the list of torrents to try adding */
702            if( g_file_test( filename, G_FILE_TEST_EXISTS ) )
703                paths = g_slist_prepend( paths, g_strdup( filename ) );
704        }
705
706        /* try to add any torrents we found */
707        if( paths )
708        {
709            paths = g_slist_reverse( paths );
710            tr_core_add_list_defaults( data->core, paths );
711            tr_core_torrents_added( data->core );
712        }
713
714        freestrlist( freeme );
715        g_strfreev( files );
716        g_free( str );
717    }
718
719    gtk_drag_finish(dc, (NULL != paths), FALSE, time);
720}
721
722static void
723setupdrag(GtkWidget *widget, struct cbdata *data) {
724  GtkTargetEntry targets[] = {
725    { "STRING",     0, 0 },
726    { "text/plain", 0, 0 },
727    { "text/uri-list", 0, 0 },
728  };
729
730  g_signal_connect(widget, "drag_data_received", G_CALLBACK(gotdrag), data);
731
732  gtk_drag_dest_set(widget, GTK_DEST_DEFAULT_ALL, targets,
733                    ALEN(targets), GDK_ACTION_COPY | GDK_ACTION_MOVE);
734}
735
736static void
737coreerr( TrCore * core UNUSED, enum tr_core_err code, const char * msg,
738         gpointer gdata )
739{
740    struct cbdata * cbdata = gdata;
741    char          * joined;
742
743    switch( code )
744    {
745        case TR_CORE_ERR_ADD_TORRENT:
746            cbdata->errqueue = g_slist_append( cbdata->errqueue,
747                                               g_strdup( msg ) );
748            return;
749        case TR_CORE_ERR_NO_MORE_TORRENTS:
750            if( cbdata->errqueue )
751            {
752                joined = joinstrlist( cbdata->errqueue, "\n" );
753                errmsg( cbdata->wind,
754                        ngettext( "Failed to load torrent file: %s",
755                                  "Failed to load torrent files: %s",
756                                  g_slist_length( cbdata->errqueue ) ),
757                        joined );
758                g_slist_foreach( cbdata->errqueue, (GFunc) g_free, NULL );
759                g_slist_free( cbdata->errqueue );
760                cbdata->errqueue = NULL;
761                g_free( joined );
762            }
763            return;
764        case TR_CORE_ERR_SAVE_STATE:
765            errmsg( cbdata->wind, "%s", msg );
766            return;
767    }
768
769    g_assert_not_reached();
770}
771
772#if GTK_CHECK_VERSION(2,8,0)
773static void
774on_main_window_focus_in( GtkWidget      * widget UNUSED,
775                         GdkEventFocus  * event UNUSED,
776                         gpointer         gdata )
777{
778    struct cbdata * cbdata = gdata;
779    gtk_window_set_urgency_hint( GTK_WINDOW( cbdata->wind ), FALSE );
780}
781#endif
782
783static void
784onAddTorrent( TrCore * core, tr_ctor * ctor, gpointer gdata )
785{
786    struct cbdata * cbdata = gdata;
787    GtkWidget * w = addSingleTorrentDialog( cbdata->wind, core, ctor );
788#if GTK_CHECK_VERSION(2,8,0)
789    g_signal_connect( w, "focus-in-event", G_CALLBACK(on_main_window_focus_in),  cbdata );
790    gtk_window_set_urgency_hint( cbdata->wind, TRUE );
791#endif
792}
793
794static void
795prefschanged( TrCore * core UNUSED, const char * key, gpointer data )
796{
797    struct cbdata * cbdata = data;
798    tr_handle     * tr     = tr_core_handle( cbdata->core );
799
800    if( !strcmp( key, PREF_KEY_ENCRYPTED_ONLY ) )
801    {
802        const gboolean crypto_only = pref_flag_get( key );
803        tr_setEncryptionMode( tr, crypto_only ? TR_ENCRYPTION_REQUIRED
804                                              : TR_ENCRYPTION_PREFERRED );
805    }
806    else if( !strcmp( key, PREF_KEY_PORT ) )
807    {
808        const int port = pref_int_get( key );
809        tr_setBindPort( tr, port );
810    }
811    else if( !strcmp( key, PREF_KEY_DL_LIMIT_ENABLED ) )
812    {
813        const gboolean b = pref_flag_get( key );
814        tr_setUseGlobalSpeedLimit( tr, TR_DOWN, b );
815    }
816    else if( !strcmp( key, PREF_KEY_DL_LIMIT ) )
817    {
818        const int limit = pref_int_get( key );
819        tr_setGlobalSpeedLimit( tr, TR_DOWN, limit );
820    }
821    else if( !strcmp( key, PREF_KEY_UL_LIMIT_ENABLED ) )
822    {
823        const gboolean b = pref_flag_get( key );
824        tr_setUseGlobalSpeedLimit( tr, TR_UP, b );
825    }
826    else if( !strcmp( key, PREF_KEY_UL_LIMIT ) )
827    {
828        const int limit = pref_int_get( key );
829        tr_setGlobalSpeedLimit( tr, TR_UP, limit );
830    }
831    else if( !strcmp( key, PREF_KEY_NAT ) )
832    {
833        const gboolean enabled = pref_flag_get( key );
834        tr_natTraversalEnable( tr, enabled );
835    }
836    else if( !strcmp( key, PREF_KEY_PEX ) )
837    {
838        const gboolean enabled = pref_flag_get( key );
839        tr_setPexEnabled( tr_core_handle( cbdata->core ), enabled );
840    }
841}
842
843gboolean
844updatemodel(gpointer gdata) {
845  struct cbdata *data = gdata;
846
847  if( !data->closing && 0 < global_sigcount )
848  {
849      wannaquit( data );
850      return FALSE;
851  }
852
853  /* update the torrent data in the model */
854  tr_core_update( data->core );
855
856  /* update the main window's statusbar and toolbar buttons */
857  if( data->wind )
858      tr_window_update( data->wind );
859
860  return TRUE;
861}
862
863static void
864about ( GtkWindow * parent )
865{
866    const char *authors[] =
867    {
868        "Charles Kerr (Backend; GTK+)",
869        "Mitchell Livingston (Backend; OS X)",
870        "Eric Petit (Backend; OS X)",
871        "Josh Elsasser (Daemon; Backend; GTK+)",
872        "Bryan Varner (BeOS)", 
873        NULL
874    };
875
876    gtk_show_about_dialog( parent,
877        "name", g_get_application_name(),
878        "comments", _("A fast and easy BitTorrent client"),
879        "version", LONG_VERSION_STRING,
880        "website", "http://www.transmissionbt.com/",
881        "copyright",_("Copyright 2005-2008 The Transmission Project"),
882        "logo-icon-name", "transmission",
883#ifdef SHOW_LICENSE
884        "license", LICENSE,
885        "wrap-license", TRUE,
886#endif
887        "authors", authors,
888        /* Translators: translate "translator-credits" as your name
889           to have it appear in the credits in the "About" dialog */
890        "translator-credits", _("translator-credits"),
891        NULL );
892}
893
894static void
895startTorrentForeach (GtkTreeModel * model,
896                     GtkTreePath  * path UNUSED,
897                     GtkTreeIter  * iter,
898                     gpointer       data UNUSED)
899{
900    TrTorrent * tor = NULL;
901    gtk_tree_model_get( model, iter, MC_TORRENT, &tor, -1 );
902    tr_torrent_start( tor );
903    g_object_unref( G_OBJECT( tor ) );
904}
905
906static void
907stopTorrentForeach (GtkTreeModel * model,
908                    GtkTreePath  * path UNUSED,
909                    GtkTreeIter  * iter,
910                    gpointer       data UNUSED)
911{
912    TrTorrent * tor = NULL;
913    gtk_tree_model_get( model, iter, MC_TORRENT, &tor, -1 );
914    tr_torrent_stop( tor );
915    g_object_unref( G_OBJECT( tor ) );
916}
917
918static void
919updateTrackerForeach (GtkTreeModel * model,
920                      GtkTreePath  * path UNUSED,
921                      GtkTreeIter  * iter,
922                      gpointer       data UNUSED)
923{
924    TrTorrent * tor = NULL;
925    gtk_tree_model_get( model, iter, MC_TORRENT, &tor, -1 );
926    tr_manualUpdate( tr_torrent_handle( tor ) );
927    g_object_unref( G_OBJECT( tor ) );
928}
929
930static void
931detailsClosed( gpointer user_data, GObject * details )
932{
933    struct cbdata * data = user_data;
934    gpointer hashString = g_hash_table_lookup( data->details2tor, details );
935    g_hash_table_remove( data->details2tor, details );
936    g_hash_table_remove( data->tor2details, hashString );
937}
938
939static void
940openFolderForeach( GtkTreeModel * model,
941                   GtkTreePath  * path UNUSED,
942                   GtkTreeIter  * iter,
943                   gpointer       user_data UNUSED )
944{
945    TrTorrent * gtor = NULL;
946    gtk_tree_model_get( model, iter, MC_TORRENT, &gtor, -1 );
947    tr_torrent_open_folder( gtor );
948    g_object_unref( G_OBJECT( gtor ) );
949}
950
951static void
952showInfoForeach (GtkTreeModel * model,
953                 GtkTreePath  * path UNUSED,
954                 GtkTreeIter  * iter,
955                 gpointer       user_data )
956{
957    const char * hashString;
958    struct cbdata * data = user_data;
959    TrTorrent * tor = NULL;
960    GtkWidget * w;
961
962    gtk_tree_model_get( model, iter, MC_TORRENT, &tor, -1 );
963    hashString = tr_torrent_info(tor)->hashString;
964    w = g_hash_table_lookup( data->tor2details, hashString );
965    if( w != NULL )
966        gtk_window_present( GTK_WINDOW( w ) );
967    else {
968        w = torrent_inspector_new( GTK_WINDOW( data->wind ), tor );
969        gtk_widget_show( w );
970        g_hash_table_insert( data->tor2details, (gpointer)hashString, w );
971        g_hash_table_insert( data->details2tor, w, (gpointer)hashString );
972        g_object_weak_ref( G_OBJECT( w ), detailsClosed, data );
973    }
974
975    g_object_unref( G_OBJECT( tor ) );
976}
977
978static void
979recheckTorrentForeach (GtkTreeModel * model,
980                       GtkTreePath  * path UNUSED,
981                       GtkTreeIter  * iter,
982                       gpointer       data UNUSED)
983{
984    TrTorrent * gtor = NULL;
985    gtk_tree_model_get( model, iter, MC_TORRENT, &gtor, -1 );
986    tr_torrentVerify( tr_torrent_handle( gtor ) );
987    g_object_unref( G_OBJECT( gtor ) );
988}
989
990static gboolean
991msgwinclosed( void )
992{
993  action_toggle( "toggle-message-log", FALSE );
994  return FALSE;
995}
996
997static void
998accumulateSelectedTorrents( GtkTreeModel * model,
999                            GtkTreePath  * path UNUSED,
1000                            GtkTreeIter  * iter,
1001                            gpointer       gdata )
1002{
1003    GSList ** data = ( GSList** ) gdata;
1004    TrTorrent * tor = NULL;
1005    gtk_tree_model_get( model, iter, MC_TORRENT, &tor, -1 );
1006    *data = g_slist_prepend( *data, tor );
1007}
1008
1009static void
1010removeSelected( struct cbdata * data, gboolean delete_files )
1011{
1012    GSList * l = NULL;
1013    GtkTreeSelection * s = tr_window_get_selection( data->wind );
1014    gtk_tree_selection_selected_foreach( s, accumulateSelectedTorrents, &l );
1015    gtk_tree_selection_unselect_all( s );
1016    if( l ) {
1017        l = g_slist_reverse( l );
1018        confirmRemove( data->wind, data->core, l, delete_files );
1019    }
1020}
1021
1022void
1023doAction ( const char * action_name, gpointer user_data )
1024{
1025    struct cbdata * data = user_data;
1026    gboolean changed = FALSE;
1027
1028    if ( !strcmp (action_name, "add-torrent-menu") ||
1029         !strcmp( action_name, "add-torrent-toolbar" ))
1030    {
1031        addDialog( data->wind, data->core );
1032    }
1033    else if (!strcmp (action_name, "show-stats"))
1034    {
1035        GtkWidget * dialog = stats_dialog_create( data->wind,
1036                                                  data->core );
1037        gtk_widget_show( dialog );
1038    }
1039    else if (!strcmp (action_name, "start-torrent"))
1040    {
1041        GtkTreeSelection * s = tr_window_get_selection(data->wind);
1042        gtk_tree_selection_selected_foreach( s, startTorrentForeach, NULL );
1043        changed |= gtk_tree_selection_count_selected_rows( s ) != 0;
1044    }
1045    else if (!strcmp (action_name, "pause-torrent"))
1046    {
1047        GtkTreeSelection * s = tr_window_get_selection(data->wind);
1048        gtk_tree_selection_selected_foreach( s, stopTorrentForeach, NULL );
1049        changed |= gtk_tree_selection_count_selected_rows( s ) != 0;
1050    }
1051    else if (!strcmp (action_name, "verify-torrent"))
1052    {
1053        GtkTreeSelection * s = tr_window_get_selection(data->wind);
1054        gtk_tree_selection_selected_foreach( s, recheckTorrentForeach, NULL );
1055        changed |= gtk_tree_selection_count_selected_rows( s ) != 0;
1056    }
1057    else if (!strcmp (action_name, "open-torrent-folder"))
1058    {
1059        GtkTreeSelection * s = tr_window_get_selection(data->wind);
1060        gtk_tree_selection_selected_foreach( s, openFolderForeach, data );
1061    }
1062    else if (!strcmp (action_name, "show-torrent-details"))
1063    {
1064        GtkTreeSelection * s = tr_window_get_selection(data->wind);
1065        gtk_tree_selection_selected_foreach( s, showInfoForeach, data );
1066    }
1067    else if (!strcmp( action_name, "update-tracker"))
1068    {
1069        GtkTreeSelection * s = tr_window_get_selection(data->wind);
1070        gtk_tree_selection_selected_foreach( s, updateTrackerForeach, data->wind );
1071    }
1072    else if (!strcmp (action_name, "new-torrent"))
1073    {
1074        GtkWidget * w = make_meta_ui( GTK_WINDOW( data->wind ),
1075                                      tr_core_handle( data->core ) );
1076        gtk_widget_show_all( w );
1077    }
1078    else if( !strcmp( action_name, "remove-torrent" ) )
1079    {
1080        removeSelected( data, FALSE );
1081    }
1082    else if( !strcmp( action_name, "delete-torrent" ) )
1083    {
1084        removeSelected( data, TRUE );
1085    }
1086    else if (!strcmp (action_name, "close"))
1087    {
1088        if( data->wind != NULL )
1089            winclose( NULL, NULL, data );
1090    }
1091    else if (!strcmp (action_name, "quit"))
1092    {
1093        askquit( data->core, data->wind, wannaquit, data );
1094    }
1095    else if (!strcmp (action_name, "select-all"))
1096    {
1097        GtkTreeSelection * s = tr_window_get_selection(data->wind);
1098        gtk_tree_selection_select_all( s );
1099    }
1100    else if (!strcmp (action_name, "deselect-all"))
1101    {
1102        GtkTreeSelection * s = tr_window_get_selection(data->wind);
1103        gtk_tree_selection_unselect_all( s );
1104    }
1105    else if (!strcmp (action_name, "edit-preferences"))
1106    {
1107        if( NULL == data->prefs )
1108        {
1109            data->prefs = tr_prefs_dialog_new( G_OBJECT(data->core), data->wind );
1110            g_signal_connect( data->prefs, "destroy",
1111                             G_CALLBACK( gtk_widget_destroyed ), &data->prefs );
1112            gtk_widget_show( GTK_WIDGET( data->prefs ) );
1113        }
1114    }
1115    else if (!strcmp (action_name, "toggle-message-log"))
1116    {
1117        if( !data->msgwin )
1118        {
1119            GtkWidget * win = msgwin_new( data->core );
1120            g_signal_connect( win, "destroy", G_CALLBACK( msgwinclosed ), 
1121                             NULL );
1122            data->msgwin = win;
1123        }
1124        else
1125        {
1126            action_toggle("toggle-message-log", FALSE);
1127            gtk_widget_destroy( data->msgwin );
1128            data->msgwin = NULL;
1129        }
1130    }
1131    else if (!strcmp (action_name, "show-about-dialog"))
1132    {
1133        about( data->wind );
1134    }
1135    else if (!strcmp (action_name, "toggle-main-window"))
1136    {
1137        toggleMainWindow( data );
1138    }
1139    else g_error ("Unhandled action: %s", action_name );
1140
1141    if( changed )
1142        updatemodel( data );
1143}
Note: See TracBrowser for help on using the repository browser.