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.
This commit is contained in:
Mike Gelfand 2023-08-06 04:26:29 +01:00 committed by GitHub
parent badeed374f
commit 586cff9506
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 904 additions and 207 deletions

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -1743,10 +1743,10 @@ void DetailsDialog::Impl::peer_page_init(Glib::RefPtr<Gtk::Builder> const& build
webseed_store_ = Gtk::ListStore::create(webseed_cols);
auto* v = gtr_get_widget<Gtk::TreeView>(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<Gtk::CellRendererText>();
@ -1775,10 +1775,10 @@ void DetailsDialog::Impl::peer_page_init(Glib::RefPtr<Gtk::Builder> 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<Gtk::Builder> 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));

107
gtk/DynamicPropertyStore.h Normal file
View File

@ -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 <glibmm/object.h>
#include <glibmm/value.h>
#include <glibmm/wrap.h>
#include <array>
#include <functional>
#include <type_traits>
template<typename ObjectT, typename PropertyT>
class DynamicPropertyStore
{
public:
using ObjectType = ObjectT;
using PropertyType = PropertyT;
using PropertyIdType = guint;
static_assert(std::is_same_v<std::underlying_type_t<PropertyType>, PropertyIdType>);
struct PropertyInfo
{
template<typename MethodT>
using ValueType = std::invoke_result_t<MethodT, ObjectType>;
PropertyIdType id = 0;
GParamSpec* spec = nullptr;
std::function<void(ObjectType const&, Glib::ValueBase&)> getter;
PropertyInfo() = default;
template<typename MethodT>
PropertyInfo(PropertyType index, char const* name, char const* nick, char const* blurb, MethodT getter_method)
: id(static_cast<PropertyIdType>(index))
, spec(gtr_get_param_spec<ValueType<MethodT>>(name, nick, blurb))
, getter([getter_method](ObjectType const& object, Glib::ValueBase& value)
{ static_cast<Glib::Value<ValueType<MethodT>>&>(value).set((object.*getter_method)()); })
{
}
};
static inline auto const PropertyCount = static_cast<PropertyIdType>(PropertyType::N_PROPS);
public:
static DynamicPropertyStore& get() noexcept
{
static auto instance = DynamicPropertyStore();
return instance;
}
void install(GObjectClass* cls, std::initializer_list<PropertyInfo> 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<PropertyIdType>(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<ObjectType const*>(Glib::wrap_auto(object)); typed_object != nullptr)
{
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
get().get_value(*typed_object, PropertyType{ id }, *reinterpret_cast<Glib::ValueBase*>(value));
}
}
private:
std::array<PropertyInfo, PropertyCount> properties_ = {};
};

View File

@ -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())

View File

@ -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<Torrent>(torrents_model->get_object(i));
if (torrent != nullptr && TorrentFilter::match_activity(*torrent.get(), static_cast<ActivityType>(type)))
if (torrent != nullptr && TorrentFilter::match_activity(*torrent, static_cast<ActivityType>(type)))
{
++hits;
}

View File

@ -127,6 +127,12 @@ inline bool operator!=(RefPtr<T> const& lhs, std::nullptr_t /*rhs*/)
return !(lhs == nullptr);
}
template<typename T>
inline T& operator*(RefPtr<T> const& ptr)
{
return *ptr.get();
}
template<typename T>
inline RefPtr<T> make_refptr_for_instance(T* object)
{

View File

@ -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 <libtransmission/transmission.h>
#include <libtransmission/utils.h> // tr_formatter_speed_KBps()
@ -36,18 +39,20 @@
#include <gtkmm/scrolledwindow.h>
#include <gtkmm/togglebutton.h>
#include <gtkmm/treemodel.h>
#include <gtkmm/treeselection.h>
#include <gtkmm/treeview.h>
#include <gtkmm/treeviewcolumn.h>
#include <gtkmm/widget.h>
#include <gtkmm/window.h>
#if GTKMM_CHECK_VERSION(4, 0, 0)
#include <gtkmm/listitemfactory.h>
#include <gtkmm/multiselection.h>
#include <gtkmm/popovermenu.h>
#else
#include <gdkmm/display.h>
#include <gdkmm/window.h>
#include <gtkmm/menu.h>
#include <gtkmm/treeselection.h>
#include <gtkmm/treeviewcolumn.h>
#endif
#include <array>
@ -78,6 +83,9 @@ class MainWindow::Impl
Glib::RefPtr<Gio::Menu> 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<Gtk::TreeSelection> get_selection() const;
[[nodiscard]] Glib::RefPtr<TorrentViewSelection> get_selection() const;
void refresh();
@ -100,7 +108,7 @@ public:
}
private:
void init_view(Gtk::TreeView* view, Glib::RefPtr<FilterBar::Model> const& model);
void init_view(TorrentView* view, Glib::RefPtr<FilterBar::Model> const& model);
Glib::RefPtr<Gio::MenuModel> createOptionsMenu();
Glib::RefPtr<Gio::MenuModel> createSpeedMenu(Glib::RefPtr<Gio::SimpleActionGroup> const& actions, tr_direction dir);
@ -138,11 +146,17 @@ private:
std::array<OptionMenuInfo, 2> speed_menu_info_;
OptionMenuInfo ratio_menu_info_;
#if GTKMM_CHECK_VERSION(4, 0, 0)
Glib::RefPtr<Gtk::ListItemFactory> item_factory_compact_;
Glib::RefPtr<Gtk::ListItemFactory> item_factory_full_;
Glib::RefPtr<Gtk::MultiSelection> 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<Gtk::PopoverMenu>(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<Gtk::Menu>(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<int>(event_x), static_cast<int>(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<int>(event_x), static_cast<int>(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<GStrvBuilder, GtrStrvBuilderDeleter>;
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<char const**>(lhs)); // NOLINT(cppcoreguidelines-pro-type-const-cast)
g_strv_builder_addv(builder.get(), const_cast<char const**>(rhs)); // NOLINT(cppcoreguidelines-pro-type-const-cast)
return g_strv_builder_end(builder.get());
}
#else
bool tree_view_search_equal_func(
Glib::RefPtr<Gtk::TreeModel> 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<FilterBar::Model> const& model)
void MainWindow::Impl::init_view(TorrentView* view, Glib::RefPtr<FilterBar::Model> 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::RefPtr<FilterBar::Mo
column_->pack_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<Torrent>(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<Gtk::ScrolledWindow>(builder, "torrents_view_scroll"))
, view_(gtr_get_widget<Gtk::TreeView>(builder, "torrents_view"))
, view_(gtr_get_widget<TorrentView>(builder, "torrents_view"))
, toolbar_(gtr_get_widget<Gtk::Widget>(builder, "toolbar"))
, filter_(gtr_get_widget_derived<FilterBar>(builder, "filterbar", core_))
, status_(gtr_get_widget<Gtk::Widget>(builder, "statusbar"))
@ -751,9 +827,9 @@ void MainWindow::Impl::refresh()
}
}
Glib::RefPtr<Gtk::TreeSelection> MainWindow::Impl::get_selection() const
Glib::RefPtr<MainWindow::Impl::TorrentViewSelection> MainWindow::Impl::get_selection() const
{
return view_->get_selection();
return IF_GTKMM4(selection_, view_->get_selection());
}
void MainWindow::for_each_selected_torrent(std::function<void(Glib::RefPtr<Torrent> const&)> const& callback) const
@ -763,12 +839,23 @@ void MainWindow::for_each_selected_torrent(std::function<void(Glib::RefPtr<Torre
bool MainWindow::for_each_selected_torrent_until(std::function<bool(Glib::RefPtr<Torrent> 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<Torrent>(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::function<bool(Glib::RefPtr
break;
}
}
#endif
return result;
}

View File

@ -502,8 +502,7 @@ MessageLogWindow::Impl::Impl(
action_group->add_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<Gtk::ComboBox>(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);

View File

@ -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

View File

@ -795,10 +795,10 @@ RemotePage::RemotePage(BaseObjectType* cast_item, Glib::RefPtr<Gtk::Builder> 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();

View File

@ -541,7 +541,7 @@ Session::Impl::Impl(Session& core, tr_session* session)
, session_{ session }
{
raw_model_ = Gio::ListStore<Torrent>::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<Torrent>::create(gtr_ptr_static_cast<Gio::ListModel>(raw_model_), sorter_);
/* init from prefs & listen to pref changes */

View File

@ -5,6 +5,7 @@
#include "Torrent.h"
#include "DynamicPropertyStore.h"
#include "IconCache.h"
#include "Percents.h"
#include "Utils.h"
@ -17,6 +18,7 @@
#include <fmt/core.h>
#include <array>
#include <cmath>
#include <utility>
@ -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<Torrent, Property>;
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<Glib::ustring> 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<std::pair<Property, ChangeFlags>, 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<Glib::ustring> Torrent::Impl::get_css_classes() const
{
auto result = std::vector<Glib::ustring>({
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<Impl>(*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<Glib::ustring> Torrent::get_css_classes() const
{
return impl_->get_css_classes();
}
Torrent::ChangeFlags Torrent::update()
{
auto result = impl_->update_cache();

View File

@ -10,6 +10,7 @@
#include <libtransmission/transmission.h>
#include <giomm/icon.h>
#include <glibmm/extraclassinit.h>
#include <glibmm/object.h>
#include <glibmm/refptr.h>
#include <glibmm/ustring.h>
@ -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<Glib::ustring> get_css_classes() const;
ChangeFlags update();

View File

@ -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<Cairo::Context> const& context,
Gtk::Widget& widget,
Gdk::Rectangle const& background_area,
Gtk::CellRendererState flags);
void render_full(
SnapshotPtr const& snapshot,
Cairo::RefPtr<Cairo::Context> 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<Cairo::Context> const& context,
Gtk::Widget& widget,
Gdk::Rectangle const& area,
Gtk::CellRendererState flags,
Gdk::RGBA const& color);
std::optional<Gdk::RGBA> const& color);
static void set_icon(Gtk::CellRendererPixbuf& renderer, Glib::RefPtr<Gio::Icon> const& icon, IconSize icon_size);
static void adjust_progress_bar_hue(
Cairo::RefPtr<Cairo::Context> const& context,
Gdk::RGBA const& color,
@ -140,19 +128,6 @@ private:
****
***/
void TorrentCellRenderer::Impl::set_icon(
Gtk::CellRendererPixbuf& renderer,
Glib::RefPtr<Gio::Icon> 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<Gdk::RGBA> 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<Cairo::Surface> get_mask_surface(Cairo::RefPtr<Cairo::Surface> const& surface, Gdk::Rectangle const& area)
@ -290,12 +295,6 @@ Cairo::RefPtr<Cairo::Surface> get_mask_surface(Cairo::RefPtr<Cairo::Surface> con
return mask_surface;
}
template<typename... Ts>
void render_impl(Gtk::CellRenderer& renderer, Ts&&... args)
{
renderer.IF_GTKMM4(snapshot, render)(std::forward<Ts>(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<TrCairoContextOperator>(CAIRO_OPERATOR_HSL_COLOR));
context->set_operator(static_cast<Cairo::Operator>(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<Cairo::Context> const& context,
Gtk::Widget& widget,
Gdk::Rectangle const& area,
Gtk::CellRendererState flags,
Gdk::RGBA const& color)
std::optional<Gdk::RGBA> 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<GskRenderNode, void (*)(GskRenderNode*)>(
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<Cairo::Context> 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<Cairo::Context> 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<Cairo::Context> 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);
}
}

View File

@ -18,8 +18,6 @@ class Torrent;
class TorrentCellRenderer : public Gtk::CellRenderer
{
using SnapshotPtr = IF_GTKMM4(Glib::RefPtr<Gtk::Snapshot>, Cairo::RefPtr<Cairo::Context>);
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<Cairo::Context> const& context,
Gtk::Widget& widget,
Gdk::Rectangle const& background_area,
Gdk::Rectangle const& cell_area,

View File

@ -44,6 +44,7 @@
#include <functional>
#include <memory>
#include <optional>
#include <stack>
#include <stdexcept>
#include <utility>
@ -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<guint> 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<guint>((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<void(double, double)> 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<int>(event_x), static_cast<int>(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<int, int> 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<int, int> convert_widget_to_bin_window_coords(Gtk::ListView const& /*view*/, int view_x, int view_y)
{
return { view_x, view_y };
}
#endif
template<typename T>
void setup_item_view_button_event_handling_impl(
T& view,
std::function<bool(guint, TrGdkModifierType, double, double, bool)> const& press_callback,
std::function<bool(double, double)> 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<int>(view_x), static_cast<int>(view_y), event_x, event_y);
auto const [event_x, event_y] = convert_widget_to_bin_window_coords(
view,
static_cast<int>(view_x),
static_cast<int>(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<int>(view_x), static_cast<int>(view_y), event_x, event_y);
auto const [event_x, event_y] = convert_widget_to_bin_window_coords(
view,
static_cast<int>(view_x),
static_cast<int>(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<bool(guint, TrGdkModifierType, double, double, bool)> const& press_callback,
std::function<bool(double, double)> 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<bool(guint, TrGdkModifierType, double, double, bool)> const& press_callback,
std::function<bool(double, double)> 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;

View File

@ -22,6 +22,10 @@
#include <gtkmm/widget.h>
#include <gtkmm/window.h>
#if GTKMM_CHECK_VERSION(4, 0, 0)
#include <gtkmm/listview.h>
#endif
#include <fmt/core.h>
#include <cstddef>
@ -98,6 +102,15 @@ using TrObjectSignalNotifyCallback = void(Glib::RefPtr<Glib::ObjectBase const> c
Glib::SignalProxy<TrObjectSignalNotifyCallback> gtr_object_signal_notify(Glib::ObjectBase& object);
void gtr_object_notify_emit(Glib::ObjectBase& object);
template<typename T>
inline GParamSpec* gtr_get_param_spec(char const* name, char const* nick, char const* blurb)
{
auto dummy_value = Glib::Value<T>();
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<void(double, double)> 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<void(double, double)> 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<bool(guint, TrGdkModifierType, double, double, bool)> const& press_callback,
std::function<bool(double, double)> const& release_callback);
#if GTKMM_CHECK_VERSION(4, 0, 0)
void setup_item_view_button_event_handling(
Gtk::ListView& view,
std::function<bool(guint, TrGdkModifierType, double, double, bool)> const& press_callback,
std::function<bool(double, double)> 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);

View File

@ -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;
}

View File

@ -115,22 +115,9 @@ horizontal</property>
<property name="hscrollbar-policy">never</property>
<property name="has-frame">1</property>
<property name="child">
<object class="GtkTreeView" id="torrents_view">
<object class="GtkListView" id="torrents_view">
<property name="focusable">1</property>
<property name="headers-visible">0</property>
<property name="fixed-height-mode">1</property>
<child internal-child="selection">
<object class="GtkTreeSelection" id="torrents_view_selection">
<property name="mode">multiple</property>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="torrent_column">
<property name="resizable">1</property>
<property name="sizing">fixed</property>
<property name="title" translatable="1">Torrent</property>
</object>
</child>
<property name="tab-behavior">item</property>
</object>
</property>
<style>

View File

@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk" version="4.0"/>
<template class="GtkListItem">
<property name="child">
<object class="GtkBox">
<property name="orientation">horizontal</property>
<property name="spacing">3</property>
<binding name="css-classes">
<closure type="GStrv" function="gtr_strv_join">
<lookup name="css-classes" type="gtkmm__CustomObject_7Torrent">
<lookup name="item">GtkListItem</lookup>
</lookup>
<constant type="GStrv">tr-list-item&#xA;tr-compact</constant>
</closure>
</binding>
<binding name="sensitive">
<lookup name="sensitive" type="gtkmm__CustomObject_7Torrent">
<lookup name="item">GtkListItem</lookup>
</lookup>
</binding>
<child>
<object class="GtkImage">
<property name="pixel-size">16</property>
<property name="valign">0.5</property>
<property name="vexpand">1</property>
<binding name="gicon">
<lookup name="icon" type="gtkmm__CustomObject_7Torrent">
<lookup name="item">GtkListItem</lookup>
</lookup>
</binding>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="ellipsize">end</property>
<property name="xalign">0</property>
<property name="valign">0.5</property>
<property name="hexpand">1</property>
<property name="vexpand">1</property>
<binding name="label">
<lookup name="name" type="gtkmm__CustomObject_7Torrent">
<lookup name="item">GtkListItem</lookup>
</lookup>
</binding>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="valign">0.5</property>
<property name="xalign">0</property>
<property name="vexpand">1</property>
<binding name="label">
<lookup name="short-status" type="gtkmm__CustomObject_7Torrent">
<lookup name="item">GtkListItem</lookup>
</lookup>
</binding>
<style>
<class name="tr-small"/>
<class name="tr-status"/>
</style>
</object>
</child>
<child>
<object class="GtkProgressBar">
<property name="show-text">1</property>
<property name="valign">0.5</property>
<property name="vexpand">1</property>
<binding name="fraction">
<lookup name="percent-done" type="gtkmm__CustomObject_7Torrent">
<lookup name="item">GtkListItem</lookup>
</lookup>
</binding>
</object>
</child>
</object>
</property>
</template>
</interface>

View File

@ -0,0 +1,102 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk" version="4.0"/>
<template class="GtkListItem">
<property name="child">
<object class="GtkBox">
<property name="orientation">horizontal</property>
<property name="spacing">6</property>
<binding name="sensitive">
<lookup name="sensitive" type="gtkmm__CustomObject_7Torrent">
<lookup name="item">GtkListItem</lookup>
</lookup>
</binding>
<binding name="css-classes">
<closure type="GStrv" function="gtr_strv_join">
<lookup name="css-classes" type="gtkmm__CustomObject_7Torrent">
<lookup name="item">GtkListItem</lookup>
</lookup>
<constant type="GStrv">tr-list-item&#xA;tr-full</constant>
</closure>
</binding>
<child>
<object class="GtkImage">
<property name="pixel-size">32</property>
<property name="valign">0.5</property>
<property name="vexpand">1</property>
<binding name="gicon">
<lookup name="icon" type="gtkmm__CustomObject_7Torrent">
<lookup name="item">GtkListItem</lookup>
</lookup>
</binding>
</object>
</child>
<child>
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="hexpand">1</property>
<property name="spacing">3</property>
<child>
<object class="GtkLabel">
<property name="ellipsize">end</property>
<property name="valign">0.5</property>
<property name="xalign">0</property>
<binding name="label">
<lookup name="name" type="gtkmm__CustomObject_7Torrent">
<lookup name="item">GtkListItem</lookup>
</lookup>
</binding>
<attributes>
<attribute name="weight" value="bold"></attribute>
</attributes>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="ellipsize">end</property>
<property name="valign">0.5</property>
<property name="xalign">0</property>
<binding name="label">
<lookup name="long-progress" type="gtkmm__CustomObject_7Torrent">
<lookup name="item">GtkListItem</lookup>
</lookup>
</binding>
<style>
<class name="tr-small"/>
</style>
</object>
</child>
<child>
<object class="GtkProgressBar">
<property name="valign">0.5</property>
<binding name="fraction">
<lookup name="percent-done" type="gtkmm__CustomObject_7Torrent">
<lookup name="item">GtkListItem</lookup>
</lookup>
</binding>
<style>
<class name="tr-status"/>
</style>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="ellipsize">end</property>
<property name="valign">0.5</property>
<property name="xalign">0</property>
<binding name="label">
<lookup name="long-status" type="gtkmm__CustomObject_7Torrent">
<lookup name="item">GtkListItem</lookup>
</lookup>
</binding>
<style>
<class name="tr-small"/>
</style>
</object>
</child>
</object>
</child>
</object>
</property>
</template>
</interface>

View File

@ -13,6 +13,8 @@
<file compressed="true" preprocess="xml-stripblanks">PrefsDialog.ui</file>
<file compressed="true" preprocess="xml-stripblanks">RelocateDialog.ui</file>
<file compressed="true" preprocess="xml-stripblanks">StatsDialog.ui</file>
<file compressed="true" preprocess="xml-stripblanks">TorrentListItemCompact.ui</file>
<file compressed="true" preprocess="xml-stripblanks">TorrentListItemFull.ui</file>
<file compressed="true" preprocess="xml-stripblanks">TorrentUrlChooserDialog.ui</file>
</gresource>
</gresources>