transmission/gtk/MainWindow.cc

685 lines
23 KiB
C++

// 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 <array>
#include <memory>
#include <string>
#include <glibmm/i18n.h>
#include <libtransmission/transmission.h>
#include <libtransmission/utils.h> // 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<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;
};
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)
Glib::RefPtr<Gtk::TreeSelection> get_selection() const;
void refresh();
void prefsChanged(tr_quark key);
private:
void init_view(Gtk::TreeView* view, Glib::RefPtr<Gtk::TreeModel> const& model);
Glib::RefPtr<Gio::MenuModel> createOptionsMenu();
Glib::RefPtr<Gio::MenuModel> createSpeedMenu(Glib::RefPtr<Gio::SimpleActionGroup> actions, tr_direction dir);
Glib::RefPtr<Gio::MenuModel> createRatioMenu(Glib::RefPtr<Gio::SimpleActionGroup> actions);
Glib::RefPtr<Gio::MenuModel> createStatsMenu();
void on_popup_menu(GdkEventButton* event);
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_;
Glib::RefPtr<Gio::ActionGroup> options_actions_;
Glib::RefPtr<Gio::ActionGroup> stats_actions_;
std::array<OptionMenuInfo, 2> 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_;
Gtk::Menu* popup_menu_ = nullptr;
};
/***
****
***/
void MainWindow::Impl::on_popup_menu(GdkEventButton* event)
{
if (popup_menu_ == nullptr)
{
popup_menu_ = Gtk::make_managed<Gtk::Menu>(gtr_action_get_object<Gio::Menu>("main-window-popup"));
popup_menu_->attach_to_widget(window_);
}
popup_menu_->popup_at_pointer(reinterpret_cast<GdkEvent*>(event));
}
namespace
{
bool tree_view_search_equal_func(
Glib::RefPtr<Gtk::TreeModel> const& /*model*/,
int /*column*/,
Glib::ustring const& key,
Gtk::TreeModel::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<Gtk::TreeModel> 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<TorrentCellRenderer>();
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;
view->signal_popup_menu().connect_notify([this]() { on_popup_menu(nullptr); });
view->signal_button_press_event().connect(
[this, view](GdkEventButton* event)
{ return on_tree_view_button_pressed(view, event, sigc::mem_fun(*this, &Impl::on_popup_menu)); },
false);
view->signal_button_release_event().connect([view](GdkEventButton* event)
{ return on_tree_view_button_released(view, event); });
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 it *does* revalidate when it thinks the style's been tweaked */
g_signal_emit_by_name(Glib::unwrap(view_), "style-updated", nullptr, nullptr);
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_image_->set_from_icon_name("turtle-symbolic", Gtk::BuiltinIconSize::ICON_SIZE_MENU);
alt_speed_button_->set_halign(TR_GTK_ALIGN(CENTER));
alt_speed_button_->set_valign(TR_GTK_ALIGN(CENTER));
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> actions, tr_direction dir)
{
auto& info = speed_menu_info_[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> actions)
{
static double const stockRatios[] = { 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()
{
static struct
{
char const* val;
char const* i18n;
} const stats_modes[] = {
{ "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<Gtk::TreeView>(builder, "torrents_view"))
, toolbar_(gtr_get_widget<Gtk::Widget>(builder, "toolbar"))
, filter_(gtr_get_widget_derived<FilterBar>(builder, "filterbar", core_->get_session(), core_->get_model()))
, 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));
window.move(gtr_pref_int_get(TR_KEY_main_window_x), gtr_pref_int_get(TR_KEY_main_window_y));
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());
gear_button->signal_clicked().connect([this]() { onOptionsClicked(); }, false);
/* 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();
/* 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);
}
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<Gtk::TreeSelection> MainWindow::get_selection() const
{
return impl_->get_selection();
}
Glib::RefPtr<Gtk::TreeSelection> MainWindow::Impl::get_selection() const
{
return view_->get_selection();
}
void MainWindow::set_busy(bool isBusy)
{
if (get_realized())
{
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();
}
}