From 586cff9506487eabf72baa3c1588e623a8b1ac63 Mon Sep 17 00:00:00 2001 From: Mike Gelfand Date: Sun, 6 Aug 2023 04:26:29 +0100 Subject: [PATCH] Switch to list view for torrents list (GTK 4) (#5858) * Add compat operator* for RefPtr * Rename `*_tree_view_*` button handling helpers to `*_item_view_*` * Move torrent item colors to CSS * Switch to list view for torrents list (GTK 4) * Bump Fedora image to 39 (current rawhide) for GTK 4.11 Enable deprecations as there're lots of them in 4.11 and I'm not keen on fixing them all right now. Disable warnings as errors due to -Warray-bounds issue somewhere in libfmt. --- .github/workflows/actions.yml | 7 +- CMakeLists.txt | 7 +- gtk/CMakeLists.txt | 7 + gtk/DetailsDialog.cc | 14 +- gtk/DynamicPropertyStore.h | 107 +++++++++++ gtk/FileList.cc | 4 +- gtk/FilterBar.cc | 2 +- gtk/GtkCompat.h | 6 + gtk/MainWindow.cc | 152 +++++++++++---- gtk/MessageLogWindow.cc | 7 +- gtk/Percents.h | 5 + gtk/PrefsDialog.cc | 4 +- gtk/Session.cc | 2 +- gtk/Torrent.cc | 135 ++++++++++++++ gtk/Torrent.h | 7 +- gtk/TorrentCellRenderer.cc | 215 ++++++++++------------ gtk/TorrentCellRenderer.h | 6 +- gtk/Utils.cc | 137 +++++++++++++- gtk/Utils.h | 36 +++- gtk/transmission-ui.css | 51 +++++ gtk/ui/gtk4/MainWindow.ui | 17 +- gtk/ui/gtk4/TorrentListItemCompact.ui | 79 ++++++++ gtk/ui/gtk4/TorrentListItemFull.ui | 102 ++++++++++ gtk/ui/gtk4/transmission-ui.gresource.xml | 2 + 24 files changed, 904 insertions(+), 207 deletions(-) create mode 100644 gtk/DynamicPropertyStore.h create mode 100644 gtk/ui/gtk4/TorrentListItemCompact.ui create mode 100644 gtk/ui/gtk4/TorrentListItemFull.ui diff --git a/.github/workflows/actions.yml b/.github/workflows/actions.yml index 450b6895e..075190865 100644 --- a/.github/workflows/actions.yml +++ b/.github/workflows/actions.yml @@ -644,14 +644,14 @@ jobs: name: binaries-${{ github.job }} path: pfx/**/* - fedora-36-from-tarball: + fedora-39-from-tarball: needs: [ make-source-tarball, what-to-make ] if: ${{ needs.what-to-make.outputs.make-cli == 'true' || needs.what-to-make.outputs.make-daemon == 'true' || needs.what-to-make.outputs.make-gtk == 'true' || needs.what-to-make.outputs.make-qt == 'true' || needs.what-to-make.outputs.make-tests == 'true' || needs.what-to-make.outputs.make-utils == 'true' }} runs-on: ubuntu-22.04 env: NODE_PATH: /usr/lib/nodejs:/usr/share/nodejs container: - image: fedora:36 + image: fedora:39 steps: - name: Show Configuration run: | @@ -706,7 +706,8 @@ jobs: -DENABLE_TESTS=${{ (needs.what-to-make.outputs.make-tests == 'true') && 'ON' || 'OFF' }} \ -DENABLE_UTILS=${{ (needs.what-to-make.outputs.make-utils == 'true') && 'ON' || 'OFF' }} \ -DREBUILD_WEB=${{ (needs.what-to-make.outputs.make-web == 'true') && 'ON' || 'OFF' }} \ - -DENABLE_WERROR=ON \ + -DENABLE_DEPRECATED=ON \ + -DENABLE_WERROR=OFF \ -DRUN_CLANG_TIDY=OFF - name: Build run: cmake --build obj --config RelWithDebInfo diff --git a/CMakeLists.txt b/CMakeLists.txt index 5f1e55123..615d06678 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,7 +38,8 @@ set(DEFLATE_MINIMUM 1.7) set(EVENT2_MINIMUM 2.1.0) set(GIOMM_MINIMUM 2.26.0) set(GLIBMM_MINIMUM 2.60.0) -set(GTKMM_MINIMUM 3.24.0) +set(GTKMM3_MINIMUM 3.24.0) +set(GTKMM4_MINIMUM 4.11.1) set(OPENSSL_MINIMUM 0.9.7) set(MBEDTLS_MINIMUM 1.3) set(NPM_MINIMUM 8.1.307) # Node.js 14 @@ -297,7 +298,7 @@ if(ENABLE_GTK) if(USE_GTK_VERSION STREQUAL "AUTO" OR USE_GTK_VERSION EQUAL 4) pkg_check_modules(GTK4 - gtkmm-4.0>=${GTKMM_MINIMUM} + gtkmm-4.0>=${GTKMM4_MINIMUM} glibmm-2.68>=${GLIBMM_MINIMUM} giomm-2.68>=${GIOMM_MINIMUM}) set(GTK_VERSION 4) @@ -306,7 +307,7 @@ if(ENABLE_GTK) if(NOT GTK_FOUND AND (USE_GTK_VERSION STREQUAL "AUTO" OR USE_GTK_VERSION EQUAL 3)) pkg_check_modules(GTK3 - gtkmm-3.0>=${GTKMM_MINIMUM} + gtkmm-3.0>=${GTKMM3_MINIMUM} glibmm-2.4>=${GLIBMM_MINIMUM} giomm-2.4>=${GIOMM_MINIMUM}) set(GTK_VERSION 3) diff --git a/gtk/CMakeLists.txt b/gtk/CMakeLists.txt index 7d956378d..c629b990c 100644 --- a/gtk/CMakeLists.txt +++ b/gtk/CMakeLists.txt @@ -12,6 +12,7 @@ target_sources(${TR_NAME}-gtk DetailsDialog.h Dialogs.cc Dialogs.h + DynamicPropertyStore.h FaviconCache.cc FileList.cc FileList.h @@ -72,6 +73,10 @@ target_sources(${TR_NAME}-gtk Utils.cc Utils.h) +tr_allow_compile_if( + [=[[GTK_VERSION EQUAL 3]]=] + TorrentCellRenderer.cc) + target_sources(${TR_NAME}-gtk PRIVATE ui/gtk3/AddTrackerDialog.ui @@ -105,6 +110,8 @@ target_sources(${TR_NAME}-gtk ui/gtk4/PrefsDialog.ui ui/gtk4/RelocateDialog.ui ui/gtk4/StatsDialog.ui + ui/gtk4/TorrentListItemCompact.ui + ui/gtk4/TorrentListItemFull.ui ui/gtk4/TorrentUrlChooserDialog.ui) source_group(Ui/GTK4 diff --git a/gtk/DetailsDialog.cc b/gtk/DetailsDialog.cc index 7a0286176..91c86fc5e 100644 --- a/gtk/DetailsDialog.cc +++ b/gtk/DetailsDialog.cc @@ -1743,10 +1743,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_); - setup_tree_view_button_event_handling( + setup_item_view_button_event_handling( *v, {}, - [v](double view_x, double view_y) { return on_tree_view_button_released(*v, view_x, view_y); }); + [v](double view_x, double view_y) { return on_item_view_button_released(*v, view_x, view_y); }); { auto* r = Gtk::make_managed(); @@ -1775,10 +1775,10 @@ 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), false); - setup_tree_view_button_event_handling( + setup_item_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); }); + [this](double view_x, double view_y) { return on_item_view_button_released(*peer_view_, view_x, view_y); }); setPeerViewColumns(peer_view_); @@ -2449,11 +2449,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_); - setup_tree_view_button_event_handling( + setup_item_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); }); + { return on_item_view_button_pressed(*tracker_view_, view_x, view_y, context_menu_requested); }, + [this](double view_x, double view_y) { return on_item_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)); diff --git a/gtk/DynamicPropertyStore.h b/gtk/DynamicPropertyStore.h new file mode 100644 index 000000000..bf4e299bf --- /dev/null +++ b/gtk/DynamicPropertyStore.h @@ -0,0 +1,107 @@ +// This file Copyright © 2023 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 "Utils.h" + +#include +#include +#include + +#include +#include +#include + +template +class DynamicPropertyStore +{ +public: + using ObjectType = ObjectT; + using PropertyType = PropertyT; + + using PropertyIdType = guint; + static_assert(std::is_same_v, PropertyIdType>); + + struct PropertyInfo + { + template + using ValueType = std::invoke_result_t; + + PropertyIdType id = 0; + GParamSpec* spec = nullptr; + std::function getter; + + PropertyInfo() = default; + + template + PropertyInfo(PropertyType index, char const* name, char const* nick, char const* blurb, MethodT getter_method) + : id(static_cast(index)) + , spec(gtr_get_param_spec>(name, nick, blurb)) + , getter([getter_method](ObjectType const& object, Glib::ValueBase& value) + { static_cast>&>(value).set((object.*getter_method)()); }) + { + } + }; + + static inline auto const PropertyCount = static_cast(PropertyType::N_PROPS); + +public: + static DynamicPropertyStore& get() noexcept + { + static auto instance = DynamicPropertyStore(); + return instance; + } + + void install(GObjectClass* cls, std::initializer_list properties) + { + cls->get_property = &DynamicPropertyStore::get_property_vfunc; + + g_assert(properties_.size() == properties.size() + 1); + std::move(properties.begin(), properties.end(), properties_.begin() + 1); + + for (auto id = PropertyIdType{ 1 }; id < PropertyCount; ++id) + { + g_assert(id == properties_[id].id); + g_object_class_install_property(cls, id, properties_[id].spec); + } + } + + void get_value(ObjectType const& object, PropertyType index, Glib::ValueBase& value) const + { + get_property(index).getter(object, value); + } + + void notify_changed(ObjectType& object, PropertyType index) const + { + g_object_notify_by_pspec(object.gobj(), get_property(index).spec); + } + +private: + PropertyInfo const& get_property(PropertyType index) const noexcept + { + auto const id = static_cast(index); + g_assert(id > 0); + g_assert(id < PropertyCount); + return properties_[id]; + } + + static void get_property_vfunc(GObject* object, PropertyIdType id, GValue* value, GParamSpec* /*param_spec*/) + { + if (id <= 0 || id >= PropertyCount) + { + return; + } + + if (auto const* const typed_object = dynamic_cast(Glib::wrap_auto(object)); typed_object != nullptr) + { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + get().get_value(*typed_object, PropertyType{ id }, *reinterpret_cast(value)); + } + } + +private: + std::array properties_ = {}; +}; diff --git a/gtk/FileList.cc b/gtk/FileList.cc index c403d0ac7..30dda1a0b 100644 --- a/gtk/FileList.cc +++ b/gtk/FileList.cc @@ -924,11 +924,11 @@ FileList::Impl::Impl( { /* create the view */ view_->signal_row_activated().connect(sigc::mem_fun(*this, &Impl::onRowActivated)); - setup_tree_view_button_event_handling( + setup_item_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); }); + [this](double view_x, double view_y) { return on_item_view_button_released(*view_, view_x, view_y); }); auto pango_font_description = view_->create_pango_context()->get_font_description(); if (auto const new_size = pango_font_description.get_size() * 0.8; pango_font_description.get_size_is_absolute()) diff --git a/gtk/FilterBar.cc b/gtk/FilterBar.cc index 21364aabb..6f22b09dc 100644 --- a/gtk/FilterBar.cc +++ b/gtk/FilterBar.cc @@ -444,7 +444,7 @@ bool FilterBar::Impl::activity_filter_model_update() for (auto i = 0U, count = torrents_model->get_n_items(); i < count; ++i) { auto const torrent = gtr_ptr_dynamic_cast(torrents_model->get_object(i)); - if (torrent != nullptr && TorrentFilter::match_activity(*torrent.get(), static_cast(type))) + if (torrent != nullptr && TorrentFilter::match_activity(*torrent, static_cast(type))) { ++hits; } diff --git a/gtk/GtkCompat.h b/gtk/GtkCompat.h index 34d1bbe22..c0ccab548 100644 --- a/gtk/GtkCompat.h +++ b/gtk/GtkCompat.h @@ -127,6 +127,12 @@ inline bool operator!=(RefPtr const& lhs, std::nullptr_t /*rhs*/) return !(lhs == nullptr); } +template +inline T& operator*(RefPtr const& ptr) +{ + return *ptr.get(); +} + template inline RefPtr make_refptr_for_instance(T* object) { diff --git a/gtk/MainWindow.cc b/gtk/MainWindow.cc index d05a05a4b..c172ac2b8 100644 --- a/gtk/MainWindow.cc +++ b/gtk/MainWindow.cc @@ -12,9 +12,12 @@ #include "PrefsDialog.h" #include "Session.h" #include "Torrent.h" -#include "TorrentCellRenderer.h" #include "Utils.h" +#if !GTKMM_CHECK_VERSION(4, 0, 0) +#include "TorrentCellRenderer.h" +#endif + #include #include // tr_formatter_speed_KBps() @@ -36,18 +39,20 @@ #include #include #include -#include #include -#include #include #include #if GTKMM_CHECK_VERSION(4, 0, 0) +#include +#include #include #else #include #include #include +#include +#include #endif #include @@ -78,6 +83,9 @@ class MainWindow::Impl Glib::RefPtr section; }; + using TorrentView = IF_GTKMM4(Gtk::ListView, Gtk::TreeView); + using TorrentViewSelection = IF_GTKMM4(Gtk::MultiSelection, Gtk::TreeSelection); + public: Impl( MainWindow& window, @@ -88,7 +96,7 @@ public: TR_DISABLE_COPY_MOVE(Impl) - [[nodiscard]] Glib::RefPtr get_selection() const; + [[nodiscard]] Glib::RefPtr get_selection() const; void refresh(); @@ -100,7 +108,7 @@ public: } private: - void init_view(Gtk::TreeView* view, Glib::RefPtr const& model); + void init_view(TorrentView* view, Glib::RefPtr const& model); Glib::RefPtr createOptionsMenu(); Glib::RefPtr createSpeedMenu(Glib::RefPtr const& actions, tr_direction dir); @@ -138,11 +146,17 @@ private: std::array speed_menu_info_; OptionMenuInfo ratio_menu_info_; +#if GTKMM_CHECK_VERSION(4, 0, 0) + Glib::RefPtr item_factory_compact_; + Glib::RefPtr item_factory_full_; + Glib::RefPtr selection_; +#else TorrentCellRenderer* renderer_ = nullptr; Gtk::TreeViewColumn* column_ = nullptr; +#endif Gtk::ScrolledWindow* scroll_ = nullptr; - Gtk::TreeView* view_ = nullptr; + TorrentView* view_ = nullptr; Gtk::Widget* toolbar_ = nullptr; FilterBar* filter_; Gtk::Widget* status_ = nullptr; @@ -167,9 +181,16 @@ void MainWindow::Impl::on_popup_menu([[maybe_unused]] double event_x, [[maybe_un #if GTKMM_CHECK_VERSION(4, 0, 0) popup_menu_ = Gtk::make_managed(menu, Gtk::PopoverMenu::Flags::NESTED); - popup_menu_->set_parent(window_); + popup_menu_->set_parent(*view_); popup_menu_->set_has_arrow(false); - popup_menu_->set_halign(window_.get_direction() == Gtk::TextDirection::RTL ? Gtk::Align::END : Gtk::Align::START); + popup_menu_->set_halign(view_->get_direction() == Gtk::TextDirection::RTL ? Gtk::Align::END : Gtk::Align::START); + + view_->signal_destroy().connect( + [this]() + { + popup_menu_->unparent(); + popup_menu_ = nullptr; + }); #else popup_menu_ = Gtk::make_managed(menu); popup_menu_->attach_to_widget(window_); @@ -177,13 +198,7 @@ void MainWindow::Impl::on_popup_menu([[maybe_unused]] double event_x, [[maybe_un } #if GTKMM_CHECK_VERSION(4, 0, 0) - int view_x = 0; - int view_y = 0; - view_->convert_bin_window_to_widget_coords(static_cast(event_x), static_cast(event_y), view_x, view_y); - 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_->set_pointing_to({ static_cast(event_x), static_cast(event_y), 1, 1 }); popup_menu_->popup(); #else popup_menu_->popup_at_pointer(nullptr); @@ -193,6 +208,38 @@ void MainWindow::Impl::on_popup_menu([[maybe_unused]] double event_x, [[maybe_un namespace { +#if GTKMM_CHECK_VERSION(4, 0, 0) + +class GtrStrvBuilderDeleter +{ +public: + void operator()(GStrvBuilder* builder) const + { + if (builder != nullptr) + { + g_strv_builder_unref(builder); + } + } +}; + +using GtrStrvBuilderPtr = std::unique_ptr; + +GStrv gtr_strv_join(GObject* /*object*/, GStrv lhs, GStrv rhs) +{ + auto const builder = GtrStrvBuilderPtr(g_strv_builder_new()); + if (builder == nullptr) + { + return nullptr; + } + + g_strv_builder_addv(builder.get(), const_cast(lhs)); // NOLINT(cppcoreguidelines-pro-type-const-cast) + g_strv_builder_addv(builder.get(), const_cast(rhs)); // NOLINT(cppcoreguidelines-pro-type-const-cast) + + return g_strv_builder_end(builder.get()); +} + +#else + bool tree_view_search_equal_func( Glib::RefPtr const& /*model*/, int /*column*/, @@ -205,10 +252,35 @@ bool tree_view_search_equal_func( return name.find(key.lowercase()) == Glib::ustring::npos; } +#endif + } // namespace -void MainWindow::Impl::init_view(Gtk::TreeView* view, Glib::RefPtr const& model) +void MainWindow::Impl::init_view(TorrentView* view, Glib::RefPtr const& model) { +#if GTKMM_CHECK_VERSION(4, 0, 0) + auto const create_builder_list_item_factory = [](std::string const& filename) + { + auto builder_scope = Glib::wrap(G_OBJECT(gtk_builder_cscope_new())); + gtk_builder_cscope_add_callback(GTK_BUILDER_CSCOPE(builder_scope->gobj()), gtr_strv_join); + + return Glib::wrap(gtk_builder_list_item_factory_new_from_resource( + GTK_BUILDER_SCOPE(builder_scope->gobj()), + gtr_get_full_resource_path(filename).c_str())); + }; + + item_factory_compact_ = create_builder_list_item_factory("TorrentListItemCompact.ui"s); + item_factory_full_ = create_builder_list_item_factory("TorrentListItemFull.ui"s); + + view->signal_activate().connect([](guint /*position*/) { gtr_action_activate("show-torrent-properties"); }); + + selection_ = Gtk::MultiSelection::create(model); + selection_->signal_selection_changed().connect([this](guint /*position*/, guint /*n_items*/) + { signal_selection_changed_.emit(); }); + + view->set_factory(gtr_pref_flag_get(TR_KEY_compact_view) ? item_factory_compact_ : item_factory_full_); + view->set_model(selection_); +#else static auto const& torrent_cols = Torrent::get_columns(); view->set_search_column(torrent_cols.name_collated); @@ -220,27 +292,27 @@ void MainWindow::Impl::init_view(Gtk::TreeView* view, Glib::RefPtrpack_start(*renderer_, false); column_->add_attribute(renderer_->property_torrent(), torrent_cols.self); -#if !GTKMM_CHECK_VERSION(4, 0, 0) view->signal_popup_menu().connect_notify([this]() { on_popup_menu(0, 0); }); + view->signal_row_activated().connect([](auto const& /*path*/, auto* /*column*/) + { gtr_action_activate("show-torrent-properties"); }); + + view->set_model(model); + + view->get_selection()->signal_changed().connect([this]() { signal_selection_changed_.emit(); }); #endif - setup_tree_view_button_event_handling( + + setup_item_view_button_event_handling( *view, [this, view](guint /*button*/, TrGdkModifierType /*state*/, double view_x, double view_y, bool context_menu_requested) { - return on_tree_view_button_pressed( + return on_item_view_button_pressed( *view, view_x, view_y, context_menu_requested, sigc::mem_fun(*this, &Impl::on_popup_menu)); }, - [view](double view_x, double view_y) { return on_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"); }); - - view->set_model(IF_GTKMM4(ListModelAdapter::create(model), model)); - - view->get_selection()->signal_changed().connect([this]() { signal_selection_changed_.emit(); }); + [view](double view_x, double view_y) { return on_item_view_button_released(*view, view_x, view_y); }); } void MainWindow::Impl::prefsChanged(tr_quark const key) @@ -248,14 +320,18 @@ void MainWindow::Impl::prefsChanged(tr_quark const key) switch (key) { case TR_KEY_compact_view: +#if GTKMM_CHECK_VERSION(4, 0, 0) + view_->set_factory(gtr_pref_flag_get(key) ? item_factory_compact_ : item_factory_full_); +#else renderer_->property_compact() = gtr_pref_flag_get(key); /* since the cell size has changed, we need gtktreeview to revalidate * its fixed-height mode values. Unfortunately there's not an API call - * for that, but this seems to work for both GTK 3 and 4 */ + * for that, but this seems to work */ view_->set_fixed_height_mode(false); view_->set_row_separator_func({}); view_->unset_row_separator_func(); view_->set_fixed_height_mode(true); +#endif break; case TR_KEY_show_statusbar: @@ -577,7 +653,7 @@ MainWindow::Impl::Impl( : window_(window) , core_(core) , scroll_(gtr_get_widget(builder, "torrents_view_scroll")) - , view_(gtr_get_widget(builder, "torrents_view")) + , view_(gtr_get_widget(builder, "torrents_view")) , toolbar_(gtr_get_widget(builder, "toolbar")) , filter_(gtr_get_widget_derived(builder, "filterbar", core_)) , status_(gtr_get_widget(builder, "statusbar")) @@ -751,9 +827,9 @@ void MainWindow::Impl::refresh() } } -Glib::RefPtr MainWindow::Impl::get_selection() const +Glib::RefPtr MainWindow::Impl::get_selection() const { - return view_->get_selection(); + return IF_GTKMM4(selection_, view_->get_selection()); } void MainWindow::for_each_selected_torrent(std::function const&)> const& callback) const @@ -763,12 +839,23 @@ void MainWindow::for_each_selected_torrent(std::function const&)> const& callback) const { - static auto const& self_col = Torrent::get_columns().self; - auto const selection = impl_->get_selection(); auto const model = selection->get_model(); bool result = false; +#if GTKMM_CHECK_VERSION(4, 0, 0) + auto const selected_items = selection->get_selection(); // TODO(C++20): Move into the `for` + for (auto const position : *selected_items) + { + if (callback(gtr_ptr_dynamic_cast(model->get_object(position)))) + { + result = true; + break; + } + } +#else + static auto const& self_col = Torrent::get_columns().self; + for (auto const& path : selection->get_selected_rows()) { auto const torrent = Glib::make_refptr_for_instance(model->get_iter(path)->get_value(self_col)); @@ -779,6 +866,7 @@ bool MainWindow::for_each_selected_torrent_until(std::functionadd_action(clear_action); auto const pause_action = Gio::SimpleAction::create_bool("pause-message-log"); - pause_action->signal_activate().connect([this, &action = *pause_action.get()](auto const& /*value*/) - { onPauseToggled(action); }); + pause_action->signal_activate().connect([this, &action = *pause_action](auto const& /*value*/) { onPauseToggled(action); }); action_group->add_action(pause_action); auto* const level_combo = gtr_get_widget(builder, "level_combo"); @@ -523,10 +522,10 @@ MessageLogWindow::Impl::Impl( filter_->set_visible_func(sigc::mem_fun(*this, &Impl::isRowVisible)); view_->set_model(sort_); - setup_tree_view_button_event_handling( + setup_item_view_button_event_handling( *view_, {}, - [this](double view_x, double view_y) { return on_tree_view_button_released(*view_, view_x, view_y); }); + [this](double view_x, double view_y) { return on_item_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); diff --git a/gtk/Percents.h b/gtk/Percents.h index d5f126500..ac68bf181 100644 --- a/gtk/Percents.h +++ b/gtk/Percents.h @@ -23,6 +23,11 @@ public: return raw_value_ / 100; } + [[nodiscard]] constexpr float to_fraction() const noexcept + { + return raw_value_ / 10000.F; + } + [[nodiscard]] std::string to_string() const; constexpr bool operator==(Percents const& rhs) const noexcept diff --git a/gtk/PrefsDialog.cc b/gtk/PrefsDialog.cc index 1dc9ebcdd..2bcdc5fb9 100644 --- a/gtk/PrefsDialog.cc +++ b/gtk/PrefsDialog.cc @@ -795,10 +795,10 @@ RemotePage::RemotePage(BaseObjectType* cast_item, Glib::RefPtr con store_ = whitelist_tree_model_new(gtr_pref_string_get(TR_KEY_rpc_whitelist)); view_->set_model(store_); - setup_tree_view_button_event_handling( + setup_item_view_button_event_handling( *view_, {}, - [this](double view_x, double view_y) { return on_tree_view_button_released(*view_, view_x, view_y); }); + [this](double view_x, double view_y) { return on_item_view_button_released(*view_, view_x, view_y); }); whitelist_widgets_.push_back(view_); auto const sel = view_->get_selection(); diff --git a/gtk/Session.cc b/gtk/Session.cc index d27ed1274..5fe2923d8 100644 --- a/gtk/Session.cc +++ b/gtk/Session.cc @@ -541,7 +541,7 @@ Session::Impl::Impl(Session& core, tr_session* session) , session_{ session } { raw_model_ = Gio::ListStore::create(); - signal_torrents_changed_.connect(sigc::hide<0>(sigc::mem_fun(*sorter_.get(), &TorrentSorter::update))); + signal_torrents_changed_.connect(sigc::hide<0>(sigc::mem_fun(*sorter_, &TorrentSorter::update))); sorted_model_ = SortListModel::create(gtr_ptr_static_cast(raw_model_), sorter_); /* init from prefs & listen to pref changes */ diff --git a/gtk/Torrent.cc b/gtk/Torrent.cc index fc51e431f..67f547160 100644 --- a/gtk/Torrent.cc +++ b/gtk/Torrent.cc @@ -5,6 +5,7 @@ #include "Torrent.h" +#include "DynamicPropertyStore.h" #include "IconCache.h" #include "Percents.h" #include "Utils.h" @@ -17,6 +18,7 @@ #include +#include #include #include @@ -87,6 +89,19 @@ std::string_view get_mime_type(tr_torrent const& torrent) return name.find('/') != std::string_view::npos ? DirectoryMimeType : tr_get_mime_type_for_filename(name); } +std::string_view get_activity_direction(tr_torrent_activity activity) +{ + switch (activity) + { + case TR_STATUS_DOWNLOAD: + return "down"sv; + case TR_STATUS_SEED: + return "up"sv; + default: + return "idle"sv; + } +} + } // namespace Torrent::Columns::Columns() @@ -98,6 +113,22 @@ Torrent::Columns::Columns() class Torrent::Impl { public: + enum class Property : guint + { + ICON = 1, + NAME, + PERCENT_DONE, + SHORT_STATUS, + LONG_PROGRESS, + LONG_STATUS, + SENSITIVE, + CSS_CLASSES, + + N_PROPS + }; + + using PropertyStore = DynamicPropertyStore; + struct Cache { Glib::ustring error_message; @@ -175,6 +206,9 @@ public: [[nodiscard]] Glib::ustring get_short_status_text() const; [[nodiscard]] Glib::ustring get_long_progress_text() const; [[nodiscard]] Glib::ustring get_long_status_text() const; + [[nodiscard]] std::vector get_css_classes() const; + + static void class_init(void* cls, void* user_data); private: [[nodiscard]] Glib::ustring get_short_transfer_text() const; @@ -293,7 +327,42 @@ void Torrent::Impl::notify_property_changes(ChangeFlags changes) const return; } +#if GTKMM_CHECK_VERSION(4, 0, 0) + + static auto constexpr properties_flags = std::array, PropertyStore::PropertyCount - 1>({ { + { Property::ICON, ChangeFlag::MIME_TYPE }, + { Property::NAME, ChangeFlag::NAME }, + { Property::PERCENT_DONE, ChangeFlag::PERCENT_DONE }, + { Property::SHORT_STATUS, + ChangeFlag::ACTIVE_PEERS_DOWN | ChangeFlag::ACTIVE_PEERS_UP | ChangeFlag::ACTIVITY | ChangeFlag::FINISHED | + ChangeFlag::RATIO | ChangeFlag::RECHECK_PROGRESS | ChangeFlag::SPEED_DOWN | ChangeFlag::SPEED_UP }, + { Property::LONG_PROGRESS, + ChangeFlag::ACTIVITY | ChangeFlag::ETA | ChangeFlag::LONG_PROGRESS | ChangeFlag::PERCENT_COMPLETE | + ChangeFlag::PERCENT_DONE | ChangeFlag::RATIO | ChangeFlag::TOTAL_SIZE }, + { Property::LONG_STATUS, + ChangeFlag::ACTIVE_PEERS_DOWN | ChangeFlag::ACTIVE_PEERS_UP | ChangeFlag::ACTIVITY | ChangeFlag::ERROR_CODE | + ChangeFlag::ERROR_MESSAGE | ChangeFlag::HAS_METADATA | ChangeFlag::LONG_STATUS | ChangeFlag::SPEED_DOWN | + ChangeFlag::SPEED_UP | ChangeFlag::STALLED }, + { Property::SENSITIVE, ChangeFlag::ACTIVITY }, + { Property::CSS_CLASSES, ChangeFlag::ACTIVITY | ChangeFlag::ERROR_CODE }, + } }); + + auto& properties = PropertyStore::get(); + + for (auto const& [property, flags] : properties_flags) + { + if (changes.test(flags)) + { + properties.notify_changed(torrent_, property); + } + } + +#else + + // Reduce redraws by emitting non-detailed signal once for all changes gtr_object_notify_emit(torrent_); + +#endif } void Torrent::Impl::get_value(int column, Glib::ValueBase& value) const @@ -453,6 +522,60 @@ Glib::ustring Torrent::Impl::get_long_status_text() const return status_str; } +std::vector Torrent::Impl::get_css_classes() const +{ + auto result = std::vector({ + fmt::format("tr-transfer-{}", get_activity_direction(cache_.activity)), + }); + + if (cache_.error_code != 0) + { + result.emplace_back("tr-error"); + } + + return result; +} + +void Torrent::Impl::class_init(void* cls, void* /*user_data*/) +{ + PropertyStore::get().install( + G_OBJECT_CLASS(cls), + { + { Property::ICON, "icon", "Icon", "Icon based on torrent's likely MIME type", &Torrent::get_icon }, + { Property::NAME, "name", "Name", "Torrent name / title", &Torrent::get_name }, + { Property::PERCENT_DONE, + "percent-done", + "Percent done", + "Percent done (0..1) for current activity (leeching or seeding)", + &Torrent::get_percent_done_fraction }, + { Property::SHORT_STATUS, + "short-status", + "Short status", + "Status text displayed in compact view mode", + &Torrent::get_short_status_text }, + { Property::LONG_PROGRESS, + "long-progress", + "Long progress", + "Progress text displayed in full view mode", + &Torrent::get_long_progress_text }, + { Property::LONG_STATUS, + "long-status", + "Long status", + "Status text displayed in full view mode", + &Torrent::get_long_status_text }, + { Property::SENSITIVE, + "sensitive", + "Sensitive", + "Visual sensitivity of the view item, unrelated to activation possibility", + &Torrent::get_sensitive }, + { Property::CSS_CLASSES, + "css-classes", + "CSS classes", + "CSS class names used for styling view items", + &Torrent::get_css_classes }, + }); +} + Glib::ustring Torrent::Impl::get_short_transfer_text() const { if (cache_.has_metadata && cache_.active_peers_down > 0) @@ -564,11 +687,13 @@ Glib::ustring Torrent::Impl::get_activity_text() const Torrent::Torrent() : Glib::ObjectBase(typeid(Torrent)) + , ExtraClassInit(&Impl::class_init) { } Torrent::Torrent(tr_torrent* torrent) : Glib::ObjectBase(typeid(Torrent)) + , ExtraClassInit(&Impl::class_init) , impl_(std::make_unique(*this, torrent)) { g_assert(torrent != nullptr); @@ -704,6 +829,11 @@ Percents Torrent::get_percent_done() const return impl_->get_cache().activity_percent_done; } +float Torrent::get_percent_done_fraction() const +{ + return get_percent_done().to_fraction(); +} + Glib::ustring Torrent::get_short_status_text() const { return impl_->get_short_status_text(); @@ -724,6 +854,11 @@ bool Torrent::get_sensitive() const return impl_->get_cache().activity != TR_STATUS_STOPPED; } +std::vector Torrent::get_css_classes() const +{ + return impl_->get_css_classes(); +} + Torrent::ChangeFlags Torrent::update() { auto result = impl_->update_cache(); diff --git a/gtk/Torrent.h b/gtk/Torrent.h index 95d1acac3..499ad97ea 100644 --- a/gtk/Torrent.h +++ b/gtk/Torrent.h @@ -10,6 +10,7 @@ #include #include +#include #include #include #include @@ -22,7 +23,9 @@ class Percents; -class Torrent : public Glib::Object +class Torrent + : public Glib::ExtraClassInit + , public Glib::Object { public: class Columns : public Gtk::TreeModelColumnRecord @@ -83,6 +86,7 @@ public: Glib::ustring get_name() const; Percents get_percent_complete() const; Percents get_percent_done() const; + float get_percent_done_fraction() const; tr_priority_t get_priority() const; size_t get_queue_position() const; float get_ratio() const; @@ -99,6 +103,7 @@ public: Glib::ustring get_long_progress_text() const; Glib::ustring get_long_status_text() const; bool get_sensitive() const; + std::vector get_css_classes() const; ChangeFlags update(); diff --git a/gtk/TorrentCellRenderer.cc b/gtk/TorrentCellRenderer.cc index d91b64979..0bb824256 100644 --- a/gtk/TorrentCellRenderer.cc +++ b/gtk/TorrentCellRenderer.cc @@ -41,6 +41,8 @@ /* #define TEST_RTL */ +using namespace std::string_literals; + /*** **** ***/ @@ -51,18 +53,8 @@ namespace auto const DefaultBarHeight = 12; auto const CompactBarWidth = 50; auto const SmallScale = 0.9; -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 get_height(Gtk::Requisition const& req) -{ - return req.IF_GTKMM4(get_height(), height); -} - -auto get_width(Gtk::Requisition const& req) -{ - return req.IF_GTKMM4(get_width(), width); -} +auto const CompactIconSize = Gtk::ICON_SIZE_MENU; +auto const FullIconSize = Gtk::ICON_SIZE_DND; } // namespace @@ -72,9 +64,6 @@ auto get_width(Gtk::Requisition const& req) class TorrentCellRenderer::Impl { - using SnapshotPtr = TorrentCellRenderer::SnapshotPtr; - using IconSize = IF_GTKMM4(Gtk::IconSize, Gtk::BuiltinIconSize); - public: explicit Impl(TorrentCellRenderer& renderer); ~Impl(); @@ -85,12 +74,12 @@ public: Gtk::Requisition get_size_full(Gtk::Widget& widget) const; void render_compact( - SnapshotPtr const& snapshot, + Cairo::RefPtr const& context, Gtk::Widget& widget, Gdk::Rectangle const& background_area, Gtk::CellRendererState flags); void render_full( - SnapshotPtr const& snapshot, + Cairo::RefPtr const& context, Gtk::Widget& widget, Gdk::Rectangle const& background_area, Gtk::CellRendererState flags); @@ -112,13 +101,12 @@ public: private: void render_progress_bar( - SnapshotPtr const& snapshot, + Cairo::RefPtr const& context, Gtk::Widget& widget, Gdk::Rectangle const& area, Gtk::CellRendererState flags, - Gdk::RGBA const& color); + std::optional const& color); - static void set_icon(Gtk::CellRendererPixbuf& renderer, Glib::RefPtr const& icon, IconSize icon_size); static void adjust_progress_bar_hue( Cairo::RefPtr const& context, Gdk::RGBA const& color, @@ -140,19 +128,6 @@ private: **** ***/ -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 -} - Gtk::Requisition TorrentCellRenderer::Impl::get_size_compact(Gtk::Widget& widget) const { int xpad = 0; @@ -170,7 +145,8 @@ Gtk::Requisition TorrentCellRenderer::Impl::get_size_compact(Gtk::Widget& widget renderer_.get_padding(xpad, ypad); /* get the idealized cell dimensions */ - set_icon(*icon_renderer_, icon, CompactIconSize); + icon_renderer_->property_gicon() = icon; + icon_renderer_->property_stock_size() = 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); @@ -184,8 +160,8 @@ Gtk::Requisition TorrentCellRenderer::Impl::get_size_compact(Gtk::Widget& widget *** LAYOUT **/ - return { xpad * 2 + get_width(icon_size) + GUI_PAD + CompactBarWidth + GUI_PAD + get_width(stat_size), - ypad * 2 + std::max(get_height(name_size), property_bar_height_.get_value()) }; + return { xpad * 2 + icon_size.width + GUI_PAD + CompactBarWidth + GUI_PAD + stat_size.width, + ypad * 2 + std::max(name_size.height, property_bar_height_.get_value()) }; } Gtk::Requisition TorrentCellRenderer::Impl::get_size_full(Gtk::Widget& widget) const @@ -207,7 +183,8 @@ Gtk::Requisition TorrentCellRenderer::Impl::get_size_full(Gtk::Widget& widget) c renderer_.get_padding(xpad, ypad); /* get the idealized cell dimensions */ - set_icon(*icon_renderer_, icon, FullIconSize); + icon_renderer_->property_gicon() = icon; + icon_renderer_->property_stock_size() = FullIconSize; icon_renderer_->get_preferred_size(widget, min_size, icon_size); text_renderer_->property_text() = name; text_renderer_->property_weight() = TR_PANGO_WEIGHT(BOLD); @@ -225,9 +202,9 @@ Gtk::Requisition TorrentCellRenderer::Impl::get_size_full(Gtk::Widget& widget) c *** LAYOUT **/ - return { xpad * 2 + get_width(icon_size) + GUI_PAD + std::max(get_width(prog_size), get_width(stat_size)), - ypad * 2 + get_height(name_size) + get_height(prog_size) + GUI_PAD_SMALL + property_bar_height_.get_value() + - GUI_PAD_SMALL + get_height(stat_size) }; + return { xpad * 2 + icon_size.width + GUI_PAD + std::max(prog_size.width, stat_size.width), + ypad * 2 + name_size.height + prog_size.height + GUI_PAD_SMALL + property_bar_height_.get_value() + GUI_PAD_SMALL + + stat_size.height }; } void TorrentCellRenderer::get_preferred_width_vfunc(Gtk::Widget& widget, int& minimum_width, int& natural_width) const @@ -237,7 +214,7 @@ void TorrentCellRenderer::get_preferred_width_vfunc(Gtk::Widget& widget, int& mi auto const size = impl_->property_compact().get_value() ? impl_->get_size_compact(widget) : impl_->get_size_full(widget); - minimum_width = get_width(size); + minimum_width = size.width; natural_width = minimum_width; } } @@ -249,7 +226,7 @@ void TorrentCellRenderer::get_preferred_height_vfunc(Gtk::Widget& widget, int& m auto const size = impl_->property_compact().get_value() ? impl_->get_size_compact(widget) : impl_->get_size_full(widget); - minimum_height = get_height(size); + minimum_height = size.height; natural_height = minimum_height; } } @@ -257,21 +234,49 @@ void TorrentCellRenderer::get_preferred_height_vfunc(Gtk::Widget& widget, int& m namespace { -Gdk::RGBA const& get_progress_bar_color(Torrent const& torrent) +void set_error_color( + Gtk::CellRendererText& text_renderer, + Torrent const& torrent, + Gtk::Widget& widget, + Gtk::CellRendererState flags) { - static auto const steelblue_color = Gdk::RGBA("steelblue"); - static auto const forestgreen_color = Gdk::RGBA("forestgreen"); - static auto const silver_color = Gdk::RGBA("silver"); + static auto const error_color_name = Glib::ustring("tr_error_color"s); + auto color = Gdk::RGBA(); + if (torrent.get_error_code() != 0 && (flags & TR_GTK_CELL_RENDERER_STATE(SELECTED)) == Gtk::CellRendererState{} && + widget.get_style_context()->lookup_color(error_color_name, color)) + { + text_renderer.property_foreground_rgba() = color; + } + else + { + text_renderer.property_foreground_set() = false; + } +} + +std::optional get_progress_bar_color(Torrent const& torrent, Gtk::Widget const& widget) +{ + static auto const down_color_name = Glib::ustring("tr_transfer_down_color"s); + static auto const up_color_name = Glib::ustring("tr_transfer_up_color"s); + static auto const idle_color_name = Glib::ustring("tr_transfer_idle_color"s); + + auto const* color_name = &idle_color_name; switch (torrent.get_activity()) { case TR_STATUS_DOWNLOAD: - return steelblue_color; + color_name = &down_color_name; + break; + case TR_STATUS_SEED: - return forestgreen_color; + color_name = &up_color_name; + break; + default: - return silver_color; + break; } + + auto color = Gdk::RGBA(); + return widget.get_style_context()->lookup_color(*color_name, color) ? std::make_optional(color) : std::nullopt; } Cairo::RefPtr get_mask_surface(Cairo::RefPtr const& surface, Gdk::Rectangle const& area) @@ -290,12 +295,6 @@ Cairo::RefPtr get_mask_surface(Cairo::RefPtr con return mask_surface; } -template -void render_impl(Gtk::CellRenderer& renderer, Ts&&... args) -{ - renderer.IF_GTKMM4(snapshot, render)(std::forward(args)...); -} - } // namespace void TorrentCellRenderer::Impl::adjust_progress_bar_hue( @@ -303,13 +302,11 @@ void TorrentCellRenderer::Impl::adjust_progress_bar_hue( Gdk::RGBA const& color, Gdk::Rectangle const& area) { - using TrCairoContextOperator = IF_GTKMM4(Cairo::Context::Operator, Cairo::Operator); - auto const mask_surface = get_mask_surface(context->get_target(), area); // Adjust surface color context->set_source_rgb(color.get_red(), color.get_green(), color.get_blue()); - context->set_operator(static_cast(CAIRO_OPERATOR_HSL_COLOR)); + context->set_operator(static_cast(CAIRO_OPERATOR_HSL_COLOR)); context->rectangle(area.get_x(), area.get_y(), area.get_width(), area.get_height()); context->fill(); @@ -320,13 +317,17 @@ void TorrentCellRenderer::Impl::adjust_progress_bar_hue( } void TorrentCellRenderer::Impl::render_progress_bar( - SnapshotPtr const& snapshot, + Cairo::RefPtr const& context, Gtk::Widget& widget, Gdk::Rectangle const& area, Gtk::CellRendererState flags, - Gdk::RGBA const& color) + std::optional const& color) { - auto const context = IF_GTKMM4(snapshot->append_cairo(area), snapshot); + if (!color.has_value()) + { + progress_renderer_->render(context, widget, area, area, flags); + return; + } auto const temp_area = Gdk::Rectangle(0, 0, area.get_width(), area.get_height()); auto const temp_surface = Cairo::Surface::create( @@ -336,23 +337,9 @@ void TorrentCellRenderer::Impl::render_progress_bar( area.get_height()); auto const temp_context = Cairo::Context::create(temp_surface); - { -#if GTKMM_CHECK_VERSION(4, 0, 0) - auto const temp_snapshot = Gtk::Snapshot::create(); -#endif + progress_renderer_->render(temp_context, widget, temp_area, temp_area, flags); - render_impl(*progress_renderer_, IF_GTKMM4(temp_snapshot, temp_context), widget, temp_area, temp_area, flags); - -#if GTKMM_CHECK_VERSION(4, 0, 0) - temp_snapshot->reference(); - auto const render_node = std::unique_ptr( - gtk_snapshot_free_to_node(Glib::unwrap(temp_snapshot)), - [](GskRenderNode* p) { gsk_render_node_unref(p); }); - gsk_render_node_draw(render_node.get(), temp_context->cobj()); -#endif - } - - adjust_progress_bar_hue(temp_context, color, temp_area); + adjust_progress_bar_hue(temp_context, color.value(), temp_area); context->set_source(temp_context->get_target(), area.get_x(), area.get_y()); context->rectangle(area.get_x(), area.get_y(), area.get_width(), area.get_height()); @@ -360,7 +347,7 @@ void TorrentCellRenderer::Impl::render_progress_bar( } void TorrentCellRenderer::Impl::render_compact( - SnapshotPtr const& snapshot, + Cairo::RefPtr const& context, Gtk::Widget& widget, Gdk::Rectangle const& background_area, Gtk::CellRendererState flags) @@ -374,18 +361,11 @@ void TorrentCellRenderer::Impl::render_compact( auto const percent_done = torrent.get_percent_done().to_int(); bool const sensitive = torrent.get_sensitive(); - if (torrent.get_error_code() != 0 && (flags & TR_GTK_CELL_RENDERER_STATE(SELECTED)) == Gtk::CellRendererState{}) - { - text_renderer_->property_foreground() = "red"; - } - else - { - text_renderer_->property_foreground_set() = false; - } + set_error_color(*text_renderer_, torrent, widget, flags); auto const icon = torrent.get_icon(); auto const name = torrent.get_name(); - auto const& progress_color = get_progress_bar_color(torrent); + auto const progress_color = get_progress_bar_color(torrent, widget); auto const gstr_stat = torrent.get_short_status_text(); renderer_.get_padding(xpad, ypad); @@ -396,7 +376,8 @@ void TorrentCellRenderer::Impl::render_compact( fill_area.set_height(fill_area.get_height() - ypad * 2); auto icon_area = fill_area; - set_icon(*icon_renderer_, icon, CompactIconSize); + icon_renderer_->property_gicon() = icon; + icon_renderer_->property_stock_size() = CompactIconSize; icon_renderer_->get_preferred_width(widget, min_width, width); icon_area.set_width(width); @@ -433,28 +414,29 @@ void TorrentCellRenderer::Impl::render_compact( *** RENDER **/ - set_icon(*icon_renderer_, icon, CompactIconSize); + icon_renderer_->property_gicon() = icon; + icon_renderer_->property_stock_size() = CompactIconSize; icon_renderer_->property_sensitive() = sensitive; - render_impl(*icon_renderer_, snapshot, widget, icon_area, icon_area, flags); + icon_renderer_->render(context, widget, icon_area, icon_area, flags); progress_renderer_->property_value() = percent_done; progress_renderer_->property_text() = fmt::format(FMT_STRING("{:d}%"), percent_done); progress_renderer_->property_sensitive() = sensitive; - render_progress_bar(snapshot, widget, prog_area, flags, progress_color); + render_progress_bar(context, widget, prog_area, flags, progress_color); text_renderer_->property_text() = gstr_stat; text_renderer_->property_scale() = SmallScale; text_renderer_->property_ellipsize() = TR_PANGO_ELLIPSIZE_MODE(END); text_renderer_->property_sensitive() = sensitive; - render_impl(*text_renderer_, snapshot, widget, stat_area, stat_area, flags); + text_renderer_->render(context, widget, stat_area, stat_area, flags); text_renderer_->property_text() = name; text_renderer_->property_scale() = 1.0; - render_impl(*text_renderer_, snapshot, widget, name_area, name_area, flags); + text_renderer_->render(context, widget, name_area, name_area, flags); } void TorrentCellRenderer::Impl::render_full( - SnapshotPtr const& snapshot, + Cairo::RefPtr const& context, Gtk::Widget& widget, Gdk::Rectangle const& background_area, Gtk::CellRendererState flags) @@ -468,28 +450,22 @@ void TorrentCellRenderer::Impl::render_full( auto const percent_done = torrent.get_percent_done().to_int(); bool const sensitive = torrent.get_sensitive(); - if (torrent.get_error_code() != 0 && (flags & TR_GTK_CELL_RENDERER_STATE(SELECTED)) == Gtk::CellRendererState{}) - { - text_renderer_->property_foreground() = "red"; - } - else - { - text_renderer_->property_foreground_set() = false; - } + set_error_color(*text_renderer_, torrent, widget, flags); auto const icon = torrent.get_icon(); auto const name = torrent.get_name(); - auto const& progress_color = get_progress_bar_color(torrent); + auto const progress_color = get_progress_bar_color(torrent, widget); auto const gstr_prog = torrent.get_long_progress_text(); auto const gstr_stat = torrent.get_long_status_text(); renderer_.get_padding(xpad, ypad); /* get the idealized cell dimensions */ Gdk::Rectangle icon_area; - set_icon(*icon_renderer_, icon, FullIconSize); + icon_renderer_->property_gicon() = icon; + icon_renderer_->property_stock_size() = FullIconSize; icon_renderer_->get_preferred_size(widget, min_size, size); - icon_area.set_width(get_width(size)); - icon_area.set_height(get_height(size)); + icon_area.set_width(size.width); + icon_area.set_height(size.height); Gdk::Rectangle name_area; text_renderer_->property_text() = name; @@ -497,19 +473,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(get_height(size)); + name_area.set_height(size.height); 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(get_height(size)); + prog_area.set_height(size.height); Gdk::Rectangle stat_area; text_renderer_->property_text() = gstr_stat; text_renderer_->get_preferred_size(widget, min_size, size); - stat_area.set_height(get_height(size)); + stat_area.set_height(size.height); Gdk::Rectangle prct_area; @@ -561,33 +537,34 @@ void TorrentCellRenderer::Impl::render_full( *** RENDER **/ - set_icon(*icon_renderer_, icon, FullIconSize); + icon_renderer_->property_gicon() = icon; + icon_renderer_->property_stock_size() = FullIconSize; icon_renderer_->property_sensitive() = sensitive; - render_impl(*icon_renderer_, snapshot, widget, icon_area, icon_area, flags); + icon_renderer_->render(context, widget, icon_area, icon_area, flags); text_renderer_->property_text() = name; text_renderer_->property_scale() = 1.0; text_renderer_->property_ellipsize() = TR_PANGO_ELLIPSIZE_MODE(END); text_renderer_->property_weight() = TR_PANGO_WEIGHT(BOLD); text_renderer_->property_sensitive() = sensitive; - render_impl(*text_renderer_, snapshot, widget, name_area, name_area, flags); + text_renderer_->render(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); - render_impl(*text_renderer_, snapshot, widget, prog_area, prog_area, flags); + text_renderer_->render(context, widget, prog_area, prog_area, flags); progress_renderer_->property_value() = percent_done; progress_renderer_->property_text() = Glib::ustring(); progress_renderer_->property_sensitive() = sensitive; - render_progress_bar(snapshot, widget, prct_area, flags, progress_color); + render_progress_bar(context, widget, prct_area, flags, progress_color); text_renderer_->property_text() = gstr_stat; - render_impl(*text_renderer_, snapshot, widget, stat_area, stat_area, flags); + text_renderer_->render(context, widget, stat_area, stat_area, flags); } -void TorrentCellRenderer::IF_GTKMM4(snapshot_vfunc, render_vfunc)( - SnapshotPtr const& snapshot, +void TorrentCellRenderer::render_vfunc( + Cairo::RefPtr const& context, Gtk::Widget& widget, Gdk::Rectangle const& background_area, Gdk::Rectangle const& /*cell_area*/, @@ -602,11 +579,11 @@ void TorrentCellRenderer::IF_GTKMM4(snapshot_vfunc, render_vfunc)( { if (impl_->property_compact().get_value()) { - impl_->render_compact(snapshot, widget, background_area, flags); + impl_->render_compact(context, widget, background_area, flags); } else { - impl_->render_full(snapshot, widget, background_area, flags); + impl_->render_full(context, widget, background_area, flags); } } diff --git a/gtk/TorrentCellRenderer.h b/gtk/TorrentCellRenderer.h index 4def6fd59..0f5e603c8 100644 --- a/gtk/TorrentCellRenderer.h +++ b/gtk/TorrentCellRenderer.h @@ -18,8 +18,6 @@ class Torrent; class TorrentCellRenderer : public Gtk::CellRenderer { - using SnapshotPtr = IF_GTKMM4(Glib::RefPtr, Cairo::RefPtr); - public: TorrentCellRenderer(); ~TorrentCellRenderer() override; @@ -34,8 +32,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 IF_GTKMM4(snapshot_vfunc, render_vfunc)( - SnapshotPtr const& snapshot, + void render_vfunc( + Cairo::RefPtr 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 df9d58c3a..a531c798e 100644 --- a/gtk/Utils.cc +++ b/gtk/Utils.cc @@ -44,6 +44,7 @@ #include #include +#include #include #include #include @@ -297,7 +298,7 @@ void gtr_add_torrent_error_dialog(Gtk::Widget& child, tr_torrent* duplicate_torr /* 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( +bool on_item_view_button_pressed( Gtk::TreeView& view, double event_x, double event_y, @@ -326,9 +327,66 @@ bool on_tree_view_button_pressed( return false; } +#if GTKMM_CHECK_VERSION(4, 0, 0) + +namespace +{ + +// NOTE: Estimated position (`get_position_from_allocation` vfunc is private) +std::optional get_position_from_allocation(Gtk::ListView& view, double view_x, double view_y) +{ + auto* child = view.pick(view_x, view_y); + while (child != nullptr && child->get_css_name() != "row") + { + child = child->get_parent(); + } + + if (child == nullptr) + { + return {}; + } + + double top_x = 0; + double top_y = 0; + child->translate_coordinates(view, 0, 0, top_x, top_y); + return static_cast((top_y + view.get_vadjustment()->get_value()) / child->get_allocated_height()); +} + +} // namespace + +bool on_item_view_button_pressed( + Gtk::ListView& view, + double event_x, + double event_y, + bool context_menu_requested, + std::function const& callback) +{ + if (context_menu_requested) + { + if (auto const position = get_position_from_allocation(view, event_x, event_y); position.has_value()) + { + if (auto const selection_model = view.get_model(); !selection_model->is_selected(position.value())) + { + selection_model->select_item(position.value(), true); + } + } + + if (callback) + { + callback(event_x, event_y); + } + + return true; + } + + return false; +} + +#endif + /* if the user clicked in an empty area of the list, * clear all the selections. */ -bool on_tree_view_button_released(Gtk::TreeView& view, double event_x, double event_y) +bool on_item_view_button_released(Gtk::TreeView& view, double event_x, double event_y) { if (Gtk::TreeModel::Path path; !view.get_path_at_pos(static_cast(event_x), static_cast(event_y), path)) { @@ -338,8 +396,43 @@ bool on_tree_view_button_released(Gtk::TreeView& view, double event_x, double ev return false; } -void setup_tree_view_button_event_handling( - Gtk::TreeView& view, +#if GTKMM_CHECK_VERSION(4, 0, 0) + +bool on_item_view_button_released(Gtk::ListView& view, double event_x, double event_y) +{ + if (!get_position_from_allocation(view, event_x, event_y).has_value()) + { + view.get_model()->unselect_all(); + } + + return false; +} + +#endif + +namespace +{ + +#if GTKMM_CHECK_VERSION(4, 0, 0) + +std::pair convert_widget_to_bin_window_coords(Gtk::TreeView const& view, int view_x, int view_y) +{ + int event_x = 0; + int event_y = 0; + view.convert_widget_to_bin_window_coords(view_x, view_y, event_x, event_y); + return { event_x, event_y }; +} + +std::pair convert_widget_to_bin_window_coords(Gtk::ListView const& /*view*/, int view_x, int view_y) +{ + return { view_x, view_y }; +} + +#endif + +template +void setup_item_view_button_event_handling_impl( + T& view, std::function const& press_callback, std::function const& release_callback) { @@ -352,9 +445,10 @@ void setup_tree_view_button_event_handling( controller->signal_pressed().connect( [&view, press_callback, controller](int /*n_press*/, double view_x, double view_y) { - int event_x = 0; - int event_y = 0; - view.convert_widget_to_bin_window_coords(static_cast(view_x), static_cast(view_y), event_x, event_y); + auto const [event_x, event_y] = convert_widget_to_bin_window_coords( + view, + static_cast(view_x), + static_cast(view_y)); auto* const sequence = controller->get_current_sequence(); auto const event = controller->get_last_event(sequence); @@ -377,9 +471,10 @@ void setup_tree_view_button_event_handling( controller->signal_released().connect( [&view, release_callback, controller](int /*n_press*/, double view_x, double view_y) { - int event_x = 0; - int event_y = 0; - view.convert_widget_to_bin_window_coords(static_cast(view_x), static_cast(view_y), event_x, event_y); + auto const [event_x, event_y] = convert_widget_to_bin_window_coords( + view, + static_cast(view_x), + static_cast(view_y)); auto* const sequence = controller->get_current_sequence(); auto const event = controller->get_last_event(sequence); @@ -407,6 +502,28 @@ void setup_tree_view_button_event_handling( #endif } +} // namespace + +void setup_item_view_button_event_handling( + Gtk::TreeView& view, + std::function const& press_callback, + std::function const& release_callback) +{ + setup_item_view_button_event_handling_impl(view, press_callback, release_callback); +} + +#if GTKMM_CHECK_VERSION(4, 0, 0) + +void setup_item_view_button_event_handling( + Gtk::ListView& view, + std::function const& press_callback, + std::function const& release_callback) +{ + setup_item_view_button_event_handling_impl(view, press_callback, release_callback); +} + +#endif + bool gtr_file_trash_or_remove(std::string const& filename, tr_error** error) { bool trashed = false; diff --git a/gtk/Utils.h b/gtk/Utils.h index e2370f1dc..fd04b7949 100644 --- a/gtk/Utils.h +++ b/gtk/Utils.h @@ -22,6 +22,10 @@ #include #include +#if GTKMM_CHECK_VERSION(4, 0, 0) +#include +#endif + #include #include @@ -98,6 +102,15 @@ using TrObjectSignalNotifyCallback = void(Glib::RefPtr c Glib::SignalProxy gtr_object_signal_notify(Glib::ObjectBase& object); void gtr_object_notify_emit(Glib::ObjectBase& object); +template +inline GParamSpec* gtr_get_param_spec(char const* name, char const* nick, char const* blurb) +{ + auto dummy_value = Glib::Value(); + dummy_value.init(decltype(dummy_value)::value_type()); + + return dummy_value.create_param_spec(name, nick, blurb, TR_GLIB_PARAM_FLAGS(READABLE)); +} + void gtr_open_uri(Glib::ustring const& uri); void gtr_open_file(std::string const& path); @@ -137,22 +150,39 @@ 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( +bool on_item_view_button_pressed( Gtk::TreeView& view, double event_x, double event_y, bool context_menu_requested, std::function const& callback = {}); +#if GTKMM_CHECK_VERSION(4, 0, 0) +bool on_item_view_button_pressed( + Gtk::ListView& view, + double event_x, + double event_y, + bool context_menu_requested, + std::function const& callback = {}); +#endif /* if the click didn't specify a row, clear the selection */ -bool on_tree_view_button_released(Gtk::TreeView& view, double event_x, double event_y); +bool on_item_view_button_released(Gtk::TreeView& view, double event_x, double event_y); +#if GTKMM_CHECK_VERSION(4, 0, 0) +bool on_item_view_button_released(Gtk::ListView& view, double event_x, double event_y); +#endif using TrGdkModifierType = IF_GTKMM4(Gdk::ModifierType, guint); -void setup_tree_view_button_event_handling( +void setup_item_view_button_event_handling( Gtk::TreeView& view, std::function const& press_callback, std::function const& release_callback); +#if GTKMM_CHECK_VERSION(4, 0, 0) +void setup_item_view_button_event_handling( + Gtk::ListView& view, + std::function const& press_callback, + std::function const& release_callback); +#endif /* 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); diff --git a/gtk/transmission-ui.css b/gtk/transmission-ui.css index b9afa38a1..7a8c06374 100644 --- a/gtk/transmission-ui.css +++ b/gtk/transmission-ui.css @@ -1,3 +1,8 @@ +@define-color tr_transfer_down_color steelblue; +@define-color tr_transfer_up_color forestgreen; +@define-color tr_transfer_idle_color silver; +@define-color tr_error_color red; + .tr-workarea.frame { border-left-width: 0; border-right-width: 0; @@ -31,3 +36,49 @@ .tr-small { font-size: small; } + +.tr-workarea row { + margin: 0; + padding: 3px; +} + +row .tr-list-item label { + margin: 0; + padding: 0; +} + +row .tr-list-item.tr-compact progressbar trough { + min-width: 50px; +} + +row .tr-list-item.tr-compact progressbar trough, +row .tr-list-item.tr-compact progressbar progress { + min-height: 3px; +} + +row .tr-list-item.tr-compact .tr-status { + margin-left: 3px; + margin-right: 3px; +} + +row .tr-list-item.tr-transfer-down progressbar progress { + background-image: none; + background-color: @tr_transfer_down_color; + border-color: @tr_transfer_down_color; +} + +row .tr-list-item.tr-transfer-up progressbar progress { + background-image: none; + background-color: @tr_transfer_up_color; + border-color: @tr_transfer_up_color; +} + +row .tr-list-item.tr-transfer-idle progressbar progress { + background-image: none; + background-color: @tr_transfer_idle_color; + border-color: @tr_transfer_idle_color; +} + +row:not(:selected) .tr-list-item.tr-error label { + color: @tr_error_color; +} diff --git a/gtk/ui/gtk4/MainWindow.ui b/gtk/ui/gtk4/MainWindow.ui index 0e1e8f89b..ce3914f7b 100644 --- a/gtk/ui/gtk4/MainWindow.ui +++ b/gtk/ui/gtk4/MainWindow.ui @@ -115,22 +115,9 @@ horizontal never 1 - + 1 - 0 - 1 - - - multiple - - - - - 1 - fixed - Torrent - - + item + + + + + 1 + 0.5 + 1 + + + GtkListItem + + + + + + + + diff --git a/gtk/ui/gtk4/TorrentListItemFull.ui b/gtk/ui/gtk4/TorrentListItemFull.ui new file mode 100644 index 000000000..0c865954c --- /dev/null +++ b/gtk/ui/gtk4/TorrentListItemFull.ui @@ -0,0 +1,102 @@ + + + + + diff --git a/gtk/ui/gtk4/transmission-ui.gresource.xml b/gtk/ui/gtk4/transmission-ui.gresource.xml index f757a1b15..08678305e 100644 --- a/gtk/ui/gtk4/transmission-ui.gresource.xml +++ b/gtk/ui/gtk4/transmission-ui.gresource.xml @@ -13,6 +13,8 @@ PrefsDialog.ui RelocateDialog.ui StatsDialog.ui + TorrentListItemCompact.ui + TorrentListItemFull.ui TorrentUrlChooserDialog.ui