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