Add support for GTK 4 (#3916)

* Make compact mode switch work for both GTK 3 and 4

* Implement GTK 4-specific view gesture handling

* Fix torrents view context menu on GTK 4

* Explicitly show/hide menubar on startup/teardown

* Switch from `Gtk::Pixbuf` to `Gio::Icon` for views

* Support GTK 4 exceptions based on `std::exception`

* Fix options menu setup with GTK 4

* Use `delete-event` (GTK 3) and `close-request` (GTK 4) signals to handle window clousure

* Add custom file chooser button implementation

GTK 4 drops FileChooserButton widget and suggests implementing it using
Button.

* Add helpers to set X11 hints with GTK 4

* Remove `HigWorkarea` class that's no longer used

* Make main menu shortcuts work with GTK 4

* Make drops work in main window and make dialog with GTK 4

* Remove unused `gtr_action_get_widget()` helper

* Fix text direction mark setup with GTK 4 (due to switch to enum class)

* Fix file tree font size calculation with GTK 4

* Fix crash during shutdown with GTK 4

* Switch from `RadioButton` to `CheckButton` for compatibility with GTK 4

* Fix opening files with GTK 4

* Rework torrent cell renderer to support both GTK 3 and 4

* Disable system tray icon support with GTK 4

* Fix windows positioning with GTK 4

* Fix focus event handling with GTK 4

* Adapt to tree model row/iterator changes in GTK 4

* Adapt to toplevel/root window changes in GTK 4

* Adapt to clipboard changes in GTK 4

* Adapt to icon/theme changes in GTK 4

* Adapt to file/path changes in GTK 4

* Random leftover fixes for GTK 4 compatibility

* Clean up unused code

* Move GTK 3 *.ui files into a subdirectory

* Add GTK 4 *.ui files

* Search for both GTK 3 and 4 during configuration
This commit is contained in:
Mike Gelfand
2022-10-08 15:50:03 -07:00
committed by GitHub
parent e3b871216f
commit c75c6bf5c8
63 changed files with 6329 additions and 849 deletions

View File

@@ -41,6 +41,7 @@
#include "MakeDialog.h"
#include "MessageLogWindow.h"
#include "OptionsDialog.h"
#include "PathButton.h"
#include "Prefs.h"
#include "PrefsDialog.h"
#include "RelocateDialog.h"
@@ -51,6 +52,13 @@
using namespace std::literals;
#if GTKMM_CHECK_VERSION(4, 0, 0)
using FileListValue = Glib::Value<GSList*>;
using FileListHandler = Glib::SListHandler<Glib::RefPtr<Gio::File>>;
using StringValue = Glib::Value<Glib::ustring>;
#endif
#define SHOW_LICENSE
namespace
@@ -103,9 +111,12 @@ private:
bool refresh_actions();
void refresh_actions_soon();
void on_main_window_size_allocated(Gtk::Allocation& alloc);
bool on_main_window_focus_in(GdkEventFocus* event);
void on_main_window_size_allocated();
void on_main_window_focus_in();
#if GTKMM_CHECK_VERSION(4, 0, 0)
bool on_drag_data_received(Glib::ValueBase const& value, double x, double y);
#else
void on_drag_data_received(
Glib::RefPtr<Gdk::DragContext> const& drag_context,
gint x,
@@ -113,6 +124,7 @@ private:
Gtk::SelectionData const& selection_data,
guint info,
guint time_);
#endif
bool on_rpc_changed_idle(tr_rpc_callback_type type, tr_torrent_id_t torrent_id);
@@ -121,7 +133,7 @@ private:
void hideMainWindow();
void toggleMainWindow();
bool winclose(GdkEventAny* event);
bool winclose();
void rowChangedCB(Gtk::TreePath const& path, Gtk::TreeModel::iterator const& iter);
void app_setup();
@@ -189,7 +201,7 @@ namespace
template<typename T>
void gtr_window_present(T const& window)
{
window->present(gtk_get_current_event_time());
window->present(GDK_CURRENT_TIME);
}
/***
@@ -234,7 +246,7 @@ void Application::Impl::show_details_dialog_for_selected_torrents()
{
auto dialog = DetailsDialog::create(*wind_, core_);
dialog->set_torrents(ids);
dialog->signal_hide().connect([this, key]() { details_.erase(key); });
gtr_window_on_close(*dialog, [this, key]() { details_.erase(key); });
dialog_it = details_.try_emplace(key, std::move(dialog)).first;
dialog_it->second->show();
}
@@ -372,23 +384,34 @@ void ensure_magnet_handler_exists()
} // namespace
void Application::Impl::on_main_window_size_allocated(Gtk::Allocation& /*alloc*/)
void Application::Impl::on_main_window_size_allocated()
{
#if GTKMM_CHECK_VERSION(4, 0, 0)
bool const is_maximized = wind_->is_maximized();
#else
auto const gdk_window = wind_->get_window();
bool const is_maximized = gdk_window != nullptr && (gdk_window->get_state() & Gdk::WINDOW_STATE_MAXIMIZED) != 0;
#endif
gtr_pref_int_set(TR_KEY_main_window_is_maximized, is_maximized);
if (!is_maximized)
{
#if !GTKMM_CHECK_VERSION(4, 0, 0)
int x;
int y;
int w;
int h;
wind_->get_position(x, y);
wind_->get_size(w, h);
gtr_pref_int_set(TR_KEY_main_window_x, x);
gtr_pref_int_set(TR_KEY_main_window_y, y);
#endif
int w;
int h;
#if GTKMM_CHECK_VERSION(4, 0, 0)
wind_->get_default_size(w, h);
#else
wind_->get_size(w, h);
#endif
gtr_pref_int_set(TR_KEY_main_window_width, w);
gtr_pref_int_set(TR_KEY_main_window_height, h);
}
@@ -534,18 +557,20 @@ void Application::on_startup()
void Application::Impl::on_startup()
{
Gtk::IconTheme::get_default()->add_resource_path(gtr_get_full_resource_path("icons"s));
IF_GTKMM4(Gtk::IconTheme::get_for_display(Gdk::Display::get_default()), Gtk::IconTheme::get_default())
->add_resource_path(gtr_get_full_resource_path("icons"s));
Gtk::Window::set_default_icon_name(AppIconName);
/* Add style provider to the window. */
auto css_provider = Gtk::CssProvider::create();
css_provider->load_from_resource(gtr_get_full_resource_path("transmission-ui.css"));
Gtk::StyleContext::add_provider_for_screen(
Gdk::Screen::get_default(),
Gtk::StyleContext::IF_GTKMM4(add_provider_for_display, add_provider_for_screen)(
IF_GTKMM4(Gdk::Display::get_default(), Gdk::Screen::get_default()),
css_provider,
GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
std::ignore = FilterBar();
std::ignore = PathButton();
tr_session* session;
@@ -576,11 +601,26 @@ void Application::Impl::on_startup()
ui_builder_ = Gtk::Builder::create_from_resource(gtr_get_full_resource_path("transmission-ui.xml"s));
auto const actions = gtr_actions_init(ui_builder_, this);
app_.set_menubar(gtr_action_get_object<Gio::Menu>("main-window-menu"));
auto const main_menu = gtr_action_get_object<Gio::Menu>("main-window-menu");
app_.set_menubar(main_menu);
/* create main window now to be a parent to any error dialogs */
wind_ = MainWindow::create(app_, actions, core_);
wind_->signal_size_allocate().connect(sigc::mem_fun(*this, &Impl::on_main_window_size_allocated));
wind_->set_show_menubar(true);
#if GTKMM_CHECK_VERSION(4, 0, 0)
wind_->property_maximized().signal_changed().connect(sigc::mem_fun(*this, &Impl::on_main_window_size_allocated));
wind_->property_default_width().signal_changed().connect(sigc::mem_fun(*this, &Impl::on_main_window_size_allocated));
wind_->property_default_height().signal_changed().connect(sigc::mem_fun(*this, &Impl::on_main_window_size_allocated));
#else
wind_->signal_size_allocate().connect(sigc::hide<0>(sigc::mem_fun(*this, &Impl::on_main_window_size_allocated)));
#endif
#if GTKMM_CHECK_VERSION(4, 0, 0)
auto const shortcut_controller = Gtk::ShortcutController::create(gtr_shortcuts_get_from_menu(main_menu));
shortcut_controller->set_scope(Gtk::ShortcutScope::GLOBAL);
wind_->add_controller(shortcut_controller);
#endif
app_.hold();
app_setup();
tr_sessionSetRPCCallback(session, &Impl::on_rpc_changed, this);
@@ -712,7 +752,7 @@ void Application::Impl::app_setup()
}
else
{
wind_->set_skip_taskbar_hint(icon_ != nullptr);
gtr_window_set_skip_taskbar_hint(*wind_, icon_ != nullptr);
is_iconified_ = false; // ensure that the next toggle iconifies
gtr_action_set_toggled("toggle-main-window", false);
}
@@ -750,8 +790,12 @@ void Application::Impl::app_setup()
void Application::Impl::placeWindowFromPrefs()
{
#if GTKMM_CHECK_VERSION(4, 0, 0)
wind_->set_default_size((int)gtr_pref_int_get(TR_KEY_main_window_width), (int)gtr_pref_int_get(TR_KEY_main_window_height));
#else
wind_->resize((int)gtr_pref_int_get(TR_KEY_main_window_width), (int)gtr_pref_int_get(TR_KEY_main_window_height));
wind_->move((int)gtr_pref_int_get(TR_KEY_main_window_x), (int)gtr_pref_int_get(TR_KEY_main_window_y));
#endif
}
void Application::Impl::presentMainWindow()
@@ -762,7 +806,7 @@ void Application::Impl::presentMainWindow()
{
is_iconified_ = false;
wind_->set_skip_taskbar_hint(false);
gtr_window_set_skip_taskbar_hint(*wind_, false);
}
if (!wind_->get_visible())
@@ -772,14 +816,14 @@ void Application::Impl::presentMainWindow()
}
gtr_window_present(wind_);
wind_->get_window()->raise();
gtr_window_raise(*wind_);
}
void Application::Impl::hideMainWindow()
{
gtr_action_set_toggled("toggle-main-window", false);
wind_->set_skip_taskbar_hint(true);
gtr_window_set_skip_taskbar_hint(*wind_, true);
gtr_widget_set_visible(*wind_, false);
is_iconified_ = true;
}
@@ -796,7 +840,7 @@ void Application::Impl::toggleMainWindow()
}
}
bool Application::Impl::winclose(GdkEventAny* /*event*/)
bool Application::Impl::winclose()
{
if (icon_ != nullptr)
{
@@ -818,6 +862,32 @@ void Application::Impl::rowChangedCB(Gtk::TreePath const& path, Gtk::TreeModel::
}
}
#if GTKMM_CHECK_VERSION(4, 0, 0)
bool Application::Impl::on_drag_data_received(Glib::ValueBase const& value, double /*x*/, double /*y*/)
{
if (G_VALUE_HOLDS(value.gobj(), GDK_TYPE_FILE_LIST))
{
FileListValue files_value;
files_value.init(value.gobj());
open_files(FileListHandler::slist_to_vector(files_value.get(), Glib::OwnershipType::OWNERSHIP_NONE));
return true;
}
else if (G_VALUE_HOLDS(value.gobj(), StringValue::value_type()))
{
StringValue string_value;
string_value.init(value.gobj());
if (auto const text = gtr_str_strip(string_value.get()); !text.empty())
{
return core_->add_from_url(text);
}
}
return false;
}
#else
void Application::Impl::on_drag_data_received(
Glib::RefPtr<Gdk::DragContext> const& drag_context,
gint /*x*/,
@@ -847,6 +917,8 @@ void Application::Impl::on_drag_data_received(
drag_context->drag_finish(true, false, time_);
}
#endif
void Application::Impl::main_window_setup()
{
// g_assert(nullptr == cbdata->wind);
@@ -857,14 +929,21 @@ void Application::Impl::main_window_setup()
refresh_actions_soon();
auto const model = core_->get_model();
model->signal_row_changed().connect(sigc::mem_fun(*this, &Impl::rowChangedCB));
wind_->signal_delete_event().connect(sigc::mem_fun(*this, &Impl::winclose));
gtr_window_on_close(*wind_, sigc::mem_fun(*this, &Impl::winclose));
refresh_actions();
/* register to handle URIs that get dragged onto our main window */
#if GTKMM_CHECK_VERSION(4, 0, 0)
auto drop_controller = Gtk::DropTarget::create(G_TYPE_INVALID, Gdk::DragAction::COPY);
drop_controller->set_gtypes({ StringValue::value_type(), GDK_TYPE_FILE_LIST });
drop_controller->signal_drop().connect(sigc::mem_fun(*this, &Impl::on_drag_data_received), false);
wind_->add_controller(drop_controller);
#else
wind_->drag_dest_set(Gtk::DEST_DEFAULT_ALL, Gdk::ACTION_COPY);
wind_->drag_dest_add_uri_targets();
wind_->drag_dest_add_text_targets(); /* links dragged from browsers are text */
wind_->signal_drag_data_received().connect(sigc::mem_fun(*this, &Impl::on_drag_data_received));
#endif
}
bool Application::Impl::on_session_closed()
@@ -900,16 +979,26 @@ void Application::Impl::on_app_exit()
/* stop the refresh-actions timer */
refresh_actions_tag_.disconnect();
#if !GTKMM_CHECK_VERSION(4, 0, 0)
auto* c = static_cast<Gtk::Container*>(wind_.get());
c->remove(*static_cast<Gtk::Bin*>(c)->get_child());
#endif
wind_->set_show_menubar(false);
auto* p = Gtk::make_managed<Gtk::Grid>();
p->set_column_spacing(GUI_PAD_BIG);
p->set_halign(TR_GTK_ALIGN(CENTER));
p->set_valign(TR_GTK_ALIGN(CENTER));
#if GTKMM_CHECK_VERSION(4, 0, 0)
wind_->set_child(*p);
#else
c->add(*p);
#endif
auto* icon = Gtk::make_managed<Gtk::Image>("network-workgroup", Gtk::ICON_SIZE_DIALOG);
auto* icon = Gtk::make_managed<Gtk::Image>();
icon->property_icon_name() = "network-workgroup";
icon->property_icon_size() = IF_GTKMM4(Gtk::IconSize::LARGE, Gtk::ICON_SIZE_DIALOG);
p->attach(*icon, 0, 0, 1, 2);
auto* top_label = Gtk::make_managed<Gtk::Label>();
@@ -930,7 +1019,9 @@ void Application::Impl::on_app_exit()
button->signal_clicked().connect([]() { ::exit(0); });
p->attach(*button, 1, 2, 1, 1);
#if !GTKMM_CHECK_VERSION(4, 0, 0)
p->show_all();
#endif
button->grab_focus();
/* clear the UI */
@@ -1000,7 +1091,7 @@ void Application::Impl::on_core_error(Session::ErrorCode code, Glib::ustring con
switch (code)
{
case Session::ERR_ADD_TORRENT_ERR:
error_list_.push_back(Glib::path_get_basename(msg));
error_list_.push_back(Glib::path_get_basename(msg.raw()));
break;
case Session::ERR_ADD_TORRENT_DUP:
@@ -1017,14 +1108,12 @@ void Application::Impl::on_core_error(Session::ErrorCode code, Glib::ustring con
}
}
bool Application::Impl::on_main_window_focus_in(GdkEventFocus* /*event*/)
void Application::Impl::on_main_window_focus_in()
{
if (wind_ != nullptr)
{
wind_->set_urgency_hint(false);
gtr_window_set_urgency_hint(*wind_, false);
}
return false;
}
void Application::Impl::on_add_torrent(tr_ctor* ctor)
@@ -1032,12 +1121,19 @@ void Application::Impl::on_add_torrent(tr_ctor* ctor)
auto w = std::shared_ptr<OptionsDialog>(
OptionsDialog::create(*wind_, core_, std::unique_ptr<tr_ctor, decltype(&tr_ctorFree)>(ctor, &tr_ctorFree)));
w->signal_hide().connect([w]() mutable { w.reset(); });
w->signal_focus_in_event().connect(sigc::mem_fun(*this, &Impl::on_main_window_focus_in));
gtr_window_on_close(*w, [w]() mutable { w.reset(); });
#if GTKMM_CHECK_VERSION(4, 0, 0)
auto focus_controller = Gtk::EventControllerFocus::create();
focus_controller->signal_enter().connect(sigc::mem_fun(*this, &Impl::on_main_window_focus_in));
w->add_controller(focus_controller);
#else
w->signal_focus_in_event().connect_notify(sigc::hide<0>(sigc::mem_fun(*this, &Impl::on_main_window_focus_in)));
#endif
if (wind_ != nullptr)
{
wind_->set_urgency_hint(true);
gtr_window_set_urgency_hint(*wind_, true);
}
w->show();
@@ -1080,7 +1176,7 @@ void Application::Impl::on_prefs_changed(tr_quark const key)
case TR_KEY_show_notification_area_icon:
if (bool const show = gtr_pref_flag_get(key); show && icon_ == nullptr)
{
icon_ = std::make_unique<SystemTrayIcon>(*wind_, core_);
icon_ = SystemTrayIcon::create(*wind_, core_);
}
else if (!show && icon_ != nullptr)
{
@@ -1327,10 +1423,8 @@ void Application::Impl::show_about_dialog()
#endif
d->set_transient_for(*wind_);
d->set_modal(true);
#if GTKMM_CHECK_VERSION(4, 0, 0)
d->signal_close_request().connect_notify([d]() mutable { d.reset(); });
#else
d->signal_delete_event().connect_notify([d](void* /*event*/) mutable { d.reset(); });
gtr_window_on_close(*d, [d]() mutable { d.reset(); });
#if !GTKMM_CHECK_VERSION(4, 0, 0)
d->signal_response().connect_notify([&dref = *d](int /*response*/) { dref.close(); });
#endif
d->show();
@@ -1420,18 +1514,13 @@ void Application::Impl::copy_magnet_link_to_clipboard(tr_torrent* tor) const
{
auto const magnet = tr_torrentGetMagnetLink(tor);
auto const display = wind_->get_display();
GdkAtom selection;
Glib::RefPtr<Gtk::Clipboard> clipboard;
/* this is The Right Thing for copy/paste... */
selection = GDK_SELECTION_CLIPBOARD;
clipboard = Gtk::Clipboard::get_for_display(display, selection);
clipboard->set_text(magnet);
IF_GTKMM4(display->get_clipboard(), Gtk::Clipboard::get_for_display(display, GDK_SELECTION_CLIPBOARD))->set_text(magnet);
/* ...but people using plain ol' X need this instead */
selection = GDK_SELECTION_PRIMARY;
clipboard = Gtk::Clipboard::get_for_display(display, selection);
clipboard->set_text(magnet);
IF_GTKMM4(display->get_primary_clipboard(), Gtk::Clipboard::get_for_display(display, GDK_SELECTION_PRIMARY))
->set_text(magnet);
}
void gtr_actions_handler(Glib::ustring const& action_name, gpointer user_data)
@@ -1446,19 +1535,19 @@ void Application::Impl::actions_handler(Glib::ustring const& action_name)
if (action_name == "open-torrent-from-url")
{
auto w = std::shared_ptr<TorrentUrlChooserDialog>(TorrentUrlChooserDialog::create(*wind_, core_));
w->signal_hide().connect([w]() mutable { w.reset(); });
gtr_window_on_close(*w, [w]() mutable { w.reset(); });
w->show();
}
else if (action_name == "open-torrent")
{
auto w = std::shared_ptr<TorrentFileChooserDialog>(TorrentFileChooserDialog::create(*wind_, core_));
w->signal_hide().connect([w]() mutable { w.reset(); });
gtr_window_on_close(*w, [w]() mutable { w.reset(); });
w->show();
}
else if (action_name == "show-stats")
{
auto dialog = std::shared_ptr<StatsDialog>(StatsDialog::create(*wind_, core_));
dialog->signal_hide().connect([dialog]() mutable { dialog.reset(); });
gtr_window_on_close(*dialog, [dialog]() mutable { dialog.reset(); });
dialog->show();
}
else if (action_name == "donate")
@@ -1489,7 +1578,7 @@ void Application::Impl::actions_handler(Glib::ustring const& action_name)
if (!ids.empty())
{
auto w = std::shared_ptr<RelocateDialog>(RelocateDialog::create(*wind_, core_, ids));
w->signal_hide().connect([w]() mutable { w.reset(); });
gtr_window_on_close(*w, [w]() mutable { w.reset(); });
w->show();
}
}
@@ -1512,7 +1601,7 @@ void Application::Impl::actions_handler(Glib::ustring const& action_name)
else if (action_name == "new-torrent")
{
auto w = std::shared_ptr<MakeDialog>(MakeDialog::create(*wind_, core_));
w->signal_hide().connect([w]() mutable { w.reset(); });
gtr_window_on_close(*w, [w]() mutable { w.reset(); });
w->show();
}
else if (action_name == "remove-torrent")
@@ -1540,7 +1629,7 @@ void Application::Impl::actions_handler(Glib::ustring const& action_name)
if (prefs_ == nullptr)
{
prefs_ = PrefsDialog::create(*wind_, core_);
prefs_->signal_hide().connect([this]() { prefs_.reset(); });
gtr_window_on_close(*prefs_, [this]() { prefs_.reset(); });
}
gtr_window_present(prefs_);
@@ -1550,12 +1639,20 @@ void Application::Impl::actions_handler(Glib::ustring const& action_name)
if (msgwin_ == nullptr)
{
msgwin_ = MessageLogWindow::create(*wind_, core_);
msgwin_->signal_hide().connect([this]() { msgwin_.reset(); });
gtr_window_on_close(
*msgwin_,
[this]()
{
gtr_action_set_toggled("toggle-message-log", false);
msgwin_.reset();
});
gtr_action_set_toggled("toggle-message-log", true);
msgwin_->show();
}
else
{
msgwin_->hide();
msgwin_->close();
}
}
else if (action_name == "show-about-dialog")