/****************************************************************************** * Copyright (c) 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 /* exit () */ #include #include #include #include #include #include #include #include #include #include "actions.h" #include "conf.h" #include "details.h" #include "dialogs.h" #include "hig.h" #include "makemeta-ui.h" #include "msgwin.h" #include "notify.h" #include "open-dialog.h" #include "relocate.h" #include "stats.h" #include "tr-core.h" #include "tr-icon.h" #include "tr-prefs.h" #include "tr-window.h" #include "util.h" #define MY_CONFIG_NAME "transmission" #define MY_READABLE_NAME "transmission-gtk" #define SHOW_LICENSE static const char * LICENSE = "Copyright 2005-2016. All code is copyrighted by the respective authors.\n" "\n" "Transmission can be redistributed and/or modified under the terms of the " "GNU GPL versions 2 or 3 or by any future license endorsed by Mnemosyne LLC.\n" "\n" "In addition, linking to and/or using OpenSSL is allowed.\n" "\n" "This program is distributed in the hope that it will be useful, " "but WITHOUT ANY WARRANTY; without even the implied warranty of " "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n" "\n" "Some of Transmission's source files have more permissive licenses. " "Those files may, of course, be used on their own under their own terms.\n"; struct cbdata { char * config_dir; gboolean start_paused; gboolean is_iconified; gboolean is_closing; guint activation_count; guint timer; guint update_model_soon_tag; guint refresh_actions_tag; gpointer icon; GtkWindow * wind; TrCore * core; GtkWidget * msgwin; GtkWidget * prefs; GSList * error_list; GSList * duplicates_list; GSList * details; GtkTreeSelection * sel; }; static void gtr_window_present (GtkWindow * window) { gtk_window_present_with_time (window, gtk_get_current_event_time ()); } /*** **** **** DETAILS DIALOGS MANAGEMENT **** ***/ static int compare_integers (gconstpointer a, gconstpointer b) { return GPOINTER_TO_INT (a) - GPOINTER_TO_INT (b); } static char* get_details_dialog_key (GSList * id_list) { GSList * l; GSList * tmp = g_slist_sort (g_slist_copy (id_list), compare_integers); GString * gstr = g_string_new (NULL); for (l=tmp; l!=NULL; l=l->next) g_string_append_printf (gstr, "%d ", GPOINTER_TO_INT (l->data)); g_slist_free (tmp); return g_string_free (gstr, FALSE); } static void get_selected_torrent_ids_foreach (GtkTreeModel * model, GtkTreePath * p UNUSED, GtkTreeIter * iter, gpointer gdata) { int id; GSList ** ids = gdata; gtk_tree_model_get (model, iter, MC_TORRENT_ID, &id, -1); *ids = g_slist_append (*ids, GINT_TO_POINTER (id)); } static GSList* get_selected_torrent_ids (struct cbdata * data) { GSList * ids = NULL; gtk_tree_selection_selected_foreach (data->sel, get_selected_torrent_ids_foreach, &ids); return ids; } static void on_details_dialog_closed (gpointer gdata, GObject * dead) { struct cbdata * data = gdata; data->details = g_slist_remove (data->details, dead); } static void show_details_dialog_for_selected_torrents (struct cbdata * data) { GtkWidget * dialog = NULL; GSList * l; GSList * ids = get_selected_torrent_ids (data); char * key = get_details_dialog_key (ids); for (l=data->details; dialog==NULL && l!=NULL; l=l->next) if (g_strcmp0 (key, g_object_get_data (l->data, "key")) == 0) dialog = l->data; if (dialog == NULL) { dialog = gtr_torrent_details_dialog_new (GTK_WINDOW (data->wind), data->core); gtr_torrent_details_dialog_set_torrents (dialog, ids); g_object_set_data_full (G_OBJECT (dialog), "key", g_strdup (key), g_free); g_object_weak_ref (G_OBJECT (dialog), on_details_dialog_closed, data); data->details = g_slist_append (data->details, dialog); gtk_widget_show (dialog); } gtr_window_present (GTK_WINDOW (dialog)); g_free (key); g_slist_free (ids); } /**** ***** ***** ON SELECTION CHANGED ***** ****/ struct counts_data { int total_count; int queued_count; int stopped_count; }; static void get_selected_torrent_counts_foreach (GtkTreeModel * model, GtkTreePath * path UNUSED, GtkTreeIter * iter, gpointer user_data) { int activity = 0; struct counts_data * counts = user_data; ++counts->total_count; gtk_tree_model_get (model, iter, MC_ACTIVITY, &activity, -1); if ((activity == TR_STATUS_DOWNLOAD_WAIT) || (activity == TR_STATUS_SEED_WAIT)) ++counts->queued_count; if (activity == TR_STATUS_STOPPED) ++counts->stopped_count; } static void get_selected_torrent_counts (struct cbdata * data, struct counts_data * counts) { counts->total_count = 0; counts->queued_count = 0; counts->stopped_count = 0; gtk_tree_selection_selected_foreach (data->sel, get_selected_torrent_counts_foreach, counts); } static void count_updatable_foreach (GtkTreeModel * model, GtkTreePath * path UNUSED, GtkTreeIter * iter, gpointer accumulated_status) { tr_torrent * tor; gtk_tree_model_get (model, iter, MC_TORRENT, &tor, -1); *(int*)accumulated_status |= tr_torrentCanManualUpdate (tor); } static gboolean refresh_actions (gpointer gdata) { struct cbdata * data = gdata; if (!data->is_closing) { int canUpdate; struct counts_data sel_counts; const size_t total = gtr_core_get_torrent_count (data->core); const size_t active = gtr_core_get_active_torrent_count (data->core); const int torrent_count = gtk_tree_model_iter_n_children (gtr_core_model (data->core), NULL); bool has_selection; get_selected_torrent_counts (data, &sel_counts); has_selection = sel_counts.total_count > 0; gtr_action_set_sensitive ("select-all", torrent_count != 0); gtr_action_set_sensitive ("deselect-all", torrent_count != 0); gtr_action_set_sensitive ("pause-all-torrents", active != 0); gtr_action_set_sensitive ("start-all-torrents", active != total); gtr_action_set_sensitive ("torrent-stop", (sel_counts.stopped_count < sel_counts.total_count)); gtr_action_set_sensitive ("torrent-start", (sel_counts.stopped_count) > 0); gtr_action_set_sensitive ("torrent-start-now", (sel_counts.stopped_count + sel_counts.queued_count) > 0); gtr_action_set_sensitive ("torrent-verify", has_selection); gtr_action_set_sensitive ("remove-torrent", has_selection); gtr_action_set_sensitive ("delete-torrent", has_selection); gtr_action_set_sensitive ("relocate-torrent", has_selection); gtr_action_set_sensitive ("queue-move-top", has_selection); gtr_action_set_sensitive ("queue-move-up", has_selection); gtr_action_set_sensitive ("queue-move-down", has_selection); gtr_action_set_sensitive ("queue-move-bottom", has_selection); gtr_action_set_sensitive ("show-torrent-properties", has_selection); gtr_action_set_sensitive ("open-torrent-folder", sel_counts.total_count == 1); gtr_action_set_sensitive ("copy-magnet-link-to-clipboard", sel_counts.total_count == 1); canUpdate = 0; gtk_tree_selection_selected_foreach (data->sel, count_updatable_foreach, &canUpdate); gtr_action_set_sensitive ("torrent-reannounce", canUpdate != 0); } data->refresh_actions_tag = 0; return G_SOURCE_REMOVE; } static void refresh_actions_soon (gpointer gdata) { struct cbdata * data = gdata; if (!data->is_closing && !data->refresh_actions_tag) data->refresh_actions_tag = gdk_threads_add_idle (refresh_actions, data); } static void on_selection_changed (GtkTreeSelection * s UNUSED, gpointer gdata) { refresh_actions_soon (gdata); } /*** **** ***/ static gboolean has_magnet_link_handler (void) { GAppInfo * app_info = g_app_info_get_default_for_uri_scheme ("magnet"); const gboolean has_handler = app_info != NULL; g_clear_object (&app_info); return has_handler; } static void register_magnet_link_handler (void) { GError * error; GAppInfo * app; const char * const content_type = "x-scheme-handler/magnet"; error = NULL; app = g_app_info_create_from_commandline ("transmission-gtk", "transmission-gtk", G_APP_INFO_CREATE_SUPPORTS_URIS, &error); g_app_info_set_as_default_for_type (app, content_type, &error); if (error != NULL) { g_warning (_("Error registering Transmission as a %s handler: %s"), content_type, error->message); g_error_free (error); } g_clear_object (&app); } static void ensure_magnet_handler_exists (void) { if (!has_magnet_link_handler ()) register_magnet_link_handler (); } static void on_main_window_size_allocated (GtkWidget * gtk_window, GtkAllocation * alloc UNUSED, gpointer gdata UNUSED) { GdkWindow * gdk_window = gtk_widget_get_window (gtk_window); const gboolean isMaximized = (gdk_window != NULL) && (gdk_window_get_state (gdk_window) & GDK_WINDOW_STATE_MAXIMIZED); gtr_pref_int_set (TR_KEY_main_window_is_maximized, isMaximized); if (!isMaximized) { int x, y, w, h; gtk_window_get_position (GTK_WINDOW (gtk_window), &x, &y); gtk_window_get_size (GTK_WINDOW (gtk_window), &w, &h); gtr_pref_int_set (TR_KEY_main_window_x, x); gtr_pref_int_set (TR_KEY_main_window_y, y); gtr_pref_int_set (TR_KEY_main_window_width, w); gtr_pref_int_set (TR_KEY_main_window_height, h); } } /*** **** listen to changes that come from RPC ***/ struct on_rpc_changed_struct { TrCore * core; tr_rpc_callback_type type; int torrent_id; }; static gboolean on_rpc_changed_idle (gpointer gdata) { tr_torrent * tor; struct on_rpc_changed_struct * data = gdata; switch (data->type) { case TR_RPC_SESSION_CLOSE: gtr_action_activate ("quit"); break; case TR_RPC_TORRENT_ADDED: if ((tor = gtr_core_find_torrent (data->core, data->torrent_id))) gtr_core_add_torrent (data->core, tor, true); break; case TR_RPC_TORRENT_REMOVING: gtr_core_remove_torrent (data->core, data->torrent_id, false); break; case TR_RPC_TORRENT_TRASHING: gtr_core_remove_torrent (data->core, data->torrent_id, true); break; case TR_RPC_SESSION_CHANGED: { int i; tr_variant tmp; tr_variant * newval; tr_variant * oldvals = gtr_pref_get_all (); tr_quark key; GSList * l; GSList * changed_keys = NULL; tr_session * session = gtr_core_session (data->core); tr_variantInitDict (&tmp, 100); tr_sessionGetSettings (session, &tmp); for (i=0; tr_variantDictChild (&tmp, i, &key, &newval); ++i) { bool changed; tr_variant * oldval = tr_variantDictFind (oldvals, key); if (!oldval) { changed = true; } else { char * a = tr_variantToStr (oldval, TR_VARIANT_FMT_BENC, NULL); char * b = tr_variantToStr (newval, TR_VARIANT_FMT_BENC, NULL); changed = g_strcmp0 (a, b) != 0; tr_free (b); tr_free (a); } if (changed) changed_keys = g_slist_append (changed_keys, GINT_TO_POINTER(key)); } tr_sessionGetSettings (session, oldvals); for (l=changed_keys; l!=NULL; l=l->next) gtr_core_pref_changed (data->core, GPOINTER_TO_INT(l->data)); g_slist_free (changed_keys); tr_variantFree (&tmp); break; } case TR_RPC_TORRENT_CHANGED: case TR_RPC_TORRENT_MOVED: case TR_RPC_TORRENT_STARTED: case TR_RPC_TORRENT_STOPPED: case TR_RPC_SESSION_QUEUE_POSITIONS_CHANGED: /* nothing interesting to do here */ break; } g_free (data); return G_SOURCE_REMOVE; } static tr_rpc_callback_status on_rpc_changed (tr_session * session G_GNUC_UNUSED, tr_rpc_callback_type type, struct tr_torrent * tor, void * gdata) { struct cbdata * cbdata = gdata; struct on_rpc_changed_struct * data; data = g_new (struct on_rpc_changed_struct, 1); data->core = cbdata->core; data->type = type; data->torrent_id = tr_torrentId (tor); gdk_threads_add_idle (on_rpc_changed_idle, data); return TR_RPC_NOREMOVE; } /*** **** signal handling ***/ static sig_atomic_t global_sigcount = 0; static struct cbdata * sighandler_cbdata = NULL; static void signal_handler (int sig) { if (++global_sigcount > 1) { signal (sig, SIG_DFL); raise (sig); } else if ((sig == SIGINT) || (sig == SIGTERM)) { g_message (_("Got signal %d; trying to shut down cleanly. Do it again if it gets stuck."), sig); gtr_actions_handler ("quit", sighandler_cbdata); } } /**** ***** ***** ****/ static void app_setup (GtkWindow * wind, struct cbdata * cbdata); static void on_startup (GApplication * application, gpointer user_data) { GError * error; const char * str; GtkWindow * win; GtkUIManager * ui_manager; tr_session * session; struct cbdata * cbdata = user_data; signal (SIGINT, signal_handler); signal (SIGTERM, signal_handler); sighandler_cbdata = cbdata; /* ensure the directories are created */ if ((str = gtr_pref_string_get (TR_KEY_download_dir))) g_mkdir_with_parents (str, 0777); if ((str = gtr_pref_string_get (TR_KEY_incomplete_dir))) g_mkdir_with_parents (str, 0777); /* initialize the libtransmission session */ session = tr_sessionInit (cbdata->config_dir, TRUE, gtr_pref_get_all ()); gtr_pref_flag_set (TR_KEY_alt_speed_enabled, tr_sessionUsesAltSpeed (session)); gtr_pref_int_set (TR_KEY_peer_port, tr_sessionGetPeerPort (session)); cbdata->core = gtr_core_new (session); /* init the ui manager */ error = NULL; ui_manager = gtk_ui_manager_new (); gtr_actions_init (ui_manager, cbdata); gtk_ui_manager_add_ui_from_resource (ui_manager, TR_RESOURCE_PATH "transmission-ui.xml", &error); g_assert_no_error (error); gtk_ui_manager_ensure_update (ui_manager); /* create main window now to be a parent to any error dialogs */ win = GTK_WINDOW (gtr_window_new (GTK_APPLICATION (application), ui_manager, cbdata->core)); g_signal_connect (win, "size-allocate", G_CALLBACK (on_main_window_size_allocated), cbdata); g_application_hold (application); g_object_weak_ref (G_OBJECT (win), (GWeakNotify)g_application_release, application); app_setup (win, cbdata); tr_sessionSetRPCCallback (session, on_rpc_changed, cbdata); /* check & see if it's time to update the blocklist */ if (gtr_pref_flag_get (TR_KEY_blocklist_enabled)) { if (gtr_pref_flag_get (TR_KEY_blocklist_updates_enabled)) { const int64_t last_time = gtr_pref_int_get (TR_KEY_blocklist_date); const int SECONDS_IN_A_WEEK = 7 * 24 * 60 * 60; const time_t now = time (NULL); if (last_time + SECONDS_IN_A_WEEK < now) gtr_core_blocklist_update (cbdata->core); } } /* if there's no magnet link handler registered, register us */ ensure_magnet_handler_exists (); } static void on_activate (GApplication * app UNUSED, struct cbdata * cbdata) { cbdata->activation_count++; /* GApplication emits an 'activate' signal when bootstrapping the primary. * Ordinarily we handle that by presenting the main window, but if the user * user started Transmission minimized, ignore that initial signal... */ if (cbdata->is_iconified && (cbdata->activation_count == 1)) return; gtr_action_activate ("present-main-window"); } static void open_files (GSList * files, gpointer gdata) { struct cbdata * cbdata = gdata; const gboolean do_start = gtr_pref_flag_get (TR_KEY_start_added_torrents) && !cbdata->start_paused; const gboolean do_prompt = gtr_pref_flag_get (TR_KEY_show_options_window); const gboolean do_notify = TRUE; gtr_core_add_files (cbdata->core, files, do_start, do_prompt, do_notify); } static void on_open (GApplication * application UNUSED, GFile ** f, gint file_count, gchar * hint UNUSED, gpointer gdata) { int i; GSList * files = NULL; for (i=0; imessage, argv[0]); g_error_free (error); g_option_context_free (option_context); return 1; } g_option_context_free (option_context); /* handle the trivial "version" option */ if (show_version) { fprintf (stderr, "%s %s\n", MY_READABLE_NAME, LONG_VERSION_STRING); return 0; } gtk_window_set_default_icon_name (MY_CONFIG_NAME); /* init the unit formatters */ tr_formatter_mem_init (mem_K, _ (mem_K_str), _ (mem_M_str), _ (mem_G_str), _ (mem_T_str)); tr_formatter_size_init (disk_K, _ (disk_K_str), _ (disk_M_str), _ (disk_G_str), _ (disk_T_str)); tr_formatter_speed_init (speed_K, _ (speed_K_str), _ (speed_M_str), _ (speed_G_str), _ (speed_T_str)); /* set up the config dir */ gtr_pref_init (cbdata.config_dir); g_mkdir_with_parents (cbdata.config_dir, 0755); /* init notifications */ gtr_notify_init (); /* init the application for the specified config dir */ stat (cbdata.config_dir, &sb); application_id = g_strdup_printf ("com.transmissionbt.transmission_%lu_%lu", (unsigned long)sb.st_dev, (unsigned long)sb.st_ino); app = gtk_application_new (application_id, G_APPLICATION_HANDLES_OPEN); g_signal_connect (app, "open", G_CALLBACK (on_open), &cbdata); g_signal_connect (app, "startup", G_CALLBACK (on_startup), &cbdata); g_signal_connect (app, "activate", G_CALLBACK (on_activate), &cbdata); ret = g_application_run (G_APPLICATION (app), argc, argv); g_object_unref (app); g_free (application_id); return ret; } static void on_core_busy (TrCore * core UNUSED, gboolean busy, struct cbdata * c) { gtr_window_set_busy (c->wind, busy); } static void on_core_error (TrCore *, guint, const char *, struct cbdata *); static void on_add_torrent (TrCore *, tr_ctor *, gpointer); static void on_prefs_changed (TrCore * core, const tr_quark key, gpointer); static void main_window_setup (struct cbdata * cbdata, GtkWindow * wind); static gboolean update_model_loop (gpointer gdata); static gboolean update_model_once (gpointer gdata); static void app_setup (GtkWindow * wind, struct cbdata * cbdata) { if (cbdata->is_iconified) gtr_pref_flag_set (TR_KEY_show_notification_area_icon, TRUE); gtr_actions_set_core (cbdata->core); /* set up core handlers */ g_signal_connect (cbdata->core, "busy", G_CALLBACK (on_core_busy), cbdata); g_signal_connect (cbdata->core, "add-error", G_CALLBACK (on_core_error), cbdata); g_signal_connect (cbdata->core, "add-prompt", G_CALLBACK (on_add_torrent), cbdata); g_signal_connect (cbdata->core, "prefs-changed", G_CALLBACK (on_prefs_changed), cbdata); /* add torrents from command-line and saved state */ gtr_core_load (cbdata->core, cbdata->start_paused); gtr_core_torrents_added (cbdata->core); /* set up main window */ main_window_setup (cbdata, wind); /* set up the icon */ on_prefs_changed (cbdata->core, TR_KEY_show_notification_area_icon, cbdata); /* start model update timer */ cbdata->timer = gdk_threads_add_timeout_seconds (MAIN_WINDOW_REFRESH_INTERVAL_SECONDS, update_model_loop, cbdata); update_model_once (cbdata); /* either show the window or iconify it */ if (!cbdata->is_iconified) { gtk_widget_show (GTK_WIDGET (wind)); } else { gtk_window_set_skip_taskbar_hint (cbdata->wind, cbdata->icon != NULL); cbdata->is_iconified = FALSE; // ensure that the next toggle iconifies gtr_action_set_toggled ("toggle-main-window", FALSE); } if (!gtr_pref_flag_get (TR_KEY_user_has_given_informed_consent)) { GtkWidget * w = gtk_message_dialog_new (GTK_WINDOW (wind), GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_OTHER, GTK_BUTTONS_NONE, "%s", _("Transmission is a file sharing program. When you run a torrent, its data will be made available to others by means of upload. Any content you share is your sole responsibility.")); gtk_dialog_add_button (GTK_DIALOG (w), GTK_STOCK_CANCEL, GTK_RESPONSE_REJECT); gtk_dialog_add_button (GTK_DIALOG (w), _("I _Agree"), GTK_RESPONSE_ACCEPT); gtk_dialog_set_default_response (GTK_DIALOG (w), GTK_RESPONSE_ACCEPT); switch (gtk_dialog_run (GTK_DIALOG (w))) { case GTK_RESPONSE_ACCEPT: /* only show it once */ gtr_pref_flag_set (TR_KEY_user_has_given_informed_consent, TRUE); gtk_widget_destroy (w); break; default: exit (0); } } } static void presentMainWindow (struct cbdata * cbdata) { GtkWindow * window = cbdata->wind; if (cbdata->is_iconified) { cbdata->is_iconified = false; gtk_window_set_skip_taskbar_hint (window, FALSE); } if (!gtk_widget_get_visible (GTK_WIDGET (window))) { gtk_window_resize (window, gtr_pref_int_get (TR_KEY_main_window_width), gtr_pref_int_get (TR_KEY_main_window_height)); gtk_window_move (window, gtr_pref_int_get (TR_KEY_main_window_x), gtr_pref_int_get (TR_KEY_main_window_y)); gtr_widget_set_visible (GTK_WIDGET (window), TRUE); } gtr_window_present (window); gdk_window_raise (gtk_widget_get_window (GTK_WIDGET(window))); } static void hideMainWindow (struct cbdata * cbdata) { GtkWindow * window = cbdata->wind; gtk_window_set_skip_taskbar_hint (window, TRUE); gtr_widget_set_visible (GTK_WIDGET (window), FALSE); cbdata->is_iconified = true; } static void toggleMainWindow (struct cbdata * cbdata) { if (cbdata->is_iconified) presentMainWindow (cbdata); else hideMainWindow (cbdata); } static void on_app_exit (gpointer vdata); static gboolean winclose (GtkWidget * w UNUSED, GdkEvent * event UNUSED, gpointer gdata) { struct cbdata * cbdata = gdata; if (cbdata->icon != NULL) gtr_action_activate ("toggle-main-window"); else on_app_exit (cbdata); return TRUE; /* don't propagate event further */ } static void rowChangedCB (GtkTreeModel * model UNUSED, GtkTreePath * path, GtkTreeIter * iter UNUSED, gpointer gdata) { struct cbdata * data = gdata; if (gtk_tree_selection_path_is_selected (data->sel, path)) refresh_actions_soon (data); } static void on_drag_data_received (GtkWidget * widget UNUSED, GdkDragContext * drag_context, gint x UNUSED, gint y UNUSED, GtkSelectionData * selection_data, guint info UNUSED, guint time_, gpointer gdata) { guint i; char ** uris = gtk_selection_data_get_uris (selection_data); const guint file_count = g_strv_length (uris); GSList * files = NULL; for (i=0; iwind); cbdata->wind = wind; cbdata->sel = sel = GTK_TREE_SELECTION (gtr_window_get_selection (cbdata->wind)); g_signal_connect (sel, "changed", G_CALLBACK (on_selection_changed), cbdata); on_selection_changed (sel, cbdata); model = gtr_core_model (cbdata->core); g_signal_connect (model, "row-changed", G_CALLBACK (rowChangedCB), cbdata); g_signal_connect (wind, "delete-event", G_CALLBACK (winclose), cbdata); refresh_actions (cbdata); /* register to handle URIs that get dragged onto our main window */ w = GTK_WIDGET (wind); gtk_drag_dest_set (w, GTK_DEST_DEFAULT_ALL, NULL, 0, GDK_ACTION_COPY); gtk_drag_dest_add_uri_targets (w); g_signal_connect (w, "drag-data-received", G_CALLBACK (on_drag_data_received), cbdata); } static gboolean on_session_closed (gpointer gdata) { GSList * tmp; struct cbdata * cbdata = gdata; tmp = g_slist_copy (cbdata->details); g_slist_foreach (tmp, (GFunc)gtk_widget_destroy, NULL); g_slist_free (tmp); 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); g_slist_foreach (cbdata->error_list, (GFunc)g_free, NULL); g_slist_free (cbdata->error_list); g_slist_foreach (cbdata->duplicates_list, (GFunc)g_free, NULL); g_slist_free (cbdata->duplicates_list); return G_SOURCE_REMOVE; } struct session_close_struct { tr_session * session; struct cbdata * cbdata; }; /* since tr_sessionClose () is a blocking function, * delegate its call to another thread here... when it's done, * punt the GUI teardown back to the GTK+ thread */ static gpointer session_close_threadfunc (gpointer gdata) { struct session_close_struct * data = gdata; tr_sessionClose (data->session); gdk_threads_add_idle (on_session_closed, data->cbdata); g_free (data); return NULL; } static void exit_now_cb (GtkWidget *w UNUSED, gpointer data UNUSED) { exit (0); } static void on_app_exit (gpointer vdata) { GtkWidget *r, *p, *b, *w, *c; struct cbdata * cbdata = vdata; struct session_close_struct * session_close_data; if (cbdata->is_closing) return; cbdata->is_closing = true; /* stop the update timer */ if (cbdata->timer) { g_source_remove (cbdata->timer); cbdata->timer = 0; } /* stop the refresh-actions timer */ if (cbdata->refresh_actions_tag) { g_source_remove (cbdata->refresh_actions_tag); cbdata->refresh_actions_tag = 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_grid_new (); gtk_grid_set_column_spacing (GTK_GRID (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_grid_attach (GTK_GRID (p), w, 0, 0, 1, 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_grid_attach (GTK_GRID (p), w, 1, 0, 1, 1); w = gtk_label_new (_("Sending upload/download totals to tracker…")); gtk_misc_set_alignment (GTK_MISC (w), 0.0, 0.5); gtk_grid_attach (GTK_GRID (p), w, 1, 1, 1, 1); b = gtk_alignment_new (0.0, 1.0, 0.01, 0.01); w = gtk_button_new_with_mnemonic (_("_Quit Now")); g_signal_connect (w, "clicked", G_CALLBACK (exit_now_cb), NULL); gtk_container_add (GTK_CONTAINER (b), w); gtk_grid_attach (GTK_GRID (p), b, 1, 2, 1, 1); gtk_widget_show_all (r); gtk_widget_grab_focus (w); /* clear the UI */ gtr_core_clear (cbdata->core); /* ensure the window is in its previous position & size. * this seems to be necessary because changing the main window's * child seems to unset the size */ gtk_window_resize (cbdata->wind, gtr_pref_int_get (TR_KEY_main_window_width), gtr_pref_int_get (TR_KEY_main_window_height)); gtk_window_move (cbdata->wind, gtr_pref_int_get (TR_KEY_main_window_x), gtr_pref_int_get (TR_KEY_main_window_y)); /* shut down libT */ session_close_data = g_new (struct session_close_struct, 1); session_close_data->cbdata = cbdata; session_close_data->session = gtr_core_close (cbdata->core); g_thread_new ("shutdown-thread", session_close_threadfunc, session_close_data); } static void show_torrent_errors (GtkWindow * window, const char * primary, GSList ** files) { GSList * l; GtkWidget * w; GString * s = g_string_new (NULL); const char * leader = g_slist_length (*files) > 1 ? gtr_get_unicode_string (GTR_UNICODE_BULLET) : ""; for (l=*files; l!=NULL; l=l->next) g_string_append_printf (s, "%s %s\n", leader, (const char*)l->data); w = gtk_message_dialog_new (window, GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "%s", primary); gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (w), "%s", s->str); g_signal_connect_swapped (w, "response", G_CALLBACK (gtk_widget_destroy), w); gtk_widget_show (w); g_string_free (s, TRUE); g_slist_foreach (*files, (GFunc)g_free, NULL); g_slist_free (*files); *files = NULL; } static void flush_torrent_errors (struct cbdata * cbdata) { if (cbdata->error_list) show_torrent_errors (cbdata->wind, ngettext ("Couldn't add corrupt torrent", "Couldn't add corrupt torrents", g_slist_length (cbdata->error_list)), &cbdata->error_list); if (cbdata->duplicates_list) show_torrent_errors (cbdata->wind, ngettext ("Couldn't add duplicate torrent", "Couldn't add duplicate torrents", g_slist_length (cbdata->duplicates_list)), &cbdata->duplicates_list); } static void on_core_error (TrCore * core UNUSED, guint code, const char * msg, struct cbdata * c) { switch (code) { case TR_PARSE_ERR: c->error_list = g_slist_append (c->error_list, g_path_get_basename (msg)); break; case TR_PARSE_DUPLICATE: c->duplicates_list = g_slist_append (c->duplicates_list, g_strdup (msg)); break; case TR_CORE_ERR_NO_MORE_TORRENTS: flush_torrent_errors (c); break; default: g_assert_not_reached (); break; } } static gboolean on_main_window_focus_in (GtkWidget * widget UNUSED, GdkEventFocus * event UNUSED, gpointer gdata) { struct cbdata * cbdata = gdata; if (cbdata->wind) gtk_window_set_urgency_hint (cbdata->wind, FALSE); return FALSE; } static void on_add_torrent (TrCore * core, tr_ctor * ctor, gpointer gdata) { struct cbdata * cbdata = gdata; GtkWidget * w = gtr_torrent_options_dialog_new (cbdata->wind, core, ctor); g_signal_connect (w, "focus-in-event", G_CALLBACK (on_main_window_focus_in), cbdata); if (cbdata->wind) gtk_window_set_urgency_hint (cbdata->wind, TRUE); gtk_widget_show (w); } static void on_prefs_changed (TrCore * core UNUSED, const tr_quark key, gpointer data) { struct cbdata * cbdata = data; tr_session * tr = gtr_core_session (cbdata->core); switch (key) { case TR_KEY_encryption: tr_sessionSetEncryption (tr, gtr_pref_int_get (key)); break; case TR_KEY_download_dir: tr_sessionSetDownloadDir (tr, gtr_pref_string_get (key)); break; case TR_KEY_message_level: tr_logSetLevel (gtr_pref_int_get (key)); break; case TR_KEY_peer_port: tr_sessionSetPeerPort (tr, gtr_pref_int_get (key)); break; case TR_KEY_blocklist_enabled: tr_blocklistSetEnabled (tr, gtr_pref_flag_get (key)); break; case TR_KEY_blocklist_url: tr_blocklistSetURL (tr, gtr_pref_string_get (key)); break; case TR_KEY_show_notification_area_icon: { const bool show = gtr_pref_flag_get (key); if (show && !cbdata->icon) cbdata->icon = gtr_icon_new (cbdata->core); else if (!show && cbdata->icon) g_clear_object (&cbdata->icon); break; } case TR_KEY_speed_limit_down_enabled: tr_sessionLimitSpeed (tr, TR_DOWN, gtr_pref_flag_get (key)); break; case TR_KEY_speed_limit_down: tr_sessionSetSpeedLimit_KBps (tr, TR_DOWN, gtr_pref_int_get (key)); break; case TR_KEY_speed_limit_up_enabled: tr_sessionLimitSpeed (tr, TR_UP, gtr_pref_flag_get (key)); break; case TR_KEY_speed_limit_up: tr_sessionSetSpeedLimit_KBps (tr, TR_UP, gtr_pref_int_get (key)); break; case TR_KEY_ratio_limit_enabled: tr_sessionSetRatioLimited (tr, gtr_pref_flag_get (key)); break; case TR_KEY_ratio_limit: tr_sessionSetRatioLimit (tr, gtr_pref_double_get (key)); break; case TR_KEY_idle_seeding_limit: tr_sessionSetIdleLimit (tr, gtr_pref_int_get (key)); break; case TR_KEY_idle_seeding_limit_enabled: tr_sessionSetIdleLimited (tr, gtr_pref_flag_get (key)); break; case TR_KEY_port_forwarding_enabled: tr_sessionSetPortForwardingEnabled (tr, gtr_pref_flag_get (key)); break; case TR_KEY_pex_enabled: tr_sessionSetPexEnabled (tr, gtr_pref_flag_get (key)); break; case TR_KEY_rename_partial_files: tr_sessionSetIncompleteFileNamingEnabled (tr, gtr_pref_flag_get (key)); break; case TR_KEY_download_queue_size: tr_sessionSetQueueSize (tr, TR_DOWN, gtr_pref_int_get (key)); break; case TR_KEY_queue_stalled_minutes: tr_sessionSetQueueStalledMinutes (tr, gtr_pref_int_get (key)); break; case TR_KEY_dht_enabled: tr_sessionSetDHTEnabled (tr, gtr_pref_flag_get (key)); break; case TR_KEY_utp_enabled: tr_sessionSetUTPEnabled (tr, gtr_pref_flag_get (key)); break; case TR_KEY_lpd_enabled: tr_sessionSetLPDEnabled (tr, gtr_pref_flag_get (key)); break; case TR_KEY_rpc_port: tr_sessionSetRPCPort (tr, gtr_pref_int_get (key)); break; case TR_KEY_rpc_enabled: tr_sessionSetRPCEnabled (tr, gtr_pref_flag_get (key)); break; case TR_KEY_rpc_whitelist: tr_sessionSetRPCWhitelist (tr, gtr_pref_string_get (key)); break; case TR_KEY_rpc_whitelist_enabled: tr_sessionSetRPCWhitelistEnabled (tr, gtr_pref_flag_get (key)); break; case TR_KEY_rpc_username: tr_sessionSetRPCUsername (tr, gtr_pref_string_get (key)); break; case TR_KEY_rpc_password: tr_sessionSetRPCPassword (tr, gtr_pref_string_get (key)); break; case TR_KEY_rpc_authentication_required: tr_sessionSetRPCPasswordEnabled (tr, gtr_pref_flag_get (key)); break; case TR_KEY_alt_speed_up: tr_sessionSetAltSpeed_KBps (tr, TR_UP, gtr_pref_int_get (key)); break; case TR_KEY_alt_speed_down: tr_sessionSetAltSpeed_KBps (tr, TR_DOWN, gtr_pref_int_get (key)); break; case TR_KEY_alt_speed_enabled: { const bool b = gtr_pref_flag_get (key); tr_sessionUseAltSpeed (tr, b); gtr_action_set_toggled (tr_quark_get_string(key,NULL), b); break; } case TR_KEY_alt_speed_time_begin: tr_sessionSetAltSpeedBegin (tr, gtr_pref_int_get (key)); break; case TR_KEY_alt_speed_time_end: tr_sessionSetAltSpeedEnd (tr, gtr_pref_int_get (key)); break; case TR_KEY_alt_speed_time_enabled: tr_sessionUseAltSpeedTime (tr, gtr_pref_flag_get (key)); break; case TR_KEY_alt_speed_time_day: tr_sessionSetAltSpeedDay (tr, gtr_pref_int_get (key)); break; case TR_KEY_peer_port_random_on_start: tr_sessionSetPeerPortRandomOnStart (tr, gtr_pref_flag_get (key)); break; case TR_KEY_incomplete_dir: tr_sessionSetIncompleteDir (tr, gtr_pref_string_get (key)); break; case TR_KEY_incomplete_dir_enabled: tr_sessionSetIncompleteDirEnabled (tr, gtr_pref_flag_get (key)); break; case TR_KEY_script_torrent_done_enabled: tr_sessionSetTorrentDoneScriptEnabled (tr, gtr_pref_flag_get (key)); break; case TR_KEY_script_torrent_done_filename: tr_sessionSetTorrentDoneScript (tr, gtr_pref_string_get (key)); break; case TR_KEY_start_added_torrents: tr_sessionSetPaused (tr, !gtr_pref_flag_get (key)); break; case TR_KEY_trash_original_torrent_files: tr_sessionSetDeleteSource (tr, gtr_pref_flag_get (key)); break; default: break; } } static gboolean update_model_once (gpointer gdata) { struct cbdata *data = gdata; /* update the torrent data in the model */ gtr_core_update (data->core); /* refresh the main window's statusbar and toolbar buttons */ if (data->wind != NULL) gtr_window_refresh (data->wind); /* update the actions */ refresh_actions (data); /* update the status tray icon */ if (data->icon != NULL) gtr_icon_refresh (data->icon); data->update_model_soon_tag = 0; return G_SOURCE_REMOVE; } static void update_model_soon (gpointer gdata) { struct cbdata *data = gdata; if (data->update_model_soon_tag == 0) data->update_model_soon_tag = gdk_threads_add_idle (update_model_once, data); } static gboolean update_model_loop (gpointer gdata) { const gboolean done = global_sigcount; if (!done) update_model_once (gdata); return !done; } static void show_about_dialog (GtkWindow * parent) { const char * uri = "https://transmissionbt.com/"; const char * authors[] = { "Jordan Lee (Backend; GTK+)", "Mitchell Livingston (Backend; OS X)", "Mike Gelfand", NULL }; gtk_show_about_dialog (parent, "authors", authors, "comments", _("A fast and easy BitTorrent client"), "copyright", _("Copyright (c) The Transmission Project"), "logo-icon-name", MY_CONFIG_NAME, "name", g_get_application_name (), /* Translators: translate "translator-credits" as your name to have it appear in the credits in the "About" dialog */ "translator-credits", _("translator-credits"), "version", LONG_VERSION_STRING, "website", uri, "website-label", uri, #ifdef SHOW_LICENSE "license", LICENSE, "wrap-license", TRUE, #endif NULL); } static void append_id_to_benc_list (GtkTreeModel * m, GtkTreePath * path UNUSED, GtkTreeIter * iter, gpointer list) { tr_torrent * tor = NULL; gtk_tree_model_get (m, iter, MC_TORRENT, &tor, -1); tr_variantListAddInt (list, tr_torrentId (tor)); } static gboolean call_rpc_for_selected_torrents (struct cbdata * data, const char * method) { tr_variant top, *args, *ids; gboolean invoked = FALSE; GtkTreeSelection * s = data->sel; tr_session * session = gtr_core_session (data->core); tr_variantInitDict (&top, 2); tr_variantDictAddStr (&top, TR_KEY_method, method); args = tr_variantDictAddDict (&top, TR_KEY_arguments, 1); ids = tr_variantDictAddList (args, TR_KEY_ids, 0); gtk_tree_selection_selected_foreach (s, append_id_to_benc_list, ids); if (tr_variantListSize (ids) != 0) { tr_rpc_request_exec_json (session, &top, NULL, NULL); invoked = TRUE; } tr_variantFree (&top); return invoked; } static void open_folder_foreach (GtkTreeModel * model, GtkTreePath * path UNUSED, GtkTreeIter * iter, gpointer core) { int id; gtk_tree_model_get (model, iter, MC_TORRENT_ID, &id, -1); gtr_core_open_folder (core, id); } static gboolean on_message_window_closed (void) { gtr_action_set_toggled ("toggle-message-log", FALSE); return FALSE; } static void accumulate_selected_torrents (GtkTreeModel * model, GtkTreePath * path UNUSED, GtkTreeIter * iter, gpointer gdata) { int id; GSList ** data = gdata; gtk_tree_model_get (model, iter, MC_TORRENT_ID, &id, -1); *data = g_slist_append (*data, GINT_TO_POINTER (id)); } static void remove_selected (struct cbdata * data, gboolean delete_files) { GSList * l = NULL; gtk_tree_selection_selected_foreach (data->sel, accumulate_selected_torrents, &l); if (l != NULL) gtr_confirm_remove (data->wind, data->core, l, delete_files); } static void start_all_torrents (struct cbdata * data) { tr_session * session = gtr_core_session (data->core); tr_variant request; tr_variantInitDict (&request, 1); tr_variantDictAddStr (&request, TR_KEY_method, "torrent-start"); tr_rpc_request_exec_json (session, &request, NULL, NULL); tr_variantFree (&request); } static void pause_all_torrents (struct cbdata * data) { tr_session * session = gtr_core_session (data->core); tr_variant request; tr_variantInitDict (&request, 1); tr_variantDictAddStr (&request, TR_KEY_method, "torrent-stop"); tr_rpc_request_exec_json (session, &request, NULL, NULL); tr_variantFree (&request); } static tr_torrent* get_first_selected_torrent (struct cbdata * data) { tr_torrent * tor = NULL; GtkTreeModel * m; GList * l = gtk_tree_selection_get_selected_rows (data->sel, &m); if (l != NULL) { GtkTreePath * p = l->data; GtkTreeIter i; if (gtk_tree_model_get_iter (m, &i, p)) gtk_tree_model_get (m, &i, MC_TORRENT, &tor, -1); } g_list_foreach (l, (GFunc)gtk_tree_path_free, NULL); g_list_free (l); return tor; } static void copy_magnet_link_to_clipboard (GtkWidget * w, tr_torrent * tor) { char * magnet = tr_torrentGetMagnetLink (tor); GdkDisplay * display = gtk_widget_get_display (w); GdkAtom selection; GtkClipboard * clipboard; /* this is The Right Thing for copy/paste... */ selection = GDK_SELECTION_CLIPBOARD; clipboard = gtk_clipboard_get_for_display (display, selection); gtk_clipboard_set_text (clipboard, magnet, -1); /* ...but people using plain ol' X need this instead */ selection = GDK_SELECTION_PRIMARY; clipboard = gtk_clipboard_get_for_display (display, selection); gtk_clipboard_set_text (clipboard, magnet, -1); /* cleanup */ tr_free (magnet); } void gtr_actions_handler (const char * action_name, gpointer user_data) { gboolean changed = FALSE; struct cbdata * data = user_data; if (g_strcmp0 (action_name, "open-torrent-from-url") == 0) { GtkWidget * w = gtr_torrent_open_from_url_dialog_new (data->wind, data->core); gtk_widget_show (w); } else if (g_strcmp0 (action_name, "open-torrent-menu") == 0 || g_strcmp0 (action_name, "open-torrent-toolbar") == 0) { GtkWidget * w = gtr_torrent_open_from_file_dialog_new (data->wind, data->core); gtk_widget_show (w); } else if (g_strcmp0 (action_name, "show-stats") == 0) { GtkWidget * dialog = gtr_stats_dialog_new (data->wind, data->core); gtk_widget_show (dialog); } else if (g_strcmp0 (action_name, "donate") == 0) { gtr_open_uri ("https://transmissionbt.com/donate/"); } else if (g_strcmp0 (action_name, "pause-all-torrents") == 0) { pause_all_torrents (data); } else if (g_strcmp0 (action_name, "start-all-torrents") == 0) { start_all_torrents (data); } else if (g_strcmp0 (action_name, "copy-magnet-link-to-clipboard") == 0) { tr_torrent * tor = get_first_selected_torrent (data); if (tor != NULL) { copy_magnet_link_to_clipboard (GTK_WIDGET (data->wind), tor); } } else if (g_strcmp0 (action_name, "relocate-torrent") == 0) { GSList * ids = get_selected_torrent_ids (data); if (ids != NULL) { GtkWindow * parent = data->wind; GtkWidget * w = gtr_relocate_dialog_new (parent, data->core, ids); gtk_widget_show (w); } } else if (g_strcmp0 (action_name, "torrent-start") == 0 || g_strcmp0 (action_name, "torrent-start-now") == 0 || g_strcmp0 (action_name, "torrent-stop") == 0 || g_strcmp0 (action_name, "torrent-reannounce") == 0 || g_strcmp0 (action_name, "torrent-verify") == 0 || g_strcmp0 (action_name, "queue-move-top") == 0 || g_strcmp0 (action_name, "queue-move-up") == 0 || g_strcmp0 (action_name, "queue-move-down") == 0 || g_strcmp0 (action_name, "queue-move-bottom") == 0) { changed |= call_rpc_for_selected_torrents (data, action_name); } else if (g_strcmp0 (action_name, "open-torrent-folder") == 0) { gtk_tree_selection_selected_foreach (data->sel, open_folder_foreach, data->core); } else if (g_strcmp0 (action_name, "show-torrent-properties") == 0) { show_details_dialog_for_selected_torrents (data); } else if (g_strcmp0 (action_name, "new-torrent") == 0) { GtkWidget * w = gtr_torrent_creation_dialog_new (data->wind, data->core); gtk_widget_show (w); } else if (g_strcmp0 (action_name, "remove-torrent") == 0) { remove_selected (data, FALSE); } else if (g_strcmp0 (action_name, "delete-torrent") == 0) { remove_selected (data, TRUE); } else if (g_strcmp0 (action_name, "quit") == 0) { on_app_exit (data); } else if (g_strcmp0 (action_name, "select-all") == 0) { gtk_tree_selection_select_all (data->sel); } else if (g_strcmp0 (action_name, "deselect-all") == 0) { gtk_tree_selection_unselect_all (data->sel); } else if (g_strcmp0 (action_name, "edit-preferences") == 0) { if (NULL == data->prefs) { data->prefs = gtr_prefs_dialog_new (data->wind, G_OBJECT (data->core)); g_signal_connect (data->prefs, "destroy", G_CALLBACK (gtk_widget_destroyed), &data->prefs); } gtr_window_present (GTK_WINDOW (data->prefs)); } else if (g_strcmp0 (action_name, "toggle-message-log") == 0) { if (!data->msgwin) { GtkWidget * win = gtr_message_log_window_new (data->wind, data->core); g_signal_connect (win, "destroy", G_CALLBACK (on_message_window_closed), NULL); data->msgwin = win; } else { gtr_action_set_toggled ("toggle-message-log", FALSE); gtk_widget_destroy (data->msgwin); data->msgwin = NULL; } } else if (g_strcmp0 (action_name, "show-about-dialog") == 0) { show_about_dialog (data->wind); } else if (g_strcmp0 (action_name, "help") == 0) { gtr_open_uri (gtr_get_help_uri ()); } else if (g_strcmp0 (action_name, "toggle-main-window") == 0) { toggleMainWindow (data); } else if (g_strcmp0 (action_name, "present-main-window") == 0) { presentMainWindow (data); } else { g_error ("Unhandled action: %s", action_name); } if (changed) update_model_soon (data); }