diff --git a/CMakeLists.txt b/CMakeLists.txt index ad9c82c2d..1525e2b3c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,7 +39,7 @@ set(PSL_MINIMUM 0.21.1) set(QT_MINIMUM 5.6) option(ENABLE_DAEMON "Build daemon" ON) -tr_auto_option(ENABLE_GTK "Build GTK+ client" AUTO) +tr_auto_option(ENABLE_GTK "Build GTK client" AUTO) tr_auto_option(ENABLE_QT "Build Qt client" AUTO) tr_auto_option(ENABLE_MAC "Build Mac client" AUTO) option(ENABLE_WEB "Build Web client" OFF) @@ -60,6 +60,7 @@ tr_auto_option(USE_SYSTEM_NATPMP "Use system natpmp library" AUTO) tr_auto_option(USE_SYSTEM_UTP "Use system utp library" AUTO) tr_auto_option(USE_SYSTEM_B64 "Use system b64 library" AUTO) tr_auto_option(USE_SYSTEM_PSL "Use system psl library" AUTO) +tr_list_option(USE_GTK_VERSION "Use specific GTK version" AUTO 3 4) tr_list_option(USE_QT_VERSION "Use specific Qt version" AUTO 5 6) tr_list_option(WITH_CRYPTO "Use specified crypto library" AUTO ccrypto cyassl mbedtls openssl polarssl wolfssl) tr_auto_option(WITH_INOTIFY "Enable inotify support (on systems that support it)" AUTO) @@ -266,17 +267,34 @@ endif() if(ENABLE_GTK) tr_get_required_flag(ENABLE_GTK GTK_IS_REQUIRED) - pkg_check_modules(GTK ${GTK_IS_REQUIRED} - gtkmm-3.0>=${GTKMM_MINIMUM} - glibmm-2.4>=${GLIBMM_MINIMUM} - giomm-2.4>=${GIOMM_MINIMUM}) + if(USE_GTK_VERSION STREQUAL "AUTO" OR USE_GTK_VERSION EQUAL 4) + pkg_check_modules(GTK + gtkmm-4.0>=${GTKMM_MINIMUM} + glibmm-2.68>=${GLIBMM_MINIMUM} + giomm-2.68>=${GIOMM_MINIMUM}) + set(GTK_VERSION 4) + endif() + + if(NOT GTK_FOUND AND (USE_GTK_VERSION STREQUAL "AUTO" OR USE_GTK_VERSION EQUAL 3)) + pkg_check_modules(GTK ${GTK_IS_REQUIRED} + gtkmm-3.0>=${GTKMM_MINIMUM} + glibmm-2.4>=${GLIBMM_MINIMUM} + giomm-2.4>=${GIOMM_MINIMUM}) + set(GTK_VERSION 3) + endif() + + if(GTK_IS_REQUIRED AND NOT GTK_FOUND) + message(FATAL_ERROR "GTK is required but wasn't found") + endif() tr_fixup_auto_option(ENABLE_GTK GTK_FOUND GTK_IS_REQUIRED) - if(ENABLE_GTK AND WITH_LIBAPPINDICATOR) + if(ENABLE_GTK AND WITH_LIBAPPINDICATOR AND GTK_VERSION EQUAL 3) tr_get_required_flag(WITH_LIBAPPINDICATOR LIBAPPINDICATOR_IS_REQUIRED) pkg_check_modules(LIBAPPINDICATOR appindicator3-0.1>=${LIBAPPINDICATOR_MINIMUM}) tr_fixup_auto_option(WITH_LIBAPPINDICATOR LIBAPPINDICATOR_FOUND LIBAPPINDICATOR_IS_REQUIRED) + else() + set(WITH_LIBAPPINDICATOR OFF) endif() else() set(WITH_LIBAPPINDICATOR OFF) diff --git a/gtk/Actions.cc b/gtk/Actions.cc index 680420a85..c00c23d7b 100644 --- a/gtk/Actions.cc +++ b/gtk/Actions.cc @@ -4,6 +4,7 @@ // License text can be found in the licenses/ folder. #include +#include #include #include #include @@ -21,6 +22,8 @@ using namespace std::string_view_literals; +using VariantString = Glib::Variant; + namespace { @@ -184,14 +187,57 @@ void gtr_action_set_toggled(Glib::ustring const& name, bool b) get_action(name)->set_state(Glib::Variant::create(b)); } -Gtk::Widget* gtr_action_get_widget(Glib::ustring const& name) -{ - Gtk::Widget* widget; - myBuilder->get_widget(name, widget); - return widget; -} - Glib::RefPtr gtr_action_get_object(Glib::ustring const& name) { return myBuilder->get_object(name); } + +#if GTKMM_CHECK_VERSION(4, 0, 0) + +Glib::RefPtr gtr_shortcuts_get_from_menu(Glib::RefPtr const& menu) +{ + auto result = Gio::ListStore::create(); + + std::stack> links; + links.push(menu); + + while (!links.empty()) + { + auto const link = links.top(); + links.pop(); + + for (int i = 0; i < link->get_n_items(); ++i) + { + Glib::ustring action_name; + Glib::ustring action_accel; + + for (auto it = link->iterate_item_attributes(i); it->next();) + { + if (auto const name = it->get_name(); name == "action") + { + action_name = Glib::VariantBase::cast_dynamic(it->get_value()).get(); + } + else if (name == "accel") + { + action_accel = Glib::VariantBase::cast_dynamic(it->get_value()).get(); + } + } + + if (!action_name.empty() && !action_accel.empty()) + { + result->append(Gtk::Shortcut::create( + Gtk::ShortcutTrigger::parse_string(action_accel), + Gtk::NamedAction::create(action_name))); + } + + for (auto it = link->iterate_item_links(i); it->next();) + { + links.push(it->get_value()); + } + } + } + + return result; +} + +#endif diff --git a/gtk/Actions.h b/gtk/Actions.h index b4f8c54ef..ca42326b9 100644 --- a/gtk/Actions.h +++ b/gtk/Actions.h @@ -18,14 +18,11 @@ void gtr_actions_handler(Glib::ustring const& action_name, gpointer user_data); void gtr_action_activate(Glib::ustring const& action_name); void gtr_action_set_sensitive(Glib::ustring const& action_name, bool is_sensitive); void gtr_action_set_toggled(Glib::ustring const& action_name, bool is_toggled); -Gtk::Widget* gtr_action_get_widget(Glib::ustring const& name); Glib::RefPtr gtr_action_get_object(Glib::ustring const& name); -template -inline T* gtr_action_get_widget(Glib::ustring const& name) -{ - return static_cast(gtr_action_get_widget(name)); -} +#if GTKMM_CHECK_VERSION(4, 0, 0) +Glib::RefPtr gtr_shortcuts_get_from_menu(Glib::RefPtr const& menu); +#endif template inline Glib::RefPtr gtr_action_get_object(Glib::ustring const& name) diff --git a/gtk/Application.cc b/gtk/Application.cc index 6e3fb2758..393a3f44b 100644 --- a/gtk/Application.cc +++ b/gtk/Application.cc @@ -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; +using FileListHandler = Glib::SListHandler>; + +using StringValue = Glib::Value; +#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 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 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("main-window-menu")); + auto const main_menu = gtr_action_get_object("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 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(wind_.get()); c->remove(*static_cast(c)->get_child()); +#endif + + wind_->set_show_menubar(false); auto* p = Gtk::make_managed(); 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("network-workgroup", Gtk::ICON_SIZE_DIALOG); + auto* icon = Gtk::make_managed(); + 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(); @@ -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::create(*wind_, core_, std::unique_ptr(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(*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 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::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::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::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::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::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") diff --git a/gtk/CMakeLists.txt b/gtk/CMakeLists.txt index f2911716f..ba2da57aa 100644 --- a/gtk/CMakeLists.txt +++ b/gtk/CMakeLists.txt @@ -20,51 +20,66 @@ endif() find_program(APPSTREAM appstreamcli) +if(GTK_VERSION EQUAL 4) + set(UI_SUBDIR ui/gtk4) +else() + set(UI_SUBDIR ui/gtk3) +endif() + set(${PROJECT_NAME}_UI_FILES - AddTrackerDialog.ui - DetailsDialog.ui - EditTrackersDialog.ui - FilterBar.ui - MainWindow.ui - MakeDialog.ui - MakeProgressDialog.ui - MessageLogWindow.ui - OptionsDialog.ui - PrefsDialog.ui - RelocateDialog.ui - StatsDialog.ui - TorrentUrlChooserDialog.ui + ${UI_SUBDIR}/AddTrackerDialog.ui + ${UI_SUBDIR}/DetailsDialog.ui + ${UI_SUBDIR}/EditTrackersDialog.ui + ${UI_SUBDIR}/FilterBar.ui + ${UI_SUBDIR}/MainWindow.ui + ${UI_SUBDIR}/MakeDialog.ui + ${UI_SUBDIR}/MakeProgressDialog.ui + ${UI_SUBDIR}/MessageLogWindow.ui + ${UI_SUBDIR}/OptionsDialog.ui + ${UI_SUBDIR}/PrefsDialog.ui + ${UI_SUBDIR}/RelocateDialog.ui + ${UI_SUBDIR}/StatsDialog.ui + ${UI_SUBDIR}/TorrentUrlChooserDialog.ui ) -add_custom_command( - OUTPUT - ${CMAKE_CURRENT_BINARY_DIR}/transmission-resources.c - ${CMAKE_CURRENT_BINARY_DIR}/transmission-resources.h - COMMAND - ${GLIB_COMPILE_RESOURCES_EXECUTABLE} - --target=${CMAKE_CURRENT_BINARY_DIR}/transmission-resources.c - --sourcedir=${CMAKE_CURRENT_SOURCE_DIR} - --generate-source - --c-name transmission - transmission.gresource.xml - COMMAND - ${GLIB_COMPILE_RESOURCES_EXECUTABLE} - --target=${CMAKE_CURRENT_BINARY_DIR}/transmission-resources.h - --sourcedir=${CMAKE_CURRENT_SOURCE_DIR} - --generate-header - --c-name transmission - transmission.gresource.xml - DEPENDS - icons/hicolor_apps_scalable_transmission.svg - icons/lock.svg - icons/options-symbolic.svg - icons/ratio-symbolic.svg - icons/turtle-symbolic.svg - transmission-ui.xml - transmission.gresource.xml - ${${PROJECT_NAME}_UI_FILES} - WORKING_DIRECTORY - ${CMAKE_CURRENT_SOURCE_DIR} +macro(gtr_compile_resources NAME INPUT_DIR INPUT_FILE OUTPUT_FILE_BASE) + add_custom_command( + OUTPUT + "${CMAKE_CURRENT_BINARY_DIR}/${OUTPUT_FILE_BASE}.c" + "${CMAKE_CURRENT_BINARY_DIR}/${OUTPUT_FILE_BASE}.h" + COMMAND + "${GLIB_COMPILE_RESOURCES_EXECUTABLE}" + "--target=${CMAKE_CURRENT_BINARY_DIR}/${OUTPUT_FILE_BASE}.c" + "--sourcedir=${INPUT_DIR}" + --generate-source + --c-name "${NAME}" + "${INPUT_DIR}/${INPUT_FILE}" + COMMAND + "${GLIB_COMPILE_RESOURCES_EXECUTABLE}" + "--target=${CMAKE_CURRENT_BINARY_DIR}/${OUTPUT_FILE_BASE}.h" + "--sourcedir=${INPUT_DIR}" + --generate-header + --c-name "${NAME}" + "${INPUT_DIR}/${INPUT_FILE}" + DEPENDS + "${INPUT_DIR}/${INPUT_FILE}" + ${ARGN} + WORKING_DIRECTORY + "${INPUT_DIR}" + ) +endmacro() + +gtr_compile_resources(transmission ${CMAKE_CURRENT_SOURCE_DIR} transmission.gresource.xml transmission-resources + icons/hicolor_apps_scalable_transmission.svg + icons/lock.svg + icons/options-symbolic.svg + icons/ratio-symbolic.svg + icons/turtle-symbolic.svg + transmission-ui.css + transmission-ui.xml +) +gtr_compile_resources(transmission_ui ${CMAKE_CURRENT_SOURCE_DIR}/${UI_SUBDIR} transmission-ui.gresource.xml transmission-ui-resources + ${${PROJECT_NAME}_UI_FILES} ) if(ENABLE_NLS) @@ -94,7 +109,6 @@ set(${PROJECT_NAME}_SOURCES FileList.cc FilterBar.cc FreeSpaceLabel.cc - HigWorkarea.cc IconCache.cc main.cc MainWindow.cc @@ -102,6 +116,7 @@ set(${PROJECT_NAME}_SOURCES MessageLogWindow.cc Notify.cc OptionsDialog.cc + PathButton.cc Prefs.cc PrefsDialog.cc RelocateDialog.cc @@ -111,6 +126,7 @@ set(${PROJECT_NAME}_SOURCES TorrentCellRenderer.cc Utils.cc ${CMAKE_CURRENT_BINARY_DIR}/transmission-resources.c + ${CMAKE_CURRENT_BINARY_DIR}/transmission-ui-resources.c ) set(${PROJECT_NAME}_HEADERS @@ -129,6 +145,7 @@ set(${PROJECT_NAME}_HEADERS MessageLogWindow.h Notify.h OptionsDialog.h + PathButton.h Prefs.h PrefsDialog.h RelocateDialog.h @@ -138,6 +155,7 @@ set(${PROJECT_NAME}_HEADERS TorrentCellRenderer.h Utils.h ${CMAKE_CURRENT_BINARY_DIR}/transmission-resources.h + ${CMAKE_CURRENT_BINARY_DIR}/transmission-ui-resources.h ) include_directories( diff --git a/gtk/DetailsDialog.cc b/gtk/DetailsDialog.cc index 9c59cc0be..e4fdbbf73 100644 --- a/gtk/DetailsDialog.cc +++ b/gtk/DetailsDialog.cc @@ -55,7 +55,7 @@ private: void tracker_page_init(Glib::RefPtr const& builder); void options_page_init(Glib::RefPtr const& builder); - void on_details_window_size_allocated(Gtk::Allocation& alloc); + void on_details_window_size_allocated(); bool onPeerViewQueryTooltip(int x, int y, bool keyboard_tip, Glib::RefPtr const& tooltip); void onMorePeerInfoToggled(); @@ -191,7 +191,7 @@ std::vector DetailsDialog::Impl::getTorrents() const namespace { -void set_togglebutton_if_different(Gtk::ToggleButton* toggle, sigc::connection& tag, bool value) +void set_togglebutton_if_different(Gtk::CheckButton* toggle, sigc::connection& tag, bool value) { bool const currentValue = toggle->get_active(); @@ -1150,7 +1150,11 @@ public: PeerModelColumns const peer_cols; -void initPeerRow(Gtk::TreeIter const& iter, std::string_view key, std::string_view torrent_name, tr_peer_stat const* peer) +void initPeerRow( + Gtk::TreeModel::iterator const& iter, + std::string_view key, + std::string_view torrent_name, + tr_peer_stat const* peer) { g_return_if_fail(peer != nullptr); @@ -1173,7 +1177,7 @@ void initPeerRow(Gtk::TreeIter const& iter, std::string_view key, std::string_vi (*iter)[peer_cols.torrent_name] = Glib::ustring{ std::data(torrent_name), std::size(torrent_name) }; } -void refreshPeerRow(Gtk::TreeIter const& iter, tr_peer_stat const* peer) +void refreshPeerRow(Gtk::TreeModel::iterator const& iter, tr_peer_stat const* peer) { std::string up_speed; std::string down_speed; @@ -1268,7 +1272,7 @@ void DetailsDialog::Impl::refreshPeerList(std::vector const& torren } /* step 2: mark all the peers in the list as not-updated */ - for (auto const& row : store->children()) + for (auto& row : store->children()) { row[peer_cols.was_updated] = false; } @@ -1347,7 +1351,7 @@ void DetailsDialog::Impl::refreshWebseedList(std::vector const& tor }; /* step 1: mark all webseeds as not-updated */ - for (auto const& row : store->children()) + for (auto& row : store->children()) { row[webseed_cols.was_updated] = false; } @@ -1698,7 +1702,10 @@ void DetailsDialog::Impl::peer_page_init(Glib::RefPtr const& build webseed_store_ = Gtk::ListStore::create(webseed_cols); auto* v = gtr_get_widget(builder, "webseeds_view"); v->set_model(webseed_store_); - v->signal_button_release_event().connect([v](GdkEventButton* event) { return on_tree_view_button_released(v, event); }); + setup_tree_view_button_event_handling( + *v, + {}, + [v](double view_x, double view_y) { return on_tree_view_button_released(*v, view_x, view_y); }); { auto* r = Gtk::make_managed(); @@ -1726,9 +1733,11 @@ void DetailsDialog::Impl::peer_page_init(Glib::RefPtr const& build peer_view_->set_model(m); peer_view_->set_has_tooltip(true); - peer_view_->signal_query_tooltip().connect(sigc::mem_fun(*this, &Impl::onPeerViewQueryTooltip)); - peer_view_->signal_button_release_event().connect([this](GdkEventButton* event) - { return on_tree_view_button_released(peer_view_, event); }); + peer_view_->signal_query_tooltip().connect(sigc::mem_fun(*this, &Impl::onPeerViewQueryTooltip), false); + setup_tree_view_button_event_handling( + *peer_view_, + {}, + [this](double view_x, double view_y) { return on_tree_view_button_released(*peer_view_, view_x, view_y); }); setPeerViewColumns(peer_view_); @@ -1756,10 +1765,12 @@ std::array const text_dir_mark = { ""sv, "\u200E"sv, "\u200 void appendAnnounceInfo(tr_tracker_view const& tracker, time_t const now, Gtk::TextDirection direction, std::ostream& gstr) { + auto const dir_mark = text_dir_mark[static_cast(direction)]; + if (tracker.hasAnnounced && tracker.announceState != TR_TRACKER_INACTIVE) { gstr << '\n'; - gstr << text_dir_mark[direction]; + gstr << dir_mark; auto const time_span_ago = tr_format_time_relative(now, tracker.lastAnnounceTime); if (tracker.lastAnnounceSucceeded) @@ -1800,13 +1811,13 @@ void appendAnnounceInfo(tr_tracker_view const& tracker, time_t const now, Gtk::T { case TR_TRACKER_INACTIVE: gstr << '\n'; - gstr << text_dir_mark[direction]; + gstr << dir_mark; gstr << _("No updates scheduled"); break; case TR_TRACKER_WAITING: gstr << '\n'; - gstr << text_dir_mark[direction]; + gstr << dir_mark; gstr << fmt::format( _("Asking for more peers {time_span_from_now}"), fmt::arg("time_span_from_now", tr_format_time_relative(now, tracker.nextAnnounceTime))); @@ -1814,13 +1825,13 @@ void appendAnnounceInfo(tr_tracker_view const& tracker, time_t const now, Gtk::T case TR_TRACKER_QUEUED: gstr << '\n'; - gstr << text_dir_mark[direction]; + gstr << dir_mark; gstr << _("Queued to ask for more peers"); break; case TR_TRACKER_ACTIVE: gstr << '\n'; - gstr << text_dir_mark[direction]; + gstr << dir_mark; gstr << fmt::format( // {markup_begin} and {markup_end} should surround time_span_ago _("Asked for more peers {markup_begin}{time_span_ago}{markup_end}"), @@ -1836,10 +1847,12 @@ void appendAnnounceInfo(tr_tracker_view const& tracker, time_t const now, Gtk::T void appendScrapeInfo(tr_tracker_view const& tracker, time_t const now, Gtk::TextDirection direction, std::ostream& gstr) { + auto const dir_mark = text_dir_mark[static_cast(direction)]; + if (tracker.hasScraped) { gstr << '\n'; - gstr << text_dir_mark[direction]; + gstr << dir_mark; auto const time_span_ago = tr_format_time_relative(now, tracker.lastScrapeTime); if (tracker.lastScrapeSucceeded) @@ -1874,7 +1887,7 @@ void appendScrapeInfo(tr_tracker_view const& tracker, time_t const now, Gtk::Tex case TR_TRACKER_WAITING: gstr << '\n'; - gstr << text_dir_mark[direction]; + gstr << dir_mark; gstr << fmt::format( _("Asking for peer counts in {time_span_from_now}"), fmt::arg("time_span_from_now", tr_format_time_relative(now, tracker.nextScrapeTime))); @@ -1882,13 +1895,13 @@ void appendScrapeInfo(tr_tracker_view const& tracker, time_t const now, Gtk::Tex case TR_TRACKER_QUEUED: gstr << '\n'; - gstr << text_dir_mark[direction]; + gstr << dir_mark; gstr << _("Queued to ask for peer counts"); break; case TR_TRACKER_ACTIVE: gstr << '\n'; - gstr << text_dir_mark[direction]; + gstr << dir_mark; gstr << fmt::format( _("Asked for peer counts {markup_begin}{time_span_ago}{markup_end}"), fmt::arg("markup_begin", ""), @@ -1909,7 +1922,7 @@ void buildTrackerSummary( Gtk::TextDirection direction) { // hostname - gstr << text_dir_mark[direction]; + gstr << text_dir_mark[static_cast(direction)]; gstr << (tracker.isBackup ? "" : ""); gstr << Glib::Markup::escape_text(!key.empty() ? fmt::format(FMT_STRING("{:s} - {:s}"), tracker.host, key) : tracker.host); gstr << (tracker.isBackup ? "" : ""); @@ -2027,7 +2040,7 @@ void DetailsDialog::Impl::refreshTracker(std::vector const& torrent } /* step 2: mark all the trackers in the list as not-updated */ - for (auto const& row : store->children()) + for (auto& row : store->children()) { row[tracker_cols.was_updated] = false; } @@ -2203,7 +2216,7 @@ void EditTrackersDialog::on_response(int response) if (do_destroy) { - hide(); + close(); } } @@ -2214,7 +2227,7 @@ void DetailsDialog::Impl::on_edit_trackers() if (auto const* const tor = tracker_list_get_current_torrent(); tor != nullptr) { auto d = std::shared_ptr(EditTrackersDialog::create(dialog_, core_, tor)); - d->signal_hide().connect([d]() mutable { d.reset(); }); + gtr_window_on_close(*d, [d]() mutable { d.reset(); }); d->show(); } } @@ -2322,7 +2335,7 @@ void AddTrackerDialog::on_response(int response) if (destroy) { - hide(); + close(); } } @@ -2333,7 +2346,7 @@ void DetailsDialog::Impl::on_tracker_list_add_button_clicked() if (auto const* const tor = tracker_list_get_current_torrent(); tor != nullptr) { auto d = std::shared_ptr(AddTrackerDialog::create(dialog_, core_, tor)); - d->signal_hide().connect([d]() mutable { d.reset(); }); + gtr_window_on_close(*d, [d]() mutable { d.reset(); }); d->show(); } } @@ -2375,10 +2388,11 @@ void DetailsDialog::Impl::tracker_page_init(Glib::RefPtr const& /* trackers_filtered_->set_visible_func(sigc::mem_fun(*this, &Impl::trackerVisibleFunc)); tracker_view_->set_model(trackers_filtered_); - tracker_view_->signal_button_press_event().connect([this](GdkEventButton* event) - { return on_tree_view_button_pressed(tracker_view_, event); }); - tracker_view_->signal_button_release_event().connect([this](GdkEventButton* event) - { return on_tree_view_button_released(tracker_view_, event); }); + setup_tree_view_button_event_handling( + *tracker_view_, + [this](guint /*button*/, TrGdkModifierType /*state*/, double view_x, double view_y, bool context_menu_requested) + { return on_tree_view_button_pressed(*tracker_view_, view_x, view_y, context_menu_requested); }, + [this](double view_x, double view_y) { return on_tree_view_button_released(*tracker_view_, view_x, view_y); }); auto sel = tracker_view_->get_selection(); sel->signal_changed().connect(sigc::mem_fun(*this, &Impl::on_tracker_list_selection_changed)); @@ -2436,11 +2450,15 @@ void DetailsDialog::Impl::refresh() } } -void DetailsDialog::Impl::on_details_window_size_allocated(Gtk::Allocation& /*alloc*/) +void DetailsDialog::Impl::on_details_window_size_allocated() { int w = 0; int h = 0; +#if GTKMM_CHECK_VERSION(4, 0, 0) + dialog_.get_default_size(w, h); +#else dialog_.get_size(w, h); +#endif gtr_pref_int_set(TR_KEY_details_window_width, w); gtr_pref_int_set(TR_KEY_details_window_height, h); } @@ -2511,10 +2529,18 @@ DetailsDialog::Impl::Impl(DetailsDialog& dialog, Glib::RefPtr cons , file_label_(gtr_get_widget(builder, "files_label")) { /* return saved window size */ - dialog_.resize((int)gtr_pref_int_get(TR_KEY_details_window_width), (int)gtr_pref_int_get(TR_KEY_details_window_height)); - dialog_.signal_size_allocate().connect(sigc::mem_fun(*this, &Impl::on_details_window_size_allocated)); + auto const width = (int)gtr_pref_int_get(TR_KEY_details_window_width); + auto const height = (int)gtr_pref_int_get(TR_KEY_details_window_height); +#if GTKMM_CHECK_VERSION(4, 0, 0) + dialog_.set_default_size(width, height); + dialog_.property_default_width().signal_changed().connect(sigc::mem_fun(*this, &Impl::on_details_window_size_allocated)); + dialog_.property_default_height().signal_changed().connect(sigc::mem_fun(*this, &Impl::on_details_window_size_allocated)); +#else + dialog_.resize(width, height); + dialog_.signal_size_allocate().connect(sigc::hide<0>(sigc::mem_fun(*this, &Impl::on_details_window_size_allocated))); +#endif - dialog_.signal_response().connect(sigc::hide<0>(sigc::mem_fun(dialog_, &DetailsDialog::hide))); + dialog_.signal_response().connect(sigc::hide<0>(sigc::mem_fun(dialog_, &DetailsDialog::close))); info_page_init(builder); peer_page_init(builder); diff --git a/gtk/Dialogs.cc b/gtk/Dialogs.cc index 6ca8c7206..59a68611b 100644 --- a/gtk/Dialogs.cc +++ b/gtk/Dialogs.cc @@ -129,5 +129,5 @@ void gtr_confirm_remove( d.reset(); }); - d->show_all(); + d->show(); } diff --git a/gtk/FileList.cc b/gtk/FileList.cc index 3ec315b69..06974567e 100644 --- a/gtk/FileList.cc +++ b/gtk/FileList.cc @@ -61,7 +61,7 @@ public: add(enabled); } - Gtk::TreeModelColumn> icon; + Gtk::TreeModelColumn> icon; Gtk::TreeModelColumn label; Gtk::TreeModelColumn label_esc; Gtk::TreeModelColumn prog; @@ -81,8 +81,6 @@ FileModelColumns const file_cols; class FileList::Impl { public: - Impl(FileList& widget, Gtk::TreeView* view, Glib::RefPtr const& core, tr_torrent_id_t torrent_id); - Impl(FileList& widget, Glib::RefPtr const& core, tr_torrent_id_t torrent_id); Impl( FileList& widget, Glib::RefPtr const& builder, @@ -99,13 +97,13 @@ private: void clearData(); void refresh(); - bool getAndSelectEventPath(GdkEventButton const* event, Gtk::TreeViewColumn*& col, Gtk::TreeModel::Path& path); + bool getAndSelectEventPath(double view_x, double view_y, Gtk::TreeViewColumn*& col, Gtk::TreeModel::Path& path); std::vector getActiveFilesForPath(Gtk::TreeModel::Path const& path) const; std::vector getSelectedFilesAndDescendants() const; std::vector getSubtree(Gtk::TreeModel::Path const& path) const; - bool onViewButtonPressed(GdkEventButton const* event); + bool onViewButtonPressed(guint button, TrGdkModifierType state, double view_x, double view_y); bool onViewPathToggled(Gtk::TreeViewColumn* col, Gtk::TreeModel::Path const& path); void onRowActivated(Gtk::TreeModel::Path const& path, Gtk::TreeViewColumn* col); void cell_edited_callback(Glib::ustring const& path_string, Glib::ustring const& newname); @@ -276,9 +274,9 @@ void gtr_tree_model_foreach_postorder_subtree( Gtk::TreeModel::iterator const& parent, Gtk::TreeModel::SlotForeachIter const& func) { - for (auto const& child : parent->children()) + for (auto& child : parent->children()) { - gtr_tree_model_foreach_postorder_subtree(child, func); + gtr_tree_model_foreach_postorder_subtree(TR_GTK_TREE_MODEL_CHILD_ITER(child), func); } if (parent) @@ -289,9 +287,9 @@ void gtr_tree_model_foreach_postorder_subtree( void gtr_tree_model_foreach_postorder(Glib::RefPtr const& model, Gtk::TreeModel::SlotForeachIter const& func) { - for (auto const& iter : model->children()) + for (auto& iter : model->children()) { - gtr_tree_model_foreach_postorder_subtree(iter, func); + gtr_tree_model_foreach_postorder_subtree(TR_GTK_TREE_MODEL_CHILD_ITER(iter), func); } } @@ -452,7 +450,7 @@ void buildTree(FileRowNode& node, build_data& build) bool const isLeaf = node.child_count() == 0; auto const mime_type = isLeaf ? tr_get_mime_type_for_filename(child_data.name.raw()) : DirectoryMimeType; - auto const icon = gtr_get_mime_type_icon(mime_type, Gtk::ICON_SIZE_MENU, *build.w); + auto const icon = gtr_get_mime_type_icon(mime_type); auto const file = isLeaf ? tr_torrentFile(build.tor, child_data.index) : tr_file_view{}; int const priority = isLeaf ? file.priority : 0; bool const enabled = isLeaf ? file.wanted : true; @@ -573,14 +571,14 @@ void FileList::Impl::set_torrent(tr_torrent_id_t tor_id) namespace { -void renderDownload(Gtk::CellRenderer* renderer, Gtk::TreeModel::iterator const& iter) +void renderDownload(Gtk::CellRenderer* renderer, Gtk::TreeModel::const_iterator const& iter) { auto const enabled = iter->get_value(file_cols.enabled); static_cast(renderer)->property_inconsistent() = enabled == MIXED; static_cast(renderer)->property_active() = enabled == true; } -void renderPriority(Gtk::CellRenderer* renderer, Gtk::TreeModel::iterator const& iter) +void renderPriority(Gtk::CellRenderer* renderer, Gtk::TreeModel::const_iterator const& iter) { Glib::ustring text; @@ -609,13 +607,14 @@ void renderPriority(Gtk::CellRenderer* renderer, Gtk::TreeModel::iterator const& /* build a filename from tr_torrentGetCurrentDir() + the model's FC_LABELs */ std::string buildFilename(tr_torrent const* tor, Gtk::TreeModel::iterator const& iter) { - std::list tokens; + std::vector tokens; for (auto child = iter; child; child = child->parent()) { - tokens.push_front(child->get_value(file_cols.label)); + tokens.push_back(child->get_value(file_cols.label)); } - tokens.emplace_front(tr_torrentGetCurrentDir(tor)); + tokens.emplace_back(tr_torrentGetCurrentDir(tor)); + std::reverse(tokens.begin(), tokens.end()); return Glib::build_filename(tokens); } @@ -709,12 +708,12 @@ bool FileList::Impl::onViewPathToggled(Gtk::TreeViewColumn* col, Gtk::TreeModel: /** * @note 'col' and 'path' are assumed not to be nullptr. */ -bool FileList::Impl::getAndSelectEventPath(GdkEventButton const* event, Gtk::TreeViewColumn*& col, Gtk::TreeModel::Path& path) +bool FileList::Impl::getAndSelectEventPath(double view_x, double view_y, Gtk::TreeViewColumn*& col, Gtk::TreeModel::Path& path) { int cell_x; int cell_y; - if (view_->get_path_at_pos(event->x, event->y, path, col, cell_x, cell_y)) + if (view_->get_path_at_pos(view_x, view_y, path, col, cell_x, cell_y)) { if (auto const sel = view_->get_selection(); !sel->is_selected(path)) { @@ -728,14 +727,15 @@ bool FileList::Impl::getAndSelectEventPath(GdkEventButton const* event, Gtk::Tre return false; } -bool FileList::Impl::onViewButtonPressed(GdkEventButton const* event) +bool FileList::Impl::onViewButtonPressed(guint button, TrGdkModifierType state, double view_x, double view_y) { Gtk::TreeViewColumn* col; Gtk::TreeModel::Path path; bool handled = false; - if (event->type == GDK_BUTTON_PRESS && event->button == 1 && (event->state & (GDK_SHIFT_MASK | GDK_CONTROL_MASK)) == 0 && - getAndSelectEventPath(event, col, path)) + if (button == GDK_BUTTON_PRIMARY && + (state & (TR_GDK_MODIFIED_TYPE(SHIFT_MASK) | TR_GDK_MODIFIED_TYPE(CONTROL_MASK))) == TrGdkModifierType{} && + getAndSelectEventPath(view_x, view_y, col, path)) { handled = onViewPathToggled(col, path); } @@ -758,7 +758,7 @@ bool FileList::Impl::on_rename_done_idle(Glib::ustring const& path_string, Glib: { bool const isLeaf = iter->children().empty(); auto const mime_type = isLeaf ? tr_get_mime_type_for_filename(newname.raw()) : DirectoryMimeType; - auto const icon = gtr_get_mime_type_icon(mime_type, Gtk::ICON_SIZE_MENU, *view_); + auto const icon = gtr_get_mime_type_icon(mime_type); (*iter)[file_cols.label] = newname; (*iter)[file_cols.icon] = icon; @@ -772,7 +772,7 @@ bool FileList::Impl::on_rename_done_idle(Glib::ustring const& path_string, Glib: else { auto w = std::make_shared( - *static_cast(widget_.get_toplevel()), + *static_cast(TR_GTK_WIDGET_GET_ROOT(widget_)), fmt::format( _("Couldn't rename '{old_path}' as '{path}': {error} ({error_code})"), fmt::arg("old_path", path_string), @@ -842,12 +842,6 @@ void FileList::Impl::cell_edited_callback(Glib::ustring const& path_string, Glib rename_data.release()); } -FileList::FileList(Glib::RefPtr const& core, tr_torrent_id_t tor_id) - : Gtk::ScrolledWindow() - , impl_(std::make_unique(*this, core, tor_id)) -{ -} - FileList::FileList( BaseObjectType* cast_item, Glib::RefPtr const& builder, @@ -859,19 +853,33 @@ FileList::FileList( { } -FileList::Impl::Impl(FileList& widget, Gtk::TreeView* view, Glib::RefPtr const& core, tr_torrent_id_t torrent_id) +FileList::Impl::Impl( + FileList& widget, + Glib::RefPtr const& builder, + Glib::ustring const& view_name, + Glib::RefPtr const& core, + tr_torrent_id_t torrent_id) : widget_(widget) , core_(core) - , view_(view) + , view_(gtr_get_widget(builder, view_name)) { /* create the view */ - view_->signal_button_press_event().connect(sigc::mem_fun(*this, &Impl::onViewButtonPressed), false); view_->signal_row_activated().connect(sigc::mem_fun(*this, &Impl::onRowActivated)); - view_->signal_button_release_event().connect([this](GdkEventButton* event) - { return on_tree_view_button_released(view_, event); }); + setup_tree_view_button_event_handling( + *view_, + [this](guint button, TrGdkModifierType state, double view_x, double view_y, bool /*context_menu_requested*/) + { return onViewButtonPressed(button, state, view_x, view_y); }, + [this](double view_x, double view_y) { return on_tree_view_button_released(*view_, view_x, view_y); }); auto pango_font_description = view_->create_pango_context()->get_font_description(); - pango_font_description.set_size(pango_font_description.get_size() * 0.8); + if (auto const new_size = pango_font_description.get_size() * 0.8; pango_font_description.get_size_is_absolute()) + { + pango_font_description.set_absolute_size(new_size); + } + else + { + pango_font_description.set_size(new_size); + } /* set up view */ auto const sel = view_->get_selection(); @@ -887,7 +895,12 @@ FileList::Impl::Impl(FileList& widget, Gtk::TreeView* view, Glib::RefPtrset_resizable(true); auto* icon_rend = Gtk::make_managed(); col->pack_start(*icon_rend, false); - col->add_attribute(icon_rend->property_pixbuf(), file_cols.icon); + col->add_attribute(icon_rend->property_gicon(), file_cols.icon); +#if GTKMM_CHECK_VERSION(4, 0, 0) + icon_rend->property_icon_size() = Gtk::IconSize::NORMAL; +#else + icon_rend->property_stock_size() = Gtk::ICON_SIZE_MENU; +#endif /* add text renderer */ auto* text_rend = Gtk::make_managed(); text_rend->property_editable() = true; @@ -974,26 +987,4 @@ FileList::Impl::Impl(FileList& widget, Gtk::TreeView* view, Glib::RefPtr const& core, tr_torrent_id_t torrent_id) - : Impl(widget, Gtk::make_managed(), core, torrent_id) -{ - view_->set_border_width(GUI_PAD_BIG); - - /* create the scrolled window and stick the view in it */ - widget_.set_policy(TR_GTK_POLICY_TYPE(AUTOMATIC), TR_GTK_POLICY_TYPE(AUTOMATIC)); - widget_.set_shadow_type(Gtk::SHADOW_IN); - widget_.add(*view_); - widget_.set_size_request(-1, 200); -} - -FileList::Impl::Impl( - FileList& widget, - Glib::RefPtr const& builder, - Glib::ustring const& view_name, - Glib::RefPtr const& core, - tr_torrent_id_t torrent_id) - : Impl(widget, gtr_get_widget(builder, view_name), core, torrent_id) -{ -} - FileList::~FileList() = default; diff --git a/gtk/FileList.h b/gtk/FileList.h index 46ccc3cad..d1e0f4318 100644 --- a/gtk/FileList.h +++ b/gtk/FileList.h @@ -16,7 +16,6 @@ class Session; class FileList : public Gtk::ScrolledWindow { public: - FileList(Glib::RefPtr const& core, tr_torrent_id_t torrent_id); FileList( BaseObjectType* cast_item, Glib::RefPtr const& builder, diff --git a/gtk/FilterBar.cc b/gtk/FilterBar.cc index 18283f55c..fb9d923c3 100644 --- a/gtk/FilterBar.cc +++ b/gtk/FilterBar.cc @@ -72,6 +72,11 @@ private: sigc::connection torrent_model_row_inserted_tag_; sigc::connection torrent_model_row_deleted_cb_tag_; + sigc::connection filter_model_row_deleted_tag_; + sigc::connection filter_model_row_inserted_tag_; + + sigc::connection update_count_label_tag_; + Glib::ustring filter_text_; }; @@ -232,7 +237,7 @@ bool tracker_filter_model_update(Glib::RefPtr const& tracker_mod else { auto const sitename = iter->get_value(tracker_filter_cols.sitename); - int const cmp = sitename.compare(sites_v.at(i).sitename); + int const cmp = sitename.raw().compare(sites_v.at(i).sitename); if (cmp < 0) { @@ -294,7 +299,7 @@ Glib::RefPtr tracker_filter_model_new(Glib::RefPtr const& /*model*/, Gtk::TreeIter const& iter) +bool is_it_a_separator(Glib::RefPtr const& /*model*/, Gtk::TreeModel::const_iterator const& iter) { return iter->get_value(tracker_filter_cols.type) == TRACKER_FILTER_TYPE_SEPARATOR; } @@ -430,7 +435,7 @@ public: ActivityFilterModelColumns const activity_filter_cols; -bool activity_is_it_a_separator(Glib::RefPtr const& /*model*/, Gtk::TreeIter const& iter) +bool activity_is_it_a_separator(Glib::RefPtr const& /*model*/, Gtk::TreeModel::const_iterator const& iter) { return iter->get_value(activity_filter_cols.type) == ACTIVITY_FILTER_SEPARATOR; } @@ -468,7 +473,7 @@ bool test_torrent_activity(tr_torrent* tor, int type) } } -void status_model_update_count(Gtk::TreeIter const& iter, int n) +void status_model_update_count(Gtk::TreeModel::iterator const& iter, int n) { if (n != iter->get_value(activity_filter_cols.count)) { @@ -495,7 +500,7 @@ bool activity_filter_model_update(Glib::RefPtr const& activity_m } } - status_model_update_count(row, hits); + status_model_update_count(TR_GTK_TREE_MODEL_CHILD_ITER(row), hits); } return false; @@ -539,7 +544,7 @@ Glib::RefPtr activity_filter_model_new(Glib::RefPtrget_value(activity_filter_cols.type); cell_renderer->property_width() = type == ACTIVITY_FILTER_ALL ? 0 : 20; @@ -724,7 +729,7 @@ void FilterBar::Impl::update_count_label_idle() if (!pending) { show_lb_->set_data(DIRTY_KEY, GINT_TO_POINTER(1)); - Glib::signal_idle().connect(sigc::mem_fun(*this, &Impl::update_count_label)); + update_count_label_tag_ = Glib::signal_idle().connect(sigc::mem_fun(*this, &Impl::update_count_label)); } } @@ -787,9 +792,10 @@ FilterBar::Impl::Impl(FilterBar& widget, tr_session* session, Glib::RefPtrsignal_row_deleted().connect([this](auto const& /*path*/) { update_count_label_idle(); }); - filter_model_->signal_row_inserted().connect([this](auto const& /*path*/, auto const& /*iter*/) - { update_count_label_idle(); }); + filter_model_row_deleted_tag_ = filter_model_->signal_row_deleted().connect([this](auto const& /*path*/) + { update_count_label_idle(); }); + filter_model_row_inserted_tag_ = filter_model_->signal_row_inserted().connect( + [this](auto const& /*path*/, auto const& /*iter*/) { update_count_label_idle(); }); static_cast(tracker_->get_model().get())->set_data(SESSION_KEY, session); @@ -798,7 +804,11 @@ FilterBar::Impl::Impl(FilterBar& widget, tr_session* session, Glib::RefPtrsignal_changed().connect(sigc::mem_fun(*this, &Impl::selection_changed_cb)); activity_->signal_changed().connect(sigc::mem_fun(*this, &Impl::selection_changed_cb)); +#if GTKMM_CHECK_VERSION(4, 0, 0) + entry_->signal_icon_release().connect([this](auto /*icon_position*/) { entry_->set_text({}); }); +#else entry_->signal_icon_release().connect([this](auto /*icon_position*/, auto const* /*event*/) { entry_->set_text({}); }); +#endif entry_->signal_changed().connect(sigc::mem_fun(*this, &Impl::filter_entry_changed)); selection_changed_cb(); @@ -807,6 +817,14 @@ FilterBar::Impl::Impl(FilterBar& widget, tr_session* session, Glib::RefPtr - -#include "HigWorkarea.h" -#include "Utils.h" - -HigWorkarea::HigWorkarea() -{ - set_border_width(GUI_PAD_BIG); - set_row_spacing(GUI_PAD); - set_column_spacing(GUI_PAD_BIG); -} - -void HigWorkarea::add_section_divider(guint& row) -{ - auto* w = Gtk::make_managed(); - w->set_size_request(0U, 6U); - attach(*w, 0, row, 2, 1); - ++row; -} - -void HigWorkarea::add_section_title_widget(guint& row, Gtk::Widget& w) -{ - w.set_hexpand(true); - attach(w, 0, row, 2, 1); - ++row; -} - -void HigWorkarea::add_section_title(guint& row, Glib::ustring const& section_title) -{ - auto* l = Gtk::make_managed(gtr_sprintf("%s", section_title)); - l->set_halign(TR_GTK_ALIGN(START)); - l->set_valign(TR_GTK_ALIGN(CENTER)); - l->set_use_markup(true); - add_section_title_widget(row, *l); -} - -void HigWorkarea::add_wide_control(guint& row, Gtk::Widget& w) -{ - w.set_hexpand(true); - w.set_margin_start(18); - attach(w, 0, row, 2, 1); - ++row; -} - -void HigWorkarea::add_wide_tall_control(guint& row, Gtk::Widget& w) -{ - w.set_hexpand(true); - w.set_vexpand(true); - add_wide_control(row, w); -} - -Gtk::CheckButton* HigWorkarea::add_wide_checkbutton(guint& row, Glib::ustring const& mnemonic_string, bool is_active) -{ - auto* w = Gtk::make_managed(mnemonic_string, true); - w->set_active(is_active); - add_wide_control(row, *w); - return w; -} - -void HigWorkarea::add_label_w(guint row, Gtk::Widget& w) -{ - w.set_margin_start(18); - - if (auto* label = dynamic_cast(&w); label != nullptr) - { - label->set_halign(TR_GTK_ALIGN(START)); - label->set_valign(TR_GTK_ALIGN(CENTER)); - label->set_use_markup(true); - } - - attach(w, 0, row, 1, 1); -} - -void HigWorkarea::add_tall_control(guint row, Gtk::Widget& control) -{ - if (auto* label = dynamic_cast(&control); label != nullptr) - { - label->set_halign(TR_GTK_ALIGN(START)); - label->set_valign(TR_GTK_ALIGN(CENTER)); - } - - control.set_hexpand(true); - control.set_vexpand(true); - attach(control, 1, row, 1, 1); -} - -void HigWorkarea::add_control(guint row, Gtk::Widget& control) -{ - if (auto* label = dynamic_cast(&control); label != nullptr) - { - label->set_halign(TR_GTK_ALIGN(START)); - label->set_valign(TR_GTK_ALIGN(CENTER)); - } - - control.set_hexpand(true); - attach(control, 1, row, 1, 1); -} - -void HigWorkarea::add_row_w(guint& row, Gtk::Widget& label_widget, Gtk::Widget& control, Gtk::Widget* mnemonic) -{ - add_label_w(row, label_widget); - add_control(row, control); - - if (auto* label = dynamic_cast(&label_widget); label != nullptr) - { - label->set_mnemonic_widget(mnemonic != nullptr ? *mnemonic : control); - } - - ++row; -} - -Gtk::Label* HigWorkarea::add_row(guint& row, Glib::ustring const& mnemonic_string, Gtk::Widget& control, Gtk::Widget* mnemonic) -{ - auto* l = Gtk::make_managed(mnemonic_string, true); - add_row_w(row, *l, control, mnemonic); - return l; -} - -Gtk::Label* HigWorkarea::add_tall_row( - guint& row, - Glib::ustring const& mnemonic_string, - Gtk::Widget& control, - Gtk::Widget* mnemonic) -{ - auto* l = Gtk::make_managed(mnemonic_string, true); - auto* h = Gtk::make_managed(TR_GTK_ORIENTATION(HORIZONTAL), 0); - auto* v = Gtk::make_managed(TR_GTK_ORIENTATION(VERTICAL), 0); - h->pack_start(*l, false, false, 0); - v->pack_start(*h, false, false, GUI_PAD_SMALL); - - add_label_w(row, *v); - add_tall_control(row, control); - - l->set_mnemonic_widget(mnemonic ? *mnemonic : control); - - ++row; - return l; -} diff --git a/gtk/HigWorkarea.h b/gtk/HigWorkarea.h index a6a5f17b0..26dd3c481 100644 --- a/gtk/HigWorkarea.h +++ b/gtk/HigWorkarea.h @@ -5,50 +5,6 @@ #pragma once -#include - -#include - -/** -*** utility code for making dialog layout that follows the Gnome HIG. -*** see section 8.2.2, Visual Design > Window Layout > Dialogs. -**/ - -class HigWorkarea : public Gtk::Grid -{ -public: - HigWorkarea(); - - TR_DISABLE_COPY_MOVE(HigWorkarea) - - void add_section_divider(guint& row); - void add_section_title_widget(guint& row, Gtk::Widget& w); - void add_section_title(guint& row, Glib::ustring const& section_title); - void add_wide_tall_control(guint& row, Gtk::Widget& w); - void add_wide_control(guint& row, Gtk::Widget& w); - Gtk::CheckButton* add_wide_checkbutton(guint& row, Glib::ustring const& mnemonic_string, bool is_active); - void add_label_w(guint row, Gtk::Widget& label_widget); - Gtk::Label* add_tall_row( - guint& row, - Glib::ustring const& mnemonic_string, - Gtk::Widget& control, - Gtk::Widget* mnemonic_or_null_for_control = nullptr); - Gtk::Label* add_row( - guint& row, - Glib::ustring const& mnemonic_string, - Gtk::Widget& control, - Gtk::Widget* mnemonic_or_null_for_control = nullptr); - void add_row_w( - guint& row, - Gtk::Widget& label_widget, - Gtk::Widget& control, - Gtk::Widget* mnemonic_or_null_for_control = nullptr); - -private: - void add_tall_control(guint row, Gtk::Widget& control); - void add_control(guint row, Gtk::Widget& control); -}; - auto inline constexpr GUI_PAD_SMALL = int{ 3 }; auto inline constexpr GUI_PAD = int{ 6 }; auto inline constexpr GUI_PAD_BIG = int{ 12 }; diff --git a/gtk/IconCache.cc b/gtk/IconCache.cc index fa587657d..10940c864 100644 --- a/gtk/IconCache.cc +++ b/gtk/IconCache.cc @@ -6,9 +6,7 @@ */ #include -#include #include -#include #include #include #include @@ -21,104 +19,14 @@ using namespace std::literals; +using IconCache = std::map, std::less<>>; + std::string_view const DirectoryMimeType = "folder"sv; std::string_view const UnknownMimeType = "unknown"sv; -namespace +Glib::RefPtr gtr_get_mime_type_icon(std::string_view mime_type) { - -auto const VoidPixbufKey = "void-pixbuf"s; - -struct IconCache -{ - Glib::RefPtr icon_theme; - int icon_size; - std::map, std::less<>> cache; -}; - -std::array, 7> icon_cache; - -Glib::RefPtr create_void_pixbuf(int width, int height) -{ - auto const p = Gdk::Pixbuf::create(TR_GDK_COLORSPACE(RGB), true, 8, width, height); - p->fill(0xFFFFFF00); - return p; -} - -int get_size_in_pixels(Gtk::IconSize icon_size) -{ - int width = 0; - int height = 0; - Gtk::IconSize::lookup(icon_size, width, height); - return std::max(width, height); -} - -std::unique_ptr icon_cache_new(Gtk::Widget& for_widget, Gtk::IconSize icon_size) -{ - auto icons = std::make_unique(); - icons->icon_theme = Gtk::IconTheme::get_for_screen(for_widget.get_screen()); - icons->icon_size = get_size_in_pixels(icon_size); - icons->cache.try_emplace(VoidPixbufKey, create_void_pixbuf(icons->icon_size, icons->icon_size)); - return icons; -} - -Glib::RefPtr get_themed_icon_pixbuf(Gio::ThemedIcon& icon, int size, Gtk::IconTheme& icon_theme) -{ - auto const icon_names = icon.get_names(); - - auto icon_info = icon_theme.choose_icon(icon_names, size); - - if (!bool{ icon_info }) - { - icon_info = icon_theme.lookup_icon("text-x-generic", size, Gtk::ICON_LOOKUP_USE_BUILTIN); - } - - try - { - return icon_info.load_icon(); - } - catch (Glib::Error const& e) - { - g_warning("could not load icon pixbuf: %s\n", e.what().c_str()); - return {}; - } -} - -Glib::RefPtr get_file_icon_pixbuf(Gio::FileIcon& icon, int size) -{ - try - { - return Gdk::Pixbuf::create_from_file(icon.get_file()->get_path(), size, -1, false); - } - catch (Glib::Error const&) - { - return {}; - } -} - -Glib::RefPtr _get_icon_pixbuf(Glib::RefPtr const& icon, int size, Gtk::IconTheme& theme) -{ - if (icon == nullptr) - { - return {}; - } - - if (auto* const ticon = dynamic_cast(icon.get()); ticon != nullptr) - { - return get_themed_icon_pixbuf(*ticon, size, theme); - } - - if (auto* const ficon = dynamic_cast(icon.get()); ficon != nullptr) - { - return get_file_icon_pixbuf(*ficon, size); - } - - return {}; -} - -Glib::RefPtr icon_cache_get_mime_type_icon(IconCache& icons, std::string_view mime_type) -{ - auto& cache = icons.cache; + static IconCache cache; if (auto mime_it = cache.find(mime_type); mime_it != std::end(cache)) { @@ -127,56 +35,10 @@ Glib::RefPtr icon_cache_get_mime_type_icon(IconCache& icons, std::s auto mime_type_str = std::string{ mime_type }; auto icon = Gio::content_type_get_icon(mime_type_str); - auto pixbuf = _get_icon_pixbuf(icon, icons.icon_size, *icons.icon_theme.get()); - if (pixbuf != nullptr) + if (icon != nullptr) { - cache.try_emplace(std::move(mime_type_str), pixbuf); + cache.try_emplace(std::move(mime_type_str), icon); } - return pixbuf; -} - -} // namespace - -Glib::RefPtr gtr_get_mime_type_icon(std::string_view mime_type, Gtk::IconSize icon_size, Gtk::Widget& for_widget) -{ - int n; - - switch (icon_size) - { - case Gtk::ICON_SIZE_MENU: - n = 1; - break; - - case Gtk::ICON_SIZE_SMALL_TOOLBAR: - n = 2; - break; - - case Gtk::ICON_SIZE_LARGE_TOOLBAR: - n = 3; - break; - - case Gtk::ICON_SIZE_BUTTON: - n = 4; - break; - - case Gtk::ICON_SIZE_DND: - n = 5; - break; - - case Gtk::ICON_SIZE_DIALOG: - n = 6; - break; - - default: /*GTK_ICON_SIZE_INVALID*/ - n = 0; - break; - } - - if (icon_cache[n] == nullptr) - { - icon_cache[n] = icon_cache_new(for_widget, icon_size); - } - - return icon_cache_get_mime_type_icon(*icon_cache[n], mime_type); + return icon; } diff --git a/gtk/IconCache.h b/gtk/IconCache.h index a5c7a36cf..b7d913965 100644 --- a/gtk/IconCache.h +++ b/gtk/IconCache.h @@ -14,4 +14,4 @@ extern std::string_view const DirectoryMimeType; extern std::string_view const UnknownMimeType; -Glib::RefPtr gtr_get_mime_type_icon(std::string_view mime_type, Gtk::IconSize icon_size, Gtk::Widget& for_widget); +Glib::RefPtr gtr_get_mime_type_icon(std::string_view mime_type); diff --git a/gtk/MainWindow.cc b/gtk/MainWindow.cc index 0e84f4e80..1e63bc0e9 100644 --- a/gtk/MainWindow.cc +++ b/gtk/MainWindow.cc @@ -70,7 +70,7 @@ private: Glib::RefPtr createStatsMenu(); - void on_popup_menu(GdkEventButton* event); + void on_popup_menu(double view_x, double view_y); void onSpeedToggled(std::string const& action_name, tr_direction dir, bool enabled); void onSpeedSet(tr_direction dir, int KBps); @@ -111,22 +111,37 @@ private: TorrentCellRenderer* renderer_ = nullptr; Gtk::TreeViewColumn* column_ = nullptr; sigc::connection pref_handler_id_; - Gtk::Menu* popup_menu_ = nullptr; + IF_GTKMM4(Gtk::PopoverMenu*, Gtk::Menu*) popup_menu_ = nullptr; }; /*** **** ***/ -void MainWindow::Impl::on_popup_menu(GdkEventButton* event) +void MainWindow::Impl::on_popup_menu([[maybe_unused]] double view_x, [[maybe_unused]] double view_y) { if (popup_menu_ == nullptr) { - popup_menu_ = Gtk::make_managed(gtr_action_get_object("main-window-popup")); + auto const menu = gtr_action_get_object("main-window-popup"); + +#if GTKMM_CHECK_VERSION(4, 0, 0) + popup_menu_ = Gtk::make_managed(menu); + popup_menu_->set_parent(window_); +#else + popup_menu_ = Gtk::make_managed(menu); popup_menu_->attach_to_widget(window_); +#endif } - popup_menu_->popup_at_pointer(reinterpret_cast(event)); +#if GTKMM_CHECK_VERSION(4, 0, 0) + double window_x = 0; + double window_y = 0; + view_->translate_coordinates(window_, view_x, view_y, window_x, window_y); + popup_menu_->set_pointing_to(Gdk::Rectangle(window_x, window_y, 1, 1)); + popup_menu_->popup(); +#else + popup_menu_->popup_at_pointer(nullptr); +#endif } namespace @@ -136,7 +151,7 @@ bool tree_view_search_equal_func( Glib::RefPtr const& /*model*/, int /*column*/, Glib::ustring const& key, - Gtk::TreeModel::iterator const& iter) + Gtk::TreeModel::const_iterator const& iter) { auto const name = iter->get_value(torrent_cols.name_collated); return name.find(key.lowercase()) == Glib::ustring::npos; @@ -160,13 +175,21 @@ void MainWindow::Impl::init_view(Gtk::TreeView* view, Glib::RefPtrproperty_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); }); +#if !GTKMM_CHECK_VERSION(4, 0, 0) + view->signal_popup_menu().connect_notify([this]() { on_popup_menu(0, 0); }); +#endif + setup_tree_view_button_event_handling( + *view, + [this, view](guint /*button*/, TrGdkModifierType /*state*/, double view_x, double view_y, bool context_menu_requested) + { + return on_tree_view_button_pressed( + *view, + view_x, + view_y, + context_menu_requested, + sigc::mem_fun(*this, &Impl::on_popup_menu)); + }, + [view](double view_x, double view_y) { return on_tree_view_button_released(*view, view_x, view_y); }); view->signal_row_activated().connect([](auto const& /*path*/, auto* /*column*/) { gtr_action_activate("show-torrent-properties"); }); @@ -181,8 +204,11 @@ void MainWindow::Impl::prefsChanged(tr_quark const key) 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); + * for that, but this seems to work for both GTK 3 and 4 */ + view_->set_fixed_height_mode(false); + view_->set_row_separator_func({}); + view_->unset_row_separator_func(); + view_->set_fixed_height_mode(true); break; case TR_KEY_show_statusbar: @@ -227,9 +253,6 @@ 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)"), @@ -516,7 +539,9 @@ MainWindow::Impl::Impl( /* 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)) { @@ -532,7 +557,18 @@ MainWindow::Impl::Impl( /* 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)); @@ -574,6 +610,7 @@ MainWindow::Impl::Impl( 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); }, @@ -581,6 +618,7 @@ MainWindow::Impl::Impl( 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() @@ -675,10 +713,14 @@ 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 } } diff --git a/gtk/MakeDialog.cc b/gtk/MakeDialog.cc index 2beab6a71..58dbda297 100644 --- a/gtk/MakeDialog.cc +++ b/gtk/MakeDialog.cc @@ -21,6 +21,7 @@ #include #include /* tr_formatter_mem_B() */ +#include "PathButton.h" #include "MakeDialog.h" #include "PrefsDialog.h" #include "Session.h" @@ -28,6 +29,11 @@ using namespace std::literals; +#if GTKMM_CHECK_VERSION(4, 0, 0) +using FileListValue = Glib::Value; +using FileListHandler = Glib::SListHandler>; +#endif + namespace { @@ -81,10 +87,13 @@ public: TR_DISABLE_COPY_MOVE(Impl) private: - void onSourceToggled2(Gtk::ToggleButton* tb, Gtk::FileChooserButton* chooser); - void onChooserChosen(Gtk::FileChooserButton* chooser); + void onSourceToggled2(Gtk::CheckButton* tb, PathButton* chooser); + void onChooserChosen(PathButton* chooser); void onResponse(int response); +#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 const& drag_context, int x, @@ -92,6 +101,9 @@ private: Gtk::SelectionData const& selection_data, guint info, guint time_); +#endif + + bool set_dropped_source_path(std::string const& filename); void updatePiecesLabel(); @@ -105,13 +117,13 @@ private: MakeDialog& dialog_; Glib::RefPtr const core_; - Gtk::RadioButton* file_radio_ = nullptr; - Gtk::FileChooserButton* file_chooser_ = nullptr; - Gtk::RadioButton* folder_radio_ = nullptr; - Gtk::FileChooserButton* folder_chooser_ = nullptr; + Gtk::CheckButton* file_radio_ = nullptr; + PathButton* file_chooser_ = nullptr; + Gtk::CheckButton* folder_radio_ = nullptr; + PathButton* folder_chooser_ = nullptr; Gtk::Label* pieces_lb_ = nullptr; Gtk::Scale* piece_size_scale_ = nullptr; - Gtk::FileChooserButton* destination_chooser_ = nullptr; + PathButton* destination_chooser_ = nullptr; Gtk::CheckButton* comment_check_ = nullptr; Gtk::Entry* comment_entry_ = nullptr; Gtk::CheckButton* private_check_ = nullptr; @@ -221,7 +233,7 @@ void MakeProgressDialog::onProgressDialogResponse(int response) { case TR_GTK_RESPONSE_TYPE(CANCEL): builder_.cancelChecksums(); - hide(); + close(); break; case TR_GTK_RESPONSE_TYPE(ACCEPT): @@ -229,7 +241,7 @@ void MakeProgressDialog::onProgressDialogResponse(int response) [[fallthrough]]; case TR_GTK_RESPONSE_TYPE(CLOSE): - hide(); + close(); break; default: @@ -273,14 +285,15 @@ void MakeDialog::Impl::makeProgressDialog(std::string_view target, std::futuresignal_hide().connect( + gtr_window_on_close( + *progress_dialog_, [this]() { auto const success = progress_dialog_->success(); progress_dialog_.reset(); if (success) { - dialog_.hide(); + dialog_.close(); } }); progress_dialog_->show(); @@ -322,7 +335,7 @@ void MakeDialog::Impl::onResponse(int response) } else if (response == TR_GTK_RESPONSE_TYPE(CLOSE)) { - dialog_.hide(); + dialog_.close(); } } @@ -333,7 +346,7 @@ void MakeDialog::Impl::onResponse(int response) namespace { -void onSourceToggled(Gtk::ToggleButton* tb, Gtk::Widget* widget) +void onSourceToggled(Gtk::CheckButton* tb, Gtk::Widget* widget) { widget->set_sensitive(tb->get_active()); } @@ -391,13 +404,13 @@ void MakeDialog::Impl::setFilename(std::string_view filename) updatePiecesLabel(); } -void MakeDialog::Impl::onChooserChosen(Gtk::FileChooserButton* chooser) +void MakeDialog::Impl::onChooserChosen(PathButton* chooser) { chooser->set_data(FileChosenKey, GINT_TO_POINTER(true)); setFilename(chooser->get_filename()); } -void MakeDialog::Impl::onSourceToggled2(Gtk::ToggleButton* tb, Gtk::FileChooserButton* chooser) +void MakeDialog::Impl::onSourceToggled2(Gtk::CheckButton* tb, PathButton* chooser) { if (tb->get_active()) { @@ -412,6 +425,47 @@ void MakeDialog::Impl::onSourceToggled2(Gtk::ToggleButton* tb, Gtk::FileChooserB } } +bool MakeDialog::Impl::set_dropped_source_path(std::string const& filename) +{ + if (Glib::file_test(filename, TR_GLIB_FILE_TEST(IS_DIR))) + { + /* a directory was dragged onto the dialog... */ + folder_radio_->set_active(true); + folder_chooser_->set_filename(filename); + return true; + } + + if (Glib::file_test(filename, TR_GLIB_FILE_TEST(IS_REGULAR))) + { + /* a file was dragged on to the dialog... */ + file_radio_->set_active(true); + file_chooser_->set_filename(filename); + return true; + } + + return false; +} + +#if GTKMM_CHECK_VERSION(4, 0, 0) + +bool MakeDialog::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()); + if (auto const files = FileListHandler::slist_to_vector(files_value.get(), Glib::OwnershipType::OWNERSHIP_NONE); + !files.empty()) + { + return set_dropped_source_path(files.front()->get_path()); + } + } + + return false; +} + +#else + void MakeDialog::Impl::on_drag_data_received( Glib::RefPtr const& drag_context, int /*x*/, @@ -424,28 +478,14 @@ void MakeDialog::Impl::on_drag_data_received( if (auto const uris = selection_data.get_uris(); !uris.empty()) { - auto const& uri = uris.front(); - auto const filename = Glib::filename_from_uri(uri); - - if (Glib::file_test(filename, TR_GLIB_FILE_TEST(IS_DIR))) - { - /* a directory was dragged onto the dialog... */ - folder_radio_->set_active(true); - folder_chooser_->set_current_folder(filename); - success = true; - } - else if (Glib::file_test(filename, TR_GLIB_FILE_TEST(IS_REGULAR))) - { - /* a file was dragged on to the dialog... */ - file_radio_->set_active(true); - file_chooser_->set_filename(filename); - success = true; - } + success = set_dropped_source_path(Glib::filename_from_uri(uris.front())); } drag_context->drag_finish(success, false, time_); } +#endif + MakeDialog::MakeDialog( BaseObjectType* cast_item, Glib::RefPtr const& builder, @@ -468,13 +508,13 @@ std::unique_ptr MakeDialog::create(Gtk::Window& parent, Glib::RefPtr MakeDialog::Impl::Impl(MakeDialog& dialog, Glib::RefPtr const& builder, Glib::RefPtr const& core) : dialog_(dialog) , core_(core) - , file_radio_(gtr_get_widget(builder, "source_file_radio")) - , file_chooser_(gtr_get_widget(builder, "source_file_button")) - , folder_radio_(gtr_get_widget(builder, "source_folder_radio")) - , folder_chooser_(gtr_get_widget(builder, "source_folder_button")) + , file_radio_(gtr_get_widget(builder, "source_file_radio")) + , file_chooser_(gtr_get_widget_derived(builder, "source_file_button")) + , folder_radio_(gtr_get_widget(builder, "source_folder_radio")) + , folder_chooser_(gtr_get_widget_derived(builder, "source_folder_button")) , pieces_lb_(gtr_get_widget(builder, "source_size_label")) , piece_size_scale_(gtr_get_widget(builder, "piece_size_scale")) - , destination_chooser_(gtr_get_widget(builder, "destination_button")) + , destination_chooser_(gtr_get_widget_derived(builder, "destination_button")) , comment_check_(gtr_get_widget(builder, "comment_check")) , comment_entry_(gtr_get_widget(builder, "comment_entry")) , private_check_(gtr_get_widget(builder, "private_check")) @@ -484,7 +524,7 @@ MakeDialog::Impl::Impl(MakeDialog& dialog, Glib::RefPtr const& bui { dialog_.signal_response().connect(sigc::mem_fun(*this, &Impl::onResponse)); - destination_chooser_->set_current_folder(Glib::get_user_special_dir(TR_GLIB_USER_DIRECTORY(DESKTOP))); + destination_chooser_->set_filename(Glib::get_user_special_dir(TR_GLIB_USER_DIRECTORY(DESKTOP))); folder_radio_->set_active(false); folder_radio_->signal_toggled().connect([this]() { onSourceToggled2(folder_radio_, folder_chooser_); }); @@ -510,9 +550,15 @@ MakeDialog::Impl::Impl(MakeDialog& dialog, Glib::RefPtr const& bui source_entry_->set_sensitive(false); source_check_->signal_toggled().connect([this]() { onSourceToggled(source_check_, source_entry_); }); +#if GTKMM_CHECK_VERSION(4, 0, 0) + auto drop_controller = Gtk::DropTarget::create(GDK_TYPE_FILE_LIST, Gdk::DragAction::COPY); + drop_controller->signal_drop().connect(sigc::mem_fun(*this, &Impl::on_drag_data_received), false); + dialog_.add_controller(drop_controller); +#else dialog_.drag_dest_set(Gtk::DEST_DEFAULT_ALL, Gdk::ACTION_COPY); dialog_.drag_dest_add_uri_targets(); dialog_.signal_drag_data_received().connect(sigc::mem_fun(*this, &Impl::on_drag_data_received)); +#endif } void MakeDialog::Impl::onPieceSizeUpdated() diff --git a/gtk/MessageLogWindow.cc b/gtk/MessageLogWindow.cc index d2394c14d..eac79fc3a 100644 --- a/gtk/MessageLogWindow.cc +++ b/gtk/MessageLogWindow.cc @@ -113,7 +113,7 @@ bool MessageLogWindow::Impl::is_pinned_to_new() const if (auto const iter = sort_->children()[row_count - 1]; iter) { - pinned_to_new = last_visible == sort_->get_path(iter); + pinned_to_new = last_visible == sort_->get_path(TR_GTK_TREE_MODEL_CHILD_ITER(iter)); } } } @@ -129,7 +129,7 @@ void MessageLogWindow::Impl::scroll_to_bottom() if (auto const iter = sort_->children()[row_count - 1]; iter) { - view_->scroll_to_row(sort_->get_path(iter), 1); + view_->scroll_to_row(sort_->get_path(TR_GTK_TREE_MODEL_CHILD_ITER(iter)), 1); } } } @@ -218,7 +218,7 @@ void MessageLogWindow::Impl::onSaveDialogResponse(std::shared_ptrget_filename()); + doSave(*d, d->get_file()->get_path()); } d.reset(); @@ -277,7 +277,7 @@ void setForegroundColor(Gtk::CellRendererText* renderer, tr_log_level level) void renderText( Gtk::CellRendererText* renderer, - Gtk::TreeModel::iterator const& iter, + Gtk::TreeModel::const_iterator const& iter, Gtk::TreeModelColumn const& col) { auto const* const node = iter->get_value(message_log_cols.tr_msg); @@ -286,7 +286,7 @@ void renderText( setForegroundColor(renderer, node->level); } -void renderTime(Gtk::CellRendererText* renderer, Gtk::TreeModel::iterator const& iter) +void renderTime(Gtk::CellRendererText* renderer, Gtk::TreeModel::const_iterator const& iter) { auto const* const node = iter->get_value(message_log_cols.tr_msg); renderer->property_text() = Glib::DateTime::create_now_local(node->when).format("%T"); @@ -356,7 +356,8 @@ tr_log_message* addMessages(Glib::RefPtr const& store, tr_log_me { char const* name = !std::empty(i->name) ? i->name.c_str() : default_name.c_str(); - auto const row = *store->prepend(); + auto row_it = store->prepend(); + auto& row = *row_it; row[message_log_cols.tr_msg] = i; row[message_log_cols.name] = name; row[message_log_cols.message] = i->message; @@ -493,8 +494,10 @@ MessageLogWindow::Impl::Impl( filter_->set_visible_func(sigc::mem_fun(*this, &Impl::isRowVisible)); view_->set_model(sort_); - view_->signal_button_release_event().connect([this](GdkEventButton* event) - { return on_tree_view_button_released(view_, event); }); + setup_tree_view_button_event_handling( + *view_, + {}, + [this](double view_x, double view_y) { return on_tree_view_button_released(*view_, view_x, view_y); }); appendColumn(view_, message_log_cols.sequence); appendColumn(view_, message_log_cols.name); appendColumn(view_, message_log_cols.message); @@ -504,17 +507,4 @@ MessageLogWindow::Impl::Impl( SECONDARY_WINDOW_REFRESH_INTERVAL_SECONDS); scroll_to_bottom(); - window_.show_all_children(); -} - -void MessageLogWindow::on_show() -{ - Gtk::Window::on_show(); - gtr_action_set_toggled("toggle-message-log", true); -} - -void MessageLogWindow::on_hide() -{ - Gtk::Window::on_hide(); - gtr_action_set_toggled("toggle-message-log", false); } diff --git a/gtk/MessageLogWindow.h b/gtk/MessageLogWindow.h index bc9783c05..b5c64d70a 100644 --- a/gtk/MessageLogWindow.h +++ b/gtk/MessageLogWindow.h @@ -27,10 +27,6 @@ public: static std::unique_ptr create(Gtk::Window& parent, Glib::RefPtr const& core); -protected: - void on_show() override; - void on_hide() override; - private: class Impl; std::unique_ptr const impl_; diff --git a/gtk/OptionsDialog.cc b/gtk/OptionsDialog.cc index 7c4f46f87..f1e6310f6 100644 --- a/gtk/OptionsDialog.cc +++ b/gtk/OptionsDialog.cc @@ -15,6 +15,7 @@ #include "FileList.h" #include "FreeSpaceLabel.h" #include "OptionsDialog.h" +#include "PathButton.h" #include "Prefs.h" #include "PrefsDialog.h" #include "Session.h" @@ -27,6 +28,8 @@ namespace { +auto const ShowOptionsDialogChoice = Glib::ustring("show_options_dialog"); + std::string get_source_file(tr_ctor& ctor) { if (char const* source_file = tr_ctorGetSourceFile(&ctor); source_file != nullptr) @@ -67,8 +70,8 @@ public: TR_DISABLE_COPY_MOVE(Impl) private: - void sourceChanged(Gtk::FileChooserButton* b); - void downloadDirChanged(Gtk::FileChooserButton* b); + void sourceChanged(PathButton* b); + void downloadDirChanged(PathButton* b); void removeOldTorrent(); void updateTorrent(); @@ -105,11 +108,7 @@ void OptionsDialog::Impl::addResponseCB(int response) { if (tor_ != nullptr) { - if (response != TR_GTK_RESPONSE_TYPE(ACCEPT)) - { - removeOldTorrent(); - } - else + if (response == TR_GTK_RESPONSE_TYPE(ACCEPT)) { tr_torrentSetPriority(tor_, gtr_priority_combo_get_value(*priority_combo_)); @@ -127,9 +126,13 @@ void OptionsDialog::Impl::addResponseCB(int response) gtr_save_recent_dir("download", core_, downloadDir_); } + else if (response == TR_GTK_RESPONSE_TYPE(CANCEL)) + { + removeOldTorrent(); + } } - dialog_.hide(); + dialog_.close(); } void OptionsDialog::Impl::updateTorrent() @@ -158,7 +161,7 @@ void OptionsDialog::Impl::updateTorrent() * The `filename' tests here are to prevent us from losing the current * metadata when that happens. */ -void OptionsDialog::Impl::sourceChanged(Gtk::FileChooserButton* b) +void OptionsDialog::Impl::sourceChanged(PathButton* b) { auto const filename = b->get_filename(); @@ -193,7 +196,7 @@ void OptionsDialog::Impl::sourceChanged(Gtk::FileChooserButton* b) } } -void OptionsDialog::Impl::downloadDirChanged(Gtk::FileChooserButton* b) +void OptionsDialog::Impl::downloadDirChanged(PathButton* b) { auto const fname = b->get_filename(); @@ -209,7 +212,8 @@ void OptionsDialog::Impl::downloadDirChanged(Gtk::FileChooserButton* b) namespace { -void addTorrentFilters(Gtk::FileChooser* chooser) +template +void addTorrentFilters(FileChooserT* chooser) { auto filter = Gtk::FileFilter::create(); filter->set_name(_("Torrent files")); @@ -274,22 +278,13 @@ OptionsDialog::Impl::Impl( gtr_priority_combo_init(*priority_combo_); gtr_priority_combo_set_value(*priority_combo_, TR_PRI_NORMAL); - auto* source_chooser = gtr_get_widget(builder, "source_button"); + auto* source_chooser = gtr_get_widget_derived(builder, "source_button"); addTorrentFilters(source_chooser); source_chooser->signal_selection_changed().connect([this, source_chooser]() { sourceChanged(source_chooser); }); - auto* destination_chooser = gtr_get_widget(builder, "destination_button"); - - if (!destination_chooser->set_current_folder(downloadDir_)) - { - g_warning("couldn't select '%s'", downloadDir_.c_str()); - } - - for (auto const& folder : gtr_get_recent_dirs("download")) - { - destination_chooser->remove_shortcut_folder(folder); - destination_chooser->add_shortcut_folder(folder); - } + auto* destination_chooser = gtr_get_widget_derived(builder, "destination_button"); + destination_chooser->set_filename(downloadDir_); + destination_chooser->set_shortcut_folders(gtr_get_recent_dirs("download")); destination_chooser->signal_selection_changed().connect([this, destination_chooser]() { downloadDirChanged(destination_chooser); }); @@ -329,21 +324,30 @@ OptionsDialog::Impl::Impl( void TorrentFileChooserDialog::onOpenDialogResponse(int response, Glib::RefPtr const& core) { - /* remember this folder the next time we use this dialog */ - gtr_pref_string_set(TR_KEY_open_dialog_dir, get_current_folder()); - if (response == TR_GTK_RESPONSE_TYPE(ACCEPT)) { - auto const* const tb = static_cast(get_extra_widget()); + /* remember this folder the next time we use this dialog */ + gtr_pref_string_set(TR_KEY_open_dialog_dir, IF_GTKMM4(get_current_folder, get_current_folder_file)()->get_path()); + bool const do_start = gtr_pref_flag_get(TR_KEY_start_added_torrents); - bool const do_prompt = tb->get_active(); + bool const do_prompt = get_choice(ShowOptionsDialogChoice) == "true"; bool const do_notify = false; + +#if GTKMM_CHECK_VERSION(4, 0, 0) + auto files = std::vector>(); + auto files_model = get_files(); + for (auto i = guint{ 0 }; i < files_model->get_n_items(); ++i) + { + files.push_back(gtr_ptr_dynamic_cast(files_model->get_object(i))); + } +#else auto const files = get_files(); +#endif core->add_files(files, do_start, do_prompt, do_notify); } - hide(); + close(); } std::unique_ptr TorrentFileChooserDialog::create( @@ -367,13 +371,11 @@ TorrentFileChooserDialog::TorrentFileChooserDialog(Gtk::Window& parent, Glib::Re if (auto const folder = gtr_pref_string_get(TR_KEY_open_dialog_dir); !folder.empty()) { - set_current_folder(folder); + IF_GTKMM4(set_current_folder, set_current_folder_file)(Gio::File::create_for_path(folder)); } - auto* c = Gtk::make_managed(_("Show _options dialog"), true); - c->set_active(gtr_pref_flag_get(TR_KEY_show_options_window)); - set_extra_widget(*c); - c->show(); + add_choice(ShowOptionsDialogChoice, _("Show options dialog")); + set_choice(ShowOptionsDialogChoice, gtr_pref_flag_get(TR_KEY_show_options_window) ? "true" : "false"); } /*** @@ -385,7 +387,7 @@ void TorrentUrlChooserDialog::onOpenURLResponse(int response, Gtk::Entry const& if (response == TR_GTK_RESPONSE_TYPE(CANCEL)) { - hide(); + close(); } else if (response == TR_GTK_RESPONSE_TYPE(ACCEPT)) { @@ -398,7 +400,7 @@ void TorrentUrlChooserDialog::onOpenURLResponse(int response, Gtk::Entry const& if (core->add_from_url(url)) { - hide(); + close(); } else { diff --git a/gtk/PathButton.cc b/gtk/PathButton.cc new file mode 100644 index 000000000..ee840ecb0 --- /dev/null +++ b/gtk/PathButton.cc @@ -0,0 +1,266 @@ +// This file Copyright © 2022 Mnemosyne LLC. +// It may be used under GPLv2 (SPDX: GPL-2.0-only), GPLv3 (SPDX: GPL-3.0-only), +// or any future license endorsed by Mnemosyne LLC. +// License text can be found in the licenses/ folder. + +#include + +#include +#include + +#include "PathButton.h" + +class PathButton::Impl +{ +public: + Impl(PathButton& widget); + + TR_DISABLE_COPY_MOVE(Impl) + +#if GTKMM_CHECK_VERSION(4, 0, 0) + std::string const& get_filename() const; + void set_filename(std::string const& value); + + void set_shortcut_folders(std::list const& value); + + void add_filter(Glib::RefPtr const& value); + + Glib::Property& property_action(); + Glib::Property& property_title(); + + sigc::signal& signal_selection_changed(); +#endif + +private: +#if GTKMM_CHECK_VERSION(4, 0, 0) + void show_dialog(); + + void update(); + void update_mode(); +#endif + +private: + PathButton& widget_; + +#if GTKMM_CHECK_VERSION(4, 0, 0) + Glib::Property action_; + Glib::Property title_; + + sigc::signal selection_changed_; + + Gtk::Image* const image_ = nullptr; + Gtk::Label* const label_ = nullptr; + Gtk::Image* const mode_ = nullptr; + + std::string current_file_; + std::list shortcut_folders_; + std::vector> filters_; +#endif +}; + +PathButton::Impl::Impl(PathButton& widget) + : widget_(widget) +#if GTKMM_CHECK_VERSION(4, 0, 0) + , action_(widget, "action", Gtk::FileChooser::Action::OPEN) + , title_(widget, "title", {}) + , image_(Gtk::make_managed()) + , label_(Gtk::make_managed()) + , mode_(Gtk::make_managed()) +#endif +{ +#if GTKMM_CHECK_VERSION(4, 0, 0) + action_.get_proxy().signal_changed().connect([this]() { update_mode(); }); + + label_->set_ellipsize(Pango::EllipsizeMode::END); + label_->set_hexpand(true); + label_->set_halign(Gtk::Align::START); + + auto* const layout = Gtk::make_managed(Gtk::Orientation::HORIZONTAL, 5); + layout->append(*image_); + layout->append(*label_); + layout->append(*Gtk::make_managed(Gtk::Orientation::VERTICAL)); + layout->append(*mode_); + widget_.set_child(*layout); + + widget_.signal_clicked().connect(sigc::mem_fun(*this, &Impl::show_dialog)); + + update(); + update_mode(); +#endif +} + +#if GTKMM_CHECK_VERSION(4, 0, 0) + +std::string const& PathButton::Impl::get_filename() const +{ + return current_file_; +} + +void PathButton::Impl::set_filename(std::string const& value) +{ + current_file_ = value; + update(); + selection_changed_.emit(); +} + +void PathButton::Impl::set_shortcut_folders(std::list const& value) +{ + shortcut_folders_ = value; +} + +void PathButton::Impl::add_filter(Glib::RefPtr const& value) +{ + filters_.push_back(value); +} + +Glib::Property& PathButton::Impl::property_action() +{ + return action_; +} + +Glib::Property& PathButton::Impl::property_title() +{ + return title_; +} + +sigc::signal& PathButton::Impl::signal_selection_changed() +{ + return selection_changed_; +} + +void PathButton::Impl::show_dialog() +{ + auto const title = title_.get_value(); + + auto dialog = std::make_shared(!title.empty() ? title : _("Select a File"), action_.get_value()); + dialog->set_transient_for(*static_cast(widget_.get_root())); + dialog->add_button(_("_Cancel"), Gtk::ResponseType::CANCEL); + dialog->add_button(_("_Open"), Gtk::ResponseType::ACCEPT); + dialog->set_modal(true); + + if (!current_file_.empty()) + { + dialog->set_file(Gio::File::create_for_path(current_file_)); + } + + for (auto const& folder : shortcut_folders_) + { + dialog->remove_shortcut_folder(Gio::File::create_for_path(folder)); + dialog->add_shortcut_folder(Gio::File::create_for_path(folder)); + } + + for (auto const& filter : filters_) + { + dialog->add_filter(filter); + } + + dialog->signal_response().connect( + [this, dialog](int response) mutable + { + if (response == Gtk::ResponseType::ACCEPT) + { + set_filename(dialog->get_file()->get_path()); + selection_changed_.emit(); + } + + dialog.reset(); + }); + + dialog->show(); +} + +void PathButton::Impl::update() +{ + if (!current_file_.empty()) + { + auto const file = Gio::File::create_for_path(current_file_); + + try + { + image_->set(file->query_info()->get_icon()); + } + catch (Glib::Error const&) + { + image_->set_from_icon_name("image-missing"); + } + + label_->set_text(file->get_basename()); + } + else + { + image_->set_from_icon_name("image-missing"); + label_->set_text(_("(None)")); + } + + widget_.set_tooltip_text(current_file_); +} + +void PathButton::Impl::update_mode() +{ + mode_->set_from_icon_name( + action_.get_value() == Gtk::FileChooser::Action::SELECT_FOLDER ? "folder-open-symbolic" : "document-open-symbolic"); +} + +#endif + +PathButton::PathButton() + : Glib::ObjectBase(typeid(PathButton)) + , impl_(std::make_unique(*this)) +{ +} + +PathButton::PathButton(BaseObjectType* cast_item, Glib::RefPtr const& /*builder*/) + : Glib::ObjectBase(typeid(PathButton)) + , BaseWidgetType(cast_item) + , impl_(std::make_unique(*this)) +{ +} + +PathButton::~PathButton() = default; + +void PathButton::set_shortcut_folders(std::list const& value) +{ +#if GTKMM_CHECK_VERSION(4, 0, 0) + impl_->set_shortcut_folders(value); +#else + for (auto const& folder : value) + { + remove_shortcut_folder(folder); + add_shortcut_folder(folder); + } +#endif +} + +#if GTKMM_CHECK_VERSION(4, 0, 0) + +std::string PathButton::get_filename() const +{ + return impl_->get_filename(); +} + +void PathButton::set_filename(std::string const& value) +{ + impl_->set_filename(value); +} + +void PathButton::add_filter(Glib::RefPtr const& value) +{ + impl_->add_filter(value); +} + +Glib::PropertyProxy PathButton::property_action() +{ + return impl_->property_action().get_proxy(); +} + +Glib::PropertyProxy PathButton::property_title() +{ + return impl_->property_title().get_proxy(); +} + +sigc::signal& PathButton::signal_selection_changed() +{ + return impl_->signal_selection_changed(); +} + +#endif diff --git a/gtk/PathButton.h b/gtk/PathButton.h new file mode 100644 index 000000000..a3e004f40 --- /dev/null +++ b/gtk/PathButton.h @@ -0,0 +1,46 @@ +// This file Copyright © 2022 Mnemosyne LLC. +// It may be used under GPLv2 (SPDX: GPL-2.0-only), GPLv3 (SPDX: GPL-3.0-only), +// or any future license endorsed by Mnemosyne LLC. +// License text can be found in the licenses/ folder. + +#pragma once + +#include +#include +#include + +#include + +#include + +#include "Utils.h" + +class PathButton : public IF_GTKMM4(Gtk::Button, Gtk::FileChooserButton) +{ + using BaseWidgetType = IF_GTKMM4(Gtk::Button, Gtk::FileChooserButton); + +public: + PathButton(); + PathButton(BaseObjectType* cast_item, Glib::RefPtr const& builder); + ~PathButton() override; + + TR_DISABLE_COPY_MOVE(PathButton) + + void set_shortcut_folders(std::list const& value); + +#if GTKMM_CHECK_VERSION(4, 0, 0) + std::string get_filename() const; + void set_filename(std::string const& value); + + void add_filter(Glib::RefPtr const& value); + + Glib::PropertyProxy property_action(); + Glib::PropertyProxy property_title(); + + sigc::signal& signal_selection_changed(); +#endif + +private: + class Impl; + std::unique_ptr const impl_; +}; diff --git a/gtk/PrefsDialog.cc b/gtk/PrefsDialog.cc index 8f25c3d73..fdcb62b1d 100644 --- a/gtk/PrefsDialog.cc +++ b/gtk/PrefsDialog.cc @@ -18,9 +18,11 @@ #include #include "FreeSpaceLabel.h" +#include "PathButton.h" #include "Prefs.h" #include "PrefsDialog.h" #include "Session.h" +#include "SystemTrayIcon.h" #include "Utils.h" /** @@ -55,7 +57,7 @@ void PrefsDialog::Impl::response_cb(int response) if (response == TR_GTK_RESPONSE_TYPE(CLOSE)) { - dialog_.hide(); + dialog_.close(); } } @@ -161,21 +163,27 @@ void init_text_view(Gtk::TextView& view, tr_quark const key, Glib::RefPtrset_text(gtr_pref_string_get(key)); + auto const save_buffer = [buffer, key, core]() + { + core->set_pref(key, buffer->get_text()); + }; + +#if GTKMM_CHECK_VERSION(4, 0, 0) + auto focus_controller = Gtk::EventControllerFocus::create(); + focus_controller->signal_leave().connect(save_buffer); + view.add_controller(focus_controller); +#else view.add_events(Gdk::FOCUS_CHANGE_MASK); - view.signal_focus_out_event().connect( - [buffer, key, core](GdkEventFocus* /*event*/) - { - core->set_pref(key, buffer->get_text()); - return false; - }); + view.signal_focus_out_event().connect_notify(sigc::hide<0>(save_buffer)); +#endif } -void chosen_cb(Gtk::FileChooser* w, tr_quark const key, Glib::RefPtr const& core) +void chosen_cb(PathButton* w, tr_quark const key, Glib::RefPtr const& core) { core->set_pref(key, w->get_filename()); } -void init_chooser_button(Gtk::FileChooserButton& button, tr_quark const key, Glib::RefPtr const& core) +void init_chooser_button(PathButton& button, tr_quark const key, Glib::RefPtr const& core) { if (auto const path = gtr_pref_string_get(key); !path.empty()) { @@ -185,7 +193,7 @@ void init_chooser_button(Gtk::FileChooserButton& button, tr_quark const key, Gli button.signal_selection_changed().connect([&button, key, core]() { chosen_cb(&button, key, core); }); } -void target_cb(Gtk::ToggleButton* tb, Gtk::Widget* target) +void target_cb(Gtk::CheckButton* tb, Gtk::Widget* target) { target->set_sensitive(tb->get_active()); } @@ -248,7 +256,7 @@ DownloadingPage::DownloadingPage( { auto* l = gtr_get_widget(builder, "watch_dir_check"); init_check_button(*l, TR_KEY_watch_dir_enabled, core_); - auto* w = gtr_get_widget(builder, "watch_dir_chooser"); + auto* w = gtr_get_widget_derived(builder, "watch_dir_chooser"); init_chooser_button(*w, TR_KEY_watch_dir, core_); w->set_sensitive(gtr_pref_flag_get(TR_KEY_watch_dir_enabled)); l->signal_toggled().connect([l, w]() { target_cb(l, w); }); @@ -266,7 +274,7 @@ DownloadingPage::DownloadingPage( TR_KEY_trash_original_torrent_files, core_); - init_chooser_button(*gtr_get_widget(builder, "download_dir_chooser"), TR_KEY_download_dir, core_); + init_chooser_button(*gtr_get_widget_derived(builder, "download_dir_chooser"), TR_KEY_download_dir, core_); init_spin_button( *gtr_get_widget(builder, "max_active_downloads_spin"), @@ -292,7 +300,7 @@ DownloadingPage::DownloadingPage( { auto* l = gtr_get_widget(builder, "incomplete_dir_check"); init_check_button(*l, TR_KEY_incomplete_dir_enabled, core_); - auto* w = gtr_get_widget(builder, "incomplete_dir_chooser"); + auto* w = gtr_get_widget_derived(builder, "incomplete_dir_chooser"); init_chooser_button(*w, TR_KEY_incomplete_dir, core_); w->set_sensitive(gtr_pref_flag_get(TR_KEY_incomplete_dir_enabled)); l->signal_toggled().connect([l, w]() { target_cb(l, w); }); @@ -301,7 +309,7 @@ DownloadingPage::DownloadingPage( { auto* l = gtr_get_widget(builder, "download_done_script_check"); init_check_button(*l, TR_KEY_script_torrent_done_enabled, core_); - auto* w = gtr_get_widget(builder, "download_done_script_chooser"); + auto* w = gtr_get_widget_derived(builder, "download_done_script_chooser"); init_chooser_button(*w, TR_KEY_script_torrent_done_filename, core_); w->set_sensitive(gtr_pref_flag_get(TR_KEY_script_torrent_done_enabled)); l->signal_toggled().connect([l, w]() { target_cb(l, w); }); @@ -358,7 +366,7 @@ SeedingPage::SeedingPage( { auto* l = gtr_get_widget(builder, "seeding_done_script_check"); init_check_button(*l, TR_KEY_script_torrent_done_seeding_enabled, core_); - auto* w = gtr_get_widget(builder, "seeding_done_script_choose"); + auto* w = gtr_get_widget_derived(builder, "seeding_done_script_choose"); init_chooser_button(*w, TR_KEY_script_torrent_done_seeding_filename, core_); w->set_sensitive(gtr_pref_flag_get(TR_KEY_script_torrent_done_seeding_enabled)); l->signal_toggled().connect([l, w]() { target_cb(l, w); }); @@ -397,10 +405,15 @@ DesktopPage::DesktopPage( TR_KEY_inhibit_desktop_hibernation, core_); - init_check_button( - *gtr_get_widget(builder, "show_systray_icon_check"), - TR_KEY_show_notification_area_icon, - core_); + if (auto* const show_systray_icon_check = gtr_get_widget(builder, "show_systray_icon_check"); + SystemTrayIcon::is_available()) + { + init_check_button(*show_systray_icon_check, TR_KEY_show_notification_area_icon, core_); + } + else + { + show_systray_icon_check->hide(); + } init_check_button( *gtr_get_widget(builder, "notify_on_torrent_add_check"), @@ -500,7 +513,7 @@ void PrivacyPage::onBlocklistUpdated(int n) void PrivacyPage::onBlocklistUpdate() { updateBlocklistDialog_ = std::make_unique( - *static_cast(get_toplevel()), + *static_cast(TR_GTK_WIDGET_GET_ROOT(*this)), _("Update Blocklist"), false, TR_GTK_MESSAGE_TYPE(INFO), @@ -736,7 +749,7 @@ RemotePage::RemotePage(BaseObjectType* cast_item, Glib::RefPtr con { /* "enabled" checkbutton */ init_check_button(*rpc_tb_, TR_KEY_rpc_enabled, core_); - rpc_tb_->signal_clicked().connect([this]() { refreshRPCSensitivity(); }); + rpc_tb_->signal_toggled().connect([this]() { refreshRPCSensitivity(); }); auto* const open_button = gtr_get_widget(builder, "open_web_client_button"); widgets_.push_back(open_button); open_button->signal_clicked().connect(&onLaunchClutchCB); @@ -750,7 +763,7 @@ RemotePage::RemotePage(BaseObjectType* cast_item, Glib::RefPtr con /* require authentication */ init_check_button(*auth_tb_, TR_KEY_rpc_authentication_required, core_); widgets_.push_back(auth_tb_); - auth_tb_->signal_clicked().connect([this]() { refreshRPCSensitivity(); }); + auth_tb_->signal_toggled().connect([this]() { refreshRPCSensitivity(); }); /* username */ auto* username_entry = gtr_get_widget(builder, "rpc_username_entry"); @@ -767,15 +780,17 @@ RemotePage::RemotePage(BaseObjectType* cast_item, Glib::RefPtr con /* require authentication */ init_check_button(*whitelist_tb_, TR_KEY_rpc_whitelist_enabled, core_); widgets_.push_back(whitelist_tb_); - whitelist_tb_->signal_clicked().connect([this]() { refreshRPCSensitivity(); }); + whitelist_tb_->signal_toggled().connect([this]() { refreshRPCSensitivity(); }); /* access control list */ { store_ = whitelist_tree_model_new(gtr_pref_string_get(TR_KEY_rpc_whitelist)); view_->set_model(store_); - view_->signal_button_release_event().connect([this](GdkEventButton* event) - { return on_tree_view_button_released(view_, event); }); + setup_tree_view_button_event_handling( + *view_, + {}, + [this](double view_x, double view_y) { return on_tree_view_button_released(*view_, view_x, view_y); }); whitelist_widgets_.push_back(view_); auto const sel = view_->get_selection(); @@ -887,7 +902,7 @@ auto SpeedPage::get_weekday_string(Glib::Date::Weekday weekday) { auto date = Glib::Date{}; date.set_time_current(); - date.add_days(weekday - date.get_weekday()); + date.add_days(static_cast(weekday) - static_cast(date.get_weekday())); return date.format_string("%A"); } diff --git a/gtk/RelocateDialog.cc b/gtk/RelocateDialog.cc index afefc0442..aecca6785 100644 --- a/gtk/RelocateDialog.cc +++ b/gtk/RelocateDialog.cc @@ -13,6 +13,7 @@ #include +#include "PathButton.h" #include "Prefs.h" /* gtr_pref_string_get */ #include "RelocateDialog.h" #include "Session.h" @@ -52,8 +53,8 @@ private: bool do_move_ = false; sigc::connection timer_; std::unique_ptr message_dialog_; - Gtk::FileChooserButton* chooser_ = nullptr; - Gtk::RadioButton* move_tb_ = nullptr; + PathButton* chooser_ = nullptr; + Gtk::CheckButton* move_tb_ = nullptr; }; RelocateDialog::Impl::~Impl() @@ -113,7 +114,7 @@ bool RelocateDialog::Impl::onTimer() } else { - dialog_.hide(); + dialog_.close(); } } @@ -153,7 +154,7 @@ void RelocateDialog::Impl::onResponse(int response) } else { - dialog_.hide(); + dialog_.close(); } } @@ -189,8 +190,8 @@ RelocateDialog::Impl::Impl( : dialog_(dialog) , core_(core) , torrent_ids_(torrent_ids) - , chooser_(gtr_get_widget(builder, "new_location_button")) - , move_tb_(gtr_get_widget(builder, "move_data_radio")) + , chooser_(gtr_get_widget_derived(builder, "new_location_button")) + , move_tb_(gtr_get_widget(builder, "move_data_radio")) { dialog_.set_default_response(TR_GTK_RESPONSE_TYPE(CANCEL)); dialog_.signal_response().connect(sigc::mem_fun(*this, &Impl::onResponse)); @@ -199,19 +200,15 @@ RelocateDialog::Impl::Impl( if (recent_dirs.empty()) { /* default to download dir */ - chooser_->set_current_folder(gtr_pref_string_get(TR_KEY_download_dir)); + chooser_->set_filename(gtr_pref_string_get(TR_KEY_download_dir)); } else { /* set last used as target */ - chooser_->set_current_folder(recent_dirs.front()); + chooser_->set_filename(recent_dirs.front()); recent_dirs.pop_front(); /* add remaining as shortcut */ - for (auto const& folder : recent_dirs) - { - chooser_->remove_shortcut_folder(folder); - chooser_->add_shortcut_folder(folder); - } + chooser_->set_shortcut_folders(recent_dirs); } } diff --git a/gtk/Session.cc b/gtk/Session.cc index 6089b821c..d48c92a47 100644 --- a/gtk/Session.cc +++ b/gtk/Session.cc @@ -402,12 +402,12 @@ int compare_time(time_t a, time_t b) return ret; } -int compare_by_name(Gtk::TreeModel::iterator const& a, Gtk::TreeModel::iterator const& b) +int compare_by_name(Gtk::TreeModel::const_iterator const& a, Gtk::TreeModel::const_iterator const& b) { return a->get_value(torrent_cols.name_collated).compare(b->get_value(torrent_cols.name_collated)); } -int compare_by_queue(Gtk::TreeModel::iterator const& a, Gtk::TreeModel::iterator const& b) +int compare_by_queue(Gtk::TreeModel::const_iterator const& a, Gtk::TreeModel::const_iterator const& b) { auto const* const sa = tr_torrentStatCached(static_cast(a->get_value(torrent_cols.torrent))); auto const* const sb = tr_torrentStatCached(static_cast(b->get_value(torrent_cols.torrent))); @@ -415,7 +415,7 @@ int compare_by_queue(Gtk::TreeModel::iterator const& a, Gtk::TreeModel::iterator return sb->queuePosition - sa->queuePosition; } -int compare_by_ratio(Gtk::TreeModel::iterator const& a, Gtk::TreeModel::iterator const& b) +int compare_by_ratio(Gtk::TreeModel::const_iterator const& a, Gtk::TreeModel::const_iterator const& b) { int ret = 0; @@ -435,7 +435,7 @@ int compare_by_ratio(Gtk::TreeModel::iterator const& a, Gtk::TreeModel::iterator return ret; } -int compare_by_activity(Gtk::TreeModel::iterator const& a, Gtk::TreeModel::iterator const& b) +int compare_by_activity(Gtk::TreeModel::const_iterator const& a, Gtk::TreeModel::const_iterator const& b) { int ret = 0; @@ -463,7 +463,7 @@ int compare_by_activity(Gtk::TreeModel::iterator const& a, Gtk::TreeModel::itera return ret; } -int compare_by_age(Gtk::TreeModel::iterator const& a, Gtk::TreeModel::iterator const& b) +int compare_by_age(Gtk::TreeModel::const_iterator const& a, Gtk::TreeModel::const_iterator const& b) { auto* const ta = static_cast(a->get_value(torrent_cols.torrent)); auto* const tb = static_cast(b->get_value(torrent_cols.torrent)); @@ -477,7 +477,7 @@ int compare_by_age(Gtk::TreeModel::iterator const& a, Gtk::TreeModel::iterator c return ret; } -int compare_by_size(Gtk::TreeModel::iterator const& a, Gtk::TreeModel::iterator const& b) +int compare_by_size(Gtk::TreeModel::const_iterator const& a, Gtk::TreeModel::const_iterator const& b) { auto const size_a = tr_torrentTotalSize(static_cast(a->get_value(torrent_cols.torrent))); auto const size_b = tr_torrentTotalSize(static_cast(b->get_value(torrent_cols.torrent))); @@ -491,7 +491,7 @@ int compare_by_size(Gtk::TreeModel::iterator const& a, Gtk::TreeModel::iterator return ret; } -int compare_by_progress(Gtk::TreeModel::iterator const& a, Gtk::TreeModel::iterator const& b) +int compare_by_progress(Gtk::TreeModel::const_iterator const& a, Gtk::TreeModel::const_iterator const& b) { auto const* const sa = tr_torrentStatCached(static_cast(a->get_value(torrent_cols.torrent))); auto const* const sb = tr_torrentStatCached(static_cast(b->get_value(torrent_cols.torrent))); @@ -510,7 +510,7 @@ int compare_by_progress(Gtk::TreeModel::iterator const& a, Gtk::TreeModel::itera return ret; } -int compare_by_eta(Gtk::TreeModel::iterator const& a, Gtk::TreeModel::iterator const& b) +int compare_by_eta(Gtk::TreeModel::const_iterator const& a, Gtk::TreeModel::const_iterator const& b) { auto const* const sa = tr_torrentStatCached(static_cast(a->get_value(torrent_cols.torrent))); auto const* const sb = tr_torrentStatCached(static_cast(b->get_value(torrent_cols.torrent))); @@ -524,7 +524,7 @@ int compare_by_eta(Gtk::TreeModel::iterator const& a, Gtk::TreeModel::iterator c return ret; } -int compare_by_state(Gtk::TreeModel::iterator const& a, Gtk::TreeModel::iterator const& b) +int compare_by_state(Gtk::TreeModel::const_iterator const& a, Gtk::TreeModel::const_iterator const& b) { auto const sa = a->get_value(torrent_cols.activity); auto const sb = b->get_value(torrent_cols.activity); @@ -820,8 +820,8 @@ Session::Impl::Impl(Session& core, tr_session* session) { raw_model_ = Gtk::ListStore::create(torrent_cols); sorted_model_ = Gtk::TreeModelSort::create(raw_model_); - sorted_model_->set_default_sort_func([](Gtk::TreeModel::iterator const& /*a*/, Gtk::TreeModel::iterator const& /*b*/) - { return 0; }); + sorted_model_->set_default_sort_func( + [](Gtk::TreeModel::const_iterator const& /*a*/, Gtk::TreeModel::const_iterator const& /*b*/) { return 0; }); /* init from prefs & listen to pref changes */ on_pref_changed(TR_KEY_sort_mode); @@ -900,11 +900,11 @@ struct metadata_callback_data Gtk::TreeModel::iterator find_row_from_torrent_id(Glib::RefPtr const& model, tr_torrent_id_t id) { - for (auto const& row : model->children()) + for (auto& row : model->children()) { if (id == row.get_value(torrent_cols.torrent_id)) { - return row; + return TR_GTK_TREE_MODEL_CHILD_ITER(row); } } @@ -1325,7 +1325,7 @@ int gtr_compare_double(double const a, double const b, int decimal_places) return 0; } -void update_foreach(Gtk::TreeModel::Row const& row) +void update_foreach(Gtk::TreeModel::Row& row) { /* get the old states */ auto* const tor = static_cast(row.get_value(torrent_cols.torrent)); @@ -1407,7 +1407,7 @@ void Session::start_now(tr_torrent_id_t id) void Session::Impl::update() { /* update the model */ - for (auto const& row : raw_model_->children()) + for (auto row : raw_model_->children()) { update_foreach(row); } diff --git a/gtk/StatsDialog.cc b/gtk/StatsDialog.cc index 732ba7787..4a0adec78 100644 --- a/gtk/StatsDialog.cc +++ b/gtk/StatsDialog.cc @@ -122,7 +122,7 @@ void StatsDialog::Impl::dialogResponse(int response) if (response == TR_GTK_RESPONSE_TYPE(CLOSE)) { - dialog_.hide(); + dialog_.close(); } } diff --git a/gtk/SystemTrayIcon.cc b/gtk/SystemTrayIcon.cc index 3c4d62a89..fb824b5fb 100644 --- a/gtk/SystemTrayIcon.cc +++ b/gtk/SystemTrayIcon.cc @@ -8,7 +8,6 @@ // We're using deprecated Gtk::StatusItem ourselves as well #undef GTKMM_DISABLE_DEPRECATED -#include #include #include @@ -26,14 +25,31 @@ #include "SystemTrayIcon.h" #include "Utils.h" +#define TR_SYS_TRAY_IMPL_NONE 0 +#define TR_SYS_TRAY_IMPL_APPINDICATOR 1 +#define TR_SYS_TRAY_IMPL_STATUS_ICON 2 + +#ifdef HAVE_LIBAPPINDICATOR +#define TR_SYS_TRAY_IMPL TR_SYS_TRAY_IMPL_APPINDICATOR +#elif !GTKMM_CHECK_VERSION(4, 0, 0) +#define TR_SYS_TRAY_IMPL TR_SYS_TRAY_IMPL_STATUS_ICON +#else +#define TR_SYS_TRAY_IMPL TR_SYS_TRAY_IMPL_NONE +#endif + using namespace std::literals; namespace { +#if TR_SYS_TRAY_IMPL != TR_SYS_TRAY_IMPL_NONE auto const TrayIconName = Glib::ustring("transmission-tray-icon"s); auto const AppIconName = Glib::ustring("transmission"s); +#endif + +#if TR_SYS_TRAY_IMPL == TR_SYS_TRAY_IMPL_APPINDICATOR auto const AppName = Glib::ustring("transmission-gtk"s); +#endif } // namespace @@ -56,16 +72,18 @@ private: private: Glib::RefPtr const core_; +#if TR_SYS_TRAY_IMPL != TR_SYS_TRAY_IMPL_NONE Gtk::Menu* menu_; +#endif -#ifdef HAVE_LIBAPPINDICATOR +#if TR_SYS_TRAY_IMPL == TR_SYS_TRAY_IMPL_APPINDICATOR AppIndicator* indicator_; -#else +#elif TR_SYS_TRAY_IMPL == TR_SYS_TRAY_IMPL_STATUS_ICON Glib::RefPtr icon_; #endif }; -#ifdef HAVE_LIBAPPINDICATOR +#if TR_SYS_TRAY_IMPL == TR_SYS_TRAY_IMPL_APPINDICATOR SystemTrayIcon::Impl::~Impl() { @@ -76,7 +94,7 @@ void SystemTrayIcon::Impl::refresh() { } -#else +#elif TR_SYS_TRAY_IMPL == TR_SYS_TRAY_IMPL_STATUS_ICON SystemTrayIcon::Impl::~Impl() = default; @@ -95,11 +113,17 @@ void SystemTrayIcon::Impl::refresh() icon_->set_tooltip_text(make_tooltip_text()); } +#else + +SystemTrayIcon::Impl::~Impl() = default; + #endif namespace { +#if TR_SYS_TRAY_IMPL != TR_SYS_TRAY_IMPL_NONE + Glib::ustring getIconName() { Glib::ustring icon_name; @@ -121,6 +145,8 @@ Glib::ustring getIconName() return icon_name; } +#endif + } // namespace SystemTrayIcon::SystemTrayIcon(Gtk::Window& main_window, Glib::RefPtr const& core) @@ -132,29 +158,43 @@ SystemTrayIcon::~SystemTrayIcon() = default; void SystemTrayIcon::refresh() { +#if TR_SYS_TRAY_IMPL != TR_SYS_TRAY_IMPL_NONE impl_->refresh(); +#endif } -SystemTrayIcon::Impl::Impl(Gtk::Window& main_window, Glib::RefPtr const& core) +bool SystemTrayIcon::is_available() +{ +#if TR_SYS_TRAY_IMPL != TR_SYS_TRAY_IMPL_NONE + return true; +#else + return false; +#endif +} + +std::unique_ptr SystemTrayIcon::create(Gtk::Window& main_window, Glib::RefPtr const& core) +{ + return is_available() ? std::make_unique(main_window, core) : nullptr; +} + +SystemTrayIcon::Impl::Impl([[maybe_unused]] Gtk::Window& main_window, Glib::RefPtr const& core) : core_(core) { +#if TR_SYS_TRAY_IMPL != TR_SYS_TRAY_IMPL_NONE auto const icon_name = getIconName(); menu_ = Gtk::make_managed(gtr_action_get_object("icon-popup")); menu_->attach_to_widget(main_window); +#endif -#ifdef HAVE_LIBAPPINDICATOR - +#if TR_SYS_TRAY_IMPL == TR_SYS_TRAY_IMPL_APPINDICATOR indicator_ = app_indicator_new(AppName.c_str(), icon_name.c_str(), APP_INDICATOR_CATEGORY_SYSTEM_SERVICES); app_indicator_set_status(indicator_, APP_INDICATOR_STATUS_ACTIVE); app_indicator_set_menu(indicator_, Glib::unwrap(menu_)); app_indicator_set_title(indicator_, Glib::get_application_name().c_str()); - -#else - +#elif TR_SYS_TRAY_IMPL == TR_SYS_TRAY_IMPL_STATUS_ICON icon_ = Gtk::StatusIcon::create(icon_name); icon_->signal_activate().connect(sigc::mem_fun(*this, &Impl::activated)); icon_->signal_popup_menu().connect(sigc::mem_fun(*this, &Impl::popup)); - #endif } diff --git a/gtk/SystemTrayIcon.h b/gtk/SystemTrayIcon.h index a0638c0c1..1e7d0d0b9 100644 --- a/gtk/SystemTrayIcon.h +++ b/gtk/SystemTrayIcon.h @@ -23,6 +23,9 @@ public: void refresh(); + static bool is_available(); + static std::unique_ptr create(Gtk::Window& main_window, Glib::RefPtr const& core); + private: class Impl; std::unique_ptr const impl_; diff --git a/gtk/TorrentCellRenderer.cc b/gtk/TorrentCellRenderer.cc index e054d763d..0affb93e7 100644 --- a/gtk/TorrentCellRenderer.cc +++ b/gtk/TorrentCellRenderer.cc @@ -30,14 +30,17 @@ **** ***/ +#define REQ_HEIGHT(Obj) IF_GTKMM4((Obj).get_height(), (Obj).height) +#define REQ_WIDTH(Obj) IF_GTKMM4((Obj).get_width(), (Obj).width) + namespace { auto const DefaultBarHeight = 12; auto const CompactBarWidth = 50; auto const SmallScale = 0.9; -auto const CompactIconSize = Gtk::ICON_SIZE_MENU; -auto const FullIconSize = Gtk::ICON_SIZE_DND; +auto const CompactIconSize = IF_GTKMM4(Gtk::IconSize::NORMAL, Gtk::ICON_SIZE_MENU); +auto const FullIconSize = IF_GTKMM4(Gtk::IconSize::LARGE, Gtk::ICON_SIZE_DND); auto getProgressString(tr_torrent const* tor, uint64_t total_size, tr_stat const* st) { @@ -280,9 +283,11 @@ std::string getStatusString( tr_torrent const* tor, tr_stat const* st, double const uploadSpeed_KBps, - double const downloadSpeed_KBps) + double const downloadSpeed_KBps, + bool ignore_errors = false) { - auto status_str = getErrorString(st).value_or(getActivityString(tor, st, uploadSpeed_KBps, downloadSpeed_KBps)); + auto status_str = (ignore_errors ? std::nullopt : getErrorString(st)) + .value_or(getActivityString(tor, st, uploadSpeed_KBps, downloadSpeed_KBps)); if (st->activity != TR_STATUS_CHECK_WAIT && st->activity != TR_STATUS_CHECK && st->activity != TR_STATUS_DOWNLOAD_WAIT && st->activity != TR_STATUS_SEED_WAIT && st->activity != TR_STATUS_STOPPED) @@ -304,6 +309,9 @@ std::string getStatusString( class TorrentCellRenderer::Impl { + using ContextPtr = TorrentCellRenderer::ContextPtr; + using IconSize = IF_GTKMM4(Gtk::IconSize, Gtk::BuiltinIconSize); + public: explicit Impl(TorrentCellRenderer& renderer); ~Impl(); @@ -314,12 +322,12 @@ public: void get_size_full(Gtk::Widget& widget, int& width, int& height) const; void render_compact( - Cairo::RefPtr const& cr, + ContextPtr const& context, Gtk::Widget& widget, Gdk::Rectangle const& background_area, Gtk::CellRendererState flags); void render_full( - Cairo::RefPtr const& cr, + ContextPtr const& context, Gtk::Widget& widget, Gdk::Rectangle const& background_area, Gtk::CellRendererState flags); @@ -339,6 +347,9 @@ public: Glib::Property compact; +private: + static void set_icon(Gtk::CellRendererPixbuf& renderer, Glib::RefPtr const& icon, IconSize icon_size); + private: TorrentCellRenderer& renderer_; @@ -354,7 +365,7 @@ private: namespace { -Glib::RefPtr get_icon(tr_torrent const* tor, Gtk::IconSize icon_size, Gtk::Widget& for_widget) +Glib::RefPtr get_icon(tr_torrent const* tor) { auto mime_type = std::string_view{}; @@ -373,7 +384,7 @@ Glib::RefPtr get_icon(tr_torrent const* tor, Gtk::IconSize icon_siz mime_type = strchr(name, '/') != nullptr ? DirectoryMimeType : tr_get_mime_type_for_filename(name); } - return gtr_get_mime_type_icon(mime_type, icon_size, for_widget); + return gtr_get_mime_type_icon(mime_type); } } // namespace @@ -382,6 +393,19 @@ Glib::RefPtr get_icon(tr_torrent const* tor, Gtk::IconSize icon_siz **** ***/ +void TorrentCellRenderer::Impl::set_icon( + Gtk::CellRendererPixbuf& renderer, + Glib::RefPtr const& icon, + IconSize icon_size) +{ + renderer.property_gicon() = icon; +#if GTKMM_CHECK_VERSION(4, 0, 0) + renderer.property_icon_size() = icon_size; +#else + renderer.property_stock_size() = icon_size; +#endif +} + void TorrentCellRenderer::Impl::get_size_compact(Gtk::Widget& widget, int& width, int& height) const { int xpad; @@ -394,13 +418,13 @@ void TorrentCellRenderer::Impl::get_size_compact(Gtk::Widget& widget, int& width auto* const tor = static_cast(torrent.get_value()); auto const* const st = tr_torrentStatCached(tor); - auto const icon = get_icon(tor, CompactIconSize, widget); + auto const icon = get_icon(tor); auto const name = Glib::ustring(tr_torrentName(tor)); auto const gstr_stat = getShortStatusString(tor, st, upload_speed_KBps.get_value(), download_speed_KBps.get_value()); renderer_.get_padding(xpad, ypad); /* get the idealized cell dimensions */ - icon_renderer_->property_pixbuf() = icon; + set_icon(*icon_renderer_, icon, CompactIconSize); icon_renderer_->get_preferred_size(widget, min_size, icon_size); text_renderer_->property_text() = name; text_renderer_->property_ellipsize() = TR_PANGO_ELLIPSIZE_MODE(NONE); @@ -414,8 +438,8 @@ void TorrentCellRenderer::Impl::get_size_compact(Gtk::Widget& widget, int& width *** LAYOUT **/ - width = xpad * 2 + icon_size.width + GUI_PAD + CompactBarWidth + GUI_PAD + stat_size.width; - height = ypad * 2 + std::max(name_size.height, bar_height.get_value()); + width = xpad * 2 + REQ_WIDTH(icon_size) + GUI_PAD + CompactBarWidth + GUI_PAD + REQ_WIDTH(stat_size); + height = ypad * 2 + std::max(REQ_HEIGHT(name_size), bar_height.get_value()); } void TorrentCellRenderer::Impl::get_size_full(Gtk::Widget& widget, int& width, int& height) const @@ -432,14 +456,14 @@ void TorrentCellRenderer::Impl::get_size_full(Gtk::Widget& widget, int& width, i auto const* const st = tr_torrentStatCached(tor); auto const total_size = tr_torrentTotalSize(tor); - auto const icon = get_icon(tor, FullIconSize, widget); + auto const icon = get_icon(tor); auto const name = Glib::ustring(tr_torrentName(tor)); - auto const gstr_stat = getStatusString(tor, st, upload_speed_KBps.get_value(), download_speed_KBps.get_value()); + auto const gstr_stat = getStatusString(tor, st, upload_speed_KBps.get_value(), download_speed_KBps.get_value(), true); auto const gstr_prog = getProgressString(tor, total_size, st); renderer_.get_padding(xpad, ypad); /* get the idealized cell dimensions */ - icon_renderer_->property_pixbuf() = icon; + set_icon(*icon_renderer_, icon, FullIconSize); icon_renderer_->get_preferred_size(widget, min_size, icon_size); text_renderer_->property_text() = name; text_renderer_->property_weight() = TR_PANGO_WEIGHT(BOLD); @@ -457,9 +481,9 @@ void TorrentCellRenderer::Impl::get_size_full(Gtk::Widget& widget, int& width, i *** LAYOUT **/ - width = xpad * 2 + icon_size.width + GUI_PAD + std::max(prog_size.width, stat_size.width); - height = ypad * 2 + name_size.height + prog_size.height + GUI_PAD_SMALL + bar_height.get_value() + GUI_PAD_SMALL + - stat_size.height; + width = xpad * 2 + REQ_WIDTH(icon_size) + GUI_PAD + std::max(REQ_WIDTH(prog_size), REQ_WIDTH(stat_size)); + height = ypad * 2 + REQ_HEIGHT(name_size) + REQ_HEIGHT(prog_size) + GUI_PAD_SMALL + bar_height.get_value() + GUI_PAD_SMALL + + REQ_HEIGHT(stat_size); } void TorrentCellRenderer::get_preferred_width_vfunc(Gtk::Widget& widget, int& minimum_width, int& natural_width) const @@ -507,24 +531,6 @@ void TorrentCellRenderer::get_preferred_height_vfunc(Gtk::Widget& widget, int& m namespace { -Gdk::RGBA get_text_color(Gtk::Widget& w, tr_stat const* st) -{ - static auto const red = Gdk::RGBA("red"); - - if (st->error != 0) - { - return red; - } - else if (st->activity == TR_STATUS_STOPPED) - { - return w.get_style_context()->get_color(Gtk::STATE_FLAG_INSENSITIVE); - } - else - { - return w.get_style_context()->get_color(Gtk::STATE_FLAG_NORMAL); - } -} - double get_percent_done(tr_torrent const* tor, tr_stat const* st, bool* seed) { double d; @@ -543,10 +549,16 @@ double get_percent_done(tr_torrent const* tor, tr_stat const* st, bool* seed) return d; } +template +void render_impl(Gtk::CellRenderer& renderer, Ts&&... args) +{ + renderer.IF_GTKMM4(snapshot, render)(std::forward(args)...); +} + } // namespace void TorrentCellRenderer::Impl::render_compact( - Cairo::RefPtr const& cr, + ContextPtr const& context, Gtk::Widget& widget, Gdk::Rectangle const& background_area, Gtk::CellRendererState flags) @@ -564,11 +576,24 @@ void TorrentCellRenderer::Impl::render_compact( auto const percentDone = get_percent_done(tor, st, &seed); bool const sensitive = active || st->error; - auto const icon = get_icon(tor, CompactIconSize, widget); + if (st->activity == TR_STATUS_STOPPED) + { + flags |= TR_GTK_CELL_RENDERER_STATE(INSENSITIVE); + } + + if (st->error != 0 && (flags & TR_GTK_CELL_RENDERER_STATE(SELECTED)) == Gtk::CellRendererState{}) + { + text_renderer_->property_foreground() = "red"; + } + else + { + text_renderer_->property_foreground_set() = false; + } + + auto const icon = get_icon(tor); auto const name = Glib::ustring(tr_torrentName(tor)); auto const gstr_stat = getShortStatusString(tor, st, upload_speed_KBps.get_value(), download_speed_KBps.get_value()); renderer_.get_padding(xpad, ypad); - auto const text_color = get_text_color(widget, st); auto fill_area = background_area; fill_area.set_x(fill_area.get_x() + xpad); @@ -577,7 +602,7 @@ void TorrentCellRenderer::Impl::render_compact( fill_area.set_height(fill_area.get_height() - ypad * 2); auto icon_area = fill_area; - icon_renderer_->property_pixbuf() = icon; + set_icon(*icon_renderer_, icon, CompactIconSize); icon_renderer_->get_preferred_width(widget, min_width, width); icon_area.set_width(width); @@ -614,30 +639,28 @@ void TorrentCellRenderer::Impl::render_compact( *** RENDER **/ - icon_renderer_->property_pixbuf() = icon; + set_icon(*icon_renderer_, icon, CompactIconSize); icon_renderer_->property_sensitive() = sensitive; - icon_renderer_->render(cr, widget, icon_area, icon_area, flags); + render_impl(*icon_renderer_, context, widget, icon_area, icon_area, flags); auto const percent_done = static_cast(percentDone * 100.0); progress_renderer_->property_value() = percent_done; progress_renderer_->property_text() = fmt::format(FMT_STRING("{:d}%"), percent_done); progress_renderer_->property_sensitive() = sensitive; - progress_renderer_->render(cr, widget, prog_area, prog_area, flags); + render_impl(*progress_renderer_, context, widget, prog_area, prog_area, flags); text_renderer_->property_text() = gstr_stat; text_renderer_->property_scale() = SmallScale; text_renderer_->property_ellipsize() = TR_PANGO_ELLIPSIZE_MODE(END); - text_renderer_->property_foreground_rgba() = text_color; - text_renderer_->render(cr, widget, stat_area, stat_area, flags); + render_impl(*text_renderer_, context, widget, stat_area, stat_area, flags); text_renderer_->property_text() = name; text_renderer_->property_scale() = 1.0; - text_renderer_->property_foreground_rgba() = text_color; - text_renderer_->render(cr, widget, name_area, name_area, flags); + render_impl(*text_renderer_, context, widget, name_area, name_area, flags); } void TorrentCellRenderer::Impl::render_full( - Cairo::RefPtr const& cr, + ContextPtr const& context, Gtk::Widget& widget, Gdk::Rectangle const& background_area, Gtk::CellRendererState flags) @@ -656,19 +679,32 @@ void TorrentCellRenderer::Impl::render_full( auto const percentDone = get_percent_done(tor, st, &seed); bool const sensitive = active || st->error; - auto const icon = get_icon(tor, FullIconSize, widget); + if (st->activity == TR_STATUS_STOPPED) + { + flags |= TR_GTK_CELL_RENDERER_STATE(INSENSITIVE); + } + + if (st->error != 0 && (flags & TR_GTK_CELL_RENDERER_STATE(SELECTED)) == Gtk::CellRendererState{}) + { + text_renderer_->property_foreground() = "red"; + } + else + { + text_renderer_->property_foreground_set() = false; + } + + auto const icon = get_icon(tor); auto const name = Glib::ustring(tr_torrentName(tor)); auto const gstr_prog = getProgressString(tor, total_size, st); auto const gstr_stat = getStatusString(tor, st, upload_speed_KBps.get_value(), download_speed_KBps.get_value()); renderer_.get_padding(xpad, ypad); - auto const text_color = get_text_color(widget, st); /* get the idealized cell dimensions */ Gdk::Rectangle icon_area; - icon_renderer_->property_pixbuf() = icon; + set_icon(*icon_renderer_, icon, FullIconSize); icon_renderer_->get_preferred_size(widget, min_size, size); - icon_area.set_width(size.width); - icon_area.set_height(size.height); + icon_area.set_width(REQ_WIDTH(size)); + icon_area.set_height(REQ_HEIGHT(size)); Gdk::Rectangle name_area; text_renderer_->property_text() = name; @@ -676,19 +712,19 @@ void TorrentCellRenderer::Impl::render_full( text_renderer_->property_ellipsize() = TR_PANGO_ELLIPSIZE_MODE(NONE); text_renderer_->property_scale() = 1.0; text_renderer_->get_preferred_size(widget, min_size, size); - name_area.set_height(size.height); + name_area.set_height(REQ_HEIGHT(size)); Gdk::Rectangle prog_area; text_renderer_->property_text() = gstr_prog; text_renderer_->property_weight() = TR_PANGO_WEIGHT(NORMAL); text_renderer_->property_scale() = SmallScale; text_renderer_->get_preferred_size(widget, min_size, size); - prog_area.set_height(size.height); + prog_area.set_height(REQ_HEIGHT(size)); Gdk::Rectangle stat_area; text_renderer_->property_text() = gstr_stat; text_renderer_->get_preferred_size(widget, min_size, size); - stat_area.set_height(size.height); + stat_area.set_height(REQ_HEIGHT(size)); Gdk::Rectangle prct_area; @@ -740,34 +776,32 @@ void TorrentCellRenderer::Impl::render_full( *** RENDER **/ - icon_renderer_->property_pixbuf() = icon; + set_icon(*icon_renderer_, icon, FullIconSize); icon_renderer_->property_sensitive() = sensitive; - icon_renderer_->render(cr, widget, icon_area, icon_area, flags); + render_impl(*icon_renderer_, context, widget, icon_area, icon_area, flags); text_renderer_->property_text() = name; text_renderer_->property_scale() = 1.0; - text_renderer_->property_foreground_rgba() = text_color; text_renderer_->property_ellipsize() = TR_PANGO_ELLIPSIZE_MODE(END); text_renderer_->property_weight() = TR_PANGO_WEIGHT(BOLD); - text_renderer_->render(cr, widget, name_area, name_area, flags); + render_impl(*text_renderer_, context, widget, name_area, name_area, flags); text_renderer_->property_text() = gstr_prog; text_renderer_->property_scale() = SmallScale; text_renderer_->property_weight() = TR_PANGO_WEIGHT(NORMAL); - text_renderer_->render(cr, widget, prog_area, prog_area, flags); + render_impl(*text_renderer_, context, widget, prog_area, prog_area, flags); progress_renderer_->property_value() = static_cast(percentDone * 100.0); progress_renderer_->property_text() = Glib::ustring(); progress_renderer_->property_sensitive() = sensitive; - progress_renderer_->render(cr, widget, prct_area, prct_area, flags); + render_impl(*progress_renderer_, context, widget, prct_area, prct_area, flags); text_renderer_->property_text() = gstr_stat; - text_renderer_->property_foreground_rgba() = text_color; - text_renderer_->render(cr, widget, stat_area, stat_area, flags); + render_impl(*text_renderer_, context, widget, stat_area, stat_area, flags); } -void TorrentCellRenderer::render_vfunc( - Cairo::RefPtr const& cr, +void TorrentCellRenderer::IF_GTKMM4(snapshot_vfunc, render_vfunc)( + ContextPtr const& context, Gtk::Widget& widget, Gdk::Rectangle const& background_area, Gdk::Rectangle const& /*cell_area*/, @@ -782,11 +816,11 @@ void TorrentCellRenderer::render_vfunc( { if (impl_->compact.get_value()) { - impl_->render_compact(cr, widget, background_area, flags); + impl_->render_compact(context, widget, background_area, flags); } else { - impl_->render_full(cr, widget, background_area, flags); + impl_->render_full(context, widget, background_area, flags); } } diff --git a/gtk/TorrentCellRenderer.h b/gtk/TorrentCellRenderer.h index de7a69f68..21c87c3dd 100644 --- a/gtk/TorrentCellRenderer.h +++ b/gtk/TorrentCellRenderer.h @@ -12,10 +12,14 @@ #include +#include "Utils.h" + struct tr_torrent; class TorrentCellRenderer : public Gtk::CellRenderer { + using ContextPtr = IF_GTKMM4(Glib::RefPtr, Cairo::RefPtr); + public: TorrentCellRenderer(); ~TorrentCellRenderer() override; @@ -31,8 +35,8 @@ public: protected: void get_preferred_width_vfunc(Gtk::Widget& widget, int& minimum_width, int& natural_width) const override; void get_preferred_height_vfunc(Gtk::Widget& widget, int& minimum_height, int& natural_height) const override; - void render_vfunc( - Cairo::RefPtr const& cr, + void IF_GTKMM4(snapshot_vfunc, render_vfunc)( + ContextPtr const& context, Gtk::Widget& widget, Gdk::Rectangle const& background_area, Gdk::Rectangle const& cell_area, diff --git a/gtk/Utils.cc b/gtk/Utils.cc index b1f9d55da..91360a6bf 100644 --- a/gtk/Utils.cc +++ b/gtk/Utils.cc @@ -14,6 +14,12 @@ #include /* g_file_trash() */ #include +#include +#include +#if GTK_CHECK_VERSION(4, 0, 0) && defined(GDK_WINDOWING_X11) +#include +#endif + #include #include /* TR_RATIO_NA, TR_RATIO_INF */ @@ -265,22 +271,24 @@ void gtr_add_torrent_error_dialog(Gtk::Widget& child, tr_torrent* duplicate_torr TR_GTK_BUTTONS_TYPE(CLOSE)); w->set_secondary_text(secondary); w->signal_response().connect([w](int /*response*/) mutable { w.reset(); }); - w->show_all(); + w->show(); } /* pop up the context menu if a user right-clicks. if the row they right-click on isn't selected, select it. */ bool on_tree_view_button_pressed( - Gtk::TreeView* view, - GdkEventButton* event, - std::function const& callback) + Gtk::TreeView& view, + double view_x, + double view_y, + bool context_menu_requested, + std::function const& callback) { - if (event->type == GDK_BUTTON_PRESS && event->button == 3) + if (context_menu_requested) { Gtk::TreeModel::Path path; - auto const selection = view->get_selection(); + auto const selection = view.get_selection(); - if (view->get_path_at_pos((int)event->x, (int)event->y, path) && !selection->is_selected(path)) + if (view.get_path_at_pos((int)view_x, (int)view_y, path) && !selection->is_selected(path)) { selection->unselect_all(); selection->select(path); @@ -288,7 +296,7 @@ bool on_tree_view_button_pressed( if (callback) { - callback(event); + callback(view_x, view_y); } return true; @@ -299,16 +307,75 @@ bool on_tree_view_button_pressed( /* if the user clicked in an empty area of the list, * clear all the selections. */ -bool on_tree_view_button_released(Gtk::TreeView* view, GdkEventButton* event) +bool on_tree_view_button_released(Gtk::TreeView& view, double view_x, double view_y) { - if (Gtk::TreeModel::Path path; !view->get_path_at_pos((int)event->x, (int)event->y, path)) + if (Gtk::TreeModel::Path path; !view.get_path_at_pos((int)view_x, (int)view_y, path)) { - view->get_selection()->unselect_all(); + view.get_selection()->unselect_all(); } return false; } +void setup_tree_view_button_event_handling( + Gtk::TreeView& view, + std::function const& press_callback, + std::function const& release_callback) +{ +#if GTKMM_CHECK_VERSION(4, 0, 0) + auto controller = Gtk::GestureClick::create(); + controller->set_button(0); + controller->set_propagation_phase(Gtk::PropagationPhase::CAPTURE); + if (press_callback) + { + controller->signal_pressed().connect( + [&view, press_callback, controller](int /*n_press*/, double event_x, double event_y) + { + auto* const sequence = controller->get_current_sequence(); + auto const event = controller->get_last_event(sequence); + if (event->get_event_type() == TR_GDK_EVENT_TYPE(BUTTON_PRESS) && + press_callback( + event->get_button(), + event->get_modifier_state(), + event_x, + event_y, + event->triggers_context_menu())) + { + controller->set_sequence_state(sequence, Gtk::EventSequenceState::CLAIMED); + } + }, + false); + } + if (release_callback) + { + controller->signal_released().connect( + [&view, release_callback, controller](int /*n_press*/, double event_x, double event_y) + { + auto* const sequence = controller->get_current_sequence(); + auto const event = controller->get_last_event(sequence); + if (event->get_event_type() == TR_GDK_EVENT_TYPE(BUTTON_RELEASE) && release_callback(event_x, event_y)) + { + controller->set_sequence_state(sequence, Gtk::EventSequenceState::CLAIMED); + } + }); + } + view.add_controller(controller); +#else + if (press_callback) + { + view.signal_button_press_event().connect( + [press_callback](GdkEventButton* event) + { return press_callback(event->button, event->state, event->x, event->y, event->button == GDK_BUTTON_SECONDARY); }, + false); + } + if (release_callback) + { + view.signal_button_release_event().connect([release_callback](GdkEventButton* event) + { return release_callback(event->x, event->y); }); + } +#endif +} + bool gtr_file_trash_or_remove(std::string const& filename, tr_error** error) { bool trashed = false; @@ -326,8 +393,8 @@ bool gtr_file_trash_or_remove(std::string const& filename, tr_error** error) } catch (Glib::Error const& e) { - g_message("Unable to trash file \"%s\": %s", filename.c_str(), e.what().c_str()); - tr_error_set(error, e.code(), e.what().raw()); + g_message("Unable to trash file \"%s\": %s", filename.c_str(), TR_GLIB_EXCEPTION_WHAT(e)); + tr_error_set(error, e.code(), TR_GLIB_EXCEPTION_WHAT(e)); } } @@ -339,9 +406,9 @@ bool gtr_file_trash_or_remove(std::string const& filename, tr_error** error) } catch (Glib::Error const& e) { - g_message("Unable to delete file \"%s\": %s", filename.c_str(), e.what().c_str()); + g_message("Unable to delete file \"%s\": %s", filename.c_str(), TR_GLIB_EXCEPTION_WHAT(e)); tr_error_clear(error); - tr_error_set(error, e.code(), e.what().raw()); + tr_error_set(error, e.code(), TR_GLIB_EXCEPTION_WHAT(e)); result = false; } } @@ -438,7 +505,7 @@ void gtr_combo_box_set_active_enum(Gtk::ComboBox& combo_box, int value) { if (row.get_value(column) == value) { - combo_box.set_active(row); + combo_box.set_active(TR_GTK_TREE_MODEL_CHILD_ITER(row)); return; } } @@ -552,11 +619,33 @@ void gtr_widget_set_visible(Gtk::Widget& w, bool b) w.set_visible(b); } -void gtr_dialog_set_content(Gtk::Dialog& dialog, Gtk::Widget& content) +void gtr_window_set_skip_taskbar_hint([[maybe_unused]] Gtk::Window& window, [[maybe_unused]] bool value) { - auto* vbox = dialog.get_content_area(); - vbox->pack_start(content, true, true, 0); - content.show_all(); +#if GTK_CHECK_VERSION(4, 0, 0) +#if defined(GDK_WINDOWING_X11) + gdk_x11_surface_set_skip_taskbar_hint(Glib::unwrap(window.get_surface()), value ? TRUE : FALSE); +#endif +#else + window.set_skip_taskbar_hint(value); +#endif +} + +void gtr_window_set_urgency_hint([[maybe_unused]] Gtk::Window& window, [[maybe_unused]] bool value) +{ +#if GTK_CHECK_VERSION(4, 0, 0) +#if defined(GDK_WINDOWING_X11) + gdk_x11_surface_set_urgency_hint(Glib::unwrap(window.get_surface()), value ? TRUE : FALSE); +#endif +#else + window.set_urgency_hint(value); +#endif +} + +void gtr_window_raise([[maybe_unused]] Gtk::Window& window) +{ +#if !GTKMM_CHECK_VERSION(4, 0, 0) + window.get_window()->raise(); +#endif } /*** @@ -596,16 +685,43 @@ void gtr_unrecognized_url_dialog(Gtk::Widget& parent, Glib::ustring const& url) void gtr_paste_clipboard_url_into_entry(Gtk::Entry& entry) { + auto const process = [&entry](Glib::ustring const& text) + { + auto const sv = tr_strvStrip(text.raw()); + if (!sv.empty() && (tr_urlIsValid(sv) || tr_magnet_metainfo{}.parseMagnet(sv))) + { + entry.set_text(text); + return true; + } + return false; + }; + +#if GTKMM_CHECK_VERSION(4, 0, 0) + auto const request = [](Glib::RefPtr const& clipboard, auto&& callback) + { + clipboard->read_text_async([clipboard, callback](Glib::RefPtr& result) + { callback(clipboard->read_text_finish(result)); }); + }; + + request( + Gdk::Display::get_default()->get_primary_clipboard(), + [request, process](Glib::ustring const& text) + { + if (!process(text)) + { + request(Gdk::Display::get_default()->get_clipboard(), process); + } + }); +#else for (auto const& str : { Gtk::Clipboard::get(GDK_SELECTION_PRIMARY)->wait_for_text(), Gtk::Clipboard::get(GDK_SELECTION_CLIPBOARD)->wait_for_text() }) { - auto const sv = tr_strvStrip(str.raw()); - if (!sv.empty() && (tr_urlIsValid(sv) || tr_magnet_metainfo{}.parseMagnet(sv))) + if (process(str)) { - entry.set_text(str); - return; + break; } } +#endif } /*** diff --git a/gtk/Utils.h b/gtk/Utils.h index 2f6989184..04296a882 100644 --- a/gtk/Utils.h +++ b/gtk/Utils.h @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -68,6 +69,7 @@ #define TR_GTK_ALIGN(Code) IF_GTKMM4(Gtk::Align::Code, Gtk::ALIGN_##Code) #define TR_GTK_BUTTONS_TYPE(Code) IF_GTKMM4(Gtk::ButtonsType::Code, Gtk::BUTTONS_##Code) +#define TR_GTK_CELL_RENDERER_STATE(Code) IF_GTKMM4(Gtk::CellRendererState::Code, Gtk::CELL_RENDERER_##Code) #define TR_GTK_FILE_CHOOSER_ACTION(Code) IF_GTKMM4(Gtk::FileChooser::Action::Code, Gtk::FILE_CHOOSER_ACTION_##Code) #define TR_GTK_MESSAGE_TYPE(Code) IF_GTKMM4(Gtk::MessageType::Code, Gtk::MESSAGE_##Code) #define TR_GTK_ORIENTATION(Code) IF_GTKMM4(Gtk::Orientation::Code, Gtk::ORIENTATION_##Code) @@ -78,14 +80,21 @@ #define TR_GTK_STATE_FLAGS(Code) IF_GTKMM4(Gtk::StateFlags::Code, Gtk::STATE_FLAG_##Code) #define TR_GTK_TREE_VIEW_COLUMN_SIZING(Code) IF_GTKMM4(Gtk::TreeViewColumn::Sizing::Code, Gtk::TREE_VIEW_COLUMN_##Code) +#define TR_GTK_TREE_MODEL_CHILD_ITER(Obj) IF_GTKMM4((Obj).get_iter(), (Obj)) +#define TR_GTK_WIDGET_GET_ROOT(Obj) IF_GTKMM4((Obj).get_root(), (Obj).get_toplevel()) + #define TR_GDK_COLORSPACE(Code) IF_GTKMM4(Gdk::Colorspace::Code, Gdk::COLORSPACE_##Code) +#define TR_GDK_EVENT_TYPE(Code) IF_GTKMM4(Gdk::Event::Type::Code, GdkEventType::GDK_##Code) #define TR_GDK_DRAG_ACTION(Code) IF_GTKMM4(Gdk::DragAction::Code, Gdk::ACTION_##Code) +#define TR_GDK_MODIFIED_TYPE(Code) IF_GTKMM4(Gdk::ModifierType::Code, GdkModifierType::GDK_##Code) #define TR_GLIB_FILE_TEST(Code) IF_GLIBMM2_68(Glib::FileTest::Code, Glib::FILE_TEST_##Code) #define TR_GLIB_NODE_TREE_TRAVERSE_FLAGS(Cls, Code) IF_GLIBMM2_68(Cls::TraverseFlags::Code, Cls::TRAVERSE_##Code) #define TR_GLIB_SPAWN_FLAGS(Code) IF_GLIBMM2_68(Glib::SpawnFlags::Code, Glib::SPAWN_##Code) #define TR_GLIB_USER_DIRECTORY(Code) IF_GLIBMM2_68(Glib::UserDirectory::Code, Glib::USER_DIRECTORY_##Code) +#define TR_GLIB_EXCEPTION_WHAT(Obj) IF_GLIBMM2_68((Obj).what(), (Obj).what().c_str()) + #define TR_GIO_APP_INFO_CREATE_FLAGS(Code) IF_GLIBMM2_68(Gio::AppInfo::CreateFlags::Code, Gio::APP_INFO_CREATE_##Code) #define TR_GIO_APPLICATION_FLAGS(Code) IF_GLIBMM2_68(Gio::Application::Flags::Code, Gio::APPLICATION_##Code) #define TR_GIO_DBUS_BUS_TYPE(Code) IF_GLIBMM2_68(Gio::DBus::BusType::Code, Gio::DBus::BUS_TYPE_##Code) @@ -155,7 +164,9 @@ Glib::ustring gtr_get_help_uri(); /* backwards-compatible wrapper around gtk_widget_set_visible() */ void gtr_widget_set_visible(Gtk::Widget&, bool); -void gtr_dialog_set_content(Gtk::Dialog& dialog, Gtk::Widget& content); +void gtr_window_set_skip_taskbar_hint(Gtk::Window& window, bool value); +void gtr_window_set_urgency_hint(Gtk::Window& window, bool value); +void gtr_window_raise(Gtk::Window& window); /*** **** @@ -182,12 +193,21 @@ void gtr_add_torrent_error_dialog(Gtk::Widget& window_or_child, tr_torrent* dupl /* pop up the context menu if a user right-clicks. if the row they right-click on isn't selected, select it. */ bool on_tree_view_button_pressed( - Gtk::TreeView* view, - GdkEventButton* event, - std::function const& callback = {}); + Gtk::TreeView& view, + double view_x, + double view_y, + bool context_menu_requested, + std::function const& callback = {}); /* if the click didn't specify a row, clear the selection */ -bool on_tree_view_button_released(Gtk::TreeView* view, GdkEventButton* event); +bool on_tree_view_button_released(Gtk::TreeView& view, double view_x, double view_y); + +using TrGdkModifierType = IF_GTKMM4(Gdk::ModifierType, guint); + +void setup_tree_view_button_event_handling( + Gtk::TreeView& view, + std::function const& press_callback, + std::function const& release_callback); /* move a file to the trashcan if GIO is available; otherwise, delete it */ bool gtr_file_trash_or_remove(std::string const& filename, tr_error** error); @@ -276,6 +296,16 @@ inline Glib::RefPtr gtr_ptr_static_cast(Glib::RefPtr const& ptr) #endif } +template +inline Glib::RefPtr gtr_ptr_dynamic_cast(Glib::RefPtr const& ptr) +{ +#if G_ENCODE_VERSION(GLIBMM_MAJOR_VERSION, GLIBMM_MINOR_VERSION) < G_ENCODE_VERSION(2, 68) + return Glib::RefPtr::cast_dynamic(ptr); +#else + return std::dynamic_pointer_cast(ptr); +#endif +} + template<> struct std::hash { @@ -295,20 +325,51 @@ struct fmt::formatter : formatter } }; -template -T* gtr_get_widget(Glib::RefPtr const& builder, Glib::ustring const& name, ArgTs&&... args) +template +T* gtr_get_widget(Glib::RefPtr const& builder, Glib::ustring const& name) { +#if GTKMM_CHECK_VERSION(4, 0, 0) + return builder->get_widget(name); +#else T* widget = nullptr; - builder->get_widget(name, widget, std::forward(args)...); + builder->get_widget(name, widget); return widget; +#endif } template T* gtr_get_widget_derived(Glib::RefPtr const& builder, Glib::ustring const& name, ArgTs&&... args) { +#if GTKMM_CHECK_VERSION(4, 0, 0) + return Gtk::Builder::get_widget_derived(builder, name, std::forward(args)...); +#else T* widget = nullptr; builder->get_widget_derived(name, widget, std::forward(args)...); return widget; +#endif +} + +template +void gtr_window_on_close(Gtk::Window& widget, F&& callback) +{ + auto bool_callback = [callback]() mutable -> bool + { + if constexpr (std::is_same_v>) + { + callback(); + return false; + } + else + { + return callback(); + } + }; + +#if GTKMM_CHECK_VERSION(4, 0, 0) + widget.signal_close_request().connect(bool_callback, false); +#else + widget.signal_delete_event().connect(sigc::hide<0>(bool_callback), false); +#endif } namespace Glib diff --git a/gtk/main.cc b/gtk/main.cc index 3e3566ca9..0ed502b45 100644 --- a/gtk/main.cc +++ b/gtk/main.cc @@ -45,9 +45,17 @@ int main(int argc, char** argv) textdomain(AppTranslationDomainName); /* init glib/gtk */ + Gio::init(); Glib::init(); Glib::set_application_name(_("Transmission")); + /* Workaround "..." */ + Gio::File::create_for_path("."); + Glib::wrap_register( + g_type_from_name("GLocalFile"), + [](GObject* object) -> Glib::ObjectBase* { return new Gio::File((GFile*)object); }); + g_type_ensure(Gio::File::get_type()); + /* default settings */ std::string config_dir; bool show_version = false; @@ -68,7 +76,9 @@ int main(int argc, char** argv) Glib::OptionContext option_context(_("[torrent files or urls]")); option_context.set_main_group(main_group); +#if !GTKMM_CHECK_VERSION(4, 0, 0) Gtk::Main::add_gtk_option_group(option_context); +#endif option_context.set_translation_domain(GETTEXT_PACKAGE); try @@ -77,7 +87,10 @@ int main(int argc, char** argv) } catch (Glib::OptionError const& e) { - g_print(_("%s\nRun '%s --help' to see a full list of available command line options.\n"), e.what().c_str(), argv[0]); + g_print( + _("%s\nRun '%s --help' to see a full list of available command line options.\n"), + TR_GLIB_EXCEPTION_WHAT(e), + argv[0]); return 1; } diff --git a/gtk/transmission-ui.css b/gtk/transmission-ui.css index beeb923ba..b9afa38a1 100644 --- a/gtk/transmission-ui.css +++ b/gtk/transmission-ui.css @@ -4,6 +4,30 @@ border-radius: 0; } +.tr-message-log.frame { + border-left-width: 0; + border-right-width: 0; + border-bottom-width: 0; + border-radius: 0; +} + +.tr-pad-small { + padding: 3px; +} + +.tr-pad-normal { + padding: 6px; +} + +.tr-pad-large { + padding: 12px; +} + +.tr-button-box, +.tr-dialog-content { + margin: 6px; +} + .tr-small { font-size: small; } diff --git a/gtk/transmission.gresource.xml b/gtk/transmission.gresource.xml index 5598b6265..2711f9d1c 100644 --- a/gtk/transmission.gresource.xml +++ b/gtk/transmission.gresource.xml @@ -8,18 +8,5 @@ icons/hicolor_apps_scalable_transmission.svg transmission-ui.css transmission-ui.xml - AddTrackerDialog.ui - DetailsDialog.ui - EditTrackersDialog.ui - FilterBar.ui - MainWindow.ui - MakeDialog.ui - MakeProgressDialog.ui - MessageLogWindow.ui - OptionsDialog.ui - PrefsDialog.ui - RelocateDialog.ui - StatsDialog.ui - TorrentUrlChooserDialog.ui diff --git a/gtk/AddTrackerDialog.ui b/gtk/ui/gtk3/AddTrackerDialog.ui similarity index 100% rename from gtk/AddTrackerDialog.ui rename to gtk/ui/gtk3/AddTrackerDialog.ui diff --git a/gtk/DetailsDialog.ui b/gtk/ui/gtk3/DetailsDialog.ui similarity index 100% rename from gtk/DetailsDialog.ui rename to gtk/ui/gtk3/DetailsDialog.ui diff --git a/gtk/EditTrackersDialog.ui b/gtk/ui/gtk3/EditTrackersDialog.ui similarity index 100% rename from gtk/EditTrackersDialog.ui rename to gtk/ui/gtk3/EditTrackersDialog.ui diff --git a/gtk/FilterBar.ui b/gtk/ui/gtk3/FilterBar.ui similarity index 100% rename from gtk/FilterBar.ui rename to gtk/ui/gtk3/FilterBar.ui diff --git a/gtk/MainWindow.ui b/gtk/ui/gtk3/MainWindow.ui similarity index 100% rename from gtk/MainWindow.ui rename to gtk/ui/gtk3/MainWindow.ui diff --git a/gtk/MakeDialog.ui b/gtk/ui/gtk3/MakeDialog.ui similarity index 100% rename from gtk/MakeDialog.ui rename to gtk/ui/gtk3/MakeDialog.ui diff --git a/gtk/MakeProgressDialog.ui b/gtk/ui/gtk3/MakeProgressDialog.ui similarity index 100% rename from gtk/MakeProgressDialog.ui rename to gtk/ui/gtk3/MakeProgressDialog.ui diff --git a/gtk/MessageLogWindow.ui b/gtk/ui/gtk3/MessageLogWindow.ui similarity index 100% rename from gtk/MessageLogWindow.ui rename to gtk/ui/gtk3/MessageLogWindow.ui diff --git a/gtk/OptionsDialog.ui b/gtk/ui/gtk3/OptionsDialog.ui similarity index 100% rename from gtk/OptionsDialog.ui rename to gtk/ui/gtk3/OptionsDialog.ui diff --git a/gtk/PrefsDialog.ui b/gtk/ui/gtk3/PrefsDialog.ui similarity index 100% rename from gtk/PrefsDialog.ui rename to gtk/ui/gtk3/PrefsDialog.ui diff --git a/gtk/RelocateDialog.ui b/gtk/ui/gtk3/RelocateDialog.ui similarity index 100% rename from gtk/RelocateDialog.ui rename to gtk/ui/gtk3/RelocateDialog.ui diff --git a/gtk/StatsDialog.ui b/gtk/ui/gtk3/StatsDialog.ui similarity index 100% rename from gtk/StatsDialog.ui rename to gtk/ui/gtk3/StatsDialog.ui diff --git a/gtk/TorrentUrlChooserDialog.ui b/gtk/ui/gtk3/TorrentUrlChooserDialog.ui similarity index 100% rename from gtk/TorrentUrlChooserDialog.ui rename to gtk/ui/gtk3/TorrentUrlChooserDialog.ui diff --git a/gtk/ui/gtk3/transmission-ui.gresource.xml b/gtk/ui/gtk3/transmission-ui.gresource.xml new file mode 100644 index 000000000..f757a1b15 --- /dev/null +++ b/gtk/ui/gtk3/transmission-ui.gresource.xml @@ -0,0 +1,18 @@ + + + + AddTrackerDialog.ui + DetailsDialog.ui + EditTrackersDialog.ui + FilterBar.ui + MainWindow.ui + MakeDialog.ui + MakeProgressDialog.ui + MessageLogWindow.ui + OptionsDialog.ui + PrefsDialog.ui + RelocateDialog.ui + StatsDialog.ui + TorrentUrlChooserDialog.ui + + diff --git a/gtk/ui/gtk4/AddTrackerDialog.ui b/gtk/ui/gtk4/AddTrackerDialog.ui new file mode 100644 index 000000000..024e8e23f --- /dev/null +++ b/gtk/ui/gtk4/AddTrackerDialog.ui @@ -0,0 +1,85 @@ + + + + + 1 + + + vertical + 6 + tr-dialog-content + 1 + + + vertical + 6 + + + Tracker + 0 + + + + + + + + 18 + 6 + 12 + + + _Announce URL: + 1 + url_entry + + 0 + 0 + + + + + + 400 + 1 + 1 + + 1 + 0 + + + + + + + + + + + + tr-button-box + 6 + + + _Cancel + 1 + 1 + 1 + + + + + _Add + 1 + 1 + 1 + + + + + + cancel_button + open_button + + + diff --git a/gtk/ui/gtk4/DetailsDialog.ui b/gtk/ui/gtk4/DetailsDialog.ui new file mode 100644 index 000000000..bb9f21b41 --- /dev/null +++ b/gtk/ui/gtk4/DetailsDialog.ui @@ -0,0 +1,880 @@ + + + + + + + vertical + 6 + tr-dialog-content + 1 + + + 1 + 1 + + + + + tr-pad-large + vertical + 6 + + + 1 + Activity + 0 + + + + + + + + 18 + 6 + 12 + + + Torrent size: + 0 + + 0 + 0 + + + + + + Have: + 0 + + 0 + 1 + + + + + + Uploaded: + 0 + + 0 + 2 + + + + + + Downloaded: + 0 + + 0 + 3 + + + + + + State: + 0 + + 0 + 4 + + + + + + Running time: + 0 + + 0 + 5 + + + + + + Remaining time: + 0 + + 0 + 6 + + + + + + Last activity: + 0 + + 0 + 7 + + + + + + Error: + 0 + 0 + + 0 + 8 + + + + + + 1 + ... + 0 + + 1 + 0 + + + + + + 1 + ... + 0 + + 1 + 1 + + + + + + 1 + ... + 0 + + 1 + 2 + + + + + + 1 + ... + 0 + + 1 + 3 + + + + + + 1 + ... + 0 + + 1 + 4 + + + + + + 1 + ... + 0 + + 1 + 5 + + + + + + 1 + ... + 0 + + 1 + 6 + + + + + + 1 + ... + 0 + + 1 + 7 + + + + + + 1 + ... + 1 + 1 + 1 + end + 10 + 0 + + 1 + 8 + + + + + + + + 6 + + + + + 1 + Details + 0 + + + + + + + + 18 + 6 + 12 + + + Location: + 0 + + 0 + 0 + + + + + + Hash: + 0 + + 0 + 1 + + + + + + Privacy: + 0 + + 0 + 2 + + + + + + Origin: + 0 + + 0 + 3 + + + + + + Added: + 0 + + 0 + 4 + + + + + + 6 + 6 + Comment: + 0 + 0 + + 0 + 5 + + + + + + ... + 1 + end + 0 + + 1 + 0 + + + + + + ... + 1 + end + 0 + + 1 + 1 + + + + + + ... + 0 + + 1 + 2 + + + + + + ... + 1 + end + 0 + + 1 + 3 + + + + + + ... + 0 + + 1 + 4 + + + + + + 350 + 36 + 1 + 1 + 1 + 1 + + + 1 + 0 + word + + + + 1 + 5 + + + + + + + + + + Information + + + + + + + 1 + + + tr-pad-large + vertical + 6 + + + 1 + 0 + 1 + vertical + + + 1 + 1 + + + 1 + + + + + + + + + + 1 + 1 + + + 1 + + + + + + + + + + + + Show _more details + 1 + center + 1 + + + + + + + Peers + + + + + + + 2 + + + tr-pad-large + vertical + 6 + + + 1 + 12 + + + 1 + 1 + 1 + + + 1 + 0 + + + + + + + + + + vertical + 6 + + + _Add + 1 + 1 + 1 + + + + + _Edit + 1 + 1 + 1 + + + + + _Remove + 1 + 1 + 1 + + + + + + + + + Show _more details + 1 + center + 1 + + + + + Show _backup trackers + 1 + center + 1 + + + + + + + Trackers + + + + + + + 3 + + + tr-pad-large + vertical + + + 1 + 1 + 1 + + + 1 + + + + + + + + + + 1 + File listing not available for combined torrent properties + + + + + + + Files + + + + + + + 4 + + + tr-pad-large + vertical + 6 + + + 1 + Speed + 0 + + + + + + + + 18 + 6 + 12 + + + Honor global _limits + 1 + center + 1 + + 0 + 0 + 2 + + + + + + Limit _download speed ({speed_units}): + 1 + center + 1 + + 0 + 1 + + + + + + Limit _upload speed ({speed_units}): + 1 + center + 1 + + 0 + 2 + + + + + + Torrent _priority: + 1 + priority_combo + 0 + + 0 + 3 + + + + + + 1 + 1 + + 1 + 1 + + + + + + 1 + 1 + + 1 + 2 + + + + + + 1 + + 1 + 3 + + + + + + + + 6 + + + + + 1 + Seeding Limits + 0 + + + + + + + + 18 + 6 + 12 + + + _Ratio: + 1 + ratio_limit_combo + 0 + + 0 + 0 + + + + + + _Idle: + 1 + idle_limit_combo + 0 + + 0 + 1 + + + + + + 1 + 6 + + + 1 + + + + + 1 + + + + 1 + 0 + + + + + + 1 + 6 + + + 1 + + + + + 1 + + + + 1 + 1 + + + + + + + + 6 + + + + + 1 + Peer Connections + 0 + + + + + + + + 18 + 6 + 12 + + + _Maximum peers: + 1 + max_peers_spin + 0 + + 0 + 0 + + + + + + 1 + 1 + + 1 + 0 + + + + + + + + + + Options + + + + + + + + + + + tr-button-box + 6 + + + _Close + 1 + 1 + 1 + + + + + + close_button + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gtk/ui/gtk4/EditTrackersDialog.ui b/gtk/ui/gtk4/EditTrackersDialog.ui new file mode 100644 index 000000000..d17449b8d --- /dev/null +++ b/gtk/ui/gtk4/EditTrackersDialog.ui @@ -0,0 +1,105 @@ + + + + + 1 + + + vertical + 6 + tr-dialog-content + 1 + + + vertical + 6 + + + Tracker Announce URLs + 0 + + + + + + + + 18 + 6 + 12 + + + To add a backup URL, add it on the next line after a primary URL. +To add a new primary URL, add it after a blank line. + 0 + + 0 + 0 + 2 + + + + + + 500 + 166 + 1 + 1 + + + 1 + 0 + + + + 0 + 1 + 2 + + + + + + Also see Default Public Trackers in Edit > Preferences > Network + 0 + + 0 + 2 + 2 + + + + + + + + + + + + tr-button-box + 6 + + + _Cancel + 1 + 1 + 1 + + + + + _Save + 1 + 1 + 1 + + + + + + cancel_button + save_button + + + diff --git a/gtk/ui/gtk4/FilterBar.ui b/gtk/ui/gtk4/FilterBar.ui new file mode 100644 index 000000000..9af6f9499 --- /dev/null +++ b/gtk/ui/gtk4/FilterBar.ui @@ -0,0 +1,34 @@ + + + + + diff --git a/gtk/ui/gtk4/MainWindow.ui b/gtk/ui/gtk4/MainWindow.ui new file mode 100644 index 000000000..16ff9736b --- /dev/null +++ b/gtk/ui/gtk4/MainWindow.ui @@ -0,0 +1,219 @@ + + + + + + + vertical + + + toolbar +horizontal + 1 + + + 1 + Open a torrent + win.open-torrent + + + center + 5 + + + document-open + normal + + + + + _Open + 1 + 1 + + + + + + + + + 1 + Start torrent + win.torrent-start + _Start + 1 + media-playback-start + + + + + 1 + Pause torrent + win.torrent-stop + _Pause + 1 + media-playback-pause + + + + + 1 + win.remove-torrent + Remove torrent + 1 + list-remove + + + + + vertical + 0 + + + + + 1 + Torrent properties + win.show-torrent-properties + + + center + 5 + + + document-properties + normal + + + + + _Properties + 1 + 1 + + + + + + + + + + + 1 + + + + + + 1 + 1 + never + 1 + + + 1 + 0 + 1 + + + multiple + + + + + 1 + fixed + Torrent + + + + + + + + + + 3 + + + 1 + 0 + 1 + Options + + + options-symbolic + normal + + + + + + + 1 + 0 + 1 + + + turtle-symbolic + normal + + + + + + + 1 + + + + + 3 + ... + 1 + + + + + 3 + ... + 1 + + + + + 9 + 3 + ... + 1 + + + + + 1 + 0 + 1 + Statistics + + + ratio-symbolic + normal + + + + + + + + + + + diff --git a/gtk/ui/gtk4/MainWindow.ui.full b/gtk/ui/gtk4/MainWindow.ui.full new file mode 100644 index 000000000..b4468707f --- /dev/null +++ b/gtk/ui/gtk4/MainWindow.ui.full @@ -0,0 +1,850 @@ + + + + + + False + tr-main + + + True + False + vertical + + + True + False + + + True + False + _File + True + + + True + False + + + True + False + open-torrent-menu + _Open + True + + + + + True + False + open-torrent-from-url + Open _URL... + True + + + + + True + False + new-torrent + _New... + True + + + + + True + False + + + + + True + False + start-all-torrents + _Start All + True + + + + + True + False + pause-all-torrents + _Pause All + True + + + + + True + False + + + + + True + False + quit + _Quit + True + + + + + + + + + True + False + _Edit + True + + + True + False + + + True + False + select-all + Select _All + True + + + + + True + False + deselect-all + Dese_lect All + True + + + + + True + False + + + + + True + False + edit-preferences + _Preferences + True + + + + + + + + + True + False + _Torrent + True + + + True + False + + + True + False + show-torrent-properties + _Properties + True + + + + + True + False + open-torrent-folder + Open Fold_er + True + + + + + True + False + + + + + True + False + torrent-start + _Start + True + + + + + True + False + torrent-start-now + Start _Now + True + + + + + True + False + torrent-reannounce + Ask Tracker for _Mode Peers + True + + + + + True + False + _Queue + True + + + True + False + + + True + False + queue-move-top + Move to _Top + True + + + + + True + False + queue-move-up + Move _Up + True + + + + + True + False + queue-move-down + Move _Down + True + + + + + True + False + queue-move-bottom + Move to _Bottom + True + + + + + + + + + True + False + torrent-stop + _Pause + True + + + + + True + False + + + + + True + False + relocate-torrent + Set _Location... + True + + + + + True + False + torrent-verify + _Verify Local Data + True + + + + + True + False + copy-magnet-link-to-clipboard + Copy _Magnet Link to Clipboard + True + + + + + True + False + + + + + True + False + remove-torrent + Remove torrent + True + + + + + True + False + delete-torrent + _Delete Files and Remove + True + + + + + + + + + True + False + _View + True + + + True + False + + + True + False + compact-view + _Compact View + True + + + + + True + False + + + + + True + False + show-toolbar + _Toolbar + True + + + + + True + False + show-filterbar + _Filterbar + True + + + + + True + False + show-statusbar + _Statusbar + True + + + + + True + False + + + + + True + False + sort-by-activity + Sort by _Activity + True + True + + + + + True + False + sort-by-age + Sort by A_ge + True + True + + + + + True + False + sort-by-name + Sort by _Name + True + True + + + + + True + False + sort-by-progress + Sort by _Progress + True + True + + + + + True + False + sort-by-queue + Sort by _Queue + True + True + + + + + True + False + sort-by-ratio + Sort by Rati_o + True + True + + + + + True + False + sort-by-size + Sort by Si_ze + True + True + + + + + True + False + sort-by-state + Sort by Stat_e + True + True + + + + + True + False + sort-by-time-left + Sort by Time _Left + True + True + + + + + True + False + + + + + True + False + sort-reversed + Re_verse Sort Order + True + + + + + + + + + True + False + _Help + True + + + True + False + + + True + False + toggle-message-log + Message _Log + True + + + + + True + False + show-stats + _Statistics + True + + + + + True + False + + + + + + + + True + False + + + + + True + False + help + _Contents + True + + + + + True + False + show-about-dialog + _About + True + + + + + + + + + False + True + 0 + + + + + True + False + + + True + False + Open a torrent + True + open-torrent-toolbar + _Open + True + document-open + + + False + True + + + + + True + False + Start torrent + torrent-start + _Start + True + media-playback-start + + + False + True + + + + + True + False + Pause torrent + torrent-stop + _Pause + True + media-playback-pause + + + False + True + + + + + True + False + remove-torrent + Remove torrent + True + list-remove + + + False + True + + + + + True + False + + + False + True + + + + + True + False + Torrent properties + True + show-torrent-properties + _Properties + True + document-properties + + + False + True + + + + + False + True + 1 + + + + + True + False + + + False + True + 2 + + + + + True + True + never + out + + + True + True + False + True + + + multiple + + + + + True + fixed + Torrent + + + + + + + + True + True + 3 + + + + + True + False + 3 + 3 + + + True + True + True + Options + none + + + True + False + options-symbolic + 1 + + + + + False + True + 0 + + + + + True + True + True + none + + + True + False + turtle-symbolic + 1 + + + + + False + True + 1 + + + + + True + False + + + True + True + 2 + + + + + True + False + 3 + ... + True + + + False + True + 3 + + + + + True + False + 3 + ... + True + + + False + True + 4 + + + + + True + True + True + Statistics + none + + + True + False + ratio-symbolic + 1 + + + + + False + True + end + 5 + + + + + True + False + 9 + 3 + ... + True + + + False + True + 6 + + + + + False + True + 4 + + + + + + diff --git a/gtk/ui/gtk4/MakeDialog.ui b/gtk/ui/gtk4/MakeDialog.ui new file mode 100644 index 000000000..216ce84f0 --- /dev/null +++ b/gtk/ui/gtk4/MakeDialog.ui @@ -0,0 +1,309 @@ + + + + + New Torrent + + + vertical + 6 + tr-dialog-content + 1 + + + vertical + 6 + + + 1 + Files + 0 + + + + + + + + 18 + 6 + 12 + + + Sa_ve to: + 1 + destination_button + 0 + + 0 + 0 + + + + + + 1 + select-folder + + 1 + 0 + + + + + + Source F_older: + center + 1 + + 0 + 1 + + + + + + 1 + select-folder + + 1 + 1 + + + + + + Source _File: + center + 1 + 1 + source_folder_radio + + 0 + 2 + + + + + + 1 + + + 1 + 2 + + + + + + 1 + No source selected + 0 + + + + + 1 + 3 + + + + + + Piece size: + 1 + piece_size_scale + 0 + + 0 + 4 + + + + + + 1 + 1 + 0 + left + + 1 + 4 + + + + + + + + + + + 6 + + + + + 1 + Properties + 0 + + + + + + + + 18 + 6 + 12 + + + start + _Trackers: + 1 + trackers_view + 0 + + 0 + 0 + + + + + + 1 + 1 + vertical + 3 + + + 80 + 1 + 1 + 1 + 1 + + + 1 + 0 + + + + + + + 1 + To add a backup URL, add it on the next line after a primary URL. +To add a new primary URL, add it after a blank line. + 0 + + + + 1 + 0 + + + + + + Co_mment: + 1 + center + 1 + + 0 + 1 + + + + + + 1 + 1 + + 1 + 1 + + + + + + _Source: + 1 + center + 1 + + 0 + 2 + + + + + + 1 + 1 + + 1 + 2 + + + + + + _Private torrent + 1 + center + 1 + + 0 + 3 + 2 + + + + + + + + + + + + tr-button-box + 6 + + + _Close + 1 + 1 + 1 + + + + + _New + 1 + 1 + 1 + + + + + + close_button + new_button + + + + + + + + + + + + + + diff --git a/gtk/ui/gtk4/MakeProgressDialog.ui b/gtk/ui/gtk4/MakeProgressDialog.ui new file mode 100644 index 000000000..1e53eeb86 --- /dev/null +++ b/gtk/ui/gtk4/MakeProgressDialog.ui @@ -0,0 +1,66 @@ + + + + + New Torrent + 1 + + + vertical + 6 + tr-dialog-content + 1 + + + vertical + 6 + + + Creating torrent… + 0 + + + + + + + + + + + + tr-button-box + 6 + + + _Cancel + 1 + 1 + 1 + + + + + _Close + 1 + 1 + 1 + + + + + _Add + 1 + 1 + 1 + + + + + + cancel_button + close_button + add_button + + + diff --git a/gtk/ui/gtk4/MessageLogWindow.ui b/gtk/ui/gtk4/MessageLogWindow.ui new file mode 100644 index 000000000..daf4662bb --- /dev/null +++ b/gtk/ui/gtk4/MessageLogWindow.ui @@ -0,0 +1,139 @@ + + + + + Message Log + 560 + 350 + + + vertical + + + center + toolbar +horizontal + + + win.save-message-log + + + center + 5 + + + document-save-as + normal + + + + + Save _As + 1 + 1 + + + + + + + + + win.clear-message-log + + + center + 5 + + + edit-clear + normal + + + + + Clear + 1 + 1 + + + + + + + + + vertical + + + + + win.pause-message-log + + + center + 5 + + + media-playback-pause + normal + + + + + P_ause + 1 + 1 + + + + + + + + + vertical + + + + + Level + 1 + level_combo + tr-pad-normal + + + + + + + + 1 + + + + + + + + + 1 + 1 + 1 + + + 1 + + + + + + + + + + + + diff --git a/gtk/ui/gtk4/OptionsDialog.ui b/gtk/ui/gtk4/OptionsDialog.ui new file mode 100644 index 000000000..7342fd45a --- /dev/null +++ b/gtk/ui/gtk4/OptionsDialog.ui @@ -0,0 +1,197 @@ + + + + + Torrent Options + + + vertical + 6 + tr-dialog-content + 1 + + + 1 + 6 + 12 + + + _Torrent file: + 1 + source_button + 0 + + 0 + 0 + + + + + + 1 + Select Source File + + 1 + 0 + + + + + + _Destination folder: + 1 + destination_button + 0 + + 0 + 1 + + + + + + 1 + select-folder + Select Destination Folder + + 1 + 1 + + + + + + 466 + 300 + 1 + 1 + + + 1 + 1 + 1 + + + + + + + 0 + 4 + 2 + + + + + + Torrent _priority: + 1 + priority_combo + 0 + + 0 + 5 + + + + + + 1 + + 1 + 5 + + + + + + _Start when added + 1 + center + 1 + + 0 + 6 + 2 + + + + + + Mo_ve torrent file to the trash + 1 + center + 1 + + 0 + 7 + 2 + + + + + + 1 + ... + 0 + + + + + 1 + 2 + + + + + + 0 + 0 + + 0 + 2 + + + + + + 6 + + 0 + 3 + 2 + + + + + + + + + + tr-button-box + 6 + + + _Cancel + 1 + 1 + 1 + + + + + _Open + 1 + 1 + 1 + + + + + + cancel_button + open_button + + + diff --git a/gtk/ui/gtk4/PrefsDialog.ui b/gtk/ui/gtk4/PrefsDialog.ui new file mode 100644 index 000000000..acb67345d --- /dev/null +++ b/gtk/ui/gtk4/PrefsDialog.ui @@ -0,0 +1,1516 @@ + + + + + Transmission Preferences + 1 + + + vertical + 6 + tr-dialog-content + 1 + + + 1 + 1 + + + + + tr-pad-large + vertical + 6 + + + Speed Limits + 0 + + + + + + + + 18 + 6 + 12 + + + _Upload ({speed_units}): + 1 + center + 1 + + 0 + 0 + + + + + + _Download ({speed_units}): + 1 + center + 1 + + 0 + 1 + + + + + + 1 + 1 + + 1 + 0 + + + + + + 1 + 1 + + 1 + 1 + + + + + + + + 6 + + + + + 6 + + + Alternative Speed Limits + 0 + + + + + + + + center + turtle + + + + + + + 18 + 6 + 12 + + + Override normal speed limits manually or at scheduled times + 0 + + + 0 + 0 + 2 + + + + + + U_pload ({speed_units}): + 1 + alt_upload_limit_spin + 0 + + 0 + 1 + + + + + + Do_wnload ({speed_units}): + 1 + alt_download_limit_spin + 0 + + 0 + 2 + + + + + + _Scheduled times: + 1 + center + 1 + + 0 + 3 + + + + + + _On days: + 1 + alt_speed_days_combo + 0 + + 0 + 4 + + + + + + 1 + 1 + + 1 + 1 + + + + + + 1 + 1 + + 1 + 2 + + + + + + 1 + 12 + + + 1 + + + + + _to + 1 + alt_speed_end_time_combo + + + + + 1 + + + + 1 + 3 + + + + + + 1 + + 1 + 4 + + + + + + + + + + Speed + + + + + + + 1 + + + tr-pad-large + vertical + 6 + + + Adding + 0 + + + + + + + + 18 + 6 + 12 + + + Automatically add torrent files _from: + 1 + center + 1 + + 0 + 0 + + + + + + Show the Torrent Options _dialog + 1 + center + 1 + 1 + + 0 + 1 + 2 + + + + + + _Start added torrents + 1 + center + 1 + 1 + + 0 + 2 + 2 + + + + + + Mo_ve torrent file to the trash + 1 + center + 1 + 1 + + 0 + 3 + 2 + + + + + + Save to _Location: + 1 + download_dir_chooser + 0 + + 0 + 4 + + + + + + 1 + select-folder + + + 1 + 0 + + + + + + select-folder + + + 1 + 4 + + + + + + ... + 0 + + + + + 1 + 5 + + + + + + 0 + 0 + + 0 + 5 + + + + + + + + 6 + + + + + Download Queue + 0 + + + + + + + + 18 + 6 + 12 + + + Ma_ximum active downloads: + 1 + max_active_downloads_spin + 0 + + 0 + 0 + + + + + + Downloads sharing data in the last _N minutes are active: + 1 + max_inactive_time_spin + 0 + + 0 + 1 + + + + + + 1 + 1 + + 1 + 0 + + + + + + 1 + 1 + + 1 + 1 + + + + + + + + 6 + + + + + Incomplete + 0 + + + + + + + + 18 + 6 + 12 + + + Append "._part" to incomplete files' names + 1 + center + 1 + 1 + + 0 + 0 + 2 + + + + + + Keep _incomplete torrents in: + 1 + center + 1 + + 0 + 1 + + + + + + Call scrip_t when done downloading: + 1 + center + 1 + + 0 + 2 + + + + + + 1 + select-folder + + + 1 + 1 + + + + + + 1 + + + 1 + 2 + + + + + + + + + + Downloading + + + + + + + 2 + + + tr-pad-large + vertical + 6 + + + Limits + 0 + + + + + + + + 18 + 6 + 12 + + + Stop seeding at _ratio: + 1 + center + 1 + + 0 + 0 + + + + + + Stop seeding if idle for _N minutes: + 1 + center + 1 + + 0 + 1 + + + + + + Call scrip_t when done seeding: + 1 + center + 1 + + 0 + 2 + + + + + + 1 + 1 + + 1 + 0 + + + + + + 1 + 1 + + 1 + 1 + + + + + + 1 + + + 1 + 2 + + + + + + + + + + Seeding + + + + + + + 3 + + + tr-pad-large + vertical + 6 + + + Privacy + 0 + + + + + + + + 18 + 6 + 12 + + + _Encryption mode: + 1 + encryption_mode_combo + 0 + + 0 + 0 + + + + + + 1 + + 1 + 0 + + + + + + + + 6 + + + + + Blocklist + 0 + + + + + + + + 18 + 6 + 12 + + + Enable _blocklist: + 1 + center + 1 + + 0 + 0 + + + + + + Enable _automatic updates + 1 + center + 1 + 1 + + 0 + 2 + 2 + + + + + + 300 + 1 + 1 + + 1 + 0 + + + + + + 1 + 12 + + + 1 + ... + 0 + + + + + + + + _Update + 1 + 1 + 1 + + + + 1 + 1 + + + + + + 0 + 0 + + 0 + 1 + + + + + + + + + + Privacy + + + + + + + 4 + + + tr-pad-large + vertical + 6 + + + Listening Port + 0 + + + + + + + + 18 + 6 + 12 + + + _Port used for incoming connections: + 1 + listening_port_spin + 0 + + 0 + 0 + + + + + + Pick a _random port every time Transmission is started + 1 + center + 1 + 1 + + 0 + 2 + 2 + + + + + + Use UPnP or NAT-PMP port _forwarding from my router + 1 + center + 1 + 1 + + 0 + 3 + 2 + + + + + + 0 + 0 + + 0 + 1 + + + + + + 1 + 1 + + 1 + 0 + + + + + + 1 + 12 + + + 1 + Status unknown + 0 + + + + + + + + Te_st Port + 1 + 1 + 1 + + + + 1 + 1 + + + + + + + + 6 + + + + + Peer Limits + 0 + + + + + + + + 18 + 6 + 12 + + + Maximum peers per _torrent: + 1 + max_torrent_peers_spin + 0 + + 0 + 0 + + + + + + Maximum peers _overall: + 1 + max_total_peers_spin + 0 + + 0 + 1 + + + + + + 1 + 1 + + 1 + 0 + + + + + + 1 + 1 + + 1 + 1 + + + + + + + + 6 + + + + + Options + 0 + + + + + + + + 18 + 6 + 12 + + + Enable _uTP for peer communication + 1 + uTP is a tool for reducing network congestion. + center + 1 + 1 + + 0 + 0 + 2 + + + + + + Use PE_X to find more peers + 1 + PEX is a tool for exchanging peer lists with the peers you're connected to. + center + 1 + 1 + + 0 + 1 + 2 + + + + + + Use _DHT to find more peers + 1 + DHT is a tool for finding peers without a tracker. + center + 1 + 1 + + 0 + 2 + 2 + + + + + + Use _Local Peer Discovery to find more peers + 1 + LPD is a tool for finding peers on your local network. + center + 1 + 1 + + 0 + 3 + 2 + + + + + + + + 6 + + + + + Default Public Trackers + 0 + + + + + + + + 18 + 6 + 12 + + + 166 + 1 + 1 + 1 + 1 + + + 1 + 0 + Trackers to use on all public torrents. + +To add a backup URL, add it on the next line after a primary URL. +To add a new primary URL, add it after a blank line. + + + + 0 + 0 + 2 + + + + + + + + + + Network + + + + + + + 5 + + + tr-pad-large + vertical + 6 + + + Desktop + 0 + + + + + + + + 18 + 6 + 12 + + + _Inhibit hibernation when torrents are active + 1 + center + 1 + 1 + + 0 + 0 + 2 + + + + + + Show Transmission icon in the _notification area + 1 + center + 1 + 1 + + 0 + 1 + 2 + + + + + + + + 6 + + + + + Notification + 0 + + + + + + + + 18 + 6 + 12 + + + Show a notification when torrents are a_dded + 1 + center + 1 + 1 + + 0 + 0 + 2 + + + + + + Show a notification when torrents _finish + 1 + center + 1 + 1 + + 0 + 1 + 2 + + + + + + Play a _sound when torrents finish + 1 + center + 1 + 1 + + 0 + 2 + 2 + + + + + + + + + + Desktop + + + + + + + 6 + + + tr-pad-large + vertical + 6 + + + Remote Control + 0 + + + + + + + + 18 + 6 + 12 + + + 1 + 12 + + + 1 + Allow _remote access + 1 + center + 1 + + + + + _Open web client + 1 + 1 + 1 + + + + 0 + 0 + 2 + + + + + + HTTP _port: + 1 + rpc_port_spin + 0 + + 0 + 1 + + + + + + Use _authentication + 1 + center + 1 + 1 + + 0 + 2 + 2 + + + + + + _Username: + 1 + rpc_username_entry + 0 + + 0 + 3 + + + + + + Pass_word: + 1 + rpc_password_entry + 0 + + 0 + 4 + + + + + + Only allow these IP a_ddresses: + 1 + center + 1 + 1 + + 0 + 5 + 2 + + + + + + 6 + 6 + Addresses: + 1 + rpc_whitelist_view + 0 + 0 + + 0 + 6 + + + + + + 0 + 0 + + 0 + 7 + + + + + + 1 + 1 + + 1 + 1 + + + + + + 1 + 1 + + 1 + 3 + + + + + + 1 + 1 + 0 + + 1 + 4 + + + + + + 6 + + + _Add + 1 + 1 + 1 + + + + + _Remove + 1 + 1 + 1 + + + + 1 + 7 + + + + + + 1 + 1 + never + never + 1 + + + 1 + IP addresses may use wildcards, such as 192.168.*.* + 0 + + + + + + + 1 + 6 + + + + + + + + + + Remote + + + + + + + + + + + tr-button-box + 6 + 1 + 0 + + + _Help + 1 + 1 + 1 + + + + + 1 + + + + + _Close + 1 + 1 + 1 + + + + + + help_button + close_button + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gtk/ui/gtk4/RelocateDialog.ui b/gtk/ui/gtk4/RelocateDialog.ui new file mode 100644 index 000000000..4dd965a6e --- /dev/null +++ b/gtk/ui/gtk4/RelocateDialog.ui @@ -0,0 +1,114 @@ + + + + + Set Torrent Location + + + vertical + 6 + tr-dialog-content + 1 + + + vertical + 6 + + + 1 + Location + 0 + + + + + + + + 18 + 6 + 12 + + + start + Torrent _location: + 1 + new_location_button + 0 + + 0 + 0 + + + + + + 1 + select-folder + + + 1 + 0 + + + + + + _Move from the current folder + center + 1 + 1 + + 0 + 1 + 2 + + + + + + Local data is _already there + center + 1 + move_data_radio + + 0 + 2 + 2 + + + + + + + + + + + + tr-button-box + 6 + + + _Cancel + 1 + 1 + 1 + + + + + _Apply + 1 + 1 + 1 + + + + + + cancel_button + apply_button + + + diff --git a/gtk/ui/gtk4/StatsDialog.ui b/gtk/ui/gtk4/StatsDialog.ui new file mode 100644 index 000000000..27dbc2a1a --- /dev/null +++ b/gtk/ui/gtk4/StatsDialog.ui @@ -0,0 +1,278 @@ + + + + + Statistics + + + vertical + 6 + tr-dialog-content + 1 + + + vertical + 6 + + + 1 + Current Session + 0 + + + + + + + + 18 + 6 + 12 + + + Uploaded: + 0 + + 0 + 0 + + + + + + 1 + ... + 0 + + 1 + 0 + + + + + + Downloaded: + 0 + + 0 + 1 + + + + + + 1 + ... + 0 + + 1 + 1 + + + + + + Ratio: + 0 + + 0 + 2 + + + + + + 1 + ... + 0 + + 1 + 2 + + + + + + Duration: + 0 + + 0 + 3 + + + + + + 1 + ... + 0 + + 1 + 3 + + + + + + + + 6 + + + + + 1 + Total + 0 + + + + + + + + 18 + 6 + 12 + + + 1 + ... + 0 + + 0 + 0 + 2 + + + + + + Uploaded: + 0 + + 0 + 1 + + + + + + 1 + ... + 0 + + 1 + 1 + + + + + + Downloaded: + 0 + + 0 + 2 + + + + + + 1 + ... + 0 + + 1 + 2 + + + + + + Ratio: + 0 + + 0 + 3 + + + + + + 1 + ... + 0 + + 1 + 3 + + + + + + Duration: + 0 + + 0 + 4 + + + + + + 1 + ... + 0 + + 1 + 4 + + + + + + + + + + + + tr-button-box + 6 + + + _Reset + 1 + 1 + 1 + + + + + _Close + 1 + 1 + 1 + + + + + + reset_button + close_button + + + + + + + + + + + + + + + diff --git a/gtk/ui/gtk4/TorrentUrlChooserDialog.ui b/gtk/ui/gtk4/TorrentUrlChooserDialog.ui new file mode 100644 index 000000000..66e80828f --- /dev/null +++ b/gtk/ui/gtk4/TorrentUrlChooserDialog.ui @@ -0,0 +1,85 @@ + + + + + Open URL + + + vertical + 6 + tr-dialog-content + 1 + + + vertical + 6 + + + Open torrent from URL + 0 + + + + + + + + 18 + 6 + 12 + + + _URL + 1 + url_entry + + 0 + 0 + + + + + + 400 + 1 + 1 + + 1 + 0 + + + + + + + + + + + + tr-button-box + 6 + + + _Cancel + 1 + 1 + 1 + + + + + _Open + 1 + 1 + 1 + + + + + + cancel_button + open_button + + + diff --git a/gtk/ui/gtk4/transmission-ui.gresource.xml b/gtk/ui/gtk4/transmission-ui.gresource.xml new file mode 100644 index 000000000..f757a1b15 --- /dev/null +++ b/gtk/ui/gtk4/transmission-ui.gresource.xml @@ -0,0 +1,18 @@ + + + + AddTrackerDialog.ui + DetailsDialog.ui + EditTrackersDialog.ui + FilterBar.ui + MainWindow.ui + MakeDialog.ui + MakeProgressDialog.ui + MessageLogWindow.ui + OptionsDialog.ui + PrefsDialog.ui + RelocateDialog.ui + StatsDialog.ui + TorrentUrlChooserDialog.ui + +