// 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 "MainWindow.h" #include "Actions.h" #include "FilterBar.h" #include "GtkCompat.h" #include "ListModelAdapter.h" #include "Prefs.h" #include "PrefsDialog.h" #include "Session.h" #include "Torrent.h" #include "Utils.h" #if !GTKMM_CHECK_VERSION(4, 0, 0) #include "TorrentCellRenderer.h" #endif #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 #include #include #include #endif #include #include #include using namespace std::string_literals; using namespace std::string_view_literals; using namespace libtransmission::Values; 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; }; using TorrentView = IF_GTKMM4(Gtk::ListView, Gtk::TreeView); using TorrentViewSelection = IF_GTKMM4(Gtk::MultiSelection, Gtk::TreeSelection); 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); auto& signal_selection_changed() { return signal_selection_changed_; } private: void init_view(TorrentView* 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 event_x, double event_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_; sigc::signal signal_selection_changed_; Glib::RefPtr options_actions_; Glib::RefPtr stats_actions_; std::array speed_menu_info_; OptionMenuInfo ratio_menu_info_; #if GTKMM_CHECK_VERSION(4, 0, 0) Glib::RefPtr item_factory_compact_; Glib::RefPtr item_factory_full_; Glib::RefPtr selection_; #else TorrentCellRenderer* renderer_ = nullptr; Gtk::TreeViewColumn* column_ = nullptr; #endif Gtk::ScrolledWindow* scroll_ = nullptr; TorrentView* 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; 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(*view_); popup_menu_->set_has_arrow(false); popup_menu_->set_halign(view_->get_direction() == Gtk::TextDirection::RTL ? Gtk::Align::END : Gtk::Align::START); view_->signal_destroy().connect( [this]() { popup_menu_->unparent(); popup_menu_ = nullptr; }); #else popup_menu_ = Gtk::make_managed(menu); popup_menu_->attach_to_widget(window_); #endif } #if GTKMM_CHECK_VERSION(4, 0, 0) popup_menu_->set_pointing_to({ static_cast(event_x), static_cast(event_y), 1, 1 }); popup_menu_->popup(); #else popup_menu_->popup_at_pointer(nullptr); #endif } namespace { #if GTKMM_CHECK_VERSION(4, 0, 0) class GtrStrvBuilderDeleter { public: void operator()(GStrvBuilder* builder) const { if (builder != nullptr) { g_strv_builder_unref(builder); } } }; using GtrStrvBuilderPtr = std::unique_ptr; GStrv gtr_strv_join(GObject* /*object*/, GStrv lhs, GStrv rhs) { auto const builder = GtrStrvBuilderPtr(g_strv_builder_new()); if (builder == nullptr) { return nullptr; } g_strv_builder_addv(builder.get(), const_cast(lhs)); // NOLINT(cppcoreguidelines-pro-type-const-cast) g_strv_builder_addv(builder.get(), const_cast(rhs)); // NOLINT(cppcoreguidelines-pro-type-const-cast) return g_strv_builder_end(builder.get()); } #else bool tree_view_search_equal_func( Glib::RefPtr const& /*model*/, int /*column*/, Glib::ustring const& key, Gtk::TreeModel::const_iterator const& iter) { static auto const& self_col = Torrent::get_columns().self; auto const name = iter->get_value(self_col)->get_name_collated(); return name.find(key.lowercase()) == Glib::ustring::npos; } #endif } // namespace void MainWindow::Impl::init_view(TorrentView* view, Glib::RefPtr const& model) { #if GTKMM_CHECK_VERSION(4, 0, 0) auto const create_builder_list_item_factory = [](std::string const& filename) { auto builder_scope = Glib::wrap(G_OBJECT(gtk_builder_cscope_new())); gtk_builder_cscope_add_callback(GTK_BUILDER_CSCOPE(builder_scope->gobj()), gtr_strv_join); return Glib::wrap(gtk_builder_list_item_factory_new_from_resource( GTK_BUILDER_SCOPE(builder_scope->gobj()), gtr_get_full_resource_path(filename).c_str())); }; item_factory_compact_ = create_builder_list_item_factory("TorrentListItemCompact.ui"s); item_factory_full_ = create_builder_list_item_factory("TorrentListItemFull.ui"s); view->signal_activate().connect([](guint /*position*/) { gtr_action_activate("show-torrent-properties"); }); selection_ = Gtk::MultiSelection::create(model); selection_->signal_selection_changed().connect([this](guint /*position*/, guint /*n_items*/) { signal_selection_changed_.emit(); }); view->set_factory(gtr_pref_flag_get(TR_KEY_compact_view) ? item_factory_compact_ : item_factory_full_); view->set_model(selection_); #else static auto const& torrent_cols = Torrent::get_columns(); 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.self); view->signal_popup_menu().connect_notify([this]() { on_popup_menu(0, 0); }); view->signal_row_activated().connect([](auto const& /*path*/, auto* /*column*/) { gtr_action_activate("show-torrent-properties"); }); view->set_model(model); view->get_selection()->signal_changed().connect([this]() { signal_selection_changed_.emit(); }); #endif setup_item_view_button_event_handling( *view, [this, view](guint /*button*/, TrGdkModifierType /*state*/, double view_x, double view_y, bool context_menu_requested) { return on_item_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_item_view_button_released(*view, view_x, view_y); }); } void MainWindow::Impl::prefsChanged(tr_quark const key) { switch (key) { case TR_KEY_compact_view: #if GTKMM_CHECK_VERSION(4, 0, 0) view_->set_factory(gtr_pref_flag_get(key) ? item_factory_compact_ : item_factory_full_); #else 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 */ view_->set_fixed_height_mode(false); view_->set_row_separator_func({}); view_->unset_row_separator_func(); view_->set_fixed_height_mode(true); #endif 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", Speed{ gtr_pref_int_get(TR_KEY_alt_speed_down), Speed::Units::KByps }.to_string()), fmt::arg("upload_speed", Speed{ gtr_pref_int_get(TR_KEY_alt_speed_up), Speed::Units::KByps }.to_string()))); } 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(Speed{ KBps, Speed::Units::KByps }.to_string(), 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], Speed{ gtr_pref_int_get(TR_KEY_speed_limit_down), Speed::Units::KByps }.to_string(), TR_KEY_speed_limit_down_enabled); update_menu( speed_menu_info_[TR_UP], Speed{ gtr_pref_int_get(TR_KEY_speed_limit_up), Speed::Units::KByps }.to_string(), 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_)) , 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 = Speed{}; auto up_count = int{}; auto up_speed = Speed{}; auto const model = core_->get_model(); for (auto i = 0U, count = model->get_n_items(); i < count; ++i) { auto const torrent = gtr_ptr_dynamic_cast(model->get_object(i)); dn_count += torrent->get_active_peers_down(); dn_speed += torrent->get_speed_down(); up_count += torrent->get_active_peers_up(); up_speed += torrent->get_speed_up(); } dl_lb_->set_text(fmt::format(fmt::runtime(_("{download_speed} ▼")), fmt::arg("download_speed", dn_speed.to_string()))); dl_lb_->set_visible(dn_count > 0); ul_lb_->set_text(fmt::format(fmt::runtime(_("{upload_speed} ▲")), fmt::arg("upload_speed", up_speed.to_string()))); 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::Impl::get_selection() const { return IF_GTKMM4(selection_, view_->get_selection()); } void MainWindow::for_each_selected_torrent(std::function const&)> const& callback) const { for_each_selected_torrent_until(sigc::bind_return(callback, false)); } bool MainWindow::for_each_selected_torrent_until(std::function const&)> const& callback) const { auto const selection = impl_->get_selection(); auto const model = selection->get_model(); bool result = false; #if GTKMM_CHECK_VERSION(4, 0, 0) auto const selected_items = selection->get_selection(); // TODO(C++20): Move into the `for` for (auto const position : *selected_items) { if (callback(gtr_ptr_dynamic_cast(model->get_object(position)))) { result = true; break; } } #else static auto const& self_col = Torrent::get_columns().self; for (auto const& path : selection->get_selected_rows()) { auto const torrent = Glib::make_refptr_for_instance(model->get_iter(path)->get_value(self_col)); torrent->reference(); if (callback(torrent)) { result = true; break; } } #endif return result; } void MainWindow::select_all() { impl_->get_selection()->select_all(); } void MainWindow::unselect_all() { impl_->get_selection()->unselect_all(); } 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 } } sigc::signal& MainWindow::signal_selection_changed() { return impl_->signal_selection_changed(); }