mirror of
https://github.com/transmission/transmission
synced 2025-01-30 10:52:00 +00:00
586cff9506
* Add compat operator* for RefPtr * Rename `*_tree_view_*` button handling helpers to `*_item_view_*` * Move torrent item colors to CSS * Switch to list view for torrents list (GTK 4) * Bump Fedora image to 39 (current rawhide) for GTK 4.11 Enable deprecations as there're lots of them in 4.11 and I'm not keen on fixing them all right now. Disable warnings as errors due to -Warray-bounds issue somewhere in libfmt.
903 lines
29 KiB
C++
903 lines
29 KiB
C++
// This file Copyright © 2005-2023 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 <libtransmission/transmission.h>
|
|
#include <libtransmission/utils.h> // tr_formatter_speed_KBps()
|
|
|
|
#include <gdkmm/cursor.h>
|
|
#include <gdkmm/rectangle.h>
|
|
#include <giomm/menu.h>
|
|
#include <giomm/menuitem.h>
|
|
#include <giomm/menumodel.h>
|
|
#include <giomm/simpleaction.h>
|
|
#include <giomm/simpleactiongroup.h>
|
|
#include <glibmm/i18n.h>
|
|
#include <glibmm/main.h>
|
|
#include <glibmm/miscutils.h>
|
|
#include <glibmm/ustring.h>
|
|
#include <glibmm/variant.h>
|
|
#include <gtkmm/image.h>
|
|
#include <gtkmm/label.h>
|
|
#include <gtkmm/menubutton.h>
|
|
#include <gtkmm/scrolledwindow.h>
|
|
#include <gtkmm/togglebutton.h>
|
|
#include <gtkmm/treemodel.h>
|
|
#include <gtkmm/treeview.h>
|
|
#include <gtkmm/widget.h>
|
|
#include <gtkmm/window.h>
|
|
|
|
#if GTKMM_CHECK_VERSION(4, 0, 0)
|
|
#include <gtkmm/listitemfactory.h>
|
|
#include <gtkmm/multiselection.h>
|
|
#include <gtkmm/popovermenu.h>
|
|
#else
|
|
#include <gdkmm/display.h>
|
|
#include <gdkmm/window.h>
|
|
#include <gtkmm/menu.h>
|
|
#include <gtkmm/treeselection.h>
|
|
#include <gtkmm/treeviewcolumn.h>
|
|
#endif
|
|
|
|
#include <array>
|
|
#include <memory>
|
|
#include <string>
|
|
|
|
using namespace std::string_literals;
|
|
using namespace std::string_view_literals;
|
|
|
|
using VariantInt = Glib::Variant<int>;
|
|
using VariantDouble = Glib::Variant<double>;
|
|
using VariantString = Glib::Variant<Glib::ustring>;
|
|
|
|
namespace
|
|
{
|
|
|
|
auto constexpr OptionsMenuActionGroupName = "options-menu"sv;
|
|
auto constexpr StatsMenuActionGroupName = "stats-menu"sv;
|
|
|
|
} // namespace
|
|
|
|
class MainWindow::Impl
|
|
{
|
|
struct OptionMenuInfo
|
|
{
|
|
Glib::RefPtr<Gio::SimpleAction> action;
|
|
Glib::RefPtr<Gio::MenuItem> on_item;
|
|
Glib::RefPtr<Gio::Menu> section;
|
|
};
|
|
|
|
using TorrentView = IF_GTKMM4(Gtk::ListView, Gtk::TreeView);
|
|
using TorrentViewSelection = IF_GTKMM4(Gtk::MultiSelection, Gtk::TreeSelection);
|
|
|
|
public:
|
|
Impl(
|
|
MainWindow& window,
|
|
Glib::RefPtr<Gtk::Builder> const& builder,
|
|
Glib::RefPtr<Gio::ActionGroup> const& actions,
|
|
Glib::RefPtr<Session> const& core);
|
|
~Impl();
|
|
|
|
TR_DISABLE_COPY_MOVE(Impl)
|
|
|
|
[[nodiscard]] Glib::RefPtr<TorrentViewSelection> 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<FilterBar::Model> const& model);
|
|
|
|
Glib::RefPtr<Gio::MenuModel> createOptionsMenu();
|
|
Glib::RefPtr<Gio::MenuModel> createSpeedMenu(Glib::RefPtr<Gio::SimpleActionGroup> const& actions, tr_direction dir);
|
|
Glib::RefPtr<Gio::MenuModel> createRatioMenu(Glib::RefPtr<Gio::SimpleActionGroup> const& actions);
|
|
|
|
Glib::RefPtr<Gio::MenuModel> 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<Session> const core_;
|
|
|
|
sigc::signal<void()> signal_selection_changed_;
|
|
|
|
Glib::RefPtr<Gio::ActionGroup> options_actions_;
|
|
Glib::RefPtr<Gio::ActionGroup> stats_actions_;
|
|
|
|
std::array<OptionMenuInfo, 2> speed_menu_info_;
|
|
OptionMenuInfo ratio_menu_info_;
|
|
|
|
#if GTKMM_CHECK_VERSION(4, 0, 0)
|
|
Glib::RefPtr<Gtk::ListItemFactory> item_factory_compact_;
|
|
Glib::RefPtr<Gtk::ListItemFactory> item_factory_full_;
|
|
Glib::RefPtr<Gtk::MultiSelection> 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<Gio::Menu>("main-window-popup");
|
|
|
|
#if GTKMM_CHECK_VERSION(4, 0, 0)
|
|
popup_menu_ = Gtk::make_managed<Gtk::PopoverMenu>(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<Gtk::Menu>(menu);
|
|
popup_menu_->attach_to_widget(window_);
|
|
#endif
|
|
}
|
|
|
|
#if GTKMM_CHECK_VERSION(4, 0, 0)
|
|
popup_menu_->set_pointing_to({ static_cast<int>(event_x), static_cast<int>(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<GStrvBuilder, GtrStrvBuilderDeleter>;
|
|
|
|
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<char const**>(lhs)); // NOLINT(cppcoreguidelines-pro-type-const-cast)
|
|
g_strv_builder_addv(builder.get(), const_cast<char const**>(rhs)); // NOLINT(cppcoreguidelines-pro-type-const-cast)
|
|
|
|
return g_strv_builder_end(builder.get());
|
|
}
|
|
|
|
#else
|
|
|
|
bool tree_view_search_equal_func(
|
|
Glib::RefPtr<Gtk::TreeModel> 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<FilterBar::Model> 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<TorrentCellRenderer>();
|
|
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", 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<Gio::MenuModel> MainWindow::Impl::createSpeedMenu(
|
|
Glib::RefPtr<Gio::SimpleActionGroup> 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<VariantInt>(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<Gio::MenuModel> MainWindow::Impl::createRatioMenu(Glib::RefPtr<Gio::SimpleActionGroup> const& actions)
|
|
{
|
|
static auto const stockRatios = std::array<double, 7>({ 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<VariantDouble>(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<Gio::MenuModel> 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<VariantString>(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<Gio::MenuModel> MainWindow::Impl::createStatsMenu()
|
|
{
|
|
struct StatsModeInfo
|
|
{
|
|
char const* val;
|
|
char const* i18n;
|
|
};
|
|
|
|
static auto const stats_modes = std::array<StatsModeInfo, 4>({ {
|
|
{ "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> MainWindow::create(
|
|
Gtk::Application& app,
|
|
Glib::RefPtr<Gio::ActionGroup> const& actions,
|
|
Glib::RefPtr<Session> const& core)
|
|
{
|
|
auto const builder = Gtk::Builder::create_from_resource(gtr_get_full_resource_path("MainWindow.ui"));
|
|
return std::unique_ptr<MainWindow>(gtr_get_widget_derived<MainWindow>(builder, "MainWindow", app, actions, core));
|
|
}
|
|
|
|
MainWindow::MainWindow(
|
|
BaseObjectType* cast_item,
|
|
Glib::RefPtr<Gtk::Builder> const& builder,
|
|
Gtk::Application& app,
|
|
Glib::RefPtr<Gio::ActionGroup> const& actions,
|
|
Glib::RefPtr<Session> const& core)
|
|
: Gtk::ApplicationWindow(cast_item)
|
|
, impl_(std::make_unique<Impl>(*this, builder, actions, core))
|
|
{
|
|
app.add_window(*this);
|
|
}
|
|
|
|
MainWindow::~MainWindow() = default;
|
|
|
|
MainWindow::Impl::Impl(
|
|
MainWindow& window,
|
|
Glib::RefPtr<Gtk::Builder> const& builder,
|
|
Glib::RefPtr<Gio::ActionGroup> const& actions,
|
|
Glib::RefPtr<Session> const& core)
|
|
: window_(window)
|
|
, core_(core)
|
|
, scroll_(gtr_get_widget<Gtk::ScrolledWindow>(builder, "torrents_view_scroll"))
|
|
, view_(gtr_get_widget<TorrentView>(builder, "torrents_view"))
|
|
, toolbar_(gtr_get_widget<Gtk::Widget>(builder, "toolbar"))
|
|
, filter_(gtr_get_widget_derived<FilterBar>(builder, "filterbar", core_))
|
|
, status_(gtr_get_widget<Gtk::Widget>(builder, "statusbar"))
|
|
, ul_lb_(gtr_get_widget<Gtk::Label>(builder, "upload_speed_label"))
|
|
, dl_lb_(gtr_get_widget<Gtk::Label>(builder, "download_speed_label"))
|
|
, stats_lb_(gtr_get_widget<Gtk::Label>(builder, "statistics_label"))
|
|
, alt_speed_image_(gtr_get_widget<Gtk::Image>(builder, "alt_speed_button_image"))
|
|
, alt_speed_button_(gtr_get_widget<Gtk::ToggleButton>(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<Gtk::MenuButton>(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<Gtk::Popover*>(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<Gtk::MenuButton>(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<Impl*>(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<Gtk::Window&>(window_).gobj(), event); },
|
|
false);
|
|
window.signal_key_release_event().connect(
|
|
[this](GdkEventKey* event) { return gtk_window_propagate_key_event(static_cast<Gtk::Window&>(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 i = 0U, count = model->get_n_items(); i < count; ++i)
|
|
{
|
|
auto const torrent = gtr_ptr_dynamic_cast<Torrent>(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(_("{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::Impl::TorrentViewSelection> MainWindow::Impl::get_selection() const
|
|
{
|
|
return IF_GTKMM4(selection_, view_->get_selection());
|
|
}
|
|
|
|
void MainWindow::for_each_selected_torrent(std::function<void(Glib::RefPtr<Torrent> const&)> const& callback) const
|
|
{
|
|
for_each_selected_torrent_until(sigc::bind_return(callback, false));
|
|
}
|
|
|
|
bool MainWindow::for_each_selected_torrent_until(std::function<bool(Glib::RefPtr<Torrent> 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<Torrent>(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<Gdk::Cursor>();
|
|
set_cursor(cursor);
|
|
#else
|
|
auto const display = get_display();
|
|
auto const cursor = isBusy ? Gdk::Cursor::create(display, Gdk::WATCH) : Glib::RefPtr<Gdk::Cursor>();
|
|
get_window()->set_cursor(cursor);
|
|
display->flush();
|
|
#endif
|
|
}
|
|
}
|
|
|
|
sigc::signal<void()>& MainWindow::signal_selection_changed()
|
|
{
|
|
return impl_->signal_selection_changed();
|
|
}
|