/****************************************************************************** * $Id$ * * Copyright (c) 2005-2008 Transmission authors and contributors * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. *****************************************************************************/ #include #include #include #include #include #include #include #include #include #include #include #include #ifdef GDK_WINDOWING_X11 #include #include #endif #include #include #include #include "actions.h" #include "add-dialog.h" #include "blocklist.h" #include "conf.h" #include "details.h" #include "dialogs.h" #include "hig.h" #include "makemeta-ui.h" #include "msgwin.h" #include "notify.h" #include "stats.h" #include "tr-core.h" #include "tr-icon.h" #include "tr-prefs.h" #include "tr-torrent.h" #include "tr-window.h" #include "util.h" #include "ui.h" #include #include /* interval in milliseconds to update the torrent list display */ #define UPDATE_INTERVAL 1666 /* interval in milliseconds to check for stopped torrents and update display */ #define EXIT_CHECK_INTERVAL 500 #if GTK_CHECK_VERSION(2,8,0) #define SHOW_LICENSE static const char * LICENSE = "The Transmission binaries and most of its source code is distributed " "license. " "\n\n" "Some files are copyrighted by Charles Kerr and are covered by " "the GPL version 2. Works owned by the Transmission project " "are granted a special exemption to clause 2(b) so that the bulk " "of its code can remain under the MIT license. This exemption does " "not extend to original or derived works not owned by the " "Transmission project. " "\n\n" "Permission is hereby granted, free of charge, to any person obtaining " "a copy of this software and associated documentation files (the " "'Software'), to deal in the Software without restriction, including " "without limitation the rights to use, copy, modify, merge, publish, " "distribute, sublicense, and/or sell copies of the Software, and to " "permit persons to whom the Software is furnished to do so, subject to " "the following conditions: " "\n\n" "The above copyright notice and this permission notice shall be included " "in all copies or substantial portions of the Software. " "\n\n" "THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, " "EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF " "MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. " "IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY " "CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, " "TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE " "SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE."; #endif struct cbdata { gboolean minimized; gboolean closing; guint timer; guint idle_hide_mainwindow_tag; gpointer icon; GtkWindow * wind; TrCore * core; GtkWidget * msgwin; GtkWidget * prefs; GSList * errqueue; GSList * dupqueue; GHashTable * tor2details; GHashTable * details2tor; }; #define CBDATA_PTR "callback-data-pointer" static GtkUIManager * myUIManager = NULL; static void appsetup( TrWindow * wind, GSList * args, struct cbdata *, gboolean paused, gboolean minimized ); static void winsetup( struct cbdata * cbdata, TrWindow * wind ); static void wannaquit( void * vdata ); static void setupdrag(GtkWidget *widget, struct cbdata *data); static void gotdrag(GtkWidget *widget, GdkDragContext *dc, gint x, gint y, GtkSelectionData *sel, guint info, guint time, gpointer gdata); static void coreerr( TrCore * core, enum tr_core_err code, const char * msg, gpointer gdata ); static void onAddTorrent( TrCore *, tr_ctor *, gpointer ); static void prefschanged( TrCore * core, const char * key, gpointer data ); static gboolean updatemodel(gpointer gdata); struct counts_data { int totalCount; int activeCount; int inactiveCount; }; static void accumulateStatusForeach( GtkTreeModel * model, GtkTreePath * path UNUSED, GtkTreeIter * iter, gpointer user_data ) { int status = 0; struct counts_data * counts = user_data; ++counts->totalCount; gtk_tree_model_get( model, iter, MC_STATUS, &status, -1 ); if( TR_STATUS_IS_ACTIVE( status ) ) ++counts->activeCount; else ++counts->inactiveCount; } static void accumulateCanUpdateForeach (GtkTreeModel * model, GtkTreePath * path UNUSED, GtkTreeIter * iter, gpointer accumulated_status) { tr_torrent * tor; gtk_tree_model_get( model, iter, MC_TORRENT_RAW, &tor, -1 ); *(int*)accumulated_status |= tr_torrentCanManualUpdate( tor ); } static void refreshTorrentActions( GtkTreeSelection * s ) { int canUpdate; struct counts_data counts; counts.activeCount = 0; counts.inactiveCount = 0; counts.totalCount = 0; gtk_tree_selection_selected_foreach( s, accumulateStatusForeach, &counts ); action_sensitize( "pause-torrent", counts.activeCount!=0 ); action_sensitize( "start-torrent", counts.inactiveCount!=0 ); action_sensitize( "remove-torrent", counts.totalCount!=0 ); action_sensitize( "delete-torrent", counts.totalCount!=0 ); action_sensitize( "verify-torrent", counts.totalCount!=0 ); action_sensitize( "open-torrent-folder", counts.totalCount==1 ); action_sensitize( "show-torrent-details", counts.totalCount==1 ); canUpdate = 0; gtk_tree_selection_selected_foreach( s, accumulateCanUpdateForeach, &canUpdate ); action_sensitize( "update-tracker", canUpdate!=0 ); { GtkTreeView * view = gtk_tree_selection_get_tree_view( s ); GtkTreeModel * model = gtk_tree_view_get_model( view ); const int torrentCount = gtk_tree_model_iter_n_children( model, NULL ) != 0; action_sensitize( "select-all", torrentCount!=0 ); action_sensitize( "deselect-all", torrentCount!=0 ); } } static void selectionChangedCB( GtkTreeSelection * s, gpointer unused UNUSED ) { refreshTorrentActions( s ); } static void onMainWindowSizeAllocated( GtkWidget * window, GtkAllocation * alloc UNUSED, gpointer gdata UNUSED ) { const gboolean isMaximized = window->window && ( gdk_window_get_state( window->window ) & GDK_WINDOW_STATE_MAXIMIZED ); if( !isMaximized ) { int x, y, w, h; gtk_window_get_position( GTK_WINDOW( window ), &x, &y ); gtk_window_get_size( GTK_WINDOW( window ), &w, &h ); pref_int_set( PREF_KEY_MAIN_WINDOW_X, x ); pref_int_set( PREF_KEY_MAIN_WINDOW_Y, y ); pref_int_set( PREF_KEY_MAIN_WINDOW_WIDTH, w ); pref_int_set( PREF_KEY_MAIN_WINDOW_HEIGHT, h ); } } static void windowStateChanged( GtkWidget * widget UNUSED, GdkEventWindowState * event, gpointer gdata ) { if( event->changed_mask & GDK_WINDOW_STATE_ICONIFIED ) { struct cbdata * cbdata = gdata; cbdata->minimized = ( event->new_window_state & GDK_WINDOW_STATE_ICONIFIED ) ? 1 : 0; } } static sig_atomic_t global_sigcount = 0; static void fatalsig( int sig ) { static const int SIGCOUNT_MAX = 3; /* revert to default handler after this many */ if( ++global_sigcount >= SIGCOUNT_MAX ) { signal( sig, SIG_DFL ); raise( sig ); } } static void setupsighandlers( void ) { #ifdef G_OS_WIN32 const int sigs[] = { SIGINT, SIGTERM }; #else const int sigs[] = { SIGHUP, SIGINT, SIGQUIT, SIGTERM }; #endif guint i; for( i=0; itype ) { case TR_RPC_TORRENT_ADDED: tr_core_add_torrent( data->cbdata->core, tr_torrent_new_preexisting( data->tor ) ); break; case TR_RPC_TORRENT_STARTED: /* this should be automatic */ break; case TR_RPC_TORRENT_STOPPED: /* this should be automatic */ break; case TR_RPC_TORRENT_REMOVING: tr_core_torrent_destroyed( data->cbdata->core, data->torrentId ); break; case TR_RPC_TORRENT_CHANGED: case TR_RPC_SESSION_CHANGED: /* nothing interesting to do here */ break; } g_free( data ); return FALSE; } static tr_rpc_callback_status onRPCChanged( tr_handle * handle UNUSED, tr_rpc_callback_type type, struct tr_torrent * tor, void * cbdata ) { /* this callback is being invoked from the libtransmission thread, * so let's push the information over to the gtk+ thread where * it's safe to update the gui */ struct rpc_data * data = g_new0( struct rpc_data, 1 ); data->type = type; data->torrentId = tor ? tr_torrentId( tor ) : -1; data->tor = type == TR_RPC_TORRENT_REMOVING ? NULL : tor; data->cbdata = cbdata; g_idle_add( onRPCIdle, data ); return TR_RPC_OK; } int main( int argc, char ** argv ) { char * err = NULL; struct cbdata * cbdata; GSList * argfiles; GError * gerr; gboolean didinit = FALSE; gboolean didlock = FALSE; gboolean startpaused = FALSE; gboolean startminimized = FALSE; char * domain = "transmission"; char * configDir = NULL; tr_lockfile_state_t tr_state; GOptionEntry entries[] = { { "paused", 'p', 0, G_OPTION_ARG_NONE, &startpaused, _("Start with all torrents paused"), NULL }, #ifdef STATUS_ICON_SUPPORTED { "minimized", 'm', 0, G_OPTION_ARG_NONE, &startminimized, _( "Start minimized in system tray"), NULL }, #endif { "config-dir", 'g', 0, G_OPTION_ARG_FILENAME, &configDir, _( "Where to look for configuration files" ), NULL }, { NULL, 0, 0, 0, NULL, NULL, NULL } }; cbdata = g_new0( struct cbdata, 1 ); cbdata->tor2details = g_hash_table_new( g_str_hash, g_str_equal ); cbdata->details2tor = g_hash_table_new( g_direct_hash, g_direct_equal ); /* bind the gettext domain */ setlocale( LC_ALL, "" ); bindtextdomain( domain, TRANSMISSIONLOCALEDIR ); bind_textdomain_codeset( domain, "UTF-8" ); textdomain( domain ); g_set_application_name( _( "Transmission" ) ); /* initialize gtk */ if( !g_thread_supported() ) g_thread_init( NULL ); gerr = NULL; if( !gtk_init_with_args( &argc, &argv, _("[torrent files]"), entries, domain, &gerr ) ) { g_message( "%s", gerr->message ); g_clear_error( &gerr ); return 0; } if( configDir == NULL ) configDir = (char*) tr_getDefaultConfigDir( ); tr_notify_init( ); didinit = cf_init( configDir, NULL ); /* must come before actions_init */ tr_prefs_init_global( ); myUIManager = gtk_ui_manager_new (); actions_init ( myUIManager, cbdata ); gtk_ui_manager_add_ui_from_string (myUIManager, fallback_ui_file, -1, NULL); gtk_ui_manager_ensure_update (myUIManager); gtk_window_set_default_icon_name ( "transmission" ); setupsighandlers( ); /* set up handlers for fatal signals */ /* either get a lockfile s.t. this is the one instance of * transmission that's running, OR if there are files to * be added, delegate that to the running instance via dbus */ didlock = cf_lock( &tr_state, &err ); argfiles = checkfilenames( argc-1, argv+1 ); if( !didlock && argfiles ) { GSList * l; gboolean delegated = FALSE; for( l=argfiles; l; l=l->next ) delegated |= gtr_dbus_add_torrent( l->data ); if( delegated ) err = NULL; } else if( (!didlock) && (tr_state == TR_LOCKFILE_ELOCK) ) { gtr_dbus_present_window(); err = NULL; } if( didlock && ( didinit || cf_init( configDir, &err ) ) ) { GtkWindow * win; tr_handle * h = tr_sessionInitFull( configDir, "gtk", pref_string_get( PREF_KEY_DOWNLOAD_DIR ), pref_flag_get( PREF_KEY_PEX ), pref_flag_get( PREF_KEY_PORT_FORWARDING ), pref_int_get( PREF_KEY_PORT ), pref_int_get( PREF_KEY_ENCRYPTION ), pref_flag_get( PREF_KEY_LAZY_BITFIELD ), pref_flag_get( PREF_KEY_UL_LIMIT_ENABLED ), pref_int_get( PREF_KEY_UL_LIMIT ), pref_flag_get( PREF_KEY_DL_LIMIT_ENABLED ), pref_int_get( PREF_KEY_DL_LIMIT ), pref_int_get( PREF_KEY_MAX_PEERS_GLOBAL ), pref_int_get( PREF_KEY_MSGLEVEL ), TRUE, /* message queueing */ pref_flag_get( PREF_KEY_BLOCKLIST_ENABLED ), pref_int_get( PREF_KEY_PEER_SOCKET_TOS ), pref_flag_get( PREF_KEY_RPC_ENABLED ), pref_int_get( PREF_KEY_RPC_PORT ), pref_string_get( PREF_KEY_RPC_ACL ), pref_flag_get( PREF_KEY_RPC_AUTH_ENABLED ), pref_string_get( PREF_KEY_RPC_USERNAME ), pref_string_get( PREF_KEY_RPC_PASSWORD ), pref_flag_get( PREF_KEY_PROXY_SERVER_ENABLED ), pref_string_get( PREF_KEY_PROXY_SERVER ), pref_int_get( PREF_KEY_PROXY_PORT ), pref_int_get( PREF_KEY_PROXY_TYPE ), pref_flag_get( PREF_KEY_PROXY_AUTH_ENABLED ), pref_string_get( PREF_KEY_PROXY_USERNAME ), pref_string_get( PREF_KEY_PROXY_PASSWORD ) ); cbdata->core = tr_core_new( h ); /* create main window now to be a parent to any error dialogs */ win = GTK_WINDOW( tr_window_new( myUIManager, cbdata->core ) ); g_signal_connect( win, "window-state-event", G_CALLBACK(windowStateChanged), cbdata ); g_signal_connect( win, "size-allocate", G_CALLBACK(onMainWindowSizeAllocated), cbdata ); appsetup( win, argfiles, cbdata, startpaused, startminimized ); tr_sessionSetRPCCallback( h, onRPCChanged, cbdata ); gtr_blocklist_maybe_autoupdate( cbdata->core ); gtk_main(); } else if( err ) { gtk_widget_show( errmsg_full( NULL, (callbackfunc_t)gtk_main_quit, NULL, "%s", err ) ); g_free( err ); gtk_main(); } return 0; } static gboolean updateScheduledLimits(gpointer data) { tr_handle *tr = (tr_handle *) data; static gboolean last_state = FALSE; gboolean in_sched_state = FALSE; if( !pref_flag_get( PREF_KEY_SCHED_LIMIT_ENABLED ) ) { in_sched_state = FALSE; } else { const int begin_time = pref_int_get( PREF_KEY_SCHED_BEGIN ); const int end_time = pref_int_get( PREF_KEY_SCHED_BEGIN ); time_t t; struct tm *tm; int cur_time; time( &t ); tm = localtime (&t); cur_time = (tm->tm_hour * 60) + tm->tm_min; if( end_time <= begin_time ) { if( (cur_time >= begin_time) && (cur_time <= end_time) ) in_sched_state = TRUE; } else { if ( (cur_time >= begin_time) || (cur_time <= end_time) ) in_sched_state = TRUE; } } if( last_state != in_sched_state ) { if( in_sched_state ) { int limit; tr_inf ( _( "Beginning to use scheduled bandwidth limits" ) ); tr_sessionSetSpeedLimitEnabled( tr, TR_DOWN, TRUE ); limit = pref_int_get( PREF_KEY_SCHED_DL_LIMIT ); tr_sessionSetSpeedLimit( tr, TR_DOWN, limit ); tr_sessionSetSpeedLimitEnabled( tr, TR_UP, TRUE ); limit = pref_int_get( PREF_KEY_SCHED_UL_LIMIT ); tr_sessionSetSpeedLimit( tr, TR_UP, limit ); } else { gboolean b; int limit; tr_inf ( _( "Ending use of scheduled bandwidth limits" ) ); b = pref_flag_get( PREF_KEY_DL_LIMIT_ENABLED ); tr_sessionSetSpeedLimitEnabled( tr, TR_DOWN, b ); limit = pref_int_get( PREF_KEY_DL_LIMIT ); tr_sessionSetSpeedLimit( tr, TR_DOWN, limit ); b = pref_flag_get( PREF_KEY_UL_LIMIT_ENABLED ); tr_sessionSetSpeedLimitEnabled( tr, TR_UP, b ); limit = pref_int_get( PREF_KEY_UL_LIMIT ); tr_sessionSetSpeedLimit( tr, TR_UP, limit ); } last_state = in_sched_state; } else if( in_sched_state ) { static int old_dl_limit = 0, old_ul_limit = 0; int dl_limit = pref_int_get( PREF_KEY_SCHED_DL_LIMIT ); int ul_limit = pref_int_get( PREF_KEY_SCHED_UL_LIMIT ); if( ( dl_limit != old_dl_limit ) || ( ul_limit != old_ul_limit ) ) { tr_sessionSetSpeedLimitEnabled( tr, TR_DOWN, TRUE ); tr_sessionSetSpeedLimit( tr, TR_DOWN, dl_limit ); tr_sessionSetSpeedLimitEnabled( tr, TR_UP, TRUE ); tr_sessionSetSpeedLimit( tr, TR_UP, ul_limit ); old_dl_limit = dl_limit; old_ul_limit = ul_limit; } } return TRUE; } static void appsetup( TrWindow * wind, GSList * torrentFiles, struct cbdata * cbdata, gboolean forcepause, gboolean minimized ) { const pref_flag_t start = forcepause ? PREF_FLAG_FALSE : PREF_FLAG_DEFAULT; const pref_flag_t prompt = PREF_FLAG_DEFAULT; /* fill out cbdata */ cbdata->wind = NULL; cbdata->icon = NULL; cbdata->msgwin = NULL; cbdata->prefs = NULL; cbdata->timer = 0; cbdata->closing = FALSE; cbdata->errqueue = NULL; cbdata->dupqueue = NULL; cbdata->minimized = minimized; if( minimized ) pref_flag_set( PREF_KEY_SHOW_TRAY_ICON, TRUE ); actions_set_core( cbdata->core ); /* set up core handlers */ g_signal_connect( cbdata->core, "error", G_CALLBACK( coreerr ), cbdata ); g_signal_connect( cbdata->core, "add-torrent-prompt", G_CALLBACK( onAddTorrent ), cbdata ); g_signal_connect_swapped( cbdata->core, "quit", G_CALLBACK( wannaquit ), cbdata ); g_signal_connect( cbdata->core, "prefs-changed", G_CALLBACK( prefschanged ), cbdata ); /* add torrents from command-line and saved state */ tr_core_load( cbdata->core, forcepause ); tr_core_add_list( cbdata->core, torrentFiles, start, prompt ); torrentFiles = NULL; tr_core_torrents_added( cbdata->core ); /* set up main window */ winsetup( cbdata, wind ); /* set up the icon */ prefschanged( cbdata->core, PREF_KEY_SHOW_TRAY_ICON, cbdata ); /* start model update timer */ cbdata->timer = g_timeout_add( UPDATE_INTERVAL, updatemodel, cbdata ); updatemodel( cbdata ); /* start scheduled rate timer */ updateScheduledLimits (tr_core_handle( cbdata->core )); g_timeout_add( 60 * 1000, updateScheduledLimits, tr_core_handle( cbdata->core ) ); /* either show the window or iconify it */ if( !minimized ) gtk_widget_show( GTK_WIDGET( wind ) ); else { gtk_window_iconify( wind ); gtk_window_set_skip_taskbar_hint( cbdata->wind, cbdata->icon != NULL ); } } /** * hideMainWindow, and the timeout hack in toggleMainWindow, * are loosely cribbed from Colin Walters' tr-shell.c in Rhythmbox */ static gboolean idle_hide_mainwindow( gpointer window ) { gtk_widget_hide( window ); return FALSE; } static void hideMainWindow( struct cbdata * cbdata ) { #if defined(STATUS_ICON_SUPPORTED) && defined(GDK_WINDOWING_X11) GdkRectangle bounds; gulong data[4]; Display *dpy; GdkWindow *gdk_window; gtk_status_icon_get_geometry( GTK_STATUS_ICON( cbdata->icon ), NULL, &bounds, NULL ); gdk_window = GTK_WIDGET (cbdata->wind)->window; dpy = gdk_x11_drawable_get_xdisplay (gdk_window); data[0] = bounds.x; data[1] = bounds.y; data[2] = bounds.width; data[3] = bounds.height; XChangeProperty (dpy, GDK_WINDOW_XID (gdk_window), gdk_x11_get_xatom_by_name_for_display (gdk_drawable_get_display (gdk_window), "_NET_WM_ICON_GEOMETRY"), XA_CARDINAL, 32, PropModeReplace, (guchar*)&data, 4); gtk_window_set_skip_taskbar_hint( cbdata->wind, TRUE ); #endif gtk_window_iconify( cbdata->wind ); } static void clearTag( guint * tag ) { if( *tag ) g_source_remove( *tag ); *tag = 0; } static void toggleMainWindow( struct cbdata * cbdata, gboolean present ) { GtkWindow * window = GTK_WINDOW( cbdata->wind ); const int hide = !cbdata->minimized; static int x=0, y=0; if( (!present) && hide ) { gtk_window_get_position( window, &x, &y ); clearTag( &cbdata->idle_hide_mainwindow_tag ); hideMainWindow( cbdata ); cbdata->idle_hide_mainwindow_tag = g_timeout_add( 250, idle_hide_mainwindow, window ); } else { gtk_window_set_skip_taskbar_hint( window, FALSE ); if ( x != 0 && y != 0 ) gtk_window_move( window, x, y ); gtk_widget_show( GTK_WIDGET( window ) ); gtk_window_deiconify( window ); #if GTK_CHECK_VERSION(2,8,0) gtk_window_present_with_time( window, gtk_get_current_event_time( ) ); #else gtk_window_present( window ); #endif } } static gboolean winclose( GtkWidget * w UNUSED, GdkEvent * event UNUSED, gpointer gdata ) { struct cbdata * cbdata = gdata; if( cbdata->icon != NULL ) action_activate ("toggle-main-window"); else askquit( cbdata->core, cbdata->wind, wannaquit, cbdata ); return TRUE; /* don't propagate event further */ } static void rowChangedCB( GtkTreeModel * model UNUSED, GtkTreePath * path, GtkTreeIter * iter UNUSED, gpointer sel) { if( gtk_tree_selection_path_is_selected ( sel, path ) ) refreshTorrentActions( GTK_TREE_SELECTION(sel) ); } static void winsetup( struct cbdata * cbdata, TrWindow * wind ) { GtkTreeModel * model; GtkTreeSelection * sel; g_assert( NULL == cbdata->wind ); cbdata->wind = GTK_WINDOW( wind ); sel = tr_window_get_selection( cbdata->wind ); g_signal_connect( sel, "changed", G_CALLBACK(selectionChangedCB), NULL ); selectionChangedCB( sel, NULL ); model = tr_core_model( cbdata->core ); g_signal_connect( model, "row-changed", G_CALLBACK(rowChangedCB), sel ); g_signal_connect( wind, "delete-event", G_CALLBACK( winclose ), cbdata ); refreshTorrentActions( sel ); setupdrag( GTK_WIDGET(wind), cbdata ); } static gpointer quitThreadFunc( gpointer gdata ) { struct cbdata * cbdata = gdata; tr_core_close( cbdata->core ); /* shutdown the gui */ if( cbdata->prefs ) gtk_widget_destroy( GTK_WIDGET( cbdata->prefs ) ); if( cbdata->wind ) gtk_widget_destroy( GTK_WIDGET( cbdata->wind ) ); g_object_unref( cbdata->core ); if( cbdata->icon ) g_object_unref( cbdata->icon ); if( cbdata->errqueue ) { g_slist_foreach( cbdata->errqueue, (GFunc)g_free, NULL ); g_slist_free( cbdata->errqueue ); } if( cbdata->dupqueue ) { g_slist_foreach( cbdata->dupqueue, (GFunc)g_free, NULL ); g_slist_free( cbdata->dupqueue ); } g_hash_table_destroy( cbdata->details2tor ); g_hash_table_destroy( cbdata->tor2details ); g_free( cbdata ); /* exit the gtk main loop */ gtk_main_quit( ); return NULL; } static void do_exit_cb( GtkWidget *w UNUSED, gpointer data UNUSED ) { exit( 0 ); } static void wannaquit( void * vdata ) { GtkWidget * r, * p, * b, * w, *c; struct cbdata * cbdata = vdata; /* stop the update timer */ if( cbdata->timer ) { g_source_remove( cbdata->timer ); cbdata->timer = 0; } c = GTK_WIDGET( cbdata->wind ); gtk_container_remove( GTK_CONTAINER( c ), gtk_bin_get_child( GTK_BIN( c ) ) ); r = gtk_alignment_new(0.5, 0.5, 0.01, 0.01); gtk_container_add(GTK_CONTAINER(c), r); p = gtk_table_new(3, 2, FALSE); gtk_table_set_col_spacings( GTK_TABLE( p ), GUI_PAD_BIG ); gtk_container_add( GTK_CONTAINER( r ), p ); w = gtk_image_new_from_stock( GTK_STOCK_NETWORK, GTK_ICON_SIZE_DIALOG ); gtk_table_attach_defaults(GTK_TABLE(p), w, 0, 1, 0, 2 ); w = gtk_label_new( NULL ); gtk_label_set_markup( GTK_LABEL( w ), _( "Closing Connections" ) ); gtk_misc_set_alignment( GTK_MISC( w ), 0.0, 0.5 ); gtk_table_attach_defaults( GTK_TABLE( p ), w, 1, 2, 0, 1 ); w = gtk_label_new( _( "Sending upload/download totals to tracker..." ) ); gtk_misc_set_alignment( GTK_MISC( w ), 0.0, 0.5 ); gtk_table_attach_defaults( GTK_TABLE( p ), w, 1, 2, 1, 2 ); b = gtk_alignment_new(0.0, 1.0, 0.01, 0.01); w = tr_button_new_from_stock( GTK_STOCK_QUIT, _( "_Quit Now" ) ); g_signal_connect(w, "clicked", G_CALLBACK(do_exit_cb), NULL); gtk_container_add(GTK_CONTAINER(b), w); gtk_table_attach(GTK_TABLE(p), b, 1, 2, 2, 3, GTK_FILL, GTK_FILL, 0, 10 ); gtk_widget_show_all(r); /* clear the UI */ gtk_list_store_clear( GTK_LIST_STORE( tr_core_model( cbdata->core ) ) ); /* shut down libT */ g_thread_create( quitThreadFunc, vdata, TRUE, NULL ); } static void gotdrag( GtkWidget * widget UNUSED, GdkDragContext * dc, gint x UNUSED, gint y UNUSED, GtkSelectionData * sel, guint info UNUSED, guint time, gpointer gdata ) { struct cbdata * data = gdata; GSList * paths = NULL; GSList * freeme = NULL; #if 0 int i; char *sele = gdk_atom_name(sel->selection); char *targ = gdk_atom_name(sel->target); char *type = gdk_atom_name(sel->type); g_message( "dropped file: sel=%s targ=%s type=%s fmt=%i len=%i", sele, targ, type, sel->format, sel->length ); g_free(sele); g_free(targ); g_free(type); if( sel->format == 8 ) { for( i=0; ilength; ++i ) fprintf(stderr, "%02X ", sel->data[i]); fprintf(stderr, "\n"); } #endif if( ( sel->format == 8 ) && ( sel->selection == gdk_atom_intern( "XdndSelection", FALSE ) ) ) { int i; char * str = g_strndup( (char*)sel->data, sel->length ); gchar ** files = g_strsplit_set( str, "\r\n", -1 ); for( i=0; files && files[i]; ++i ) { char * filename; if( !*files[i] ) /* empty filename... */ continue; /* decode the filename */ filename = decode_uri( files[i] ); freeme = g_slist_prepend( freeme, filename ); if( !g_utf8_validate( filename, -1, NULL ) ) continue; /* walk past "file://", if present */ if( g_str_has_prefix( filename, "file:" ) ) { filename += 5; while( g_str_has_prefix( filename, "//" ) ) ++filename; } /* if the file doesn't exist, the first part might be a hostname ... walk past it. */ if( !g_file_test( filename, G_FILE_TEST_EXISTS ) ) { char * pch = strchr( filename + 1, '/' ); if( pch != NULL ) filename = pch; } /* finally, add it to the list of torrents to try adding */ if( g_file_test( filename, G_FILE_TEST_EXISTS ) ) paths = g_slist_prepend( paths, g_strdup( filename ) ); } /* try to add any torrents we found */ if( paths ) { paths = g_slist_reverse( paths ); tr_core_add_list_defaults( data->core, paths ); tr_core_torrents_added( data->core ); } freestrlist( freeme ); g_strfreev( files ); g_free( str ); } gtk_drag_finish(dc, (NULL != paths), FALSE, time); } static void setupdrag(GtkWidget *widget, struct cbdata *data) { GtkTargetEntry targets[] = { { "STRING", 0, 0 }, { "text/plain", 0, 0 }, { "text/uri-list", 0, 0 }, }; g_signal_connect(widget, "drag_data_received", G_CALLBACK(gotdrag), data); gtk_drag_dest_set(widget, GTK_DEST_DEFAULT_ALL, targets, ALEN(targets), GDK_ACTION_COPY | GDK_ACTION_MOVE); } static void flushAddTorrentErrors( GtkWindow * window, const char * primary, GSList ** files ) { GString * s = g_string_new( NULL ); GSList * l; GtkWidget * w; for( l=*files; l; l=l->next ) g_string_append_printf( s, "%s\n", (const char*)l->data ); w = gtk_message_dialog_new( window, GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, primary ); gtk_message_dialog_format_secondary_text( GTK_MESSAGE_DIALOG( w ), s->str ); g_signal_connect_swapped( w, "response", G_CALLBACK( gtk_widget_destroy ), w ); gtk_widget_show_all( w ); g_string_free( s, TRUE ); g_slist_foreach( *files, (GFunc)g_free, NULL ); g_slist_free( *files ); *files = NULL; } static void showTorrentErrors( struct cbdata * cbdata ) { if( cbdata->errqueue ) flushAddTorrentErrors( GTK_WINDOW( cbdata->wind ), ngettext( "Couldn't add corrupt torrent", "Couldn't add corrupt torrents", g_slist_length( cbdata->errqueue ) ), &cbdata->errqueue ); if( cbdata->dupqueue ) flushAddTorrentErrors( GTK_WINDOW( cbdata->wind ), ngettext( "Couldn't add duplicate torrent", "Couldn't add duplicate torrents", g_slist_length( cbdata->dupqueue ) ), &cbdata->dupqueue ); } static void coreerr( TrCore * core UNUSED, enum tr_core_err code, const char * msg, gpointer gdata ) { struct cbdata * c = gdata; switch( code ) { case TR_EINVALID: c->errqueue = g_slist_append( c->errqueue, g_path_get_basename( msg ) ); break; case TR_EDUPLICATE: c->dupqueue = g_slist_append( c->dupqueue, g_path_get_basename( msg ) ); break; case TR_CORE_ERR_NO_MORE_TORRENTS: showTorrentErrors( c ); break; case TR_CORE_ERR_SAVE_STATE: errmsg( c->wind, "%s", msg ); break; default: g_assert_not_reached(); break; } } #if GTK_CHECK_VERSION(2,8,0) static void on_main_window_focus_in( GtkWidget * widget UNUSED, GdkEventFocus * event UNUSED, gpointer gdata ) { struct cbdata * cbdata = gdata; gtk_window_set_urgency_hint( GTK_WINDOW( cbdata->wind ), FALSE ); } #endif static void onAddTorrent( TrCore * core, tr_ctor * ctor, gpointer gdata ) { struct cbdata * cbdata = gdata; GtkWidget * w = addSingleTorrentDialog( cbdata->wind, core, ctor ); #if GTK_CHECK_VERSION(2,8,0) g_signal_connect( w, "focus-in-event", G_CALLBACK(on_main_window_focus_in), cbdata ); gtk_window_set_urgency_hint( cbdata->wind, TRUE ); #endif } static void prefschanged( TrCore * core UNUSED, const char * key, gpointer data ) { struct cbdata * cbdata = data; tr_handle * tr = tr_core_handle( cbdata->core ); if( !strcmp( key, PREF_KEY_ENCRYPTION ) ) { const int encryption = pref_int_get( key ); g_message( "setting encryption to %d", encryption ); tr_sessionSetEncryption( tr, encryption ); } else if( !strcmp( key, PREF_KEY_PORT ) ) { const int port = pref_int_get( key ); tr_sessionSetPeerPort( tr, port ); } else if( !strcmp( key, PREF_KEY_SHOW_TRAY_ICON ) ) { const int show = pref_flag_get( key ); if( show && !cbdata->icon ) cbdata->icon = tr_icon_new( cbdata->core ); else if( !show && cbdata->icon ) { g_object_unref( cbdata->icon ); cbdata->icon = NULL; } } else if( !strcmp( key, PREF_KEY_DL_LIMIT_ENABLED ) ) { const gboolean b = pref_flag_get( key ); tr_sessionSetSpeedLimitEnabled( tr, TR_DOWN, b ); } else if( !strcmp( key, PREF_KEY_DL_LIMIT ) ) { const int limit = pref_int_get( key ); tr_sessionSetSpeedLimit( tr, TR_DOWN, limit ); } else if( !strcmp( key, PREF_KEY_UL_LIMIT_ENABLED ) ) { const gboolean b = pref_flag_get( key ); tr_sessionSetSpeedLimitEnabled( tr, TR_UP, b ); } else if( !strcmp( key, PREF_KEY_UL_LIMIT ) ) { const int limit = pref_int_get( key ); tr_sessionSetSpeedLimit( tr, TR_UP, limit ); } else if ( !strncmp( key, "sched-", 6 ) ) { updateScheduledLimits( tr ); } else if( !strcmp( key, PREF_KEY_PORT_FORWARDING ) ) { const gboolean enabled = pref_flag_get( key ); tr_sessionSetPortForwardingEnabled( tr, enabled ); } else if( !strcmp( key, PREF_KEY_PEX ) ) { const gboolean b = pref_flag_get( key ); tr_sessionSetPortForwardingEnabled( tr, b ); } else if( !strcmp( key, PREF_KEY_RPC_ENABLED ) ) { tr_sessionSetRPCEnabled( tr, pref_flag_get( key ) ); } else if( !strcmp( key, PREF_KEY_RPC_PORT ) ) { tr_sessionSetRPCPort( tr, pref_int_get( key ) ); } else if( !strcmp( key, PREF_KEY_RPC_ENABLED ) ) { tr_sessionSetRPCEnabled( tr, pref_flag_get( key ) ); } else if( !strcmp( key, PREF_KEY_RPC_ACL ) ) { const char * s = pref_string_get( key ); tr_sessionSetRPCACL( tr, s, NULL ); } else if( !strcmp( key, PREF_KEY_RPC_USERNAME ) ) { const char * s = pref_string_get( key ); tr_sessionSetRPCUsername( tr, s ); } else if( !strcmp( key, PREF_KEY_RPC_PASSWORD ) ) { const char * s = pref_string_get( key ); tr_sessionSetRPCPassword( tr, s ); } else if( !strcmp( key, PREF_KEY_RPC_AUTH_ENABLED ) ) { const gboolean enabled = pref_flag_get( key ); tr_sessionSetRPCPasswordEnabled( tr, enabled ); } else if( !strcmp( key, PREF_KEY_PROXY_SERVER ) ) { const char * s = pref_string_get( key ); tr_sessionSetProxy( tr, s ); } else if( !strcmp( key, PREF_KEY_PROXY_TYPE ) ) { const int i = pref_int_get( key ); tr_sessionSetProxyType( tr, i ); } else if( !strcmp( key, PREF_KEY_PROXY_SERVER_ENABLED ) ) { const gboolean enabled = pref_flag_get( key ); tr_sessionSetProxyEnabled( tr, enabled ); } else if( !strcmp( key, PREF_KEY_PROXY_AUTH_ENABLED ) ) { const gboolean enabled = pref_flag_get( key ); tr_sessionSetProxyAuthEnabled( tr, enabled ); } else if( !strcmp( key, PREF_KEY_PROXY_USERNAME ) ) { const char * s = pref_string_get( key ); tr_sessionSetProxyUsername( tr, s ); } else if( !strcmp( key, PREF_KEY_PROXY_PASSWORD ) ) { const char * s = pref_string_get( key ); tr_sessionSetProxyPassword( tr, s ); } } static gboolean updatemodel( gpointer gdata ) { struct cbdata *data = gdata; const gboolean done = data->closing || global_sigcount; if( !done ) { /* update the torrent data in the model */ tr_core_update( data->core ); /* update the main window's statusbar and toolbar buttons */ if( data->wind ) tr_window_update( data->wind ); /* update the actions */ refreshTorrentActions( tr_window_get_selection( data->wind ) ); } return !done; } static void aboutDialogActivateLink( GtkAboutDialog * dialog UNUSED, const gchar * link_, gpointer user_data UNUSED ) { gtr_open_file( link_ ); } static void about ( GtkWindow * parent ) { const char *authors[] = { "Charles Kerr (Backend; GTK+)", "Mitchell Livingston (Backend; OS X)", "Eric Petit (Backend; OS X)", "Josh Elsasser (Daemon; Backend; GTK+)", "Bryan Varner (BeOS)", NULL }; const char *website_url = "http://www.transmissionbt.com/"; gtk_about_dialog_set_url_hook( aboutDialogActivateLink, NULL, NULL ); gtk_show_about_dialog( parent, "name", g_get_application_name(), "comments", _("A fast and easy BitTorrent client"), "version", LONG_VERSION_STRING, "website", website_url, "website-label", website_url, "copyright",_("Copyright 2005-2008 The Transmission Project"), "logo-icon-name", "transmission", #ifdef SHOW_LICENSE "license", LICENSE, "wrap-license", TRUE, #endif "authors", authors, /* Translators: translate "translator-credits" as your name to have it appear in the credits in the "About" dialog */ "translator-credits", _("translator-credits"), NULL ); } static void startTorrentForeach (GtkTreeModel * model, GtkTreePath * path UNUSED, GtkTreeIter * iter, gpointer data UNUSED) { tr_torrent * tor = NULL; gtk_tree_model_get( model, iter, MC_TORRENT_RAW, &tor, -1 ); tr_torrentStart( tor ); } static void stopTorrentForeach (GtkTreeModel * model, GtkTreePath * path UNUSED, GtkTreeIter * iter, gpointer data UNUSED) { tr_torrent * tor = NULL; gtk_tree_model_get( model, iter, MC_TORRENT_RAW, &tor, -1 ); tr_torrentStop( tor ); } static void updateTrackerForeach (GtkTreeModel * model, GtkTreePath * path UNUSED, GtkTreeIter * iter, gpointer data UNUSED) { tr_torrent * tor = NULL; gtk_tree_model_get( model, iter, MC_TORRENT_RAW, &tor, -1 ); tr_torrentManualUpdate( tor ); } static void detailsClosed( gpointer user_data, GObject * details ) { struct cbdata * data = user_data; gpointer hashString = g_hash_table_lookup( data->details2tor, details ); g_hash_table_remove( data->details2tor, details ); g_hash_table_remove( data->tor2details, hashString ); } static void openFolderForeach( GtkTreeModel * model, GtkTreePath * path UNUSED, GtkTreeIter * iter, gpointer user_data UNUSED ) { TrTorrent * gtor = NULL; gtk_tree_model_get( model, iter, MC_TORRENT, >or, -1 ); tr_torrent_open_folder( gtor ); g_object_unref( G_OBJECT( gtor ) ); } static void showInfoForeach (GtkTreeModel * model, GtkTreePath * path UNUSED, GtkTreeIter * iter, gpointer user_data ) { const char * hashString; struct cbdata * data = user_data; TrTorrent * tor = NULL; GtkWidget * w; gtk_tree_model_get( model, iter, MC_TORRENT, &tor, -1 ); hashString = tr_torrent_info(tor)->hashString; w = g_hash_table_lookup( data->tor2details, hashString ); if( w != NULL ) gtk_window_present( GTK_WINDOW( w ) ); else { w = torrent_inspector_new( GTK_WINDOW( data->wind ), tor ); gtk_widget_show( w ); g_hash_table_insert( data->tor2details, (gpointer)hashString, w ); g_hash_table_insert( data->details2tor, w, (gpointer)hashString ); g_object_weak_ref( G_OBJECT( w ), detailsClosed, data ); } g_object_unref( G_OBJECT( tor ) ); } static void recheckTorrentForeach (GtkTreeModel * model, GtkTreePath * path UNUSED, GtkTreeIter * iter, gpointer data UNUSED) { TrTorrent * gtor = NULL; gtk_tree_model_get( model, iter, MC_TORRENT, >or, -1 ); tr_torrentVerify( tr_torrent_handle( gtor ) ); g_object_unref( G_OBJECT( gtor ) ); } static gboolean msgwinclosed( void ) { action_toggle( "toggle-message-log", FALSE ); return FALSE; } static void accumulateSelectedTorrents( GtkTreeModel * model, GtkTreePath * path UNUSED, GtkTreeIter * iter, gpointer gdata ) { GSList ** data = ( GSList** ) gdata; TrTorrent * tor = NULL; gtk_tree_model_get( model, iter, MC_TORRENT, &tor, -1 ); *data = g_slist_prepend( *data, tor ); } static void removeSelected( struct cbdata * data, gboolean delete_files ) { GSList * l = NULL; GtkTreeSelection * s = tr_window_get_selection( data->wind ); gtk_tree_selection_selected_foreach( s, accumulateSelectedTorrents, &l ); gtk_tree_selection_unselect_all( s ); if( l ) { l = g_slist_reverse( l ); confirmRemove( data->wind, data->core, l, delete_files ); } } void doAction ( const char * action_name, gpointer user_data ) { struct cbdata * data = user_data; gboolean changed = FALSE; if ( !strcmp (action_name, "add-torrent-menu") || !strcmp( action_name, "add-torrent-toolbar" )) { addDialog( data->wind, data->core ); } else if (!strcmp (action_name, "show-stats")) { GtkWidget * dialog = stats_dialog_create( data->wind, data->core ); gtk_widget_show( dialog ); } else if (!strcmp (action_name, "start-torrent")) { GtkTreeSelection * s = tr_window_get_selection(data->wind); gtk_tree_selection_selected_foreach( s, startTorrentForeach, NULL ); changed |= gtk_tree_selection_count_selected_rows( s ) != 0; } else if (!strcmp (action_name, "pause-torrent")) { GtkTreeSelection * s = tr_window_get_selection(data->wind); gtk_tree_selection_selected_foreach( s, stopTorrentForeach, NULL ); changed |= gtk_tree_selection_count_selected_rows( s ) != 0; } else if (!strcmp (action_name, "verify-torrent")) { GtkTreeSelection * s = tr_window_get_selection(data->wind); gtk_tree_selection_selected_foreach( s, recheckTorrentForeach, NULL ); changed |= gtk_tree_selection_count_selected_rows( s ) != 0; } else if (!strcmp (action_name, "open-torrent-folder")) { GtkTreeSelection * s = tr_window_get_selection(data->wind); gtk_tree_selection_selected_foreach( s, openFolderForeach, data ); } else if (!strcmp (action_name, "show-torrent-details")) { GtkTreeSelection * s = tr_window_get_selection(data->wind); gtk_tree_selection_selected_foreach( s, showInfoForeach, data ); } else if (!strcmp( action_name, "update-tracker")) { GtkTreeSelection * s = tr_window_get_selection(data->wind); gtk_tree_selection_selected_foreach( s, updateTrackerForeach, data->wind ); } else if (!strcmp (action_name, "new-torrent")) { GtkWidget * w = make_meta_ui( GTK_WINDOW( data->wind ), tr_core_handle( data->core ) ); gtk_widget_show_all( w ); } else if( !strcmp( action_name, "remove-torrent" ) ) { removeSelected( data, FALSE ); } else if( !strcmp( action_name, "delete-torrent" ) ) { removeSelected( data, TRUE ); } else if (!strcmp (action_name, "quit")) { askquit( data->core, data->wind, wannaquit, data ); } else if (!strcmp (action_name, "select-all")) { GtkTreeSelection * s = tr_window_get_selection(data->wind); gtk_tree_selection_select_all( s ); } else if (!strcmp (action_name, "deselect-all")) { GtkTreeSelection * s = tr_window_get_selection(data->wind); gtk_tree_selection_unselect_all( s ); } else if (!strcmp (action_name, "edit-preferences")) { if( NULL == data->prefs ) { data->prefs = tr_prefs_dialog_new( G_OBJECT(data->core), data->wind ); g_signal_connect( data->prefs, "destroy", G_CALLBACK( gtk_widget_destroyed ), &data->prefs ); gtk_widget_show( GTK_WIDGET( data->prefs ) ); } } else if (!strcmp (action_name, "toggle-message-log")) { if( !data->msgwin ) { GtkWidget * win = msgwin_new( data->core ); g_signal_connect( win, "destroy", G_CALLBACK( msgwinclosed ), NULL ); data->msgwin = win; } else { action_toggle("toggle-message-log", FALSE); gtk_widget_destroy( data->msgwin ); data->msgwin = NULL; } } else if (!strcmp (action_name, "show-about-dialog")) { about( data->wind ); } else if (!strcmp (action_name, "help")) { char * url = gtr_get_help_url( ); gtr_open_file( url ); g_free( url ); } else if (!strcmp (action_name, "toggle-main-window")) { toggleMainWindow( data, FALSE ); } else if (!strcmp (action_name, "present-main-window")) { toggleMainWindow( data, TRUE ); } else g_error ("Unhandled action: %s", action_name ); if( changed ) updatemodel( data ); }