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