// This file Copyright © 2005-2022 Transmission authors and contributors. // It may be used under the MIT (SPDX: MIT) license. // License text can be found in the licenses/ folder. #include #include #include #include #include #include // tr_formatter_speed_KBps() #include "Actions.h" #include "FilterBar.h" #include "HigWorkarea.h" // GUI_PAD_SMALL #include "MainWindow.h" #include "Prefs.h" #include "PrefsDialog.h" #include "Session.h" #include "TorrentCellRenderer.h" #include "Utils.h" using namespace std::string_literals; using namespace std::string_view_literals; using VariantInt = Glib::Variant; using VariantDouble = Glib::Variant; using VariantString = Glib::Variant; namespace { auto constexpr OptionsMenuActionGroupName = "options-menu"sv; auto constexpr StatsMenuActionGroupName = "stats-menu"sv; } // namespace class MainWindow::Impl { struct OptionMenuInfo { Glib::RefPtr action; Glib::RefPtr on_item; Glib::RefPtr section; }; public: Impl( MainWindow& window, Glib::RefPtr const& builder, Glib::RefPtr const& actions, Glib::RefPtr const& core); ~Impl(); TR_DISABLE_COPY_MOVE(Impl) [[nodiscard]] Glib::RefPtr get_selection() const; void refresh(); void prefsChanged(tr_quark key); private: void init_view(Gtk::TreeView* view, Glib::RefPtr const& model); Glib::RefPtr createOptionsMenu(); Glib::RefPtr createSpeedMenu(Glib::RefPtr const& actions, tr_direction dir); Glib::RefPtr createRatioMenu(Glib::RefPtr const& actions); Glib::RefPtr createStatsMenu(); void on_popup_menu(double view_x, double view_y); void onSpeedToggled(std::string const& action_name, tr_direction dir, bool enabled); void onSpeedSet(tr_direction dir, int KBps); void onRatioToggled(std::string const& action_name, bool enabled); void onRatioSet(double ratio); void updateStats(); void updateSpeeds(); void syncAltSpeedButton(); void status_menu_toggled_cb(std::string const& action_name, Glib::ustring const& val); void onOptionsClicked(); void alt_speed_toggled_cb(); void onAltSpeedToggledIdle(); private: MainWindow& window_; Glib::RefPtr const core_; Glib::RefPtr options_actions_; Glib::RefPtr stats_actions_; std::array speed_menu_info_; OptionMenuInfo ratio_menu_info_; Gtk::ScrolledWindow* scroll_ = nullptr; Gtk::TreeView* view_ = nullptr; Gtk::Widget* toolbar_ = nullptr; FilterBar* filter_; Gtk::Widget* status_ = nullptr; Gtk::Label* ul_lb_ = nullptr; Gtk::Label* dl_lb_ = nullptr; Gtk::Label* stats_lb_ = nullptr; Gtk::Image* alt_speed_image_ = nullptr; Gtk::ToggleButton* alt_speed_button_ = nullptr; TorrentCellRenderer* renderer_ = nullptr; Gtk::TreeViewColumn* column_ = nullptr; sigc::connection pref_handler_id_; IF_GTKMM4(Gtk::PopoverMenu*, Gtk::Menu*) popup_menu_ = nullptr; }; /*** **** ***/ void MainWindow::Impl::on_popup_menu([[maybe_unused]] double event_x, [[maybe_unused]] double event_y) { if (popup_menu_ == nullptr) { auto const menu = gtr_action_get_object("main-window-popup"); #if GTKMM_CHECK_VERSION(4, 0, 0) popup_menu_ = Gtk::make_managed(menu, Gtk::PopoverMenu::Flags::NESTED); popup_menu_->set_parent(window_); popup_menu_->set_has_arrow(false); popup_menu_->set_halign(window_.get_direction() == Gtk::TextDirection::RTL ? Gtk::Align::END : Gtk::Align::START); #else popup_menu_ = Gtk::make_managed(menu); popup_menu_->attach_to_widget(window_); #endif } #if GTKMM_CHECK_VERSION(4, 0, 0) int view_x = 0; int view_y = 0; view_->convert_bin_window_to_widget_coords(static_cast(event_x), static_cast(event_y), view_x, view_y); double window_x = 0; double window_y = 0; view_->translate_coordinates(window_, view_x, view_y, window_x, window_y); popup_menu_->set_pointing_to(Gdk::Rectangle(window_x, window_y, 1, 1)); popup_menu_->popup(); #else popup_menu_->popup_at_pointer(nullptr); #endif } namespace { bool tree_view_search_equal_func( Glib::RefPtr const& /*model*/, int /*column*/, Glib::ustring const& key, Gtk::TreeModel::const_iterator const& iter) { auto const name = iter->get_value(torrent_cols.name_collated); return name.find(key.lowercase()) == Glib::ustring::npos; } } // namespace void MainWindow::Impl::init_view(Gtk::TreeView* view, Glib::RefPtr const& model) { view->set_search_column(torrent_cols.name_collated); view->set_search_equal_func(&tree_view_search_equal_func); column_ = view->get_column(0); renderer_ = Gtk::make_managed(); column_->pack_start(*renderer_, false); column_->add_attribute(renderer_->property_torrent(), torrent_cols.torrent); column_->add_attribute(renderer_->property_piece_upload_speed(), torrent_cols.speed_up); column_->add_attribute(renderer_->property_piece_download_speed(), torrent_cols.speed_down); renderer_->property_xpad() = GUI_PAD_SMALL; renderer_->property_ypad() = GUI_PAD_SMALL; #if !GTKMM_CHECK_VERSION(4, 0, 0) view->signal_popup_menu().connect_notify([this]() { on_popup_menu(0, 0); }); #endif setup_tree_view_button_event_handling( *view, [this, view](guint /*button*/, TrGdkModifierType /*state*/, double view_x, double view_y, bool context_menu_requested) { return on_tree_view_button_pressed( *view, view_x, view_y, context_menu_requested, sigc::mem_fun(*this, &Impl::on_popup_menu)); }, [view](double view_x, double view_y) { return on_tree_view_button_released(*view, view_x, view_y); }); view->signal_row_activated().connect([](auto const& /*path*/, auto* /*column*/) { gtr_action_activate("show-torrent-properties"); }); view->set_model(model); } void MainWindow::Impl::prefsChanged(tr_quark const key) { switch (key) { case TR_KEY_compact_view: renderer_->property_compact() = gtr_pref_flag_get(key); /* since the cell size has changed, we need gtktreeview to revalidate * its fixed-height mode values. Unfortunately there's not an API call * for that, but this seems to work for both GTK 3 and 4 */ view_->set_fixed_height_mode(false); view_->set_row_separator_func({}); view_->unset_row_separator_func(); view_->set_fixed_height_mode(true); break; case TR_KEY_show_statusbar: status_->set_visible(gtr_pref_flag_get(key)); break; case TR_KEY_show_filterbar: filter_->set_visible(gtr_pref_flag_get(key)); break; case TR_KEY_show_toolbar: toolbar_->set_visible(gtr_pref_flag_get(key)); break; case TR_KEY_statusbar_stats: refresh(); break; case TR_KEY_alt_speed_enabled: case TR_KEY_alt_speed_up: case TR_KEY_alt_speed_down: syncAltSpeedButton(); break; default: break; } } MainWindow::Impl::~Impl() { pref_handler_id_.disconnect(); } void MainWindow::Impl::status_menu_toggled_cb(std::string const& action_name, Glib::ustring const& val) { stats_actions_->change_action_state(action_name, VariantString::create(val)); core_->set_pref(TR_KEY_statusbar_stats, val.raw()); } void MainWindow::Impl::syncAltSpeedButton() { bool const b = gtr_pref_flag_get(TR_KEY_alt_speed_enabled); alt_speed_button_->set_active(b); alt_speed_button_->set_tooltip_text(fmt::format( b ? _("Click to disable Alternative Speed Limits\n ({download_speed} down, {upload_speed} up)") : _("Click to enable Alternative Speed Limits\n ({download_speed} down, {upload_speed} up)"), fmt::arg("download_speed", tr_formatter_speed_KBps(gtr_pref_int_get(TR_KEY_alt_speed_down))), fmt::arg("upload_speed", tr_formatter_speed_KBps(gtr_pref_int_get(TR_KEY_alt_speed_up))))); } void MainWindow::Impl::alt_speed_toggled_cb() { core_->set_pref(TR_KEY_alt_speed_enabled, alt_speed_button_->get_active()); } /*** **** FILTER ***/ void MainWindow::Impl::onAltSpeedToggledIdle() { core_->set_pref(TR_KEY_alt_speed_enabled, tr_sessionUsesAltSpeed(core_->get_session())); } /*** **** Speed limit menu ***/ void MainWindow::Impl::onSpeedToggled(std::string const& action_name, tr_direction dir, bool enabled) { options_actions_->change_action_state(action_name, VariantInt::create(enabled ? 1 : 0)); core_->set_pref(dir == TR_UP ? TR_KEY_speed_limit_up_enabled : TR_KEY_speed_limit_down_enabled, enabled); } void MainWindow::Impl::onSpeedSet(tr_direction dir, int KBps) { core_->set_pref(dir == TR_UP ? TR_KEY_speed_limit_up : TR_KEY_speed_limit_down, KBps); core_->set_pref(dir == TR_UP ? TR_KEY_speed_limit_up_enabled : TR_KEY_speed_limit_down_enabled, true); } Glib::RefPtr MainWindow::Impl::createSpeedMenu( Glib::RefPtr const& actions, tr_direction dir) { auto& info = speed_menu_info_.at(dir); auto m = Gio::Menu::create(); auto const action_name = fmt::format("speed-limit-{}", dir == TR_UP ? "up" : "down"); auto const full_action_name = fmt::format("{}.{}", OptionsMenuActionGroupName, action_name); info.action = actions->add_action_radio_integer( action_name, [this, action_name, dir](int value) { onSpeedToggled(action_name, dir, value != 0); }, 0); info.section = Gio::Menu::create(); auto speedlimit_off_item = Gio::MenuItem::create(_("Unlimited"), full_action_name); speedlimit_off_item->set_action_and_target(full_action_name, VariantInt::create(0)); info.section->append_item(speedlimit_off_item); info.on_item = Gio::MenuItem::create("...", full_action_name); info.on_item->set_action_and_target(full_action_name, VariantInt::create(1)); info.section->append_item(info.on_item); m->append_section(info.section); auto section = Gio::Menu::create(); auto const stock_action_name = fmt::format("{}-stock", action_name); auto const full_stock_action_name = fmt::format("{}.{}", OptionsMenuActionGroupName, stock_action_name); actions->add_action_with_parameter( stock_action_name, VariantInt::variant_type(), [this, dir](Glib::VariantBase const& value) { onSpeedSet(dir, Glib::VariantBase::cast_dynamic(value).get()); }); for (auto const KBps : { 50, 100, 250, 500, 1000, 2500, 5000, 10000 }) { auto item = Gio::MenuItem::create(tr_formatter_speed_KBps(KBps), full_stock_action_name); item->set_action_and_target(full_stock_action_name, VariantInt::create(KBps)); section->append_item(item); } m->append_section(section); return m; } /*** **** Speed limit menu ***/ void MainWindow::Impl::onRatioToggled(std::string const& action_name, bool enabled) { options_actions_->change_action_state(action_name, VariantInt::create(enabled ? 1 : 0)); core_->set_pref(TR_KEY_ratio_limit_enabled, enabled); } void MainWindow::Impl::onRatioSet(double ratio) { core_->set_pref(TR_KEY_ratio_limit, ratio); core_->set_pref(TR_KEY_ratio_limit_enabled, true); } Glib::RefPtr MainWindow::Impl::createRatioMenu(Glib::RefPtr const& actions) { static auto const stockRatios = std::array({ 0.25, 0.5, 0.75, 1, 1.5, 2, 3 }); auto& info = ratio_menu_info_; auto m = Gio::Menu::create(); auto const action_name = "ratio-limit"s; auto const full_action_name = fmt::format("{}.{}", OptionsMenuActionGroupName, action_name); info.action = actions->add_action_radio_integer( action_name, [this, action_name](int value) { onRatioToggled(action_name, value != 0); }, 0); info.section = Gio::Menu::create(); auto ratio_off_item = Gio::MenuItem::create(_("Seed Forever"), full_action_name); ratio_off_item->set_action_and_target(full_action_name, VariantInt::create(0)); info.section->append_item(ratio_off_item); info.on_item = Gio::MenuItem::create("...", full_action_name); info.on_item->set_action_and_target(full_action_name, VariantInt::create(1)); info.section->append_item(info.on_item); m->append_section(info.section); auto section = Gio::Menu::create(); auto const stock_action_name = fmt::format("{}-stock", action_name); auto const full_stock_action_name = fmt::format("{}.{}", OptionsMenuActionGroupName, stock_action_name); actions->add_action_with_parameter( stock_action_name, VariantDouble::variant_type(), [this](Glib::VariantBase const& value) { onRatioSet(Glib::VariantBase::cast_dynamic(value).get()); }); for (auto const ratio : stockRatios) { auto item = Gio::MenuItem::create(tr_strlratio(ratio), full_stock_action_name); item->set_action_and_target(full_stock_action_name, VariantDouble::create(ratio)); section->append_item(item); } m->append_section(section); return m; } /*** **** Option menu ***/ Glib::RefPtr MainWindow::Impl::createOptionsMenu() { auto top = Gio::Menu::create(); auto actions = Gio::SimpleActionGroup::create(); auto section = Gio::Menu::create(); section->append_submenu(_("Limit Download Speed"), createSpeedMenu(actions, TR_DOWN)); section->append_submenu(_("Limit Upload Speed"), createSpeedMenu(actions, TR_UP)); top->append_section(section); section = Gio::Menu::create(); section->append_submenu(_("Stop Seeding at Ratio"), createRatioMenu(actions)); top->append_section(section); window_.insert_action_group(std::string(OptionsMenuActionGroupName), actions); options_actions_ = actions; return top; } void MainWindow::Impl::onOptionsClicked() { static auto const update_menu = [](OptionMenuInfo& info, Glib::ustring const& new_on_label, tr_quark on_off_key) { if (auto on_label = Glib::VariantBase::cast_dynamic(info.on_item->get_attribute_value("label")).get(); on_label != new_on_label) { info.on_item->set_label(new_on_label); // Items aren't refed by menu on insert but their attributes copied instead, so need to replace. // (see https://docs.gtk.org/gio/method.Menu.insert_item.html) info.section->remove(info.section->get_n_items() - 1); info.section->append_item(info.on_item); } info.action->change_state(gtr_pref_flag_get(on_off_key) ? 1 : 0); }; update_menu( speed_menu_info_[TR_DOWN], tr_formatter_speed_KBps(gtr_pref_int_get(TR_KEY_speed_limit_down)), TR_KEY_speed_limit_down_enabled); update_menu( speed_menu_info_[TR_UP], tr_formatter_speed_KBps(gtr_pref_int_get(TR_KEY_speed_limit_up)), TR_KEY_speed_limit_up_enabled); update_menu( ratio_menu_info_, fmt::format(_("Stop at Ratio ({ratio})"), fmt::arg("ratio", tr_strlratio(gtr_pref_double_get(TR_KEY_ratio_limit)))), TR_KEY_ratio_limit_enabled); } Glib::RefPtr MainWindow::Impl::createStatsMenu() { struct StatsModeInfo { char const* val; char const* i18n; }; static auto const stats_modes = std::array({ { { "total-ratio", N_("Total Ratio") }, { "session-ratio", N_("Session Ratio") }, { "total-transfer", N_("Total Transfer") }, { "session-transfer", N_("Session Transfer") }, } }); auto top = Gio::Menu::create(); auto actions = Gio::SimpleActionGroup::create(); auto const action_name = "stats-mode"s; auto const full_action_name = fmt::format("{}.{}", StatsMenuActionGroupName, action_name); auto stats_mode_action = actions->add_action_radio_string( action_name, [this, action_name](Glib::ustring const& value) { status_menu_toggled_cb(action_name, value); }, gtr_pref_string_get(TR_KEY_statusbar_stats)); for (auto const& mode : stats_modes) { auto item = Gio::MenuItem::create(_(mode.i18n), full_action_name); item->set_action_and_target(full_action_name, VariantString::create(mode.val)); top->append_item(item); } window_.insert_action_group(std::string(StatsMenuActionGroupName), actions); stats_actions_ = actions; return top; } /*** **** PUBLIC ***/ std::unique_ptr MainWindow::create( Gtk::Application& app, Glib::RefPtr const& actions, Glib::RefPtr const& core) { auto const builder = Gtk::Builder::create_from_resource(gtr_get_full_resource_path("MainWindow.ui")); return std::unique_ptr(gtr_get_widget_derived(builder, "MainWindow", app, actions, core)); } MainWindow::MainWindow( BaseObjectType* cast_item, Glib::RefPtr const& builder, Gtk::Application& app, Glib::RefPtr const& actions, Glib::RefPtr const& core) : Gtk::ApplicationWindow(cast_item) , impl_(std::make_unique(*this, builder, actions, core)) { app.add_window(*this); } MainWindow::~MainWindow() = default; MainWindow::Impl::Impl( MainWindow& window, Glib::RefPtr const& builder, Glib::RefPtr const& actions, Glib::RefPtr const& core) : window_(window) , core_(core) , scroll_(gtr_get_widget(builder, "torrents_view_scroll")) , view_(gtr_get_widget(builder, "torrents_view")) , toolbar_(gtr_get_widget(builder, "toolbar")) , filter_(gtr_get_widget_derived(builder, "filterbar", core_->get_session(), core_->get_model())) , status_(gtr_get_widget(builder, "statusbar")) , ul_lb_(gtr_get_widget(builder, "upload_speed_label")) , dl_lb_(gtr_get_widget(builder, "download_speed_label")) , stats_lb_(gtr_get_widget(builder, "statistics_label")) , alt_speed_image_(gtr_get_widget(builder, "alt_speed_button_image")) , alt_speed_button_(gtr_get_widget(builder, "alt_speed_button")) { /* make the window */ window.set_title(Glib::get_application_name()); window.set_default_size(gtr_pref_int_get(TR_KEY_main_window_width), gtr_pref_int_get(TR_KEY_main_window_height)); #if !GTKMM_CHECK_VERSION(4, 0, 0) window.move(gtr_pref_int_get(TR_KEY_main_window_x), gtr_pref_int_get(TR_KEY_main_window_y)); #endif if (gtr_pref_flag_get(TR_KEY_main_window_is_maximized)) { window.maximize(); } window.insert_action_group("win", actions); /** *** Statusbar **/ /* gear */ auto* gear_button = gtr_get_widget(builder, "gear_button"); gear_button->set_menu_model(createOptionsMenu()); #if GTKMM_CHECK_VERSION(4, 0, 0) for (auto* child = gear_button->get_first_child(); child != nullptr; child = child->get_next_sibling()) { if (auto* popover = dynamic_cast(child); popover != nullptr) { popover->signal_show().connect([this]() { onOptionsClicked(); }, false); break; } } #else gear_button->signal_clicked().connect([this]() { onOptionsClicked(); }, false); #endif /* turtle */ alt_speed_button_->signal_toggled().connect(sigc::mem_fun(*this, &Impl::alt_speed_toggled_cb)); /* ratio selector */ auto* ratio_button = gtr_get_widget(builder, "ratio_button"); ratio_button->set_menu_model(createStatsMenu()); /** *** Workarea **/ init_view(view_, filter_->get_filter_model()); { /* this is to determine the maximum width/height for the label */ int width = 0; int height = 0; auto const pango_layout = ul_lb_->create_pango_layout("999.99 kB/s"); pango_layout->get_pixel_size(width, height); ul_lb_->set_size_request(width, height); dl_lb_->set_size_request(width, height); } /* listen for prefs changes that affect the window */ prefsChanged(TR_KEY_compact_view); prefsChanged(TR_KEY_show_filterbar); prefsChanged(TR_KEY_show_statusbar); prefsChanged(TR_KEY_statusbar_stats); prefsChanged(TR_KEY_show_toolbar); prefsChanged(TR_KEY_alt_speed_enabled); pref_handler_id_ = core_->signal_prefs_changed().connect(sigc::mem_fun(*this, &Impl::prefsChanged)); tr_sessionSetAltSpeedFunc( core_->get_session(), [](tr_session* /*s*/, bool /*isEnabled*/, bool /*byUser*/, gpointer p) { Glib::signal_idle().connect_once([p]() { static_cast(p)->onAltSpeedToggledIdle(); }); }, this); refresh(); #if !GTKMM_CHECK_VERSION(4, 0, 0) /* prevent keyboard events being sent to the window first */ window.signal_key_press_event().connect( [this](GdkEventKey* event) { return gtk_window_propagate_key_event(static_cast(window_).gobj(), event); }, false); window.signal_key_release_event().connect( [this](GdkEventKey* event) { return gtk_window_propagate_key_event(static_cast(window_).gobj(), event); }, false); #endif } void MainWindow::Impl::updateStats() { Glib::ustring buf; auto const* const session = core_->get_session(); /* update the stats */ if (auto const pch = gtr_pref_string_get(TR_KEY_statusbar_stats); pch == "session-ratio") { auto const stats = tr_sessionGetStats(session); buf = fmt::format(_("Ratio: {ratio}"), fmt::arg("ratio", tr_strlratio(stats.ratio))); } else if (pch == "session-transfer") { auto const stats = tr_sessionGetStats(session); buf = fmt::format( C_("current session totals", "Down: {downloaded_size}, Up: {uploaded_size}"), fmt::arg("downloaded_size", tr_strlsize(stats.downloadedBytes)), fmt::arg("uploaded_size", tr_strlsize(stats.uploadedBytes))); } else if (pch == "total-transfer") { auto const stats = tr_sessionGetCumulativeStats(session); buf = fmt::format( C_("all-time totals", "Down: {downloaded_size}, Up: {uploaded_size}"), fmt::arg("downloaded_size", tr_strlsize(stats.downloadedBytes)), fmt::arg("uploaded_size", tr_strlsize(stats.uploadedBytes))); } else /* default is total-ratio */ { auto const stats = tr_sessionGetCumulativeStats(session); buf = fmt::format(_("Ratio: {ratio}"), fmt::arg("ratio", tr_strlratio(stats.ratio))); } stats_lb_->set_text(buf); } void MainWindow::Impl::updateSpeeds() { auto const* const session = core_->get_session(); if (session != nullptr) { auto dn_count = int{}; auto dn_speed = double{}; auto up_count = int{}; auto up_speed = double{}; auto const model = core_->get_model(); for (auto const& row : model->children()) { dn_count += row.get_value(torrent_cols.active_peers_down); dn_speed += row.get_value(torrent_cols.speed_down); up_count += row.get_value(torrent_cols.active_peers_up); up_speed += row.get_value(torrent_cols.speed_up); } dl_lb_->set_text(fmt::format(_("{download_speed} ▼"), fmt::arg("download_speed", tr_formatter_speed_KBps(dn_speed)))); dl_lb_->set_visible(dn_count > 0); ul_lb_->set_text(fmt::format(_("{upload_speed} ▲"), fmt::arg("upload_speed", tr_formatter_speed_KBps(up_speed)))); ul_lb_->set_visible(dn_count > 0 || up_count > 0); } } void MainWindow::refresh() { impl_->refresh(); } void MainWindow::Impl::refresh() { if (core_ != nullptr && core_->get_session() != nullptr) { updateSpeeds(); updateStats(); } } Glib::RefPtr MainWindow::get_selection() const { return impl_->get_selection(); } Glib::RefPtr MainWindow::Impl::get_selection() const { return view_->get_selection(); } void MainWindow::set_busy(bool isBusy) { if (get_realized()) { #if GTKMM_CHECK_VERSION(4, 0, 0) auto const cursor = isBusy ? Gdk::Cursor::create("wait") : Glib::RefPtr(); set_cursor(cursor); #else auto const display = get_display(); auto const cursor = isBusy ? Gdk::Cursor::create(display, Gdk::WATCH) : Glib::RefPtr(); get_window()->set_cursor(cursor); display->flush(); #endif } }