// This file Copyright © Transmission authors and contributors. // It may be used under the MIT (SPDX: MIT) license. // License text can be found in the licenses/ folder. #include "Application.h" #include "Actions.h" #include "DetailsDialog.h" #include "Dialogs.h" #include "FilterBar.h" #include "GtkCompat.h" #include "HigWorkarea.h" // GUI_PAD, GUI_PAD_BIG #include "MainWindow.h" #include "MakeDialog.h" #include "MessageLogWindow.h" #include "OptionsDialog.h" #include "PathButton.h" #include "Prefs.h" #include "PrefsDialog.h" #include "RelocateDialog.h" #include "Session.h" #include "StatsDialog.h" #include "SystemTrayIcon.h" #include "Torrent.h" #include "Utils.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if GTKMM_CHECK_VERSION(4, 0, 0) #include #include #include #else #include #include #endif #include #include #include #include // exit() #include #include // std::back_inserter #include #include #include #include #include #include #include #include #include #include #ifdef G_OS_UNIX #include #endif using namespace std::literals; #if GTKMM_CHECK_VERSION(4, 0, 0) using FileListValue = Glib::Value; using FileListHandler = Glib::SListHandler>; using StringValue = Glib::Value; #endif #define SHOW_LICENSE namespace { auto const AppIconName = "transmission"sv; // TODO(C++20): Use ""s char const* const LICENSE = "Copyright 2005-2024. 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" "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"; } // namespace class Application::Impl { public: Impl(Application& app, std::string const& config_dir, bool start_paused, bool start_iconified); ~Impl() = default; TR_DISABLE_COPY_MOVE(Impl) void open_files(std::vector> const& files); void on_startup(); void on_activate(); void actions_handler(Glib::ustring const& action_name); private: struct counts_data { int total_count = 0; int queued_count = 0; int stopped_count = 0; }; private: void show_details_dialog_for_selected_torrents(); void show_about_dialog(); bool refresh_actions(); void refresh_actions_soon(); void on_main_window_size_allocated(); void on_main_window_focus_in(); #if GTKMM_CHECK_VERSION(4, 0, 0) bool on_drag_data_received(Glib::ValueBase const& value, double x, double y); #else void on_drag_data_received( Glib::RefPtr const& drag_context, gint x, gint y, Gtk::SelectionData const& selection_data, guint info, guint time_); #endif bool on_rpc_changed_idle(tr_rpc_callback_type type, tr_torrent_id_t torrent_id); void placeWindowFromPrefs(); void presentMainWindow(); void hideMainWindow(); void toggleMainWindow(); bool winclose(); void rowChangedCB(std::unordered_set const& torrent_ids, Torrent::ChangeFlags changes); void app_setup(); void main_window_setup(); bool on_session_closed(); void on_app_exit(); void show_torrent_errors(Glib::ustring const& primary, std::vector& files); void flush_torrent_errors(); bool update_model_once(); void update_model_soon(); bool update_model_loop(); void on_core_busy(bool busy); void on_core_error(Session::ErrorCode code, Glib::ustring const& msg); void on_add_torrent(tr_ctor* ctor); void on_prefs_changed(tr_quark key); [[nodiscard]] std::vector get_selected_torrent_ids() const; [[nodiscard]] counts_data get_selected_torrent_counts() const; void start_all_torrents(); void pause_all_torrents(); void copy_magnet_link_to_clipboard(Glib::RefPtr const& torrent) const; bool call_rpc_for_selected_torrents(std::string const& method); void remove_selected(bool delete_files); static tr_rpc_callback_status on_rpc_changed( tr_session* session, tr_rpc_callback_type type, tr_torrent* tor, gpointer gdata); private: Application& app_; std::string const config_dir_; bool const start_paused_; bool const start_iconified_; bool is_iconified_ = false; bool is_closing_ = false; Glib::RefPtr ui_builder_; unsigned int activation_count_ = 0; sigc::connection timer_; sigc::connection update_model_soon_tag_; sigc::connection refresh_actions_tag_; std::unique_ptr icon_; std::unique_ptr wind_; Glib::RefPtr core_; std::unique_ptr msgwin_; std::unique_ptr prefs_; std::vector error_list_; std::vector duplicates_list_; std::map> details_; }; namespace { template void gtr_window_present(T const& window) { window->present(GDK_CURRENT_TIME); } /*** **** **** DETAILS DIALOGS MANAGEMENT **** ***/ std::string get_details_dialog_key(std::vector const& id_list) { auto tmp = id_list; std::sort(tmp.begin(), tmp.end()); std::ostringstream gstr; for (auto const id : tmp) { gstr << id << ' '; } return gstr.str(); } } // namespace std::vector Application::Impl::get_selected_torrent_ids() const { std::vector ids; wind_->for_each_selected_torrent([&ids](auto const& torrent) { ids.push_back(torrent->get_id()); }); return ids; } void Application::Impl::show_details_dialog_for_selected_torrents() { auto const ids = get_selected_torrent_ids(); auto const key = get_details_dialog_key(ids); auto dialog_it = details_.find(key); if (dialog_it == details_.end()) { auto dialog = DetailsDialog::create(*wind_, core_); dialog->set_torrents(ids); gtr_window_on_close(*dialog, [this, key]() { details_.erase(key); }); dialog_it = details_.try_emplace(key, std::move(dialog)).first; dialog_it->second->show(); } gtr_window_present(dialog_it->second); } /**** ***** ***** ON SELECTION CHANGED ***** ****/ Application::Impl::counts_data Application::Impl::get_selected_torrent_counts() const { counts_data counts; wind_->for_each_selected_torrent( [&counts](auto const& torrent) { ++counts.total_count; auto const activity = torrent->get_activity(); if (activity == TR_STATUS_DOWNLOAD_WAIT || activity == TR_STATUS_SEED_WAIT) { ++counts.queued_count; } if (activity == TR_STATUS_STOPPED) { ++counts.stopped_count; } }); return counts; } bool Application::Impl::refresh_actions() { if (!is_closing_) { size_t const total = core_->get_torrent_count(); size_t const active = core_->get_active_torrent_count(); auto const torrent_count = core_->get_model()->get_n_items(); auto const sel_counts = get_selected_torrent_counts(); bool const 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); bool const can_update = wind_ != nullptr && wind_->for_each_selected_torrent_until([](auto const& torrent) { return tr_torrentCanManualUpdate(&torrent->get_underlying()); }); gtr_action_set_sensitive("torrent-reannounce", can_update); } refresh_actions_tag_.disconnect(); return false; } void Application::Impl::refresh_actions_soon() { if (!is_closing_ && !refresh_actions_tag_.connected()) { refresh_actions_tag_ = Glib::signal_idle().connect(sigc::mem_fun(*this, &Impl::refresh_actions)); } } /*** **** ***/ namespace { bool has_magnet_link_handler() { return bool{ Gio::AppInfo::get_default_for_uri_scheme("magnet") }; } void register_magnet_link_handler() { std::string const content_type = "x-scheme-handler/magnet"; try { auto const app = Gio::AppInfo::create_from_commandline( "transmission-gtk", "transmission-gtk", TR_GIO_APP_INFO_CREATE_FLAGS(SUPPORTS_URIS)); app->set_as_default_for_type(content_type); } catch (Gio::Error const& e) { gtr_warning(fmt::format( _("Couldn't register Transmission as a {content_type} handler: {error} ({error_code})"), fmt::arg("content_type", content_type), fmt::arg("error", e.what()), fmt::arg("error_code", static_cast(e.code())))); } } void ensure_magnet_handler_exists() { if (!has_magnet_link_handler()) { register_magnet_link_handler(); } } } // namespace void Application::Impl::on_main_window_size_allocated() { #if GTKMM_CHECK_VERSION(4, 0, 0) bool const is_maximized = wind_->is_maximized(); #else auto const gdk_window = wind_->get_window(); bool const is_maximized = gdk_window != nullptr && (gdk_window->get_state() & Gdk::WINDOW_STATE_MAXIMIZED) != 0; #endif gtr_pref_flag_set(TR_KEY_main_window_is_maximized, is_maximized); if (!is_maximized) { #if !GTKMM_CHECK_VERSION(4, 0, 0) int x = 0; int y = 0; wind_->get_position(x, y); gtr_pref_int_set(TR_KEY_main_window_x, x); gtr_pref_int_set(TR_KEY_main_window_y, y); #endif int w = 0; int h = 0; #if GTKMM_CHECK_VERSION(4, 0, 0) wind_->get_default_size(w, h); #else wind_->get_size(w, h); #endif 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 ***/ bool Application::Impl::on_rpc_changed_idle(tr_rpc_callback_type type, tr_torrent_id_t torrent_id) { switch (type) { case TR_RPC_SESSION_CLOSE: gtr_action_activate("quit"); break; case TR_RPC_TORRENT_ADDED: if (auto* tor = core_->find_torrent(torrent_id); tor != nullptr) { core_->add_torrent(Torrent::create(tor), true); } break; case TR_RPC_TORRENT_REMOVING: core_->remove_torrent(torrent_id, false); break; case TR_RPC_TORRENT_TRASHING: core_->remove_torrent(torrent_id, true); break; case TR_RPC_SESSION_CHANGED: { auto const* const session = core_->get_session(); auto const newvals = tr_sessionGetSettings(session); // determine which settings changed auto changed_keys = std::set{}; auto& oldvals = gtr_pref_get_all(); auto const serde = tr_variant_serde::benc(); if (auto const* const newvals_map = newvals.get_if(); newvals_map != nullptr) { for (auto const& [key, newval] : *newvals_map) { bool changed = true; if (tr_variant const* oldval = tr_variantDictFind(&oldvals, key); oldval != nullptr) { changed = serde.to_string(*oldval) != serde.to_string(newval); } if (changed) { changed_keys.emplace(key); } } } // update our settings oldvals.merge(newvals); // emit change notifications for (auto const& changed_key : changed_keys) { core_->signal_prefs_changed().emit(changed_key); } 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; default: g_assert_not_reached(); } return false; } tr_rpc_callback_status Application::Impl::on_rpc_changed( tr_session* /*session*/, tr_rpc_callback_type type, tr_torrent* tor, gpointer gdata) { auto* impl = static_cast(gdata); auto const torrent_id = tr_torrentId(tor); Glib::signal_idle().connect([impl, type, torrent_id]() { return impl->on_rpc_changed_idle(type, torrent_id); }); return TR_RPC_NOREMOVE; } /*** **** signal handling ***/ namespace { #ifdef G_OS_UNIX gboolean signal_handler(gpointer user_data) { gtr_message(_("Got termination signal, trying to shut down cleanly. Do it again if it gets stuck.")); gtr_actions_handler("quit", user_data); return G_SOURCE_REMOVE; } #endif } // namespace /**** ***** ***** ****/ void Application::on_startup() { Gtk::Application::on_startup(); impl_->on_startup(); } void Application::Impl::on_startup() { IF_GTKMM4(Gtk::IconTheme::get_for_display(Gdk::Display::get_default()), Gtk::IconTheme::get_default()) ->add_resource_path(gtr_get_full_resource_path("icons"s)); Gtk::Window::set_default_icon_name(std::string(AppIconName)); /* Add style provider to the window. */ auto css_provider = Gtk::CssProvider::create(); css_provider->load_from_resource(gtr_get_full_resource_path("transmission-ui.css")); Gtk::StyleContext::IF_GTKMM4(add_provider_for_display, add_provider_for_screen)( IF_GTKMM4(Gdk::Display::get_default(), Gdk::Screen::get_default()), css_provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); FilterBar(); PathButton(); tr_session* session = nullptr; #ifdef G_OS_UNIX g_unix_signal_add(SIGINT, &signal_handler, this); g_unix_signal_add(SIGTERM, &signal_handler, this); #endif /* ensure the directories are created */ if (auto const str = gtr_pref_string_get(TR_KEY_download_dir); !str.empty()) { (void)g_mkdir_with_parents(str.c_str(), 0777); } if (auto const str = gtr_pref_string_get(TR_KEY_incomplete_dir); !str.empty()) { (void)g_mkdir_with_parents(str.c_str(), 0777); } /* initialize the libtransmission session */ session = tr_sessionInit(config_dir_.c_str(), 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)); core_ = Session::create(session); /* init the ui manager */ ui_builder_ = Gtk::Builder::create_from_resource(gtr_get_full_resource_path("transmission-ui.xml"s)); auto const actions = gtr_actions_init(ui_builder_, this); auto const main_menu = gtr_action_get_object("main-window-menu"); app_.set_menubar(main_menu); /* create main window now to be a parent to any error dialogs */ wind_ = MainWindow::create(app_, actions, core_); wind_->set_show_menubar(true); #if GTKMM_CHECK_VERSION(4, 0, 0) wind_->property_maximized().signal_changed().connect(sigc::mem_fun(*this, &Impl::on_main_window_size_allocated)); wind_->property_default_width().signal_changed().connect(sigc::mem_fun(*this, &Impl::on_main_window_size_allocated)); wind_->property_default_height().signal_changed().connect(sigc::mem_fun(*this, &Impl::on_main_window_size_allocated)); #else wind_->signal_size_allocate().connect(sigc::hide<0>(sigc::mem_fun(*this, &Impl::on_main_window_size_allocated))); #endif #if GTKMM_CHECK_VERSION(4, 0, 0) auto const shortcut_controller = Gtk::ShortcutController::create(gtr_shortcuts_get_from_menu(main_menu)); shortcut_controller->set_scope(Gtk::ShortcutScope::GLOBAL); wind_->add_controller(shortcut_controller); #endif app_.hold(); app_setup(); tr_sessionSetRPCCallback(session, &Impl::on_rpc_changed, this); /* check & see if it's time to update the blocklist */ if (gtr_pref_flag_get(TR_KEY_blocklist_enabled) && gtr_pref_flag_get(TR_KEY_blocklist_updates_enabled)) { int64_t const last_time = gtr_pref_int_get(TR_KEY_blocklist_date); int const SECONDS_IN_A_WEEK = 7 * 24 * 60 * 60; time_t const now = time(nullptr); if (last_time + SECONDS_IN_A_WEEK < now) { core_->blocklist_update(); } } /* if there's no magnet link handler registered, register us */ ensure_magnet_handler_exists(); } void Application::on_activate() { Gtk::Application::on_activate(); impl_->on_activate(); } void Application::Impl::on_activate() { activation_count_++; /* GApplication emits an 'activate' signal when bootstrapping the primary. * Ordinarily we handle that by presenting the main window, but if the user * started Transmission minimized, ignore that initial signal... */ if (start_iconified_ && activation_count_ == 1) { return; } gtr_action_activate("present-main-window"); } void Application::Impl::open_files(std::vector> const& files) { bool const do_start = gtr_pref_flag_get(TR_KEY_start_added_torrents) && !start_paused_; bool const do_prompt = gtr_pref_flag_get(TR_KEY_show_options_window); bool const do_notify = true; core_->add_files(files, do_start, do_prompt, do_notify); } void Application::on_open(std::vector> const& files, Glib::ustring const& hint) { Gtk::Application::on_open(files, hint); impl_->open_files(files); } namespace { std::string get_application_id(std::string const& config_dir) { struct stat sb = {}; (void)::stat(config_dir.c_str(), &sb); return fmt::format("com.transmissionbt.transmission_{}_{}", sb.st_dev, sb.st_ino); } } // namespace Application::Application(std::string const& config_dir, bool start_paused, bool start_iconified) : Gtk::Application(get_application_id(config_dir), TR_GIO_APPLICATION_FLAGS(HANDLES_OPEN)) , impl_(std::make_unique(*this, config_dir, start_paused, start_iconified)) { } Application::~Application() = default; Application::Impl::Impl(Application& app, std::string const& config_dir, bool start_paused, bool start_iconified) : app_(app) , config_dir_(config_dir) , start_paused_(start_paused) , start_iconified_(start_iconified) { } void Application::Impl::on_core_busy(bool busy) { wind_->set_busy(busy); } void Application::Impl::app_setup() { if (start_iconified_) { gtr_pref_flag_set(TR_KEY_show_notification_area_icon, true); } gtr_actions_set_core(core_); /* set up core handlers */ core_->signal_busy().connect(sigc::mem_fun(*this, &Impl::on_core_busy)); core_->signal_add_error().connect(sigc::mem_fun(*this, &Impl::on_core_error)); core_->signal_add_prompt().connect(sigc::mem_fun(*this, &Impl::on_add_torrent)); core_->signal_prefs_changed().connect(sigc::mem_fun(*this, &Impl::on_prefs_changed)); /* add torrents from command-line and saved state */ core_->load(start_paused_); core_->torrents_added(); /* set up main window */ main_window_setup(); /* set up the icon */ on_prefs_changed(TR_KEY_show_notification_area_icon); /* start model update timer */ timer_ = Glib::signal_timeout().connect_seconds( sigc::mem_fun(*this, &Impl::update_model_loop), MAIN_WINDOW_REFRESH_INTERVAL_SECONDS); update_model_once(); /* either show the window or iconify it */ if (!start_iconified_) { wind_->show(); gtr_action_set_toggled("toggle-main-window", true); } else { gtr_window_set_skip_taskbar_hint(*wind_, icon_ != nullptr); gtr_action_set_toggled("toggle-main-window", false); } if (!gtr_pref_flag_get(TR_KEY_user_has_given_informed_consent)) { auto w = std::make_shared( *wind_, _("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."), false, TR_GTK_MESSAGE_TYPE(OTHER), TR_GTK_BUTTONS_TYPE(NONE), true); w->add_button(_("_Cancel"), TR_GTK_RESPONSE_TYPE(REJECT)); w->add_button(_("I _Agree"), TR_GTK_RESPONSE_TYPE(ACCEPT)); w->set_default_response(TR_GTK_RESPONSE_TYPE(ACCEPT)); w->signal_response().connect( [w](int response) mutable { if (response == TR_GTK_RESPONSE_TYPE(ACCEPT)) { // only show it once gtr_pref_flag_set(TR_KEY_user_has_given_informed_consent, true); w.reset(); } else { exit(0); } }); w->show(); } } void Application::Impl::placeWindowFromPrefs() { #if GTKMM_CHECK_VERSION(4, 0, 0) wind_->set_default_size((int)gtr_pref_int_get(TR_KEY_main_window_width), (int)gtr_pref_int_get(TR_KEY_main_window_height)); #else wind_->resize((int)gtr_pref_int_get(TR_KEY_main_window_width), (int)gtr_pref_int_get(TR_KEY_main_window_height)); wind_->move((int)gtr_pref_int_get(TR_KEY_main_window_x), (int)gtr_pref_int_get(TR_KEY_main_window_y)); #endif } void Application::Impl::presentMainWindow() { gtr_action_set_toggled("toggle-main-window", true); if (is_iconified_) { is_iconified_ = false; gtr_window_set_skip_taskbar_hint(*wind_, false); } if (!wind_->get_visible()) { placeWindowFromPrefs(); gtr_widget_set_visible(*wind_, true); } gtr_window_present(wind_); gtr_window_raise(*wind_); } void Application::Impl::hideMainWindow() { gtr_action_set_toggled("toggle-main-window", false); gtr_window_set_skip_taskbar_hint(*wind_, true); gtr_widget_set_visible(*wind_, false); is_iconified_ = true; } void Application::Impl::toggleMainWindow() { if (is_iconified_) { presentMainWindow(); } else { hideMainWindow(); } } bool Application::Impl::winclose() { if (icon_ != nullptr) { gtr_action_activate("toggle-main-window"); } else { on_app_exit(); } return true; /* don't propagate event further */ } void Application::Impl::rowChangedCB(std::unordered_set const& torrent_ids, Torrent::ChangeFlags changes) { if (changes.test(Torrent::ChangeFlag::ACTIVITY) && wind_->for_each_selected_torrent_until([&torrent_ids](auto const& torrent) { return torrent_ids.find(torrent->get_id()) != torrent_ids.end(); })) { refresh_actions_soon(); } } #if GTKMM_CHECK_VERSION(4, 0, 0) bool Application::Impl::on_drag_data_received(Glib::ValueBase const& value, double /*x*/, double /*y*/) { if (G_VALUE_HOLDS(value.gobj(), GDK_TYPE_FILE_LIST)) { FileListValue files_value; files_value.init(value.gobj()); open_files(FileListHandler::slist_to_vector(files_value.get(), Glib::OwnershipType::OWNERSHIP_NONE)); return true; } if (G_VALUE_HOLDS(value.gobj(), StringValue::value_type())) { StringValue string_value; string_value.init(value.gobj()); if (auto const text = gtr_str_strip(string_value.get()); !text.empty()) { return core_->add_from_url(text); } } return false; } #else void Application::Impl::on_drag_data_received( Glib::RefPtr const& drag_context, gint /*x*/, gint /*y*/, Gtk::SelectionData const& selection_data, guint /*info*/, guint time_) { if (auto const uris = selection_data.get_uris(); !uris.empty()) { auto files = std::vector>(); files.reserve(uris.size()); std::transform(uris.begin(), uris.end(), std::back_inserter(files), &Gio::File::create_for_uri); open_files(files); } else { auto const text = gtr_str_strip(selection_data.get_text()); if (!text.empty()) { core_->add_from_url(text); } } drag_context->drag_finish(true, false, time_); } #endif void Application::Impl::main_window_setup() { wind_->signal_selection_changed().connect(sigc::mem_fun(*this, &Impl::refresh_actions_soon)); refresh_actions_soon(); core_->signal_torrents_changed().connect(sigc::mem_fun(*this, &Impl::rowChangedCB)); gtr_window_on_close(*wind_, sigc::mem_fun(*this, &Impl::winclose)); refresh_actions(); /* register to handle URIs that get dragged onto our main window */ #if GTKMM_CHECK_VERSION(4, 0, 0) auto drop_controller = Gtk::DropTarget::create(G_TYPE_INVALID, Gdk::DragAction::COPY); drop_controller->set_gtypes({ StringValue::value_type(), GDK_TYPE_FILE_LIST }); drop_controller->signal_drop().connect(sigc::mem_fun(*this, &Impl::on_drag_data_received), false); wind_->add_controller(drop_controller); #else wind_->drag_dest_set(Gtk::DEST_DEFAULT_ALL, Gdk::ACTION_COPY); wind_->drag_dest_add_uri_targets(); wind_->drag_dest_add_text_targets(); /* links dragged from browsers are text */ wind_->signal_drag_data_received().connect(sigc::mem_fun(*this, &Impl::on_drag_data_received)); #endif } bool Application::Impl::on_session_closed() { details_.clear(); prefs_.reset(); wind_.reset(); core_.reset(); icon_.reset(); error_list_.clear(); duplicates_list_.clear(); app_.release(); return false; } void Application::Impl::on_app_exit() { if (is_closing_) { return; } is_closing_ = true; refresh_actions_tag_.disconnect(); update_model_soon_tag_.disconnect(); timer_.disconnect(); #if !GTKMM_CHECK_VERSION(4, 0, 0) wind_->remove(); #endif wind_->set_show_menubar(false); auto* p = Gtk::make_managed(); p->set_column_spacing(GUI_PAD_BIG); p->set_halign(TR_GTK_ALIGN(CENTER)); p->set_valign(TR_GTK_ALIGN(CENTER)); #if GTKMM_CHECK_VERSION(4, 0, 0) wind_->set_child(*p); #else wind_->add(*p); #endif auto* icon = Gtk::make_managed(); icon->property_icon_name() = "network-workgroup"; icon->property_icon_size() = IF_GTKMM4(Gtk::IconSize::LARGE, Gtk::ICON_SIZE_DIALOG); p->attach(*icon, 0, 0, 1, 2); auto* top_label = Gtk::make_managed(); top_label->set_markup(fmt::format("{:s}", _("Closing Connections…"))); top_label->set_halign(TR_GTK_ALIGN(START)); top_label->set_valign(TR_GTK_ALIGN(CENTER)); p->attach(*top_label, 1, 0, 1, 1); auto* bottom_label = Gtk::make_managed(_("Sending upload/download totals to tracker…")); bottom_label->set_halign(TR_GTK_ALIGN(START)); bottom_label->set_valign(TR_GTK_ALIGN(CENTER)); p->attach(*bottom_label, 1, 1, 1, 1); auto* button = Gtk::make_managed(_("_Quit Now"), true); button->set_margin_top(GUI_PAD); button->set_halign(TR_GTK_ALIGN(START)); button->set_valign(TR_GTK_ALIGN(END)); button->signal_clicked().connect([]() { ::exit(0); }); p->attach(*button, 1, 2, 1, 1); #if !GTKMM_CHECK_VERSION(4, 0, 0) p->show_all(); #endif button->grab_focus(); /* clear the UI */ core_->clear(); /* 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 */ placeWindowFromPrefs(); /* shut down libT */ /* 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 */ std::thread( [this, session = core_->close()]() { tr_sessionClose(session); Glib::signal_idle().connect(sigc::mem_fun(*this, &Impl::on_session_closed)); }) .detach(); } void Application::Impl::show_torrent_errors(Glib::ustring const& primary, std::vector& files) { std::ostringstream s; auto const leader = files.size() > 1 ? gtr_get_unicode_string(GtrUnicode::Bullet) : ""; for (auto const& f : files) { s << leader << ' ' << f << '\n'; } auto w = std::make_shared( *wind_, primary, false, TR_GTK_MESSAGE_TYPE(ERROR), TR_GTK_BUTTONS_TYPE(CLOSE), true); w->set_secondary_text(s.str()); w->signal_response().connect([w](int /*response*/) mutable { w.reset(); }); w->show(); files.clear(); } void Application::Impl::flush_torrent_errors() { if (!error_list_.empty()) { show_torrent_errors( ngettext("Couldn't add corrupt torrent", "Couldn't add corrupt torrents", error_list_.size()), error_list_); } if (!duplicates_list_.empty()) { show_torrent_errors( ngettext("Couldn't add duplicate torrent", "Couldn't add duplicate torrents", duplicates_list_.size()), duplicates_list_); } } void Application::Impl::on_core_error(Session::ErrorCode code, Glib::ustring const& msg) { switch (code) { case Session::ERR_ADD_TORRENT_ERR: error_list_.push_back(Glib::path_get_basename(msg.raw())); break; case Session::ERR_ADD_TORRENT_DUP: duplicates_list_.push_back(msg); break; case Session::ERR_NO_MORE_TORRENTS: flush_torrent_errors(); break; default: g_assert_not_reached(); break; } } void Application::Impl::on_main_window_focus_in() { if (wind_ != nullptr) { gtr_window_set_urgency_hint(*wind_, false); } } void Application::Impl::on_add_torrent(tr_ctor* ctor) { auto w = std::shared_ptr( OptionsDialog::create(*wind_, core_, std::unique_ptr(ctor, &tr_ctorFree))); gtr_window_on_close(*w, [w]() mutable { w.reset(); }); #if GTKMM_CHECK_VERSION(4, 0, 0) auto focus_controller = Gtk::EventControllerFocus::create(); focus_controller->signal_enter().connect(sigc::mem_fun(*this, &Impl::on_main_window_focus_in)); w->add_controller(focus_controller); #else w->signal_focus_in_event().connect_notify(sigc::hide<0>(sigc::mem_fun(*this, &Impl::on_main_window_focus_in))); #endif if (wind_ != nullptr) { gtr_window_set_urgency_hint(*wind_, true); } w->show(); } void Application::Impl::on_prefs_changed(tr_quark const key) { auto* tr = core_->get_session(); switch (key) { case TR_KEY_encryption: tr_sessionSetEncryption(tr, static_cast(gtr_pref_int_get(key))); break; case TR_KEY_default_trackers: tr_sessionSetDefaultTrackers(tr, gtr_pref_string_get(key).c_str()); break; case TR_KEY_download_dir: tr_sessionSetDownloadDir(tr, gtr_pref_string_get(key).c_str()); break; case TR_KEY_message_level: tr_logSetLevel(static_cast(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).c_str()); break; case TR_KEY_show_notification_area_icon: if (bool const show = gtr_pref_flag_get(key); show && icon_ == nullptr) { icon_ = SystemTrayIcon::create(*wind_, core_); } else if (!show && icon_ != nullptr) { icon_.reset(); } 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).c_str()); 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).c_str()); break; case TR_KEY_rpc_password: tr_sessionSetRPCPassword(tr, gtr_pref_string_get(key).c_str()); 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: { bool const b = gtr_pref_flag_get(key); tr_sessionUseAltSpeed(tr, b); gtr_action_set_toggled(std::string(tr_quark_get_string_view(key)), 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, static_cast(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).c_str()); break; case TR_KEY_incomplete_dir_enabled: tr_sessionSetIncompleteDirEnabled(tr, gtr_pref_flag_get(key)); break; case TR_KEY_script_torrent_done_enabled: tr_sessionSetScriptEnabled(tr, TR_SCRIPT_ON_TORRENT_DONE, gtr_pref_flag_get(key)); break; case TR_KEY_script_torrent_done_filename: tr_sessionSetScript(tr, TR_SCRIPT_ON_TORRENT_DONE, gtr_pref_string_get(key).c_str()); break; case TR_KEY_script_torrent_done_seeding_enabled: tr_sessionSetScriptEnabled(tr, TR_SCRIPT_ON_TORRENT_DONE_SEEDING, gtr_pref_flag_get(key)); break; case TR_KEY_script_torrent_done_seeding_filename: tr_sessionSetScript(tr, TR_SCRIPT_ON_TORRENT_DONE_SEEDING, gtr_pref_string_get(key).c_str()); 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; } } bool Application::Impl::update_model_once() { /* update the torrent data in the model */ core_->update(); /* refresh the main window's statusbar and toolbar buttons */ if (wind_ != nullptr) { wind_->refresh(); } /* update the actions */ refresh_actions(); /* update the status tray icon */ if (icon_ != nullptr) { icon_->refresh(); } update_model_soon_tag_.disconnect(); return false; } void Application::Impl::update_model_soon() { if (!update_model_soon_tag_.connected()) { update_model_soon_tag_ = Glib::signal_idle().connect(sigc::mem_fun(*this, &Impl::update_model_once)); } } bool Application::Impl::update_model_loop() { if (!is_closing_) { update_model_once(); } return !is_closing_; } void Application::Impl::show_about_dialog() { auto const uri = Glib::ustring("https://transmissionbt.com/"); auto const authors = std::vector({ "Charles Kerr (Backend; GTK+)", "Mitchell Livingston (Backend; macOS)", "Mike Gelfand", }); auto d = std::make_shared(); d->set_authors(authors); d->set_comments(_("A fast and easy BitTorrent client")); d->set_copyright(_("Copyright © The Transmission Project")); d->set_logo_icon_name(std::string(AppIconName)); d->set_name(Glib::get_application_name()); /* Translators: translate "translator-credits" as your name to have it appear in the credits in the "About" dialog */ d->set_translator_credits(_("translator-credits")); d->set_version(LONG_VERSION_STRING); d->set_website(uri); d->set_website_label(uri); #ifdef SHOW_LICENSE d->set_license(LICENSE); d->set_wrap_license(true); #endif d->set_transient_for(*wind_); d->set_modal(true); gtr_window_on_close(*d, [d]() mutable { d.reset(); }); #if !GTKMM_CHECK_VERSION(4, 0, 0) d->signal_response().connect_notify([&dref = *d](int /*response*/) { dref.close(); }); #endif d->show(); } bool Application::Impl::call_rpc_for_selected_torrents(std::string const& method) { tr_variant top; bool invoked = false; auto* session = core_->get_session(); tr_variantInitDict(&top, 2); tr_variantDictAddStrView(&top, TR_KEY_method, method); auto* const args = tr_variantDictAddDict(&top, TR_KEY_arguments, 1); auto* const ids = tr_variantDictAddList(args, TR_KEY_ids, 0); wind_->for_each_selected_torrent([ids](auto const& torrent) { tr_variantListAddInt(ids, torrent->get_id()); }); if (tr_variantListSize(ids) != 0) { tr_rpc_request_exec(session, top, {}); invoked = true; } return invoked; } void Application::Impl::remove_selected(bool delete_files) { if (auto const l = get_selected_torrent_ids(); !l.empty()) { gtr_confirm_remove(*wind_, core_, l, delete_files); } } void Application::Impl::start_all_torrents() { auto* session = core_->get_session(); tr_variant request; tr_variantInitDict(&request, 1); tr_variantDictAddStrView(&request, TR_KEY_method, "torrent-start"sv); tr_rpc_request_exec(session, request, {}); } void Application::Impl::pause_all_torrents() { auto* session = core_->get_session(); tr_variant request; tr_variantInitDict(&request, 1); tr_variantDictAddStrView(&request, TR_KEY_method, "torrent-stop"sv); tr_rpc_request_exec(session, request, {}); } void Application::Impl::copy_magnet_link_to_clipboard(Glib::RefPtr const& torrent) const { auto const magnet = tr_torrentGetMagnetLink(&torrent->get_underlying()); auto const display = wind_->get_display(); /* this is The Right Thing for copy/paste... */ IF_GTKMM4(display->get_clipboard(), Gtk::Clipboard::get_for_display(display, GDK_SELECTION_CLIPBOARD))->set_text(magnet); /* ...but people using plain ol' X need this instead */ IF_GTKMM4(display->get_primary_clipboard(), Gtk::Clipboard::get_for_display(display, GDK_SELECTION_PRIMARY)) ->set_text(magnet); } void gtr_actions_handler(Glib::ustring const& action_name, gpointer user_data) { static_cast(user_data)->actions_handler(action_name); } void Application::Impl::actions_handler(Glib::ustring const& action_name) { bool changed = false; if (action_name == "open-torrent-from-url") { auto w = std::shared_ptr(TorrentUrlChooserDialog::create(*wind_, core_)); gtr_window_on_close(*w, [w]() mutable { w.reset(); }); w->show(); } else if (action_name == "open-torrent") { auto w = std::shared_ptr(TorrentFileChooserDialog::create(*wind_, core_)); gtr_window_on_close(*w, [w]() mutable { w.reset(); }); w->show(); } else if (action_name == "show-stats") { auto dialog = std::shared_ptr(StatsDialog::create(*wind_, core_)); gtr_window_on_close(*dialog, [dialog]() mutable { dialog.reset(); }); dialog->show(); } else if (action_name == "donate") { gtr_open_uri("https://transmissionbt.com/donate/"); } else if (action_name == "pause-all-torrents") { pause_all_torrents(); } else if (action_name == "start-all-torrents") { start_all_torrents(); } else if (action_name == "copy-magnet-link-to-clipboard") { wind_->for_each_selected_torrent_until( sigc::bind_return(sigc::mem_fun(*this, &Impl::copy_magnet_link_to_clipboard), true)); } else if (action_name == "relocate-torrent") { auto const ids = get_selected_torrent_ids(); if (!ids.empty()) { auto w = std::shared_ptr(RelocateDialog::create(*wind_, core_, ids)); gtr_window_on_close(*w, [w]() mutable { w.reset(); }); w->show(); } } else if ( action_name == "torrent-start" || action_name == "torrent-start-now" || action_name == "torrent-stop" || action_name == "torrent-reannounce" || action_name == "torrent-verify" || action_name == "queue-move-top" || action_name == "queue-move-up" || action_name == "queue-move-down" || action_name == "queue-move-bottom") { changed = call_rpc_for_selected_torrents(action_name); } else if (action_name == "open-torrent-folder") { wind_->for_each_selected_torrent([this](auto const& torrent) { core_->open_folder(torrent->get_id()); }); } else if (action_name == "show-torrent-properties") { show_details_dialog_for_selected_torrents(); } else if (action_name == "new-torrent") { auto w = std::shared_ptr(MakeDialog::create(*wind_, core_)); gtr_window_on_close(*w, [w]() mutable { w.reset(); }); w->show(); } else if (action_name == "remove-torrent") { remove_selected(false); } else if (action_name == "delete-torrent") { remove_selected(true); } else if (action_name == "quit") { on_app_exit(); } else if (action_name == "select-all") { wind_->select_all(); } else if (action_name == "deselect-all") { wind_->unselect_all(); } else if (action_name == "edit-preferences") { if (prefs_ == nullptr) { prefs_ = PrefsDialog::create(*wind_, core_); gtr_window_on_close(*prefs_, [this]() { prefs_.reset(); }); } gtr_window_present(prefs_); } else if (action_name == "toggle-message-log") { if (msgwin_ == nullptr) { msgwin_ = MessageLogWindow::create(*wind_, core_); gtr_window_on_close( *msgwin_, [this]() { gtr_action_set_toggled("toggle-message-log", false); msgwin_.reset(); }); gtr_action_set_toggled("toggle-message-log", true); msgwin_->show(); } else { msgwin_->close(); } } else if (action_name == "show-about-dialog") { show_about_dialog(); } else if (action_name == "help") { gtr_open_uri(gtr_get_help_uri()); } else if (action_name == "toggle-main-window") { toggleMainWindow(); } else if (action_name == "present-main-window") { presentMainWindow(); } else { gtr_error(fmt::format("Unhandled action: {}", action_name)); } if (changed) { update_model_soon(); } }