Use `Gio::List{Model,Store}` for torrents (#4430)

* Use `Gio::List{Model,Store}` for torrents

Switch from `Gtk::{TreeModel,ListStore}` in preparation for cell
renderers deprecation in GTK 4.10. That will require switching to the
new view classes (`Gtk::{Column,List}View`) which only work with `Gio`
models. Implement an adapter to support GTK+ 3 where the old view class
(`Gtk::TreeView`) only works with `Gtk` models; it is effective enough
but requires a signal connection per item to notify on row changes.

Refactor filtering and sorting (which now happen over the new model) to
use compatible `Gtk::Filter` and `Gtk::Sorter` classes. Although these
classes are only present in GTK 4, the abstraction is suitable for GTK+
3 as well so make our subclasses work for both versions.

Since items (of `Torrent` class) of the new model provide only a very
limited (by design) layer of compatibility with GTK+ 3 way of doing
things, refactor selection handling to do it the new way. Move selection
helpers into `MainWindow` to abstract them away since new view classes
handle it differently.

* Improve session load performance based on profiling results
This commit is contained in:
Mike Gelfand 2022-12-21 13:26:25 -08:00 committed by GitHub
parent db802afc4f
commit 32531fe5ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 2570 additions and 1327 deletions

View File

@ -49,6 +49,7 @@
#include "Session.h"
#include "StatsDialog.h"
#include "SystemTrayIcon.h"
#include "Torrent.h"
#include "Utils.h"
using namespace std::literals;
@ -136,7 +137,7 @@ private:
void toggleMainWindow();
bool winclose();
void rowChangedCB(Gtk::TreePath const& path, Gtk::TreeModel::iterator const& iter);
void rowChangedCB(std::unordered_set<tr_torrent_id_t> const& torrent_ids, Torrent::ChangeFlags changes);
void app_setup();
void main_window_setup();
@ -157,12 +158,11 @@ private:
void on_prefs_changed(tr_quark key);
[[nodiscard]] std::vector<tr_torrent_id_t> get_selected_torrent_ids() const;
[[nodiscard]] tr_torrent* get_first_selected_torrent() const;
[[nodiscard]] counts_data get_selected_torrent_counts() const;
void start_all_torrents();
void pause_all_torrents();
void copy_magnet_link_to_clipboard(tr_torrent* tor) const;
void copy_magnet_link_to_clipboard(Glib::RefPtr<Torrent> const& torrent) const;
bool call_rpc_for_selected_torrents(std::string const& method);
void remove_selected(bool delete_files);
@ -194,7 +194,6 @@ private:
std::vector<std::string> error_list_;
std::vector<std::string> duplicates_list_;
std::map<std::string, std::unique_ptr<DetailsDialog>> details_;
Glib::RefPtr<Gtk::TreeSelection> sel_;
};
namespace
@ -232,8 +231,7 @@ std::string get_details_dialog_key(std::vector<tr_torrent_id_t> const& id_list)
std::vector<tr_torrent_id_t> Application::Impl::get_selected_torrent_ids() const
{
std::vector<tr_torrent_id_t> ids;
sel_->selected_foreach([&ids](auto const& /*path*/, auto const& iter)
{ ids.push_back(iter->get_value(torrent_cols.torrent_id)); });
wind_->for_each_selected_torrent([&ids](auto const& torrent) { ids.push_back(torrent->get_id()); });
return ids;
}
@ -266,12 +264,12 @@ Application::Impl::counts_data Application::Impl::get_selected_torrent_counts()
{
counts_data counts;
sel_->selected_foreach(
[&counts](auto const& /*path*/, auto const& iter)
wind_->for_each_selected_torrent(
[&counts](auto const& torrent)
{
++counts.total_count;
auto const activity = iter->get_value(torrent_cols.activity);
auto const activity = torrent->get_activity();
if (activity == TR_STATUS_DOWNLOAD_WAIT || activity == TR_STATUS_SEED_WAIT)
{
@ -293,7 +291,7 @@ bool Application::Impl::refresh_actions()
{
size_t const total = core_->get_torrent_count();
size_t const active = core_->get_active_torrent_count();
auto const torrent_count = core_->get_model()->children().size();
auto const torrent_count = core_->get_model()->get_n_items();
auto const sel_counts = get_selected_torrent_counts();
bool const has_selection = sel_counts.total_count > 0;
@ -318,14 +316,9 @@ bool Application::Impl::refresh_actions()
gtr_action_set_sensitive("open-torrent-folder", sel_counts.total_count == 1);
gtr_action_set_sensitive("copy-magnet-link-to-clipboard", sel_counts.total_count == 1);
bool canUpdate = false;
sel_->selected_foreach(
[&canUpdate](auto const& /*path*/, auto const& iter)
{
auto const* tor = static_cast<tr_torrent const*>(iter->get_value(torrent_cols.torrent));
canUpdate = canUpdate || tr_torrentCanManualUpdate(tor);
});
gtr_action_set_sensitive("torrent-reannounce", canUpdate);
bool const can_update = wind_->for_each_selected_torrent_until(
[](auto const& torrent) { return tr_torrentCanManualUpdate(&torrent->get_underlying()); });
gtr_action_set_sensitive("torrent-reannounce", can_update);
}
refresh_actions_tag_.disconnect();
@ -432,7 +425,7 @@ bool Application::Impl::on_rpc_changed_idle(tr_rpc_callback_type type, tr_torren
case TR_RPC_TORRENT_ADDED:
if (auto* tor = core_->find_torrent(torrent_id); tor != nullptr)
{
core_->add_torrent(tor, true);
core_->add_torrent(Torrent::create(tor), true);
}
break;
@ -844,9 +837,11 @@ bool Application::Impl::winclose()
return true; /* don't propagate event further */
}
void Application::Impl::rowChangedCB(Gtk::TreePath const& path, Gtk::TreeModel::iterator const& /*iter*/)
void Application::Impl::rowChangedCB(std::unordered_set<tr_torrent_id_t> const& torrent_ids, Torrent::ChangeFlags changes)
{
if (sel_->is_selected(path))
if (changes.test(Torrent::ChangeFlag::ACTIVITY) &&
wind_->for_each_selected_torrent_until([&torrent_ids](auto const& torrent)
{ return torrent_ids.find(torrent->get_id()) != torrent_ids.end(); }))
{
refresh_actions_soon();
}
@ -912,14 +907,9 @@ void Application::Impl::on_drag_data_received(
void Application::Impl::main_window_setup()
{
// g_assert(nullptr == cbdata->wind);
// cbdata->wind = wind;
sel_ = wind_->get_selection();
sel_->signal_changed().connect(sigc::mem_fun(*this, &Impl::refresh_actions_soon));
wind_->signal_selection_changed().connect(sigc::mem_fun(*this, &Impl::refresh_actions_soon));
refresh_actions_soon();
auto const model = core_->get_model();
model->signal_row_changed().connect(sigc::mem_fun(*this, &Impl::rowChangedCB));
core_->signal_torrents_changed().connect(sigc::mem_fun(*this, &Impl::rowChangedCB));
gtr_window_on_close(*wind_, sigc::mem_fun(*this, &Impl::winclose));
refresh_actions();
@ -1426,12 +1416,7 @@ bool Application::Impl::call_rpc_for_selected_torrents(std::string const& method
tr_variantDictAddStrView(&top, TR_KEY_method, method);
auto* const args = tr_variantDictAddDict(&top, TR_KEY_arguments, 1);
auto* const ids = tr_variantDictAddList(args, TR_KEY_ids, 0);
sel_->selected_foreach(
[ids](auto const& /*path*/, auto const& iter)
{
auto const* const tor = static_cast<tr_torrent*>(iter->get_value(torrent_cols.torrent));
tr_variantListAddInt(ids, tr_torrentId(tor));
});
wind_->for_each_selected_torrent([ids](auto const& torrent) { tr_variantListAddInt(ids, torrent->get_id()); });
if (tr_variantListSize(ids) != 0)
{
@ -1445,12 +1430,7 @@ bool Application::Impl::call_rpc_for_selected_torrents(std::string const& method
void Application::Impl::remove_selected(bool delete_files)
{
auto l = std::vector<tr_torrent_id_t>{};
sel_->selected_foreach([&l](auto const& /*path*/, auto const& iter)
{ l.push_back(iter->get_value(torrent_cols.torrent_id)); });
if (!l.empty())
if (auto const l = get_selected_torrent_ids(); !l.empty())
{
gtr_confirm_remove(*wind_, core_, l, delete_files);
}
@ -1478,25 +1458,9 @@ void Application::Impl::pause_all_torrents()
tr_variantClear(&request);
}
tr_torrent* Application::Impl::get_first_selected_torrent() const
void Application::Impl::copy_magnet_link_to_clipboard(Glib::RefPtr<Torrent> const& torrent) const
{
tr_torrent* tor = nullptr;
Glib::RefPtr<Gtk::TreeModel> m;
if (auto const l = sel_->get_selected_rows(m); !l.empty())
{
if (auto iter = m->get_iter(l.front()); iter)
{
tor = static_cast<tr_torrent*>(iter->get_value(torrent_cols.torrent));
}
}
return tor;
}
void Application::Impl::copy_magnet_link_to_clipboard(tr_torrent* tor) const
{
auto const magnet = tr_torrentGetMagnetLink(tor);
auto const magnet = tr_torrentGetMagnetLink(&torrent->get_underlying());
auto const display = wind_->get_display();
/* this is The Right Thing for copy/paste... */
@ -1548,12 +1512,8 @@ void Application::Impl::actions_handler(Glib::ustring const& action_name)
}
else if (action_name == "copy-magnet-link-to-clipboard")
{
tr_torrent* tor = get_first_selected_torrent();
if (tor != nullptr)
{
copy_magnet_link_to_clipboard(tor);
}
wind_->for_each_selected_torrent_until(
sigc::bind_return(sigc::mem_fun(*this, &Impl::copy_magnet_link_to_clipboard), true));
}
else if (action_name == "relocate-torrent")
{
@ -1575,8 +1535,7 @@ void Application::Impl::actions_handler(Glib::ustring const& action_name)
}
else if (action_name == "open-torrent-folder")
{
sel_->selected_foreach([this](auto const& /*path*/, auto const& iter)
{ core_->open_folder(iter->get_value(torrent_cols.torrent_id)); });
wind_->for_each_selected_torrent([this](auto const& torrent) { core_->open_folder(torrent->get_id()); });
}
else if (action_name == "show-torrent-properties")
{
@ -1602,11 +1561,11 @@ void Application::Impl::actions_handler(Glib::ustring const& action_name)
}
else if (action_name == "select-all")
{
sel_->select_all();
wind_->select_all();
}
else if (action_name == "deselect-all")
{
sel_->unselect_all();
wind_->unselect_all();
}
else if (action_name == "edit-preferences")
{

View File

@ -110,6 +110,7 @@ set(${PROJECT_NAME}_SOURCES
FilterBar.cc
FreeSpaceLabel.cc
IconCache.cc
ListModelAdapter.cc
main.cc
MainWindow.cc
MakeDialog.cc
@ -123,7 +124,10 @@ set(${PROJECT_NAME}_SOURCES
Session.cc
StatsDialog.cc
SystemTrayIcon.cc
Torrent.cc
TorrentCellRenderer.cc
TorrentFilter.cc
TorrentSorter.cc
Utils.cc
${CMAKE_CURRENT_BINARY_DIR}/transmission-resources.c
${CMAKE_CURRENT_BINARY_DIR}/transmission-ui-resources.c
@ -137,9 +141,11 @@ set(${PROJECT_NAME}_HEADERS
FaviconCache.h
FileList.h
FilterBar.h
Flags.h
FreeSpaceLabel.h
HigWorkarea.h
IconCache.h
ListModelAdapter.h
MainWindow.h
MakeDialog.h
MessageLogWindow.h
@ -152,7 +158,10 @@ set(${PROJECT_NAME}_HEADERS
Session.h
StatsDialog.h
SystemTrayIcon.h
Torrent.h
TorrentCellRenderer.h
TorrentFilter.h
TorrentSorter.h
Utils.h
${CMAKE_CURRENT_BINARY_DIR}/transmission-resources.h
${CMAKE_CURRENT_BINARY_DIR}/transmission-ui-resources.h

View File

@ -18,18 +18,26 @@
#include "FaviconCache.h" // gtr_get_favicon()
#include "FilterBar.h"
#include "HigWorkarea.h" // GUI_PAD
#include "ListModelAdapter.h"
#include "Session.h" // torrent_cols
#include "Torrent.h"
#include "TorrentFilter.h"
#include "Utils.h"
class FilterBar::Impl
{
using FilterModel = IF_GTKMM4(Gtk::FilterListModel, Gtk::TreeModelFilter);
using TrackerType = TorrentFilter::Tracker;
using ActivityType = TorrentFilter::Activity;
public:
Impl(FilterBar& widget, tr_session* session, Glib::RefPtr<Gtk::TreeModel> const& torrent_model);
Impl(FilterBar& widget, Glib::RefPtr<Session> const& core);
~Impl();
TR_DISABLE_COPY_MOVE(Impl)
[[nodiscard]] Glib::RefPtr<Gtk::TreeModel> get_filter_model() const;
[[nodiscard]] Glib::RefPtr<FilterModel> get_filter_model() const;
private:
template<typename T>
@ -42,39 +50,34 @@ private:
static void render_pixbuf_func(Gtk::CellRendererPixbuf& cell_renderer, Gtk::TreeModel::const_iterator const& iter);
static void render_number_func(Gtk::CellRendererText& cell_renderer, Gtk::TreeModel::const_iterator const& iter);
void selection_changed_cb();
void filter_entry_changed();
void update_filter_activity();
void update_filter_tracker();
void update_filter_text();
Glib::RefPtr<Gtk::ListStore> activity_filter_model_new();
void activity_model_update_idle();
bool activity_filter_model_update();
void status_model_update_count(Gtk::TreeModel::iterator const& iter, int n);
bool activity_is_it_a_separator(Gtk::TreeModel::const_iterator const& iter);
Glib::RefPtr<Gtk::TreeStore> tracker_filter_model_new();
void tracker_model_update_idle();
bool tracker_filter_model_update();
void tracker_model_update_count(Gtk::TreeModel::iterator const& iter, int n);
bool is_it_a_separator(Gtk::TreeModel::const_iterator const& iter);
void favicon_ready_cb(Glib::RefPtr<Gdk::Pixbuf> const& pixbuf, Gtk::TreeRowReference& reference);
void update_filter_models(Torrent::ChangeFlags changes);
void update_filter_models_idle(Torrent::ChangeFlags changes);
void update_count_label_idle();
bool update_count_label();
bool is_row_visible(Gtk::TreeModel::const_iterator const& iter);
bool test_tracker(tr_torrent const& tor, int active_tracker_type, Glib::ustring const& host);
bool test_torrent_activity(tr_torrent& tor, int type);
static Glib::ustring get_name_from_host(std::string const& host);
static Gtk::CellRendererText* number_renderer_new();
static bool testText(tr_torrent const& tor, Glib::ustring const& key);
private:
FilterBar& widget_;
tr_session* const session_;
Glib::RefPtr<Gtk::TreeModel> const torrent_model_;
Glib::RefPtr<Session> const core_;
Glib::RefPtr<Gtk::ListStore> const activity_model_;
Glib::RefPtr<Gtk::TreeStore> const tracker_model_;
@ -83,27 +86,16 @@ private:
Gtk::ComboBox* tracker_ = nullptr;
Gtk::Entry* entry_ = nullptr;
Gtk::Label* show_lb_ = nullptr;
Glib::RefPtr<Gtk::TreeModelFilter> filter_model_;
int active_activity_type_ = 0;
int active_tracker_type_ = 0;
Glib::ustring active_tracker_sitename_;
sigc::connection activity_model_row_changed_tag_;
sigc::connection activity_model_row_inserted_tag_;
sigc::connection activity_model_row_deleted_cb_tag_;
sigc::connection activity_model_update_tag_;
sigc::connection tracker_model_row_changed_tag_;
sigc::connection tracker_model_row_inserted_tag_;
sigc::connection tracker_model_row_deleted_cb_tag_;
sigc::connection tracker_model_update_tag_;
sigc::connection filter_model_row_deleted_tag_;
sigc::connection filter_model_row_inserted_tag_;
Glib::RefPtr<TorrentFilter> filter_ = TorrentFilter::create();
Glib::RefPtr<FilterModel> filter_model_;
#if GTKMM_CHECK_VERSION(4, 0, 0)
sigc::connection filter_model_items_changed_tag_;
#endif
sigc::connection update_count_label_tag_;
Glib::ustring filter_text_;
sigc::connection update_filter_models_tag_;
sigc::connection update_filter_models_on_add_remove_tag_;
sigc::connection update_filter_models_on_change_tag_;
};
/***
@ -115,13 +107,6 @@ private:
namespace
{
enum
{
TRACKER_FILTER_TYPE_ALL,
TRACKER_FILTER_TYPE_HOST,
TRACKER_FILTER_TYPE_SEPARATOR,
};
class TrackerFilterModelColumns : public Gtk::TreeModelColumnRecord
{
public:
@ -194,19 +179,27 @@ bool FilterBar::Impl::tracker_filter_model_update()
}
};
auto const torrents_model = core_->get_model();
/* Walk through all the torrents, tallying how many matches there are
* for the various categories. Also make a sorted list of all tracker
* hosts s.t. we can merge it with the existing list */
auto n_torrents = int{ 0 };
auto site_infos = std::unordered_map<std::string /*site*/, site_info>{};
for (auto const& row : torrent_model_->children())
for (auto i = 0U, count = torrents_model->get_n_items(); i < count; ++i)
{
auto const* tor = static_cast<tr_torrent const*>(row.get_value(torrent_cols.torrent));
auto const torrent = gtr_ptr_dynamic_cast<Torrent>(torrents_model->get_object(i));
if (torrent == nullptr)
{
continue;
}
auto const& raw_torrent = torrent->get_underlying();
auto torrent_sites_and_hosts = std::map<std::string, std::string>{};
for (size_t i = 0, n = tr_torrentTrackerCount(tor); i < n; ++i)
for (size_t j = 0, n = tr_torrentTrackerCount(&raw_torrent); j < n; ++j)
{
auto const view = tr_torrentTracker(tor, i);
auto const view = tr_torrentTracker(&raw_torrent, j);
torrent_sites_and_hosts.try_emplace(std::data(view.sitename), view.host);
}
@ -286,10 +279,10 @@ bool FilterBar::Impl::tracker_filter_model_update()
add->set_value(tracker_filter_cols.sitename, Glib::ustring{ site.sitename });
add->set_value(tracker_filter_cols.displayname, get_name_from_host(site.sitename));
add->set_value(tracker_filter_cols.count, site.count);
add->set_value(tracker_filter_cols.type, static_cast<int>(TRACKER_FILTER_TYPE_HOST));
add->set_value(tracker_filter_cols.type, static_cast<int>(TrackerType::HOST));
auto path = tracker_model_->get_path(add);
gtr_get_favicon(
session_,
core_->get_session(),
site.host,
[this, ref = Gtk::TreeRowReference(tracker_model_, path)](auto const& pixbuf) mutable
{ favicon_ready_cb(pixbuf, ref); });
@ -313,30 +306,22 @@ Glib::RefPtr<Gtk::TreeStore> FilterBar::Impl::tracker_filter_model_new()
auto iter = store->append();
iter->set_value(tracker_filter_cols.displayname, Glib::ustring(_("All")));
iter->set_value(tracker_filter_cols.type, static_cast<int>(TRACKER_FILTER_TYPE_ALL));
iter->set_value(tracker_filter_cols.type, static_cast<int>(TrackerType::ALL));
iter = store->append();
iter->set_value(tracker_filter_cols.type, static_cast<int>(TRACKER_FILTER_TYPE_SEPARATOR));
iter->set_value(tracker_filter_cols.type, -1);
return store;
}
bool FilterBar::Impl::is_it_a_separator(Gtk::TreeModel::const_iterator const& iter)
{
return iter->get_value(tracker_filter_cols.type) == TRACKER_FILTER_TYPE_SEPARATOR;
}
void FilterBar::Impl::tracker_model_update_idle()
{
if (!tracker_model_update_tag_.connected())
{
tracker_model_update_tag_ = Glib::signal_idle().connect([this]() { return tracker_filter_model_update(); });
}
return iter->get_value(tracker_filter_cols.type) == -1;
}
void FilterBar::Impl::render_pixbuf_func(Gtk::CellRendererPixbuf& cell_renderer, Gtk::TreeModel::const_iterator const& iter)
{
cell_renderer.property_width() = (iter->get_value(tracker_filter_cols.type) == TRACKER_FILTER_TYPE_HOST) ? 20 : 0;
cell_renderer.property_width() = TrackerType{ iter->get_value(tracker_filter_cols.type) } == TrackerType::HOST ? 20 : 0;
}
void FilterBar::Impl::render_number_func(Gtk::CellRendererText& cell_renderer, Gtk::TreeModel::const_iterator const& iter)
@ -381,31 +366,6 @@ void FilterBar::Impl::tracker_combo_box_init(Gtk::ComboBox& combo)
combo.pack_end(*r, true);
combo.set_cell_data_func(*r, [r](auto const& iter) { render_number_func(*r, iter); });
}
tracker_model_row_changed_tag_ = torrent_model_->signal_row_changed().connect( //
[this](auto const& /*path*/, auto const& /*iter*/) { tracker_model_update_idle(); });
tracker_model_row_inserted_tag_ = torrent_model_->signal_row_inserted().connect( //
[this](auto const& /*path*/, auto const& /*iter*/) { tracker_model_update_idle(); });
tracker_model_row_deleted_cb_tag_ = torrent_model_->signal_row_deleted().connect( //
[this](auto const& /*path*/) { tracker_model_update_idle(); });
}
bool FilterBar::Impl::test_tracker(tr_torrent const& tor, int active_tracker_type, Glib::ustring const& host)
{
if (active_tracker_type != TRACKER_FILTER_TYPE_HOST)
{
return true;
}
for (size_t i = 0, n = tr_torrentTrackerCount(&tor); i < n; ++i)
{
if (auto const tracker = tr_torrentTracker(&tor, i); std::data(tracker.sitename) == host)
{
return true;
}
}
return false;
}
namespace
@ -417,19 +377,6 @@ namespace
****
***/
enum
{
ACTIVITY_FILTER_ALL,
ACTIVITY_FILTER_DOWNLOADING,
ACTIVITY_FILTER_SEEDING,
ACTIVITY_FILTER_ACTIVE,
ACTIVITY_FILTER_PAUSED,
ACTIVITY_FILTER_FINISHED,
ACTIVITY_FILTER_VERIFYING,
ACTIVITY_FILTER_ERROR,
ACTIVITY_FILTER_SEPARATOR
};
class ActivityFilterModelColumns : public Gtk::TreeModelColumnRecord
{
public:
@ -453,40 +400,7 @@ ActivityFilterModelColumns const activity_filter_cols;
bool FilterBar::Impl::activity_is_it_a_separator(Gtk::TreeModel::const_iterator const& iter)
{
return iter->get_value(activity_filter_cols.type) == ACTIVITY_FILTER_SEPARATOR;
}
bool FilterBar::Impl::test_torrent_activity(tr_torrent& tor, int type)
{
auto const* st = tr_torrentStatCached(&tor);
switch (type)
{
case ACTIVITY_FILTER_DOWNLOADING:
return st->activity == TR_STATUS_DOWNLOAD || st->activity == TR_STATUS_DOWNLOAD_WAIT;
case ACTIVITY_FILTER_SEEDING:
return st->activity == TR_STATUS_SEED || st->activity == TR_STATUS_SEED_WAIT;
case ACTIVITY_FILTER_ACTIVE:
return st->peersSendingToUs > 0 || st->peersGettingFromUs > 0 || st->webseedsSendingToUs > 0 ||
st->activity == TR_STATUS_CHECK;
case ACTIVITY_FILTER_PAUSED:
return st->activity == TR_STATUS_STOPPED;
case ACTIVITY_FILTER_FINISHED:
return st->finished;
case ACTIVITY_FILTER_VERIFYING:
return st->activity == TR_STATUS_CHECK || st->activity == TR_STATUS_CHECK_WAIT;
case ACTIVITY_FILTER_ERROR:
return st->error != 0;
default: /* ACTIVITY_FILTER_ALL */
return true;
}
return iter->get_value(activity_filter_cols.type) == -1;
}
void FilterBar::Impl::status_model_update_count(Gtk::TreeModel::iterator const& iter, int n)
@ -499,14 +413,22 @@ void FilterBar::Impl::status_model_update_count(Gtk::TreeModel::iterator const&
bool FilterBar::Impl::activity_filter_model_update()
{
auto const torrents_model = core_->get_model();
for (auto& row : activity_model_->children())
{
auto const type = row.get_value(activity_filter_cols.type);
if (type == -1)
{
continue;
}
auto hits = 0;
for (auto const& torrent_row : torrent_model_->children())
for (auto i = 0U, count = torrents_model->get_n_items(); i < count; ++i)
{
if (test_torrent_activity(*static_cast<tr_torrent*>(torrent_row.get_value(torrent_cols.torrent)), type))
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)))
{
++hits;
}
@ -522,22 +444,22 @@ Glib::RefPtr<Gtk::ListStore> FilterBar::Impl::activity_filter_model_new()
{
struct FilterTypeInfo
{
int type;
ActivityType type;
char const* context;
char const* name;
Glib::ustring icon_name;
char const* icon_name;
};
static auto const types = std::array<FilterTypeInfo, 9>({ {
{ ACTIVITY_FILTER_ALL, nullptr, N_("All"), {} },
{ ACTIVITY_FILTER_SEPARATOR, nullptr, nullptr, {} },
{ ACTIVITY_FILTER_ACTIVE, nullptr, N_("Active"), "system-run" },
{ ACTIVITY_FILTER_DOWNLOADING, "Verb", NC_("Verb", "Downloading"), "network-receive" },
{ ACTIVITY_FILTER_SEEDING, "Verb", NC_("Verb", "Seeding"), "network-transmit" },
{ ACTIVITY_FILTER_PAUSED, nullptr, N_("Paused"), "media-playback-pause" },
{ ACTIVITY_FILTER_FINISHED, nullptr, N_("Finished"), "media-playback-stop" },
{ ACTIVITY_FILTER_VERIFYING, "Verb", NC_("Verb", "Verifying"), "view-refresh" },
{ ACTIVITY_FILTER_ERROR, nullptr, N_("Error"), "dialog-error" },
static auto constexpr types = std::array<FilterTypeInfo, 9>({ {
{ ActivityType::ALL, nullptr, N_("All"), nullptr },
{ ActivityType{ -1 }, nullptr, nullptr, nullptr },
{ ActivityType::ACTIVE, nullptr, N_("Active"), "system-run" },
{ ActivityType::DOWNLOADING, "Verb", NC_("Verb", "Downloading"), "network-receive" },
{ ActivityType::SEEDING, "Verb", NC_("Verb", "Seeding"), "network-transmit" },
{ ActivityType::PAUSED, nullptr, N_("Paused"), "media-playback-pause" },
{ ActivityType::FINISHED, nullptr, N_("Finished"), "media-playback-stop" },
{ ActivityType::VERIFYING, "Verb", NC_("Verb", "Verifying"), "view-refresh" },
{ ActivityType::ERROR, nullptr, N_("Error"), "dialog-error" },
} });
auto store = Gtk::ListStore::create(activity_filter_cols);
@ -549,8 +471,8 @@ Glib::RefPtr<Gtk::ListStore> FilterBar::Impl::activity_filter_model_new()
Glib::ustring();
auto const iter = store->append();
iter->set_value(activity_filter_cols.name, name);
iter->set_value(activity_filter_cols.type, type.type);
iter->set_value(activity_filter_cols.icon_name, type.icon_name);
iter->set_value(activity_filter_cols.type, static_cast<int>(type.type));
iter->set_value(activity_filter_cols.icon_name, Glib::ustring(type.icon_name != nullptr ? type.icon_name : ""));
}
return store;
@ -560,17 +482,9 @@ void FilterBar::Impl::render_activity_pixbuf_func(
Gtk::CellRendererPixbuf& cell_renderer,
Gtk::TreeModel::const_iterator const& iter)
{
auto const type = iter->get_value(activity_filter_cols.type);
cell_renderer.property_width() = type == ACTIVITY_FILTER_ALL ? 0 : 20;
cell_renderer.property_ypad() = type == ACTIVITY_FILTER_ALL ? 0 : 2;
}
void FilterBar::Impl::activity_model_update_idle()
{
if (!activity_model_update_tag_.connected())
{
activity_model_update_tag_ = Glib::signal_idle().connect([this]() { return activity_filter_model_update(); });
}
auto const type = ActivityType{ iter->get_value(activity_filter_cols.type) };
cell_renderer.property_width() = type == ActivityType::ALL ? 0 : 20;
cell_renderer.property_ypad() = type == ActivityType::ALL ? 0 : 2;
}
void FilterBar::Impl::activity_combo_box_init(Gtk::ComboBox& combo)
@ -597,100 +511,45 @@ void FilterBar::Impl::activity_combo_box_init(Gtk::ComboBox& combo)
combo.pack_end(*r, true);
combo.set_cell_data_func(*r, [r](auto const& iter) { render_number_func(*r, iter); });
}
activity_model_row_changed_tag_ = torrent_model_->signal_row_changed().connect( //
[this](auto const& /*path*/, auto const& /*iter*/) { activity_model_update_idle(); });
activity_model_row_inserted_tag_ = torrent_model_->signal_row_inserted().connect( //
[this](auto const& /*path*/, auto const& /*iter*/) { activity_model_update_idle(); });
activity_model_row_deleted_cb_tag_ = torrent_model_->signal_row_deleted().connect( //
[this](auto const& /*path*/) { activity_model_update_idle(); });
}
/****
*****
***** ENTRY FIELD
*****
****/
bool FilterBar::Impl::testText(tr_torrent const& tor, Glib::ustring const& key)
void FilterBar::Impl::update_filter_text()
{
bool ret = false;
if (key.empty())
{
ret = true;
}
else
{
/* test the torrent name... */
ret = Glib::ustring(tr_torrentName(&tor)).casefold().find(key) != Glib::ustring::npos;
/* test the files... */
for (tr_file_index_t i = 0, n = tr_torrentFileCount(&tor); i < n && !ret; ++i)
{
ret = Glib::ustring(tr_torrentFile(&tor, i).name).casefold().find(key) != Glib::ustring::npos;
}
}
return ret;
filter_->set_text(entry_->get_text());
}
void FilterBar::Impl::filter_entry_changed()
{
filter_text_ = gtr_str_strip(entry_->get_text().casefold());
filter_model_->refilter();
}
/*****
******
******
******
*****/
bool FilterBar::Impl::is_row_visible(Gtk::TreeModel::const_iterator const& iter)
{
auto* const tor = static_cast<tr_torrent*>(iter->get_value(torrent_cols.torrent));
return tor != nullptr && test_tracker(*tor, active_tracker_type_, active_tracker_sitename_) &&
test_torrent_activity(*tor, active_activity_type_) && testText(*tor, filter_text_);
}
void FilterBar::Impl::selection_changed_cb()
void FilterBar::Impl::update_filter_activity()
{
/* set active_activity_type_ from the activity combobox */
if (auto const iter = activity_->get_active(); iter)
{
active_activity_type_ = iter->get_value(activity_filter_cols.type);
filter_->set_activity(ActivityType{ iter->get_value(activity_filter_cols.type) });
}
else
{
active_activity_type_ = ACTIVITY_FILTER_ALL;
filter_->set_activity(ActivityType::ALL);
}
}
void FilterBar::Impl::update_filter_tracker()
{
/* set the active tracker type & host from the tracker combobox */
if (auto const iter = tracker_->get_active(); iter)
{
active_tracker_type_ = iter->get_value(tracker_filter_cols.type);
active_tracker_sitename_ = iter->get_value(tracker_filter_cols.sitename);
filter_->set_tracker(
static_cast<TrackerType>(iter->get_value(tracker_filter_cols.type)),
iter->get_value(tracker_filter_cols.sitename));
}
else
{
active_tracker_type_ = TRACKER_FILTER_TYPE_ALL;
active_tracker_sitename_.clear();
filter_->set_tracker(TrackerType::ALL, {});
}
/* refilter */
filter_model_->refilter();
}
/***
****
***/
bool FilterBar::Impl::update_count_label()
{
/* get the visible count */
auto const visibleCount = static_cast<int>(filter_model_->children().size());
auto const visibleCount = static_cast<int>(filter_model_->IF_GTKMM4(get_n_items(), children().size()));
/* get the tracker count */
int trackerCount = 0;
@ -723,6 +582,39 @@ void FilterBar::Impl::update_count_label_idle()
}
}
void FilterBar::Impl::update_filter_models(Torrent::ChangeFlags changes)
{
static auto constexpr activity_flags = Torrent::ChangeFlag::ACTIVE_PEERS_DOWN | Torrent::ChangeFlag::ACTIVE_PEERS_UP |
Torrent::ChangeFlag::ACTIVE | Torrent::ChangeFlag::ACTIVITY | Torrent::ChangeFlag::ERROR_CODE |
Torrent::ChangeFlag::FINISHED;
static auto constexpr tracker_flags = Torrent::ChangeFlag::TRACKERS;
if (changes.test(activity_flags))
{
activity_filter_model_update();
}
if (changes.test(tracker_flags))
{
tracker_filter_model_update();
}
filter_->update(changes);
}
void FilterBar::Impl::update_filter_models_idle(Torrent::ChangeFlags changes)
{
if (!update_filter_models_tag_.connected())
{
update_filter_models_tag_ = Glib::signal_idle().connect(
[this, changes]()
{
update_filter_models(changes);
return false;
});
}
}
/***
****
***/
@ -761,20 +653,18 @@ FilterBar::FilterBar()
FilterBar::FilterBar(
BaseObjectType* cast_item,
Glib::RefPtr<Gtk::Builder> const& /*builder*/,
tr_session* session,
Glib::RefPtr<Gtk::TreeModel> const& torrent_model)
Glib::RefPtr<Session> const& core)
: Glib::ObjectBase(typeid(FilterBar))
, Gtk::Box(cast_item)
, impl_(std::make_unique<Impl>(*this, session, torrent_model))
, impl_(std::make_unique<Impl>(*this, core))
{
}
FilterBar::~FilterBar() = default;
FilterBar::Impl::Impl(FilterBar& widget, tr_session* session, Glib::RefPtr<Gtk::TreeModel> const& torrent_model)
FilterBar::Impl::Impl(FilterBar& widget, Glib::RefPtr<Session> const& core)
: widget_(widget)
, session_(session)
, torrent_model_(torrent_model)
, core_(core)
, activity_model_(activity_filter_model_new())
, tracker_model_(tracker_filter_model_new())
, activity_(get_template_child<Gtk::ComboBox>("activity_combo"))
@ -782,58 +672,62 @@ FilterBar::Impl::Impl(FilterBar& widget, tr_session* session, Glib::RefPtr<Gtk::
, entry_(get_template_child<Gtk::Entry>("text_entry"))
, show_lb_(get_template_child<Gtk::Label>("show_label"))
{
update_filter_models_on_add_remove_tag_ = core_->get_model()->signal_items_changed().connect(
[this](guint /*position*/, guint /*removed*/, guint /*added*/) { update_filter_models_idle(~Torrent::ChangeFlags()); });
update_filter_models_on_change_tag_ = core_->signal_torrents_changed().connect(
sigc::hide<0>(sigc::mem_fun(*this, &Impl::update_filter_models_idle)));
activity_filter_model_update();
tracker_filter_model_update();
activity_combo_box_init(*activity_);
tracker_combo_box_init(*tracker_);
filter_model_ = Gtk::TreeModelFilter::create(torrent_model);
filter_model_row_deleted_tag_ = filter_model_->signal_row_deleted().connect([this](auto const& /*path*/)
{ update_count_label_idle(); });
filter_model_row_inserted_tag_ = filter_model_->signal_row_inserted().connect(
[this](auto const& /*path*/, auto const& /*iter*/) { update_count_label_idle(); });
#if GTKMM_CHECK_VERSION(4, 0, 0)
filter_model_ = Gtk::FilterListModel::create(core_->get_sorted_model(), filter_);
filter_model_items_changed_tag_ = filter_model_->signal_items_changed().connect(
[this](guint /*position*/, guint /*removed*/, guint /*added*/) { update_count_label_idle(); });
#else
static auto const& self_col = Torrent::get_columns().self;
filter_model_->set_visible_func(sigc::mem_fun(*this, &Impl::is_row_visible));
auto const filter_func = [this](FilterModel::const_iterator const& iter)
{
return filter_->match(*iter->get_value(self_col));
};
tracker_->signal_changed().connect(sigc::mem_fun(*this, &Impl::selection_changed_cb));
activity_->signal_changed().connect(sigc::mem_fun(*this, &Impl::selection_changed_cb));
filter_model_ = Gtk::TreeModelFilter::create(core_->get_sorted_model());
filter_model_->set_visible_func(filter_func);
filter_->signal_changed().connect([this]() { filter_model_->refilter(); });
#endif
tracker_->signal_changed().connect(sigc::mem_fun(*this, &Impl::update_filter_tracker));
activity_->signal_changed().connect(sigc::mem_fun(*this, &Impl::update_filter_activity));
#if GTKMM_CHECK_VERSION(4, 0, 0)
entry_->signal_icon_release().connect([this](auto /*icon_position*/) { entry_->set_text({}); });
#else
entry_->signal_icon_release().connect([this](auto /*icon_position*/, auto const* /*event*/) { entry_->set_text({}); });
#endif
entry_->signal_changed().connect(sigc::mem_fun(*this, &Impl::filter_entry_changed));
selection_changed_cb();
update_count_label();
entry_->signal_changed().connect(sigc::mem_fun(*this, &Impl::update_filter_text));
}
FilterBar::Impl::~Impl()
{
update_filter_models_on_change_tag_.disconnect();
update_filter_models_on_add_remove_tag_.disconnect();
update_filter_models_tag_.disconnect();
update_count_label_tag_.disconnect();
filter_model_row_deleted_tag_.disconnect();
filter_model_row_inserted_tag_.disconnect();
activity_model_update_tag_.disconnect();
tracker_model_row_deleted_cb_tag_.disconnect();
tracker_model_row_inserted_tag_.disconnect();
tracker_model_row_changed_tag_.disconnect();
activity_model_update_tag_.disconnect();
activity_model_row_deleted_cb_tag_.disconnect();
activity_model_row_inserted_tag_.disconnect();
activity_model_row_changed_tag_.disconnect();
#if GTKMM_CHECK_VERSION(4, 0, 0)
filter_model_items_changed_tag_.disconnect();
#endif
}
Glib::RefPtr<Gtk::TreeModel> FilterBar::get_filter_model() const
Glib::RefPtr<FilterBar::Model> FilterBar::get_filter_model() const
{
return impl_->get_filter_model();
}
Glib::RefPtr<Gtk::TreeModel> FilterBar::Impl::get_filter_model() const
Glib::RefPtr<FilterBar::Impl::FilterModel> FilterBar::Impl::get_filter_model() const
{
return filter_model_;
}

View File

@ -12,7 +12,9 @@
#include <libtransmission/tr-macros.h>
typedef struct tr_session tr_session;
#include "Utils.h"
class Session;
class FilterBarExtraInit : public Glib::ExtraClassInit
{
@ -28,18 +30,17 @@ class FilterBar
: public FilterBarExtraInit
, public Gtk::Box
{
public:
using Model = IF_GTKMM4(Gio::ListModel, Gtk::TreeModel);
public:
FilterBar();
FilterBar(
BaseObjectType* cast_item,
Glib::RefPtr<Gtk::Builder> const& builder,
tr_session* session,
Glib::RefPtr<Gtk::TreeModel> const& torrent_model);
FilterBar(BaseObjectType* cast_item, Glib::RefPtr<Gtk::Builder> const& builder, Glib::RefPtr<Session> const& core);
~FilterBar() override;
TR_DISABLE_COPY_MOVE(FilterBar)
Glib::RefPtr<Gtk::TreeModel> get_filter_model() const;
Glib::RefPtr<Model> get_filter_model() const;
private:
class Impl;

96
gtk/Flags.h Normal file
View File

@ -0,0 +1,96 @@
// This file Copyright © 2022 Mnemosyne LLC.
// It may be used under GPLv2 (SPDX: GPL-2.0-only), GPLv3 (SPDX: GPL-3.0-only),
// or any future license endorsed by Mnemosyne LLC.
// License text can be found in the licenses/ folder.
#pragma once
#include <initializer_list>
#include <type_traits>
#define DEFINE_FLAGS_OPERATORS(FlagType) \
constexpr inline Flags<FlagType> operator|(FlagType lhs, FlagType rhs) noexcept \
{ \
return { lhs, rhs }; \
}
template<typename T>
class Flags
{
public:
using FlagType = T;
using ValueType = std::underlying_type_t<FlagType>;
static_assert(std::is_enum_v<FlagType> && !std::is_convertible_v<FlagType, ValueType>);
public:
constexpr Flags() noexcept = default;
constexpr Flags(FlagType flag) noexcept
{
set(flag);
}
constexpr Flags(std::initializer_list<FlagType> flags) noexcept
{
for (auto const flag : flags)
{
set(flag);
}
}
constexpr bool none() const noexcept
{
return value_ == 0;
}
constexpr bool any() const noexcept
{
return !none();
}
constexpr bool test(FlagType flag) const noexcept
{
return (value_ & get_mask(flag)) != 0;
}
constexpr bool test(Flags rhs) const noexcept
{
return (value_ & rhs.value_) != 0;
}
constexpr void set(FlagType flag) noexcept
{
value_ |= get_mask(flag);
}
constexpr Flags operator|(Flags rhs) noexcept
{
return Flags(value_ | rhs.value_);
}
constexpr Flags& operator|=(Flags rhs) noexcept
{
value_ |= rhs.value_;
return *this;
}
constexpr Flags operator~() const noexcept
{
return Flags(~value_);
}
private:
constexpr explicit Flags(ValueType value) noexcept
: value_(value)
{
}
static constexpr ValueType get_mask(FlagType flag) noexcept
{
return ValueType{ 1 } << static_cast<ValueType>(flag);
}
private:
ValueType value_ = {};
};

281
gtk/ListModelAdapter.cc Normal file
View File

@ -0,0 +1,281 @@
// This file Copyright © 2022 Mnemosyne LLC.
// It may be used under GPLv2 (SPDX: GPL-2.0-only), GPLv3 (SPDX: GPL-3.0-only),
// or any future license endorsed by Mnemosyne LLC.
// License text can be found in the licenses/ folder.
#include "ListModelAdapter.h"
namespace
{
template<typename T>
int iter_get_stamp(T const& iter)
{
return iter.gobj()->stamp;
}
template<typename T>
void iter_set_stamp(T& iter, int value)
{
iter.gobj()->stamp = value;
}
template<typename T>
int iter_get_item_id(T const& iter)
{
return GPOINTER_TO_INT(iter.gobj()->user_data);
}
template<typename T>
void iter_set_item_id(T& iter, int value)
{
iter.gobj()->user_data = GINT_TO_POINTER(value);
}
template<typename T>
void iter_clear(T& iter)
{
iter_set_stamp(iter, 0);
iter_set_item_id(iter, 0);
}
} // namespace
ListModelAdapter::ListModelAdapter(
Glib::RefPtr<Gio::ListModel> const& adaptee,
Gtk::TreeModelColumnRecord const& columns,
IdGetter id_getter,
ValueGetter value_getter)
: Glib::ObjectBase(typeid(ListModelAdapter))
, adaptee_(adaptee)
, columns_(columns)
, id_getter_(std::move(id_getter))
, value_getter_(std::move(value_getter))
{
adaptee_->signal_items_changed().connect(sigc::mem_fun(*this, &ListModelAdapter::on_adaptee_items_changed));
on_adaptee_items_changed(0, 0, adaptee_->get_n_items());
}
ListModelAdapter::TrTreeModelFlags ListModelAdapter::get_flags_vfunc() const
{
return TR_GTK_TREE_MODEL_FLAGS(ITERS_PERSIST) | TR_GTK_TREE_MODEL_FLAGS(LIST_ONLY);
}
int ListModelAdapter::get_n_columns_vfunc() const
{
return columns_.size();
}
GType ListModelAdapter::get_column_type_vfunc(int index) const
{
g_return_val_if_fail(index >= 0, G_TYPE_INVALID);
g_return_val_if_fail(index < get_n_columns_vfunc(), G_TYPE_INVALID);
return columns_.types()[index];
}
bool ListModelAdapter::iter_next_vfunc(iterator const& iter, iterator& iter_next) const
{
iter_clear(iter_next);
if (iter)
{
g_return_val_if_fail(iter_get_stamp(iter) == stamp_, false);
if (auto const position = find_item_position_by_id(iter_get_item_id(iter)); position.has_value())
{
if (auto const next_position = position.value() + 1; next_position < items_.size())
{
iter_set_stamp(iter_next, stamp_);
iter_set_item_id(iter_next, items_.at(next_position).id);
return true;
}
}
}
return false;
}
bool ListModelAdapter::get_iter_vfunc(Path const& path, iterator& iter) const
{
iter_clear(iter);
g_return_val_if_fail(path.size() == 1, false);
return iter_nth_root_child_vfunc(path.front(), iter);
}
bool ListModelAdapter::iter_children_vfunc(iterator const& parent, iterator& iter) const
{
iter_clear(iter);
if (parent || items_.empty())
{
return false;
}
iter_set_stamp(iter, stamp_);
iter_set_item_id(iter, items_.front().id);
return true;
}
bool ListModelAdapter::iter_parent_vfunc(iterator const& /*child*/, iterator& iter) const
{
iter_clear(iter);
return false;
}
bool ListModelAdapter::iter_nth_root_child_vfunc(int position, iterator& iter) const
{
iter_clear(iter);
g_return_val_if_fail(position >= 0, false);
if (position >= iter_n_root_children_vfunc())
{
return false;
}
iter_set_stamp(iter, stamp_);
iter_set_item_id(iter, items_.at(position).id);
return true;
}
bool ListModelAdapter::iter_has_child_vfunc(const_iterator const& /*iter*/) const
{
return false;
}
int ListModelAdapter::iter_n_root_children_vfunc() const
{
return items_.size();
}
Gtk::TreeModel::Path ListModelAdapter::get_path_vfunc(const_iterator const& iter) const
{
auto path = Path();
if (iter)
{
g_return_val_if_fail(iter_get_stamp(iter) == stamp_, path);
if (auto const position = find_item_position_by_id(iter_get_item_id(iter)); position.has_value())
{
path.push_back(position.value());
}
}
return path;
}
void ListModelAdapter::get_value_vfunc(const_iterator const& iter, int column, Glib::ValueBase& value) const
{
g_return_if_fail(column >= 0);
g_return_if_fail(column < get_n_columns_vfunc());
value.init(get_column_type_vfunc(column));
if (!iter)
{
return;
}
auto const position = find_item_position_by_id(iter_get_item_id(iter));
if (!position.has_value())
{
return;
}
auto const item = adaptee_->get_object(position.value());
if (item == nullptr)
{
return;
}
value_getter_(item, column, value);
}
std::optional<guint> ListModelAdapter::find_item_position_by_id(int item_id) const
{
auto const item_position_it = item_positions_.find(item_id);
return item_position_it != item_positions_.end() ? std::make_optional(item_position_it->second) : std::nullopt;
}
void ListModelAdapter::adjust_item_positions(guint min_position, int delta)
{
for (auto item_it = std::next(items_.begin(), min_position); item_it != items_.end(); ++item_it)
{
if (auto const item_position_it = item_positions_.find(item_it->id); item_position_it != item_positions_.end())
{
item_position_it->second += delta;
}
}
}
void ListModelAdapter::on_adaptee_items_changed(guint position, guint removed, guint added)
{
g_assert(position + removed <= items_.size());
g_assert(position + added <= adaptee_->get_n_items());
for (auto i = 0U; i < removed; ++i)
{
auto const removed_position = position + removed - i - 1;
auto info = items_.at(removed_position);
items_.erase(std::next(items_.begin(), removed_position));
info.notify_tag.disconnect();
item_positions_.erase(info.id);
adjust_item_positions(removed_position, -1);
auto path = Path();
path.push_back(removed_position);
row_deleted(path);
}
for (auto i = 0U; i < added; ++i)
{
auto const added_position = position + i;
auto const item = adaptee_->get_object(added_position);
auto const info = ItemInfo{
.id = id_getter_(item),
.notify_tag = gtr_object_signal_notify(*item.get())
.connect(sigc::mem_fun(*this, &ListModelAdapter::on_adaptee_item_changed)),
};
items_.insert(std::next(items_.begin(), added_position), info);
adjust_item_positions(added_position, 1);
item_positions_.emplace(info.id, added_position);
auto path = Path();
path.push_back(added_position);
auto iter = iterator(this);
iter_set_stamp(iter, stamp_);
iter_set_item_id(iter, info.id);
row_inserted(path, iter);
}
}
void ListModelAdapter::on_adaptee_item_changed(Glib::RefPtr<Glib::ObjectBase const> const& item)
{
g_return_if_fail(item != nullptr);
auto const item_id = id_getter_(item);
if (auto const position = find_item_position_by_id(item_id); position.has_value())
{
auto path = Path();
path.push_back(position.value());
auto iter = iterator(this);
iter_set_stamp(iter, stamp_);
iter_set_item_id(iter, item_id);
row_changed(path, iter);
}
}

77
gtk/ListModelAdapter.h Normal file
View File

@ -0,0 +1,77 @@
// This file Copyright © 2022 Mnemosyne LLC.
// It may be used under GPLv2 (SPDX: GPL-2.0-only), GPLv3 (SPDX: GPL-3.0-only),
// or any future license endorsed by Mnemosyne LLC.
// License text can be found in the licenses/ folder.
#pragma once
#include <optional>
#include <unordered_map>
#include <vector>
#include <glibmm.h>
#include <gtkmm.h>
#include "Utils.h"
class ListModelAdapter
: public Gtk::TreeModel
, public Glib::Object
{
using IdGetter = std::function<int(Glib::RefPtr<Glib::ObjectBase const> const&)>;
using ValueGetter = std::function<void(Glib::RefPtr<Glib::ObjectBase const> const&, int, Glib::ValueBase&)>;
struct ItemInfo
{
int id = 0;
sigc::connection notify_tag;
};
using TrTreeModelFlags = IF_GTKMM4(Gtk::TreeModel::Flags, Gtk::TreeModelFlags);
public:
template<typename T>
static Glib::RefPtr<ListModelAdapter> create(Glib::RefPtr<Gio::ListModel> const& adaptee)
{
return Glib::make_refptr_for_instance(
new ListModelAdapter(adaptee, T::get_columns(), &T::get_item_id, &T::get_item_value));
}
protected:
// Gtk::TreeModel
TrTreeModelFlags get_flags_vfunc() const override;
int get_n_columns_vfunc() const override;
GType get_column_type_vfunc(int index) const override;
bool iter_next_vfunc(iterator const& iter, iterator& iter_next) const override;
bool get_iter_vfunc(Path const& path, iterator& iter) const override;
bool iter_children_vfunc(iterator const& parent, iterator& iter) const override;
bool iter_parent_vfunc(iterator const& child, iterator& iter) const override;
bool iter_nth_root_child_vfunc(int position, iterator& iter) const override;
bool iter_has_child_vfunc(const_iterator const& iter) const override;
int iter_n_root_children_vfunc() const override;
TreeModel::Path get_path_vfunc(const_iterator const& iter) const override;
void get_value_vfunc(const_iterator const& iter, int column, Glib::ValueBase& value) const override;
private:
ListModelAdapter(
Glib::RefPtr<Gio::ListModel> const& adaptee,
Gtk::TreeModelColumnRecord const& columns,
IdGetter id_getter,
ValueGetter value_getter);
std::optional<guint> find_item_position_by_id(int item_id) const;
void adjust_item_positions(guint min_position, int delta);
void on_adaptee_items_changed(guint position, guint removed, guint added);
void on_adaptee_item_changed(Glib::RefPtr<Glib::ObjectBase const> const& item);
private:
Glib::RefPtr<Gio::ListModel> const adaptee_;
Gtk::TreeModelColumnRecord const& columns_;
IdGetter const id_getter_;
ValueGetter const value_getter_;
int const stamp_ = 1;
std::vector<ItemInfo> items_;
std::unordered_map<int, guint> mutable item_positions_;
};

View File

@ -13,11 +13,12 @@
#include "Actions.h"
#include "FilterBar.h"
#include "HigWorkarea.h" // GUI_PAD_SMALL
#include "ListModelAdapter.h"
#include "MainWindow.h"
#include "Prefs.h"
#include "PrefsDialog.h"
#include "Session.h"
#include "Torrent.h"
#include "TorrentCellRenderer.h"
#include "Utils.h"
@ -61,8 +62,13 @@ public:
void prefsChanged(tr_quark key);
auto& signal_selection_changed()
{
return signal_selection_changed_;
}
private:
void init_view(Gtk::TreeView* view, Glib::RefPtr<Gtk::TreeModel> const& model);
void init_view(Gtk::TreeView* 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);
@ -92,12 +98,17 @@ private:
MainWindow& window_;
Glib::RefPtr<Session> const core_;
sigc::signal<void()> signal_selection_changed_;
Glib::RefPtr<Gio::ActionGroup> options_actions_;
Glib::RefPtr<Gio::ActionGroup> stats_actions_;
std::array<OptionMenuInfo, 2> speed_menu_info_;
OptionMenuInfo ratio_menu_info_;
TorrentCellRenderer* renderer_ = nullptr;
Gtk::TreeViewColumn* column_ = nullptr;
Gtk::ScrolledWindow* scroll_ = nullptr;
Gtk::TreeView* view_ = nullptr;
Gtk::Widget* toolbar_ = nullptr;
@ -108,8 +119,6 @@ private:
Gtk::Label* stats_lb_ = nullptr;
Gtk::Image* alt_speed_image_ = nullptr;
Gtk::ToggleButton* alt_speed_button_ = nullptr;
TorrentCellRenderer* renderer_ = nullptr;
Gtk::TreeViewColumn* column_ = nullptr;
sigc::connection pref_handler_id_;
IF_GTKMM4(Gtk::PopoverMenu*, Gtk::Menu*) popup_menu_ = nullptr;
};
@ -158,14 +167,18 @@ bool tree_view_search_equal_func(
Glib::ustring const& key,
Gtk::TreeModel::const_iterator const& iter)
{
auto const name = iter->get_value(torrent_cols.name_collated);
static auto const& self_col = Torrent::get_columns().self;
auto const name = iter->get_value(self_col)->get_name_collated();
return name.find(key.lowercase()) == Glib::ustring::npos;
}
} // namespace
void MainWindow::Impl::init_view(Gtk::TreeView* view, Glib::RefPtr<Gtk::TreeModel> const& model)
void MainWindow::Impl::init_view(Gtk::TreeView* view, Glib::RefPtr<FilterBar::Model> const& model)
{
static auto const& torrent_cols = Torrent::get_columns();
view->set_search_column(torrent_cols.name_collated);
view->set_search_equal_func(&tree_view_search_equal_func);
@ -173,12 +186,7 @@ void MainWindow::Impl::init_view(Gtk::TreeView* view, Glib::RefPtr<Gtk::TreeMode
renderer_ = Gtk::make_managed<TorrentCellRenderer>();
column_->pack_start(*renderer_, false);
column_->add_attribute(renderer_->property_torrent(), torrent_cols.torrent);
column_->add_attribute(renderer_->property_piece_upload_speed(), torrent_cols.speed_up);
column_->add_attribute(renderer_->property_piece_download_speed(), torrent_cols.speed_down);
renderer_->property_xpad() = GUI_PAD_SMALL;
renderer_->property_ypad() = GUI_PAD_SMALL;
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); });
@ -198,7 +206,9 @@ void MainWindow::Impl::init_view(Gtk::TreeView* view, Glib::RefPtr<Gtk::TreeMode
view->signal_row_activated().connect([](auto const& /*path*/, auto* /*column*/)
{ gtr_action_activate("show-torrent-properties"); });
view->set_model(model);
view->set_model(IF_GTKMM4(ListModelAdapter::create<Torrent>(model), model));
view->get_selection()->signal_changed().connect([this]() { signal_selection_changed_.emit(); });
}
void MainWindow::Impl::prefsChanged(tr_quark const key)
@ -537,7 +547,7 @@ MainWindow::Impl::Impl(
, scroll_(gtr_get_widget<Gtk::ScrolledWindow>(builder, "torrents_view_scroll"))
, view_(gtr_get_widget<Gtk::TreeView>(builder, "torrents_view"))
, toolbar_(gtr_get_widget<Gtk::Widget>(builder, "toolbar"))
, filter_(gtr_get_widget_derived<FilterBar>(builder, "filterbar", core_->get_session(), core_->get_model()))
, filter_(gtr_get_widget_derived<FilterBar>(builder, "filterbar", core_))
, status_(gtr_get_widget<Gtk::Widget>(builder, "statusbar"))
, ul_lb_(gtr_get_widget<Gtk::Label>(builder, "upload_speed_label"))
, dl_lb_(gtr_get_widget<Gtk::Label>(builder, "download_speed_label"))
@ -678,12 +688,13 @@ void MainWindow::Impl::updateSpeeds()
auto up_speed = double{};
auto const model = core_->get_model();
for (auto const& row : model->children())
for (auto i = 0U, count = model->get_n_items(); i < count; ++i)
{
dn_count += row.get_value(torrent_cols.active_peers_down);
dn_speed += row.get_value(torrent_cols.speed_down);
up_count += row.get_value(torrent_cols.active_peers_up);
up_speed += row.get_value(torrent_cols.speed_up);
auto const torrent = gtr_ptr_dynamic_cast<Torrent>(model->get_object(i));
dn_count += torrent->get_active_peers_down();
dn_speed += torrent->get_speed_down();
up_count += torrent->get_active_peers_up();
up_speed += torrent->get_speed_up();
}
dl_lb_->set_text(fmt::format(_("{download_speed} ▼"), fmt::arg("download_speed", tr_formatter_speed_KBps(dn_speed))));
@ -708,16 +719,48 @@ void MainWindow::Impl::refresh()
}
}
Glib::RefPtr<Gtk::TreeSelection> MainWindow::get_selection() const
{
return impl_->get_selection();
}
Glib::RefPtr<Gtk::TreeSelection> MainWindow::Impl::get_selection() const
{
return view_->get_selection();
}
void MainWindow::for_each_selected_torrent(std::function<void(Glib::RefPtr<Torrent> const&)> callback) const
{
for_each_selected_torrent_until(sigc::bind_return(callback, false));
}
bool MainWindow::for_each_selected_torrent_until(std::function<bool(Glib::RefPtr<Torrent> 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;
for (auto const& path : selection->get_selected_rows())
{
auto const torrent = Glib::make_refptr_for_instance(model->get_iter(path)->get_value(self_col));
torrent->reference();
if (callback(torrent))
{
result = true;
break;
}
}
return result;
}
void MainWindow::select_all()
{
impl_->get_selection()->select_all();
}
void MainWindow::unselect_all()
{
impl_->get_selection()->unselect_all();
}
void MainWindow::set_busy(bool isBusy)
{
if (get_realized())
@ -733,3 +776,8 @@ void MainWindow::set_busy(bool isBusy)
#endif
}
}
sigc::signal<void()>& MainWindow::signal_selection_changed()
{
return impl_->signal_selection_changed();
}

View File

@ -12,6 +12,7 @@
#include <libtransmission/tr-macros.h>
class Session;
class Torrent;
class MainWindow : public Gtk::ApplicationWindow
{
@ -31,11 +32,17 @@ public:
Glib::RefPtr<Gio::ActionGroup> const& actions,
Glib::RefPtr<Session> const& core);
Glib::RefPtr<Gtk::TreeSelection> get_selection() const;
void for_each_selected_torrent(std::function<void(Glib::RefPtr<Torrent> const&)> callback) const;
bool for_each_selected_torrent_until(std::function<bool(Glib::RefPtr<Torrent> const&)> callback) const;
void select_all();
void unselect_all();
void set_busy(bool isBusy);
void refresh();
sigc::signal<void()>& signal_selection_changed();
private:
class Impl;
std::unique_ptr<Impl> const impl_;

View File

@ -120,7 +120,7 @@ void OptionsDialog::Impl::addResponseCB(int response)
tr_torrentStart(tor_);
}
core_->add_torrent(tor_, false);
core_->add_torrent(Torrent::create(tor_), false);
if (trash_check_->get_active())
{

View File

@ -31,10 +31,13 @@
#include <libtransmission/variant.h>
#include "Actions.h"
#include "ListModelAdapter.h"
#include "Notify.h"
#include "Prefs.h"
#include "PrefsDialog.h"
#include "Session.h"
#include "Torrent.h"
#include "TorrentSorter.h"
#include "Utils.h"
using namespace std::literals;
@ -61,44 +64,24 @@ TrVariantPtr create_variant(tr_variant& other)
return result;
}
class ScopedModelSortBlocker
{
public:
explicit ScopedModelSortBlocker(Gtk::TreeSortable& model)
: model_(model)
{
model_.get_sort_column_id(sort_column_id_, sort_type_);
model_.set_sort_column(Gtk::TreeSortable::DEFAULT_SORT_COLUMN_ID, TR_GTK_SORT_TYPE(ASCENDING));
}
~ScopedModelSortBlocker()
{
model_.set_sort_column(sort_column_id_, sort_type_);
}
TR_DISABLE_COPY_MOVE(ScopedModelSortBlocker)
private:
Gtk::TreeSortable& model_;
int sort_column_id_ = -1;
Gtk::SortType sort_type_ = TR_GTK_SORT_TYPE(ASCENDING);
};
} // namespace
class Session::Impl
{
using SortModel = IF_GTKMM4(Gtk::SortListModel, Gtk::TreeModelSort);
public:
Impl(Session& core, tr_session* session);
~Impl();
tr_session* close();
Glib::RefPtr<Gtk::ListStore> get_raw_model() const;
Glib::RefPtr<Gtk::TreeModelSort> get_model();
Glib::RefPtr<Gtk::TreeModelSort const> get_model() const;
Glib::RefPtr<Gio::ListStore<Torrent>> get_raw_model() const;
Glib::RefPtr<SortModel> get_model();
tr_session* get_session() const;
std::pair<Glib::RefPtr<Torrent>, guint> find_torrent_by_id(tr_torrent_id_t torrent_id) const;
size_t get_active_torrent_count() const;
void update();
@ -106,7 +89,7 @@ public:
void add_files(std::vector<Glib::RefPtr<Gio::File>> const& files, bool do_start, bool do_prompt, bool do_notify);
int add_ctor(tr_ctor* ctor, bool do_prompt, bool do_notify);
void add_torrent(tr_torrent* tor, bool do_notify);
void add_torrent(Glib::RefPtr<Torrent> const& torrent, bool do_notify);
bool add_from_url(Glib::ustring const& url);
void send_rpc_request(tr_variant const* request, int64_t tag, std::function<void(tr_variant&)> const& response_func);
@ -143,6 +126,11 @@ public:
return signal_port_tested_;
}
auto& signal_torrents_changed()
{
return signal_torrents_changed_;
}
private:
Glib::RefPtr<Session> get_core_ptr() const;
@ -159,9 +147,7 @@ private:
bool do_prompt,
bool do_notify);
tr_torrent* create_new_torrent(tr_ctor* ctor);
void set_sort_mode(std::string_view mode, bool is_reversed);
Glib::RefPtr<Torrent> create_new_torrent(tr_ctor* ctor);
void maybe_inhibit_hibernation();
void set_hibernation_allowed(bool allowed);
@ -178,7 +164,7 @@ private:
void on_pref_changed(tr_quark key);
void on_torrent_completeness_changed(tr_torrent* tor, tr_completeness completeness, bool was_running);
void on_torrent_metadata_changed(tr_torrent* tor);
void on_torrent_metadata_changed(tr_torrent* raw_torrent);
private:
Session& core_;
@ -189,6 +175,7 @@ private:
sigc::signal<void(bool)> signal_busy_;
sigc::signal<void(tr_quark)> signal_prefs_changed_;
sigc::signal<void(bool)> signal_port_tested_;
sigc::signal<void(std::unordered_set<tr_torrent_id_t> const&, Torrent::ChangeFlags)> signal_torrents_changed_;
Glib::RefPtr<Gio::FileMonitor> monitor_;
sigc::connection monitor_tag_;
@ -202,33 +189,12 @@ private:
bool dbus_error_ = false;
guint inhibit_cookie_ = 0;
gint busy_count_ = 0;
Glib::RefPtr<Gtk::ListStore> raw_model_;
Glib::RefPtr<Gtk::TreeModelSort> sorted_model_;
Glib::RefPtr<Gio::ListStore<Torrent>> raw_model_;
Glib::RefPtr<SortModel> sorted_model_;
Glib::RefPtr<TorrentSorter> sorter_ = TorrentSorter::create();
tr_session* session_ = nullptr;
};
TorrentModelColumns::TorrentModelColumns() noexcept
{
add(name_collated);
add(torrent);
add(torrent_id);
add(speed_up);
add(speed_down);
add(active_peers_up);
add(active_peers_down);
add(recheck_progress);
add(active);
add(activity);
add(finished);
add(priority);
add(queue_position);
add(trackers);
add(error);
add(active_peer_count);
}
TorrentModelColumns const torrent_cols;
Glib::RefPtr<Session> Session::Impl::get_core_ptr() const
{
core_.reference();
@ -239,22 +205,22 @@ Glib::RefPtr<Session> Session::Impl::get_core_ptr() const
****
***/
Glib::RefPtr<Gtk::ListStore> Session::Impl::get_raw_model() const
Glib::RefPtr<Gio::ListStore<Torrent>> Session::Impl::get_raw_model() const
{
return raw_model_;
}
Glib::RefPtr<Gtk::TreeModel> Session::get_model() const
Glib::RefPtr<Gio::ListModel> Session::get_model() const
{
return impl_->get_raw_model();
}
Glib::RefPtr<Session::Model> Session::get_sorted_model() const
{
return impl_->get_model();
}
Glib::RefPtr<Gtk::TreeModelSort> Session::Impl::get_model()
{
return sorted_model_;
}
Glib::RefPtr<Gtk::TreeModelSort const> Session::Impl::get_model() const
Glib::RefPtr<Session::Impl::SortModel> Session::Impl::get_model()
{
return sorted_model_;
}
@ -300,278 +266,6 @@ void Session::Impl::dec_busy()
add_to_busy(-1);
}
/***
****
**** SORTING THE MODEL
****
***/
namespace
{
template<typename T>
// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
constexpr int compare_generic(T const& a, T const& b)
{
if (a < b)
{
return -1;
}
if (a > b)
{
return 1;
}
return 0;
}
constexpr bool is_valid_eta(time_t t)
{
return t != TR_ETA_NOT_AVAIL && t != TR_ETA_UNKNOWN;
}
// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
constexpr int compare_eta(time_t a, time_t b)
{
bool const a_valid = is_valid_eta(a);
bool const b_valid = is_valid_eta(b);
if (!a_valid && !b_valid)
{
return 0;
}
if (!a_valid)
{
return -1;
}
if (!b_valid)
{
return 1;
}
return -compare_generic(a, b);
}
// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
constexpr int compare_ratio(double a, double b)
{
if (static_cast<int>(a) == TR_RATIO_INF && static_cast<int>(b) == TR_RATIO_INF)
{
return 0;
}
if (static_cast<int>(a) == TR_RATIO_INF)
{
return 1;
}
if (static_cast<int>(b) == TR_RATIO_INF)
{
return -1;
}
return compare_generic(a, b);
}
// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
int compare_by_name(Gtk::TreeModel::const_iterator const& a, Gtk::TreeModel::const_iterator const& b)
{
return a->get_value(torrent_cols.name_collated).compare(b->get_value(torrent_cols.name_collated));
}
// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
int compare_by_queue(Gtk::TreeModel::const_iterator const& a, Gtk::TreeModel::const_iterator const& b)
{
auto const* const sa = tr_torrentStatCached(static_cast<tr_torrent*>(a->get_value(torrent_cols.torrent)));
auto const* const sb = tr_torrentStatCached(static_cast<tr_torrent*>(b->get_value(torrent_cols.torrent)));
return sb->queuePosition - sa->queuePosition;
}
// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
int compare_by_ratio(Gtk::TreeModel::const_iterator const& a, Gtk::TreeModel::const_iterator const& b)
{
int ret = 0;
auto const* const sa = tr_torrentStatCached(static_cast<tr_torrent*>(a->get_value(torrent_cols.torrent)));
auto const* const sb = tr_torrentStatCached(static_cast<tr_torrent*>(b->get_value(torrent_cols.torrent)));
if (ret == 0)
{
ret = compare_ratio(sa->ratio, sb->ratio);
}
if (ret == 0)
{
ret = compare_by_queue(a, b);
}
return ret;
}
// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
int compare_by_activity(Gtk::TreeModel::const_iterator const& a, Gtk::TreeModel::const_iterator const& b)
{
int ret = 0;
auto* const ta = static_cast<tr_torrent*>(a->get_value(torrent_cols.torrent));
auto* const tb = static_cast<tr_torrent*>(b->get_value(torrent_cols.torrent));
auto const aUp = a->get_value(torrent_cols.speed_up);
auto const aDown = a->get_value(torrent_cols.speed_down);
auto const bUp = b->get_value(torrent_cols.speed_up);
auto const bDown = b->get_value(torrent_cols.speed_down);
ret = compare_generic(aUp + aDown, bUp + bDown);
if (ret == 0)
{
auto const* const sa = tr_torrentStatCached(ta);
auto const* const sb = tr_torrentStatCached(tb);
ret = compare_generic(sa->peersSendingToUs + sa->peersGettingFromUs, sb->peersSendingToUs + sb->peersGettingFromUs);
}
if (ret == 0)
{
ret = compare_by_queue(a, b);
}
return ret;
}
// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
int compare_by_age(Gtk::TreeModel::const_iterator const& a, Gtk::TreeModel::const_iterator const& b)
{
auto* const ta = static_cast<tr_torrent*>(a->get_value(torrent_cols.torrent));
auto* const tb = static_cast<tr_torrent*>(b->get_value(torrent_cols.torrent));
int ret = compare_generic(tr_torrentStatCached(ta)->addedDate, tr_torrentStatCached(tb)->addedDate);
if (ret == 0)
{
ret = compare_by_name(a, b);
}
return ret;
}
// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
int compare_by_size(Gtk::TreeModel::const_iterator const& a, Gtk::TreeModel::const_iterator const& b)
{
auto const size_a = tr_torrentTotalSize(static_cast<tr_torrent*>(a->get_value(torrent_cols.torrent)));
auto const size_b = tr_torrentTotalSize(static_cast<tr_torrent*>(b->get_value(torrent_cols.torrent)));
int ret = compare_generic(size_a, size_b);
if (ret == 0)
{
ret = compare_by_name(a, b);
}
return ret;
}
// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
int compare_by_progress(Gtk::TreeModel::const_iterator const& a, Gtk::TreeModel::const_iterator const& b)
{
auto const* const sa = tr_torrentStatCached(static_cast<tr_torrent*>(a->get_value(torrent_cols.torrent)));
auto const* const sb = tr_torrentStatCached(static_cast<tr_torrent*>(b->get_value(torrent_cols.torrent)));
int ret = compare_generic(sa->percentComplete, sb->percentComplete);
if (ret == 0)
{
ret = compare_generic(sa->seedRatioPercentDone, sb->seedRatioPercentDone);
}
if (ret == 0)
{
ret = compare_by_ratio(a, b);
}
return ret;
}
// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
int compare_by_eta(Gtk::TreeModel::const_iterator const& a, Gtk::TreeModel::const_iterator const& b)
{
auto const* const sa = tr_torrentStatCached(static_cast<tr_torrent*>(a->get_value(torrent_cols.torrent)));
auto const* const sb = tr_torrentStatCached(static_cast<tr_torrent*>(b->get_value(torrent_cols.torrent)));
int ret = compare_eta(sa->eta, sb->eta);
if (ret == 0)
{
ret = compare_by_name(a, b);
}
return ret;
}
// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
int compare_by_state(Gtk::TreeModel::const_iterator const& a, Gtk::TreeModel::const_iterator const& b)
{
auto const sa = a->get_value(torrent_cols.activity);
auto const sb = b->get_value(torrent_cols.activity);
int ret = compare_generic(sa, sb);
if (ret == 0)
{
ret = compare_by_queue(a, b);
}
return ret;
}
} // namespace
void Session::Impl::set_sort_mode(std::string_view mode, bool is_reversed)
{
auto const& col = torrent_cols.torrent;
Gtk::TreeSortable::SlotCompare sort_func;
auto type = is_reversed ? TR_GTK_SORT_TYPE(ASCENDING) : TR_GTK_SORT_TYPE(DESCENDING);
auto const sortable = get_model();
if (mode == "sort-by-activity")
{
sort_func = &compare_by_activity;
}
else if (mode == "sort-by-age")
{
sort_func = &compare_by_age;
}
else if (mode == "sort-by-progress")
{
sort_func = &compare_by_progress;
}
else if (mode == "sort-by-queue")
{
sort_func = &compare_by_queue;
}
else if (mode == "sort-by-time-left")
{
sort_func = &compare_by_eta;
}
else if (mode == "sort-by-ratio")
{
sort_func = &compare_by_ratio;
}
else if (mode == "sort-by-state")
{
sort_func = &compare_by_state;
}
else if (mode == "sort-by-size")
{
sort_func = &compare_by_size;
}
else
{
sort_func = &compare_by_name;
type = is_reversed ? TR_GTK_SORT_TYPE(DESCENDING) : TR_GTK_SORT_TYPE(ASCENDING);
}
sortable->set_sort_func(col, sort_func);
sortable->set_sort_column(col, type);
}
/***
****
**** WATCHDIR
@ -773,13 +467,12 @@ void Session::Impl::on_pref_changed(tr_quark const key)
switch (key)
{
case TR_KEY_sort_mode:
sorter_->set_mode(gtr_pref_string_get(TR_KEY_sort_mode));
break;
case TR_KEY_sort_reversed:
{
auto const mode = gtr_pref_string_get(TR_KEY_sort_mode);
bool const is_reversed = gtr_pref_flag_get(TR_KEY_sort_reversed);
set_sort_mode(mode, is_reversed);
break;
}
sorter_->set_reversed(gtr_pref_flag_get(TR_KEY_sort_reversed));
break;
case TR_KEY_peer_limit_global:
tr_sessionSetPeerLimit(session_, gtr_pref_int_get(key));
@ -825,10 +518,22 @@ Session::Impl::Impl(Session& core, tr_session* session)
: core_(core)
, session_(session)
{
raw_model_ = Gtk::ListStore::create(torrent_cols);
sorted_model_ = Gtk::TreeModelSort::create(raw_model_);
sorted_model_->set_default_sort_func(
[](Gtk::TreeModel::const_iterator const& /*a*/, Gtk::TreeModel::const_iterator const& /*b*/) { return 0; });
raw_model_ = Gio::ListStore<Torrent>::create();
signal_torrents_changed_.connect(sigc::hide<0>(sigc::mem_fun(*sorter_.get(), &TorrentSorter::update)));
#if GTKMM_CHECK_VERSION(4, 0, 0)
sorted_model_ = Gtk::SortListModel::create(raw_model_, sorter_);
#else
static auto const& self_col = Torrent::get_columns().self;
auto const sort_func = [this](SortModel::const_iterator const& lhs, SortModel::const_iterator const& rhs)
{
return sorter_->compare(*lhs->get_value(self_col), *rhs->get_value(self_col));
};
sorted_model_ = Gtk::TreeModelSort::create(ListModelAdapter::create<Torrent>(raw_model_));
sorted_model_->set_default_sort_func(sort_func);
sorter_->signal_changed().connect([this, sort_func]() { sorted_model_->set_default_sort_func(sort_func); });
#endif
/* init from prefs & listen to pref changes */
on_pref_changed(TR_KEY_sort_mode);
@ -899,46 +604,48 @@ void Session::Impl::on_torrent_completeness_changed(tr_torrent* tor, tr_complete
namespace
{
Glib::ustring get_collated_name(tr_torrent const* tor)
{
return fmt::format("{}\t{}", Glib::ustring(tr_torrentName(tor)).lowercase(), tr_torrentView(tor).hash_string);
}
struct metadata_callback_data
{
Session* core;
tr_torrent_id_t torrent_id;
};
Gtk::TreeModel::iterator find_row_from_torrent_id(Glib::RefPtr<Gtk::TreeModel> const& model, tr_torrent_id_t id)
} // namespace
std::pair<Glib::RefPtr<Torrent>, guint> Session::Impl::find_torrent_by_id(tr_torrent_id_t torrent_id) const
{
for (auto& row : model->children())
auto begin_position = 0U;
auto end_position = raw_model_->get_n_items();
while (begin_position < end_position)
{
if (id == row.get_value(torrent_cols.torrent_id))
auto const position = begin_position + (end_position - begin_position) / 2;
auto const torrent = raw_model_->get_item(position);
if (auto const current_torrent_id = torrent->get_id(); current_torrent_id == torrent_id)
{
return TR_GTK_TREE_MODEL_CHILD_ITER(row);
return { torrent, position };
}
else
{
(current_torrent_id < torrent_id ? begin_position : end_position) = position;
}
}
return {};
}
} // namespace
/* this is called in the libtransmission thread, *NOT* the GTK+ thread,
so delegate to the GTK+ thread before changing our list store... */
void Session::Impl::on_torrent_metadata_changed(tr_torrent* tor)
void Session::Impl::on_torrent_metadata_changed(tr_torrent* raw_torrent)
{
Glib::signal_idle().connect(
[this, core = get_core_ptr(), torrent_id = tr_torrentId(tor)]()
[this, core = get_core_ptr(), torrent_id = tr_torrentId(raw_torrent)]()
{
/* update the torrent's collated name */
if (auto const* const tor2 = tr_torrentFindFromId(session_, torrent_id); tor2 != nullptr)
if (auto const [torrent, position] = find_torrent_by_id(torrent_id); torrent != nullptr)
{
if (auto const iter = find_row_from_torrent_id(raw_model_, torrent_id); iter)
{
(*iter)[torrent_cols.name_collated] = get_collated_name(tor2);
}
torrent->update();
}
return false;
@ -951,70 +658,25 @@ void Session::Impl::on_torrent_metadata_changed(tr_torrent* tor)
****
***/
namespace
void Session::add_torrent(Glib::RefPtr<Torrent> const& torrent, bool do_notify)
{
impl_->add_torrent(torrent, do_notify);
}
unsigned int build_torrent_trackers_hash(tr_torrent* tor)
void Session::Impl::add_torrent(Glib::RefPtr<Torrent> const& torrent, bool do_notify)
{
auto hash = uint64_t{};
for (size_t i = 0, n = tr_torrentTrackerCount(tor); i < n; ++i)
if (torrent != nullptr)
{
for (auto const ch : std::string_view{ tr_torrentTracker(tor, i).announce })
{
hash = (hash << 4) ^ (hash >> 28) ^ ch;
}
}
return hash;
}
bool is_torrent_active(tr_stat const* st)
{
return st->peersSendingToUs > 0 || st->peersGettingFromUs > 0 || st->activity == TR_STATUS_CHECK;
}
} // namespace
void Session::add_torrent(tr_torrent* tor, bool do_notify)
{
ScopedModelSortBlocker const disable_sort(*impl_->get_model().get());
impl_->add_torrent(tor, do_notify);
}
void Session::Impl::add_torrent(tr_torrent* tor, bool do_notify)
{
if (tor != nullptr)
{
tr_stat const* st = tr_torrentStat(tor);
auto const collated = get_collated_name(tor);
auto const trackers_hash = build_torrent_trackers_hash(tor);
auto const store = get_raw_model();
auto const iter = store->append();
(*iter)[torrent_cols.name_collated] = collated;
(*iter)[torrent_cols.torrent] = tor;
(*iter)[torrent_cols.torrent_id] = tr_torrentId(tor);
(*iter)[torrent_cols.speed_up] = st->pieceUploadSpeed_KBps;
(*iter)[torrent_cols.speed_down] = st->pieceDownloadSpeed_KBps;
(*iter)[torrent_cols.active_peers_up] = st->peersGettingFromUs;
(*iter)[torrent_cols.active_peers_down] = st->peersSendingToUs + st->webseedsSendingToUs;
(*iter)[torrent_cols.recheck_progress] = st->recheckProgress;
(*iter)[torrent_cols.active] = is_torrent_active(st);
(*iter)[torrent_cols.activity] = st->activity;
(*iter)[torrent_cols.finished] = st->finished;
(*iter)[torrent_cols.priority] = tr_torrentGetPriority(tor);
(*iter)[torrent_cols.queue_position] = st->queuePosition;
(*iter)[torrent_cols.trackers] = trackers_hash;
raw_model_->insert_sorted(torrent, &Torrent::compare_by_id);
if (do_notify)
{
gtr_notify_torrent_added(get_core_ptr(), tr_torrentId(tor));
gtr_notify_torrent_added(get_core_ptr(), torrent->get_id());
}
}
}
tr_torrent* Session::Impl::create_new_torrent(tr_ctor* ctor)
Glib::RefPtr<Torrent> Session::Impl::create_new_torrent(tr_ctor* ctor)
{
bool do_trash = false;
@ -1041,7 +703,7 @@ tr_torrent* Session::Impl::create_new_torrent(tr_ctor* ctor)
}
}
return tor;
return Torrent::create(tor);
}
int Session::Impl::add_ctor(tr_ctor* ctor, bool do_prompt, bool do_notify)
@ -1068,7 +730,6 @@ int Session::Impl::add_ctor(tr_ctor* ctor, bool do_prompt, bool do_notify)
if (!do_prompt)
{
ScopedModelSortBlocker const disable_sort(*sorted_model_.get());
add_torrent(create_new_torrent(ctor), do_notify);
tr_ctorFree(ctor);
return 0;
@ -1249,31 +910,22 @@ void Session::Impl::torrents_added()
void Session::torrent_changed(tr_torrent_id_t id)
{
auto const model = impl_->get_raw_model();
if (auto const iter = find_row_from_torrent_id(model, id); iter)
if (auto const [torrent, position] = impl_->find_torrent_by_id(id); torrent != nullptr)
{
model->row_changed(model->get_path(iter), iter);
torrent->update();
}
}
void Session::remove_torrent(tr_torrent_id_t id, bool delete_files)
{
auto* tor = find_torrent(id);
if (tor != nullptr)
if (auto const [torrent, position] = impl_->find_torrent_by_id(id); torrent != nullptr)
{
/* remove from the gui */
auto const model = impl_->get_raw_model();
if (auto const iter = find_row_from_torrent_id(model, id); iter)
{
model->erase(iter);
}
impl_->get_raw_model()->remove(position);
/* remove the torrent */
tr_torrentRemove(
tor,
&torrent->get_underlying(),
delete_files,
[](char const* filename, void* /*user_data*/, tr_error** error)
{ return gtr_file_trash_or_remove(filename, error); },
@ -1296,109 +948,28 @@ void Session::load(bool force_paused)
auto const n_torrents = tr_sessionLoadTorrents(session, ctor);
tr_ctorFree(ctor);
ScopedModelSortBlocker const disable_sort(*impl_->get_model().get());
auto raw_torrents = std::vector<tr_torrent*>{};
raw_torrents.resize(n_torrents);
tr_sessionGetAllTorrents(session, std::data(raw_torrents), std::size(raw_torrents));
auto torrents = std::vector<tr_torrent*>{};
torrents.resize(n_torrents);
tr_sessionGetAllTorrents(session, std::data(torrents), std::size(torrents));
for (auto* tor : torrents)
{
impl_->add_torrent(tor, false);
}
auto torrents = std::vector<Glib::RefPtr<Torrent>>();
torrents.reserve(raw_torrents.size());
std::transform(raw_torrents.begin(), raw_torrents.end(), std::back_inserter(torrents), &Torrent::create);
std::sort(torrents.begin(), torrents.end(), &Torrent::less_by_id);
auto const model = impl_->get_raw_model();
model->splice(0, model->get_n_items(), torrents);
}
void Session::clear()
{
impl_->get_raw_model()->clear();
impl_->get_raw_model()->remove_all();
}
/***
****
***/
namespace
{
// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
int gtr_compare_double(double const a, double const b, int decimal_places)
{
auto const ia = int64_t(a * pow(10, decimal_places));
auto const ib = int64_t(b * pow(10, decimal_places));
if (ia < ib)
{
return -1;
}
if (ia > ib)
{
return 1;
}
return 0;
}
void update_foreach(Gtk::TreeModel::Row& row)
{
/* get the old states */
auto* const tor = static_cast<tr_torrent*>(row.get_value(torrent_cols.torrent));
auto const oldActive = row.get_value(torrent_cols.active);
auto const oldActivePeerCount = row.get_value(torrent_cols.active_peer_count);
auto const oldUploadPeerCount = row.get_value(torrent_cols.active_peers_up);
auto const oldDownloadPeerCount = row.get_value(torrent_cols.active_peers_down);
auto const oldError = row.get_value(torrent_cols.error);
auto const oldActivity = row.get_value(torrent_cols.activity);
auto const oldFinished = row.get_value(torrent_cols.finished);
auto const oldPriority = row.get_value(torrent_cols.priority);
auto const oldQueuePosition = row.get_value(torrent_cols.queue_position);
auto const oldTrackers = row.get_value(torrent_cols.trackers);
auto const oldUpSpeed = row.get_value(torrent_cols.speed_up);
auto const oldRecheckProgress = row.get_value(torrent_cols.recheck_progress);
auto const oldDownSpeed = row.get_value(torrent_cols.speed_down);
/* get the new states */
auto const* const st = tr_torrentStat(tor);
auto const newActive = is_torrent_active(st);
auto const newActivity = st->activity;
auto const newFinished = st->finished;
auto const newPriority = tr_torrentGetPriority(tor);
auto const newQueuePosition = st->queuePosition;
auto const newTrackers = build_torrent_trackers_hash(tor);
auto const newUpSpeed = st->pieceUploadSpeed_KBps;
auto const newDownSpeed = st->pieceDownloadSpeed_KBps;
auto const newRecheckProgress = st->recheckProgress;
auto const newActivePeerCount = st->peersSendingToUs + st->peersGettingFromUs + st->webseedsSendingToUs;
auto const newDownloadPeerCount = st->peersSendingToUs;
auto const newUploadPeerCount = st->peersGettingFromUs + st->webseedsSendingToUs;
auto const newError = st->error;
/* updating the model triggers off resort/refresh,
so don't do it unless something's actually changed... */
if (newActive != oldActive || newActivity != oldActivity || newFinished != oldFinished || newPriority != oldPriority ||
newQueuePosition != oldQueuePosition || newError != oldError || newActivePeerCount != oldActivePeerCount ||
newDownloadPeerCount != oldDownloadPeerCount || newUploadPeerCount != oldUploadPeerCount ||
newTrackers != oldTrackers || gtr_compare_double(newUpSpeed, oldUpSpeed, 2) != 0 ||
gtr_compare_double(newDownSpeed, oldDownSpeed, 2) != 0 ||
gtr_compare_double(newRecheckProgress, oldRecheckProgress, 2) != 0)
{
row[torrent_cols.active] = newActive;
row[torrent_cols.active_peer_count] = newActivePeerCount;
row[torrent_cols.active_peers_up] = newUploadPeerCount;
row[torrent_cols.active_peers_down] = newDownloadPeerCount;
row[torrent_cols.error] = newError;
row[torrent_cols.activity] = newActivity;
row[torrent_cols.finished] = newFinished;
row[torrent_cols.priority] = newPriority;
row[torrent_cols.queue_position] = newQueuePosition;
row[torrent_cols.trackers] = newTrackers;
row[torrent_cols.speed_up] = newUpSpeed;
row[torrent_cols.speed_down] = newDownSpeed;
row[torrent_cols.recheck_progress] = newRecheckProgress;
}
}
} // namespace
void Session::update()
{
impl_->update();
@ -1419,14 +990,27 @@ void Session::start_now(tr_torrent_id_t id)
void Session::Impl::update()
{
auto torrent_ids = std::unordered_set<tr_torrent_id_t>();
auto changes = Torrent::ChangeFlags();
/* update the model */
for (auto row : raw_model_->children())
for (auto i = 0U, count = raw_model_->get_n_items(); i < count; ++i)
{
update_foreach(row);
auto const torrent = raw_model_->get_item(i);
if (auto const torrent_changes = torrent->update(); torrent_changes.any())
{
torrent_ids.insert(torrent->get_id());
changes |= torrent_changes;
}
}
/* update hibernation */
maybe_inhibit_hibernation();
if (changes.any())
{
signal_torrents_changed_.emit(torrent_ids, changes);
}
}
/**
@ -1576,7 +1160,7 @@ void Session::set_pref(tr_quark const key, int newval)
void Session::set_pref(tr_quark const key, double newval)
{
if (gtr_compare_double(newval, gtr_pref_double_get(key), 4) != 0)
if (std::fabs(newval - gtr_pref_double_get(key)) >= 0.0001)
{
gtr_pref_double_set(key, newval);
impl_->commit_prefs_change(key);
@ -1738,7 +1322,7 @@ void Session::exec(tr_variant const* request)
size_t Session::get_torrent_count() const
{
return impl_->get_raw_model()->children().size();
return impl_->get_raw_model()->get_n_items();
}
size_t Session::get_active_torrent_count() const
@ -1750,9 +1334,9 @@ size_t Session::Impl::get_active_torrent_count() const
{
size_t activeCount = 0;
for (auto const& row : raw_model_->children())
for (auto i = 0U, count = raw_model_->get_n_items(); i < count; ++i)
{
if (row.get_value(torrent_cols.activity) != TR_STATUS_STOPPED)
if (raw_model_->get_item(i)->get_activity() != TR_STATUS_STOPPED)
{
++activeCount;
}
@ -1822,3 +1406,8 @@ sigc::signal<void(bool)>& Session::signal_port_tested()
{
return impl_->signal_port_tested();
}
sigc::signal<void(std::unordered_set<tr_torrent_id_t> const&, Torrent::ChangeFlags)>& Session::signal_torrents_changed()
{
return impl_->signal_torrents_changed();
}

View File

@ -7,6 +7,7 @@
#include <cstddef>
#include <memory>
#include <string>
#include <unordered_set>
#include <vector>
#include <giomm.h>
@ -17,6 +18,9 @@
#include <libtransmission/tr-macros.h>
#include <libtransmission/variant.h>
#include "Torrent.h"
#include "Utils.h"
class Session : public Glib::Object
{
public:
@ -27,6 +31,8 @@ public:
ERR_NO_MORE_TORRENTS = 1000 /* finished adding a batch */
};
using Model = IF_GTKMM4(Gio::ListModel, Gtk::TreeModel);
public:
~Session() override;
@ -36,8 +42,8 @@ public:
tr_session* close();
/* Return the model used without incrementing the reference count */
Glib::RefPtr<Gtk::TreeModel> get_model() const;
Glib::RefPtr<Gio::ListModel> get_model() const;
Glib::RefPtr<Model> get_sorted_model() const;
void clear();
@ -76,7 +82,7 @@ public:
void add_ctor(tr_ctor* ctor);
/** Add a torrent. */
void add_torrent(tr_torrent*, bool do_notify);
void add_torrent(Glib::RefPtr<Torrent> const& torrent, bool do_notify);
/**
* Notifies listeners that torrents have been added.
@ -128,6 +134,7 @@ public:
sigc::signal<void(bool)>& signal_busy();
sigc::signal<void(tr_quark)>& signal_prefs_changed();
sigc::signal<void(bool)>& signal_port_tested();
sigc::signal<void(std::unordered_set<tr_torrent_id_t> const&, Torrent::ChangeFlags)>& signal_torrents_changed();
protected:
explicit Session(tr_session* session);
@ -136,36 +143,3 @@ private:
class Impl;
std::unique_ptr<Impl> const impl_;
};
/**
***
**/
class TorrentModelColumns : public Gtk::TreeModelColumnRecord
{
public:
TorrentModelColumns() noexcept;
Gtk::TreeModelColumn<Glib::ustring> name_collated;
Gtk::TreeModelColumn<gpointer> torrent;
Gtk::TreeModelColumn<tr_torrent_id_t> torrent_id;
Gtk::TreeModelColumn<double> speed_up;
Gtk::TreeModelColumn<double> speed_down;
Gtk::TreeModelColumn<int> active_peers_up;
Gtk::TreeModelColumn<int> active_peers_down;
Gtk::TreeModelColumn<double> recheck_progress;
Gtk::TreeModelColumn<bool> active;
Gtk::TreeModelColumn<tr_torrent_activity> activity;
Gtk::TreeModelColumn<bool> finished;
Gtk::TreeModelColumn<tr_priority_t> priority;
Gtk::TreeModelColumn<size_t> queue_position;
Gtk::TreeModelColumn<unsigned int> trackers;
/* tr_stat.error
* Tracked because ACTIVITY_FILTER_ERROR needs the row-changed events */
Gtk::TreeModelColumn<int> error;
/* tr_stat.{ peersSendingToUs + peersGettingFromUs + webseedsSendingToUs }
* Tracked because ACTIVITY_FILTER_ACTIVE needs the row-changed events */
Gtk::TreeModelColumn<int> active_peer_count;
};
extern TorrentModelColumns const torrent_cols;

766
gtk/Torrent.cc Normal file
View File

@ -0,0 +1,766 @@
// This file Copyright © 2022 Mnemosyne LLC.
// It may be used under GPLv2 (SPDX: GPL-2.0-only), GPLv3 (SPDX: GPL-3.0-only),
// or any future license endorsed by Mnemosyne LLC.
// License text can be found in the licenses/ folder.
#include <array>
#include <functional>
#include <fmt/core.h>
#include <libtransmission/transmission.h>
#include <libtransmission/utils.h>
#include "IconCache.h"
#include "Torrent.h"
#include "Utils.h"
using namespace std::string_view_literals;
namespace
{
template<typename T>
Glib::Value<T>& column_value_cast(Glib::ValueBase& value, Gtk::TreeModelColumn<T> const&)
{
return static_cast<Glib::Value<T>&>(value);
}
template<typename T, typename U, typename = std::enable_if_t<!std::is_floating_point_v<T>>>
void update_cache_value(T& value, U&& new_value, Torrent::ChangeFlags& changes, Torrent::ChangeFlag flag)
{
if (value != new_value)
{
value = std::forward<U>(new_value);
changes.set(flag);
}
}
template<typename T, typename U, typename = std::enable_if_t<std::is_floating_point_v<T>>>
void update_cache_value(T& value, U new_value, T epsilon, Torrent::ChangeFlags& changes, Torrent::ChangeFlag flag)
{
if (std::fabs(value - new_value) >= epsilon)
{
value = new_value;
changes.set(flag);
}
}
unsigned int build_torrent_trackers_hash(tr_torrent const& torrent)
{
auto hash = uint64_t(0);
for (auto i = size_t(0), n = tr_torrentTrackerCount(&torrent); i < n; ++i)
{
for (auto const ch : std::string_view{ tr_torrentTracker(&torrent, i).announce })
{
hash = (hash << 4) ^ (hash >> 28) ^ ch;
}
}
return hash;
}
std::string_view get_mime_type(tr_torrent const& torrent)
{
auto const n_files = tr_torrentFileCount(&torrent);
if (n_files == 0)
{
return UnknownMimeType;
}
if (n_files > 1)
{
return DirectoryMimeType;
}
auto const name = std::string_view(tr_torrentFile(&torrent, 0).name);
return name.find('/') != std::string_view::npos ? DirectoryMimeType : tr_get_mime_type_for_filename(name);
}
} // namespace
Torrent::Columns::Columns()
{
add(self);
add(name_collated);
}
class Torrent::Impl
{
public:
struct Cache
{
Glib::ustring error_message;
Glib::ustring name;
Glib::ustring name_collated;
std::string_view mime_type;
uint64_t have_unchecked = {};
uint64_t have_valid = {};
uint64_t left_until_done = {};
uint64_t size_when_done = {};
uint64_t total_size = {};
uint64_t uploaded_ever = {};
size_t queue_position = {};
time_t added_date = {};
time_t eta = {};
tr_torrent_activity activity = {};
unsigned int trackers = {};
int active_peer_count = {};
int active_peers_down = {};
int active_peers_up = {};
int error_code = {};
uint16_t peers_connected = {};
uint16_t peers_getting_from_us = {};
uint16_t peers_sending_to_us = {};
uint16_t webseeds_sending_to_us = {};
float activity_percent_done = {};
float metadata_percent_complete = {};
float percent_complete = {};
float percent_done = {};
float ratio = {};
float recheck_progress = {};
float seed_ratio = {};
float seed_ratio_percent_done = {};
float speed_down = {};
float speed_up = {};
tr_priority_t priority = {};
bool active = {};
bool finished = {};
bool has_metadata = {};
bool has_seed_ratio = {};
bool stalled = {};
};
public:
Impl(Torrent& torrent, tr_torrent* raw_torrent);
tr_torrent* get_raw_torrent()
{
return raw_torrent_;
}
Cache& get_cache()
{
return cache_;
}
ChangeFlags update_cache();
void notify_property_changes(ChangeFlags changes) const;
void get_value(int column, Glib::ValueBase& value) const;
Glib::RefPtr<Gio::Icon> get_icon() const;
Glib::ustring get_short_status_text() const;
Glib::ustring get_long_progress_text() const;
Glib::ustring get_long_status_text() const;
private:
Glib::ustring get_short_transfer_text() const;
Glib::ustring get_error_text() const;
Glib::ustring get_activity_text() const;
private:
Torrent& torrent_;
tr_torrent* const raw_torrent_;
Cache cache_;
};
Torrent::Impl::Impl(Torrent& torrent, tr_torrent* raw_torrent)
: torrent_(torrent)
, raw_torrent_(raw_torrent)
{
if (raw_torrent_ != nullptr)
{
update_cache();
}
}
Torrent::ChangeFlags Torrent::Impl::update_cache()
{
auto result = ChangeFlags();
auto const* const stats = tr_torrentStat(raw_torrent_);
g_return_val_if_fail(stats != nullptr, Torrent::ChangeFlags());
auto seed_ratio = 0.0;
auto const has_seed_ratio = tr_torrentGetSeedRatio(raw_torrent_, &seed_ratio);
update_cache_value(cache_.name, tr_torrentName(raw_torrent_), result, ChangeFlag::NAME);
update_cache_value(cache_.speed_up, stats->pieceUploadSpeed_KBps, 0.01F, result, ChangeFlag::SPEED_UP);
update_cache_value(cache_.speed_down, stats->pieceDownloadSpeed_KBps, 0.01F, result, ChangeFlag::SPEED_DOWN);
update_cache_value(cache_.active_peers_up, stats->peersGettingFromUs, result, ChangeFlag::ACTIVE_PEERS_UP);
update_cache_value(
cache_.active_peers_down,
stats->peersSendingToUs + stats->webseedsSendingToUs,
result,
ChangeFlag::ACTIVE_PEERS_DOWN);
update_cache_value(cache_.recheck_progress, stats->recheckProgress, 0.01F, result, ChangeFlag::RECHECK_PROGRESS);
update_cache_value(
cache_.active,
stats->peersSendingToUs > 0 || stats->peersGettingFromUs > 0 || stats->activity == TR_STATUS_CHECK,
result,
ChangeFlag::ACTIVE);
update_cache_value(cache_.activity, stats->activity, result, ChangeFlag::ACTIVITY);
update_cache_value(
cache_.activity_percent_done,
std::clamp(
stats->activity == TR_STATUS_SEED && has_seed_ratio ? stats->seedRatioPercentDone : stats->percentDone,
0.0F,
1.0F),
0.01F,
result,
ChangeFlag::PERCENT_DONE);
update_cache_value(cache_.finished, stats->finished, result, ChangeFlag::FINISHED);
update_cache_value(cache_.priority, tr_torrentGetPriority(raw_torrent_), result, ChangeFlag::PRIORITY);
update_cache_value(cache_.queue_position, stats->queuePosition, result, ChangeFlag::QUEUE_POSITION);
update_cache_value(cache_.trackers, build_torrent_trackers_hash(*raw_torrent_), result, ChangeFlag::TRACKERS);
update_cache_value(cache_.error_code, stats->error, result, ChangeFlag::ERROR_CODE);
update_cache_value(cache_.error_message, stats->errorString, result, ChangeFlag::ERROR_MESSAGE);
update_cache_value(
cache_.active_peer_count,
stats->peersSendingToUs + stats->peersGettingFromUs + stats->webseedsSendingToUs,
result,
ChangeFlag::ACTIVE_PEER_COUNT);
update_cache_value(cache_.mime_type, get_mime_type(*raw_torrent_), result, ChangeFlag::MIME_TYPE);
update_cache_value(cache_.has_metadata, tr_torrentHasMetadata(raw_torrent_), result, ChangeFlag::HAS_METADATA);
update_cache_value(cache_.stalled, stats->isStalled, result, ChangeFlag::STALLED);
update_cache_value(cache_.ratio, stats->ratio, 0.01F, result, ChangeFlag::RATIO);
update_cache_value(cache_.added_date, stats->addedDate, result, ChangeFlag::ADDED_DATE);
update_cache_value(cache_.eta, stats->eta, result, ChangeFlag::ETA);
update_cache_value(cache_.percent_complete, stats->percentComplete, 0.01F, result, ChangeFlag::PERCENT_COMPLETE);
update_cache_value(
cache_.seed_ratio_percent_done,
stats->seedRatioPercentDone,
0.01F,
result,
ChangeFlag::SEED_RATIO_PERCENT_DONE);
update_cache_value(cache_.total_size, tr_torrentTotalSize(raw_torrent_), result, ChangeFlag::TOTAL_SIZE);
update_cache_value(cache_.has_seed_ratio, has_seed_ratio, result, ChangeFlag::LONG_PROGRESS);
update_cache_value(cache_.have_unchecked, stats->haveUnchecked, result, ChangeFlag::LONG_PROGRESS);
update_cache_value(cache_.have_valid, stats->haveValid, result, ChangeFlag::LONG_PROGRESS);
update_cache_value(cache_.left_until_done, stats->leftUntilDone, result, ChangeFlag::LONG_PROGRESS);
update_cache_value(cache_.percent_done, stats->percentDone, 0.01F, result, ChangeFlag::LONG_PROGRESS);
update_cache_value(cache_.seed_ratio, static_cast<float>(seed_ratio), 0.01F, result, ChangeFlag::LONG_PROGRESS);
update_cache_value(cache_.size_when_done, stats->sizeWhenDone, result, ChangeFlag::LONG_PROGRESS);
update_cache_value(cache_.uploaded_ever, stats->uploadedEver, result, ChangeFlag::LONG_PROGRESS);
update_cache_value(
cache_.metadata_percent_complete,
stats->metadataPercentComplete,
0.01F,
result,
ChangeFlag::LONG_STATUS);
update_cache_value(cache_.peers_connected, stats->peersConnected, result, ChangeFlag::LONG_STATUS);
update_cache_value(cache_.peers_getting_from_us, stats->peersGettingFromUs, result, ChangeFlag::LONG_STATUS);
update_cache_value(cache_.peers_sending_to_us, stats->peersSendingToUs, result, ChangeFlag::LONG_STATUS);
update_cache_value(cache_.webseeds_sending_to_us, stats->webseedsSendingToUs, result, ChangeFlag::LONG_STATUS);
if (result.test(ChangeFlag::NAME))
{
cache_.name_collated = fmt::format("{}\t{}", cache_.name.lowercase(), tr_torrentView(raw_torrent_).hash_string);
}
return result;
}
void Torrent::Impl::notify_property_changes(ChangeFlags changes) const
{
// Updating the model triggers off resort/refresh, so don't notify unless something's actually changed
if (changes.none())
{
return;
}
gtr_object_notify_emit(torrent_);
}
void Torrent::Impl::get_value(int column, Glib::ValueBase& value) const
{
static auto const& columns = get_columns();
if (column == columns.self.index())
{
column_value_cast(value, columns.self).set(&torrent_);
}
else if (column == columns.name_collated.index())
{
column_value_cast(value, columns.name_collated).set(cache_.name_collated);
}
}
Glib::RefPtr<Gio::Icon> Torrent::Impl::get_icon() const
{
return gtr_get_mime_type_icon(cache_.mime_type);
}
Glib::ustring Torrent::Impl::get_short_status_text() const
{
switch (cache_.activity)
{
case TR_STATUS_STOPPED:
return cache_.finished ? _("Finished") : _("Paused");
case TR_STATUS_CHECK_WAIT:
return _("Queued for verification");
case TR_STATUS_DOWNLOAD_WAIT:
return _("Queued for download");
case TR_STATUS_SEED_WAIT:
return _("Queued for seeding");
case TR_STATUS_CHECK:
return fmt::format(
// xgettext:no-c-format
_("Verifying local data ({percent_done}% tested)"),
fmt::arg("percent_done", tr_truncd(cache_.recheck_progress * 100.0, 1)));
case TR_STATUS_DOWNLOAD:
case TR_STATUS_SEED:
return fmt::format(
FMT_STRING("{:s} {:s}"),
get_short_transfer_text(),
fmt::format(_("Ratio: {ratio}"), fmt::arg("ratio", tr_strlratio(cache_.ratio))));
default:
return {};
}
}
Glib::ustring Torrent::Impl::get_long_progress_text() const
{
Glib::ustring gstr;
bool const isDone = cache_.left_until_done == 0;
auto const haveTotal = cache_.have_unchecked + cache_.have_valid;
bool const isSeed = cache_.have_valid >= cache_.total_size;
if (!isDone) // downloading
{
// 50 MB of 200 MB (25%)
gstr += fmt::format(
_("{current_size} of {complete_size} ({percent_done}%)"),
fmt::arg("current_size", tr_strlsize(haveTotal)),
fmt::arg("complete_size", tr_strlsize(cache_.size_when_done)),
fmt::arg("percent_done", tr_strpercent(cache_.percent_done * 100.0)));
}
else if (!isSeed && cache_.has_seed_ratio) // partial seed, seed ratio
{
// 50 MB of 200 MB (25%), uploaded 30 MB (Ratio: X%, Goal: Y%)
gstr += fmt::format(
// xgettext:no-c-format
_("{current_size} of {complete_size} ({percent_complete}%), uploaded {uploaded_size} (Ratio: {ratio}, Goal: {seed_ratio})"),
fmt::arg("current_size", tr_strlsize(haveTotal)),
fmt::arg("complete_size", tr_strlsize(cache_.total_size)),
fmt::arg("percent_complete", tr_strpercent(cache_.percent_complete * 100.0)),
fmt::arg("uploaded_size", tr_strlsize(cache_.uploaded_ever)),
fmt::arg("ratio", tr_strlratio(cache_.ratio)),
fmt::arg("seed_ratio", tr_strlratio(cache_.seed_ratio)));
}
else if (!isSeed) // partial seed, no seed ratio
{
gstr += fmt::format(
// xgettext:no-c-format
_("{current_size} of {complete_size} ({percent_complete}%), uploaded {uploaded_size} (Ratio: {ratio})"),
fmt::arg("current_size", tr_strlsize(haveTotal)),
fmt::arg("complete_size", tr_strlsize(cache_.total_size)),
fmt::arg("percent_complete", tr_strpercent(cache_.percent_complete * 100.0)),
fmt::arg("uploaded_size", tr_strlsize(cache_.uploaded_ever)),
fmt::arg("ratio", tr_strlratio(cache_.ratio)));
}
else if (cache_.has_seed_ratio) // seed, seed ratio
{
gstr += fmt::format(
_("{complete_size}, uploaded {uploaded_size} (Ratio: {ratio}, Goal: {seed_ratio})"),
fmt::arg("complete_size", tr_strlsize(cache_.total_size)),
fmt::arg("uploaded_size", tr_strlsize(cache_.uploaded_ever)),
fmt::arg("ratio", tr_strlratio(cache_.ratio)),
fmt::arg("seed_ratio", tr_strlratio(cache_.seed_ratio)));
}
else // seed, no seed ratio
{
gstr += fmt::format(
_("{complete_size}, uploaded {uploaded_size} (Ratio: {ratio})"),
fmt::arg("complete_size", tr_strlsize(cache_.total_size)),
fmt::arg("uploaded_size", tr_strlsize(cache_.uploaded_ever)),
fmt::arg("ratio", tr_strlratio(cache_.ratio)));
}
// add time remaining when applicable
if (cache_.activity == TR_STATUS_DOWNLOAD || (cache_.has_seed_ratio && cache_.activity == TR_STATUS_SEED))
{
gstr += " - ";
if (cache_.eta < 0)
{
gstr += _("Remaining time unknown");
}
else
{
gstr += tr_format_time_left(cache_.eta);
}
}
return gstr;
}
Glib::ustring Torrent::Impl::get_long_status_text() const
{
auto status_str = get_error_text();
if (status_str.empty())
{
status_str = get_activity_text();
}
switch (cache_.activity)
{
case TR_STATUS_CHECK_WAIT:
case TR_STATUS_CHECK:
case TR_STATUS_DOWNLOAD_WAIT:
case TR_STATUS_SEED_WAIT:
case TR_STATUS_STOPPED:
break;
default:
if (auto const buf = get_short_transfer_text(); !std::empty(buf))
{
status_str += fmt::format(FMT_STRING(" - {:s}"), buf);
}
}
return status_str;
}
Glib::ustring Torrent::Impl::get_short_transfer_text() const
{
if (cache_.has_metadata && cache_.active_peers_down > 0)
{
return fmt::format(
_("{download_speed} ▼ {upload_speed} ▲"),
fmt::arg("upload_speed", tr_formatter_speed_KBps(cache_.speed_up)),
fmt::arg("download_speed", tr_formatter_speed_KBps(cache_.speed_down)));
}
if (cache_.has_metadata && cache_.active_peers_up > 0)
{
return fmt::format(_("{upload_speed} ▲"), fmt::arg("upload_speed", tr_formatter_speed_KBps(cache_.speed_up)));
}
if (cache_.stalled)
{
return _("Stalled");
}
return {};
}
Glib::ustring Torrent::Impl::get_error_text() const
{
switch (cache_.error_code)
{
case TR_STAT_TRACKER_WARNING:
return fmt::format(_("Tracker warning: '{warning}'"), fmt::arg("warning", cache_.error_message));
case TR_STAT_TRACKER_ERROR:
return fmt::format(_("Tracker Error: '{error}'"), fmt::arg("error", cache_.error_message));
case TR_STAT_LOCAL_ERROR:
return fmt::format(_("Local error: '{error}'"), fmt::arg("error", cache_.error_message));
default:
return {};
}
}
Glib::ustring Torrent::Impl::get_activity_text() const
{
switch (cache_.activity)
{
case TR_STATUS_STOPPED:
case TR_STATUS_CHECK_WAIT:
case TR_STATUS_CHECK:
case TR_STATUS_DOWNLOAD_WAIT:
case TR_STATUS_SEED_WAIT:
return get_short_status_text();
case TR_STATUS_DOWNLOAD:
if (!cache_.has_metadata)
{
return fmt::format(
ngettext(
// xgettext:no-c-format
"Downloading metadata from {active_count} connected peer ({percent_done}% done)",
"Downloading metadata from {active_count} connected peers ({percent_done}% done)",
cache_.peers_connected),
fmt::arg("active_count", cache_.peers_connected),
fmt::arg("percent_done", tr_strpercent(cache_.metadata_percent_complete * 100.0)));
}
if (cache_.peers_sending_to_us != 0 && cache_.webseeds_sending_to_us != 0)
{
return fmt::format(
ngettext(
"Downloading from {active_count} of {connected_count} connected peer and webseed",
"Downloading from {active_count} of {connected_count} connected peers and webseeds",
cache_.peers_connected + cache_.webseeds_sending_to_us),
fmt::arg("active_count", cache_.peers_sending_to_us + cache_.webseeds_sending_to_us),
fmt::arg("connected_count", cache_.peers_connected + cache_.webseeds_sending_to_us));
}
if (cache_.webseeds_sending_to_us != 0)
{
return fmt::format(
ngettext(
"Downloading from {active_count} webseed",
"Downloading from {active_count} webseeds",
cache_.webseeds_sending_to_us),
fmt::arg("active_count", cache_.webseeds_sending_to_us));
}
return fmt::format(
ngettext(
"Downloading from {active_count} of {connected_count} connected peer",
"Downloading from {active_count} of {connected_count} connected peers",
cache_.peers_connected),
fmt::arg("active_count", cache_.peers_sending_to_us),
fmt::arg("connected_count", cache_.peers_connected));
case TR_STATUS_SEED:
return fmt::format(
ngettext(
"Seeding to {active_count} of {connected_count} connected peer",
"Seeding to {active_count} of {connected_count} connected peers",
cache_.peers_connected),
fmt::arg("active_count", cache_.peers_getting_from_us),
fmt::arg("connected_count", cache_.peers_connected));
default:
g_assert_not_reached();
return {};
}
}
Torrent::Torrent()
: Glib::ObjectBase(typeid(Torrent))
{
}
Torrent::Torrent(tr_torrent* torrent)
: Glib::ObjectBase(typeid(Torrent))
, impl_(std::make_unique<Impl>(*this, torrent))
{
g_assert(torrent != nullptr);
}
Glib::ustring const& Torrent::get_name_collated() const
{
return impl_->get_cache().name_collated;
}
tr_torrent_id_t Torrent::get_id() const
{
return tr_torrentId(impl_->get_raw_torrent());
}
tr_torrent& Torrent::get_underlying() const
{
return *impl_->get_raw_torrent();
}
float Torrent::get_speed_up() const
{
return impl_->get_cache().speed_up;
}
float Torrent::get_speed_down() const
{
return impl_->get_cache().speed_down;
}
int Torrent::get_active_peers_up() const
{
return impl_->get_cache().active_peers_up;
}
int Torrent::get_active_peers_down() const
{
return impl_->get_cache().active_peers_down;
}
float Torrent::get_recheck_progress() const
{
return impl_->get_cache().recheck_progress;
}
bool Torrent::get_active() const
{
return impl_->get_cache().active;
}
tr_torrent_activity Torrent::get_activity() const
{
return impl_->get_cache().activity;
}
bool Torrent::get_finished() const
{
return impl_->get_cache().finished;
}
tr_priority_t Torrent::get_priority() const
{
return impl_->get_cache().priority;
}
size_t Torrent::get_queue_position() const
{
return impl_->get_cache().queue_position;
}
unsigned int Torrent::get_trackers() const
{
return impl_->get_cache().trackers;
}
int Torrent::get_error_code() const
{
return impl_->get_cache().error_code;
}
Glib::ustring const& Torrent::get_error_message() const
{
return impl_->get_cache().error_message;
}
int Torrent::get_active_peer_count() const
{
return impl_->get_cache().active_peer_count;
}
uint64_t Torrent::get_total_size() const
{
return impl_->get_cache().total_size;
}
float Torrent::get_ratio() const
{
return impl_->get_cache().ratio;
}
time_t Torrent::get_eta() const
{
return impl_->get_cache().eta;
}
time_t Torrent::get_added_date() const
{
return impl_->get_cache().added_date;
}
float Torrent::get_percent_complete() const
{
return impl_->get_cache().percent_complete;
}
float Torrent::get_seed_ratio_percent_done() const
{
return impl_->get_cache().seed_ratio_percent_done;
}
Glib::RefPtr<Gio::Icon> Torrent::get_icon() const
{
return impl_->get_icon();
}
Glib::ustring Torrent::get_name() const
{
return impl_->get_cache().name;
}
float Torrent::get_percent_done() const
{
return impl_->get_cache().activity_percent_done;
}
Glib::ustring Torrent::get_short_status_text() const
{
return impl_->get_short_status_text();
}
Glib::ustring Torrent::get_long_progress_text() const
{
return impl_->get_long_progress_text();
}
Glib::ustring Torrent::get_long_status_text() const
{
return impl_->get_long_status_text();
}
bool Torrent::get_sensitive() const
{
return impl_->get_cache().activity != TR_STATUS_STOPPED;
}
Torrent::ChangeFlags Torrent::update()
{
auto result = impl_->update_cache();
impl_->notify_property_changes(result);
return result;
}
Glib::RefPtr<Torrent> Torrent::create(tr_torrent* torrent)
{
return Glib::make_refptr_for_instance(new Torrent(torrent));
}
Torrent::Columns const& Torrent::get_columns()
{
static Columns const columns;
return columns;
}
int Torrent::get_item_id(Glib::RefPtr<Glib::ObjectBase const> const& item)
{
if (auto const torrent = gtr_ptr_dynamic_cast<Torrent const>(item); torrent != nullptr)
{
return torrent->get_id();
}
return 0;
}
void Torrent::get_item_value(Glib::RefPtr<Glib::ObjectBase const> const& item, int column, Glib::ValueBase& value)
{
if (auto const torrent = gtr_ptr_dynamic_cast<Torrent const>(item); torrent != nullptr)
{
torrent->impl_->get_value(column, value);
}
}
int Torrent::compare_by_id(Glib::RefPtr<Torrent const> const& lhs, Glib::RefPtr<Torrent const> const& rhs)
{
return gtr_compare_generic(lhs->get_id(), rhs->get_id());
}
bool Torrent::less_by_id(Glib::RefPtr<Torrent const> const& lhs, Glib::RefPtr<Torrent const> const& rhs)
{
return lhs->get_id() < rhs->get_id();
}

120
gtk/Torrent.h Normal file
View File

@ -0,0 +1,120 @@
// This file Copyright © 2022 Mnemosyne LLC.
// It may be used under GPLv2 (SPDX: GPL-2.0-only), GPLv3 (SPDX: GPL-3.0-only),
// or any future license endorsed by Mnemosyne LLC.
// License text can be found in the licenses/ folder.
#pragma once
#include <algorithm>
#include <bitset>
#include <initializer_list>
#include <memory>
#include <vector>
#include <glibmm.h>
#include <gtkmm.h>
#include <libtransmission/transmission.h>
#include "Flags.h"
class Torrent : public Glib::Object
{
public:
class Columns : public Gtk::TreeModelColumnRecord
{
public:
Columns();
Gtk::TreeModelColumn<Torrent*> self;
Gtk::TreeModelColumn<Glib::ustring> name_collated;
};
enum class ChangeFlag
{
ACTIVE_PEER_COUNT,
ACTIVE_PEERS_DOWN,
ACTIVE_PEERS_UP,
ACTIVE,
ACTIVITY,
ADDED_DATE,
ERROR_CODE,
ERROR_MESSAGE,
ETA,
FINISHED,
HAS_METADATA,
LONG_PROGRESS,
LONG_STATUS,
MIME_TYPE,
NAME,
PERCENT_COMPLETE,
PERCENT_DONE,
PRIORITY,
QUEUE_POSITION,
RATIO,
RECHECK_PROGRESS,
SEED_RATIO_PERCENT_DONE,
SPEED_DOWN,
SPEED_UP,
STALLED,
TOTAL_SIZE,
TRACKERS,
};
using ChangeFlags = Flags<ChangeFlag>;
public:
int get_active_peer_count() const;
int get_active_peers_down() const;
int get_active_peers_up() const;
bool get_active() const;
tr_torrent_activity get_activity() const;
time_t get_added_date() const;
int get_error_code() const;
Glib::ustring const& get_error_message() const;
time_t get_eta() const;
bool get_finished() const;
tr_torrent_id_t get_id() const;
Glib::ustring const& get_name_collated() const;
Glib::ustring get_name() const;
float get_percent_complete() const;
float get_percent_done() const;
tr_priority_t get_priority() const;
size_t get_queue_position() const;
float get_ratio() const;
float get_recheck_progress() const;
float get_seed_ratio_percent_done() const;
float get_speed_down() const;
float get_speed_up() const;
tr_torrent& get_underlying() const;
uint64_t get_total_size() const;
unsigned int get_trackers() const;
Glib::RefPtr<Gio::Icon> get_icon() const;
Glib::ustring get_short_status_text() const;
Glib::ustring get_long_progress_text() const;
Glib::ustring get_long_status_text() const;
bool get_sensitive() const;
ChangeFlags update();
static Glib::RefPtr<Torrent> create(tr_torrent* torrent);
static Columns const& get_columns();
static int get_item_id(Glib::RefPtr<Glib::ObjectBase const> const& item);
static void get_item_value(Glib::RefPtr<Glib::ObjectBase const> const& item, int column, Glib::ValueBase& value);
static int compare_by_id(Glib::RefPtr<Torrent const> const& lhs, Glib::RefPtr<Torrent const> const& rhs);
static bool less_by_id(Glib::RefPtr<Torrent const> const& lhs, Glib::RefPtr<Torrent const> const& rhs);
private:
Torrent();
explicit Torrent(tr_torrent* torrent);
private:
class Impl;
std::unique_ptr<Impl> const impl_;
};
DEFINE_FLAGS_OPERATORS(Torrent::ChangeFlag)

View File

@ -19,7 +19,7 @@
#include <libtransmission/utils.h> /* tr_truncd() */
#include "HigWorkarea.h" // GUI_PAD, GUI_PAD_SMALL
#include "IconCache.h"
#include "Torrent.h"
#include "TorrentCellRenderer.h"
#include "Utils.h"
@ -48,265 +48,6 @@ auto get_width(Gtk::Requisition const& req)
return req.IF_GTKMM4(get_width(), width);
}
auto getProgressString(tr_torrent const* tor, uint64_t total_size, tr_stat const* st)
{
Glib::ustring gstr;
bool const isDone = st->leftUntilDone == 0;
uint64_t const haveTotal = st->haveUnchecked + st->haveValid;
bool const isSeed = st->haveValid >= total_size;
double seedRatio = 0;
bool const hasSeedRatio = tr_torrentGetSeedRatio(tor, &seedRatio);
if (!isDone) // downloading
{
// 50 MB of 200 MB (25%)
gstr += fmt::format(
_("{current_size} of {complete_size} ({percent_done}%)"),
fmt::arg("current_size", tr_strlsize(haveTotal)),
fmt::arg("complete_size", tr_strlsize(st->sizeWhenDone)),
fmt::arg("percent_done", tr_strpercent(st->percentDone * 100.0)));
}
else if (!isSeed && hasSeedRatio) // partial seed, seed ratio
{
// 50 MB of 200 MB (25%), uploaded 30 MB (Ratio: X%, Goal: Y%)
gstr += fmt::format(
// xgettext:no-c-format
_("{current_size} of {complete_size} ({percent_complete}%), uploaded {uploaded_size} (Ratio: {ratio}, Goal: {seed_ratio})"),
fmt::arg("current_size", tr_strlsize(haveTotal)),
fmt::arg("complete_size", tr_strlsize(total_size)),
fmt::arg("percent_complete", tr_strpercent(st->percentComplete * 100.0)),
fmt::arg("uploaded_size", tr_strlsize(st->uploadedEver)),
fmt::arg("ratio", tr_strlratio(st->ratio)),
fmt::arg("seed_ratio", tr_strlratio(seedRatio)));
}
else if (!isSeed) // partial seed, no seed ratio
{
gstr += fmt::format(
// xgettext:no-c-format
_("{current_size} of {complete_size} ({percent_complete}%), uploaded {uploaded_size} (Ratio: {ratio})"),
fmt::arg("current_size", tr_strlsize(haveTotal)),
fmt::arg("complete_size", tr_strlsize(total_size)),
fmt::arg("percent_complete", tr_strpercent(st->percentComplete * 100.0)),
fmt::arg("uploaded_size", tr_strlsize(st->uploadedEver)),
fmt::arg("ratio", tr_strlratio(st->ratio)));
}
else if (hasSeedRatio) // seed, seed ratio
{
gstr += fmt::format(
_("{complete_size}, uploaded {uploaded_size} (Ratio: {ratio}, Goal: {seed_ratio})"),
fmt::arg("complete_size", tr_strlsize(total_size)),
fmt::arg("uploaded_size", tr_strlsize(st->uploadedEver)),
fmt::arg("ratio", tr_strlratio(st->ratio)),
fmt::arg("seed_ratio", tr_strlratio(seedRatio)));
}
else // seed, no seed ratio
{
gstr += fmt::format(
_("{complete_size}, uploaded {uploaded_size} (Ratio: {ratio})"),
fmt::arg("complete_size", tr_strlsize(total_size)),
fmt::arg("uploaded_size", tr_strlsize(st->uploadedEver)),
fmt::arg("ratio", tr_strlratio(st->ratio)));
}
// add time remaining when applicable
if (st->activity == TR_STATUS_DOWNLOAD || (hasSeedRatio && st->activity == TR_STATUS_SEED))
{
int const eta = st->eta;
gstr += " - ";
if (eta < 0)
{
gstr += _("Remaining time unknown");
}
else
{
gstr += tr_format_time_left(eta);
}
}
return gstr;
}
std::string getShortTransferString(
tr_torrent const* const tor,
tr_stat const* const st,
double uploadSpeed_KBps,
double downloadSpeed_KBps)
{
bool const have_meta = tr_torrentHasMetadata(tor);
if (bool const have_down = have_meta && (st->peersSendingToUs > 0 || st->webseedsSendingToUs > 0); have_down)
{
return fmt::format(
_("{download_speed} ▼ {upload_speed} ▲"),
fmt::arg("upload_speed", tr_formatter_speed_KBps(uploadSpeed_KBps)),
fmt::arg("download_speed", tr_formatter_speed_KBps(downloadSpeed_KBps)));
}
if (bool const have_up = have_meta && st->peersGettingFromUs > 0; have_up)
{
return fmt::format(_("{upload_speed} ▲"), fmt::arg("upload_speed", tr_formatter_speed_KBps(uploadSpeed_KBps)));
}
if (st->isStalled)
{
return _("Stalled");
}
return {};
}
std::string getShortStatusString(
tr_torrent const* const tor,
tr_stat const* const st,
double uploadSpeed_KBps,
double downloadSpeed_KBps)
{
switch (st->activity)
{
case TR_STATUS_STOPPED:
return st->finished ? _("Finished") : _("Paused");
case TR_STATUS_CHECK_WAIT:
return _("Queued for verification");
case TR_STATUS_DOWNLOAD_WAIT:
return _("Queued for download");
case TR_STATUS_SEED_WAIT:
return _("Queued for seeding");
case TR_STATUS_CHECK:
return fmt::format(
// xgettext:no-c-format
_("Verifying local data ({percent_done}% tested)"),
fmt::arg("percent_done", tr_truncd(st->recheckProgress * 100.0, 1)));
case TR_STATUS_DOWNLOAD:
case TR_STATUS_SEED:
return fmt::format(
FMT_STRING("{:s} {:s}"),
getShortTransferString(tor, st, uploadSpeed_KBps, downloadSpeed_KBps),
fmt::format(_("Ratio: {ratio}"), fmt::arg("ratio", tr_strlratio(st->ratio))));
default:
return {};
}
}
std::optional<std::string> getErrorString(tr_stat const* st)
{
switch (st->error)
{
case TR_STAT_TRACKER_WARNING:
return fmt::format(_("Tracker warning: '{warning}'"), fmt::arg("warning", st->errorString));
case TR_STAT_TRACKER_ERROR:
return fmt::format(_("Tracker Error: '{error}'"), fmt::arg("error", st->errorString));
case TR_STAT_LOCAL_ERROR:
return fmt::format(_("Local error: '{error}'"), fmt::arg("error", st->errorString));
default:
return std::nullopt;
}
}
auto getActivityString(
tr_torrent const* const tor,
tr_stat const* const st,
double const uploadSpeed_KBps,
double const downloadSpeed_KBps)
{
switch (st->activity)
{
case TR_STATUS_STOPPED:
case TR_STATUS_CHECK_WAIT:
case TR_STATUS_CHECK:
case TR_STATUS_DOWNLOAD_WAIT:
case TR_STATUS_SEED_WAIT:
return getShortStatusString(tor, st, uploadSpeed_KBps, downloadSpeed_KBps);
case TR_STATUS_DOWNLOAD:
if (!tr_torrentHasMetadata(tor))
{
return fmt::format(
ngettext(
// xgettext:no-c-format
"Downloading metadata from {active_count} connected peer ({percent_done}% done)",
"Downloading metadata from {active_count} connected peers ({percent_done}% done)",
st->peersConnected),
fmt::arg("active_count", st->peersConnected),
fmt::arg("percent_done", tr_strpercent(st->metadataPercentComplete * 100.0)));
}
if (st->peersSendingToUs != 0 && st->webseedsSendingToUs != 0)
{
return fmt::format(
ngettext(
"Downloading from {active_count} of {connected_count} connected peer and webseed",
"Downloading from {active_count} of {connected_count} connected peers and webseeds",
st->peersConnected + st->webseedsSendingToUs),
fmt::arg("active_count", st->peersSendingToUs + st->webseedsSendingToUs),
fmt::arg("connected_count", st->peersConnected + st->webseedsSendingToUs));
}
if (st->webseedsSendingToUs != 0)
{
return fmt::format(
ngettext(
"Downloading from {active_count} webseed",
"Downloading from {active_count} webseeds",
st->webseedsSendingToUs),
fmt::arg("active_count", st->webseedsSendingToUs));
}
return fmt::format(
ngettext(
"Downloading from {active_count} of {connected_count} connected peer",
"Downloading from {active_count} of {connected_count} connected peers",
st->peersConnected),
fmt::arg("active_count", st->peersSendingToUs),
fmt::arg("connected_count", st->peersConnected));
case TR_STATUS_SEED:
return fmt::format(
ngettext(
"Seeding to {active_count} of {connected_count} connected peer",
"Seeding to {active_count} of {connected_count} connected peers",
st->peersConnected),
fmt::arg("active_count", st->peersGettingFromUs),
fmt::arg("connected_count", st->peersConnected));
default:
g_assert_not_reached();
return std::string{};
}
}
std::string getStatusString(
tr_torrent const* tor,
tr_stat const* st,
double const uploadSpeed_KBps,
double const downloadSpeed_KBps,
bool ignore_errors = false)
{
auto status_str = (ignore_errors ? std::nullopt : getErrorString(st))
.value_or(getActivityString(tor, st, uploadSpeed_KBps, downloadSpeed_KBps));
if (st->activity != TR_STATUS_CHECK_WAIT && st->activity != TR_STATUS_CHECK && st->activity != TR_STATUS_DOWNLOAD_WAIT &&
st->activity != TR_STATUS_SEED_WAIT && st->activity != TR_STATUS_STOPPED)
{
if (auto const buf = getShortTransferString(tor, st, uploadSpeed_KBps, downloadSpeed_KBps); !std::empty(buf))
{
status_str += fmt::format(FMT_STRING(" - {:s}"), buf);
}
}
return status_str;
}
} // namespace
/***
@ -348,16 +89,6 @@ public:
return property_bar_height_;
}
auto& property_upload_speed_KBps()
{
return property_upload_speed_KBps_;
}
auto& property_download_speed_KBps()
{
return property_download_speed_KBps_;
}
auto& property_compact()
{
return property_compact_;
@ -383,10 +114,8 @@ private:
private:
TorrentCellRenderer& renderer_;
Glib::Property<gpointer> property_torrent_;
Glib::Property<Torrent*> property_torrent_;
Glib::Property<int> property_bar_height_;
Glib::Property<double> property_upload_speed_KBps_;
Glib::Property<double> property_download_speed_KBps_;
Glib::Property<bool> property_compact_;
Gtk::CellRendererText* text_renderer_ = nullptr;
@ -398,37 +127,6 @@ private:
****
***/
namespace
{
Glib::RefPtr<Gio::Icon> get_icon(tr_torrent const* tor)
{
auto mime_type = std::string_view{};
if (auto const n_files = tr_torrentFileCount(tor); n_files == 0)
{
mime_type = UnknownMimeType;
}
else if (n_files > 1)
{
mime_type = DirectoryMimeType;
}
else
{
auto const* const name = tr_torrentFile(tor, 0).name;
mime_type = strchr(name, '/') != nullptr ? DirectoryMimeType : tr_get_mime_type_for_filename(name);
}
return gtr_get_mime_type_icon(mime_type);
}
} // namespace
/***
****
***/
void TorrentCellRenderer::Impl::set_icon(
Gtk::CellRendererPixbuf& renderer,
Glib::RefPtr<Gio::Icon> const& icon,
@ -451,16 +149,11 @@ Gtk::Requisition TorrentCellRenderer::Impl::get_size_compact(Gtk::Widget& widget
Gtk::Requisition name_size;
Gtk::Requisition stat_size;
auto* const tor = static_cast<tr_torrent*>(property_torrent_.get_value());
auto const* const st = tr_torrentStatCached(tor);
auto const& torrent = *property_torrent_.get_value();
auto const icon = get_icon(tor);
auto const name = Glib::ustring(tr_torrentName(tor));
auto const gstr_stat = getShortStatusString(
tor,
st,
property_upload_speed_KBps_.get_value(),
property_download_speed_KBps_.get_value());
auto const icon = torrent.get_icon();
auto const name = torrent.get_name();
auto const gstr_stat = torrent.get_short_status_text();
renderer_.get_padding(xpad, ypad);
/* get the idealized cell dimensions */
@ -492,19 +185,12 @@ Gtk::Requisition TorrentCellRenderer::Impl::get_size_full(Gtk::Widget& widget) c
Gtk::Requisition stat_size;
Gtk::Requisition prog_size;
auto* const tor = static_cast<tr_torrent*>(property_torrent_.get_value());
auto const* const st = tr_torrentStatCached(tor);
auto const total_size = tr_torrentTotalSize(tor);
auto const& torrent = *property_torrent_.get_value();
auto const icon = get_icon(tor);
auto const name = Glib::ustring(tr_torrentName(tor));
auto const gstr_stat = getStatusString(
tor,
st,
property_upload_speed_KBps_.get_value(),
property_download_speed_KBps_.get_value(),
true);
auto const gstr_prog = getProgressString(tor, total_size, st);
auto const icon = torrent.get_icon();
auto const name = torrent.get_name();
auto const gstr_stat = torrent.get_long_status_text();
auto const gstr_prog = torrent.get_long_progress_text();
renderer_.get_padding(xpad, ypad);
/* get the idealized cell dimensions */
@ -558,20 +244,21 @@ void TorrentCellRenderer::get_preferred_height_vfunc(Gtk::Widget& widget, int& m
namespace
{
int get_percent_done(tr_torrent const* tor, tr_stat const* st)
{
auto const seed = st->activity == TR_STATUS_SEED && tr_torrentGetSeedRatio(tor, nullptr);
return static_cast<int>((seed ? std::max(0.0F, st->seedRatioPercentDone) : std::max(0.0F, st->percentDone)) * 100);
}
Gdk::RGBA const& get_progress_bar_color(tr_stat const& st)
Gdk::RGBA const& get_progress_bar_color(Torrent const& torrent)
{
static auto const steelblue_color = Gdk::RGBA("steelblue");
static auto const forestgreen_color = Gdk::RGBA("forestgreen");
static auto const silver_color = Gdk::RGBA("silver");
return st.activity == TR_STATUS_DOWNLOAD ? steelblue_color :
(st.activity == TR_STATUS_SEED ? forestgreen_color : silver_color);
switch (torrent.get_activity())
{
case TR_STATUS_DOWNLOAD:
return steelblue_color;
case TR_STATUS_SEED:
return forestgreen_color;
default:
return silver_color;
}
}
Cairo::RefPtr<Cairo::Surface> get_mask_surface(Cairo::RefPtr<Cairo::Surface> const& surface, Gdk::Rectangle const& area)
@ -690,19 +377,11 @@ void TorrentCellRenderer::Impl::render_compact(
int min_width = 0;
int width = 0;
auto* const tor = static_cast<tr_torrent*>(property_torrent_.get_value());
auto const* const st = tr_torrentStatCached(tor);
bool const active = st->activity != TR_STATUS_STOPPED && st->activity != TR_STATUS_DOWNLOAD_WAIT &&
st->activity != TR_STATUS_SEED_WAIT;
auto const percent_done = get_percent_done(tor, st);
bool const sensitive = active || st->error != 0;
auto const& torrent = *property_torrent_.get_value();
auto const percent_done = static_cast<int>(torrent.get_percent_done() * 100);
bool const sensitive = torrent.get_sensitive();
if (st->activity == TR_STATUS_STOPPED)
{
flags |= TR_GTK_CELL_RENDERER_STATE(INSENSITIVE);
}
if (st->error != 0 && (flags & TR_GTK_CELL_RENDERER_STATE(SELECTED)) == Gtk::CellRendererState{})
if (torrent.get_error_code() != 0 && (flags & TR_GTK_CELL_RENDERER_STATE(SELECTED)) == Gtk::CellRendererState{})
{
text_renderer_->property_foreground() = "red";
}
@ -711,14 +390,10 @@ void TorrentCellRenderer::Impl::render_compact(
text_renderer_->property_foreground_set() = false;
}
auto const icon = get_icon(tor);
auto const name = Glib::ustring(tr_torrentName(tor));
auto const& progress_color = get_progress_bar_color(*st);
auto const gstr_stat = getShortStatusString(
tor,
st,
property_upload_speed_KBps_.get_value(),
property_download_speed_KBps_.get_value());
auto const icon = torrent.get_icon();
auto const name = torrent.get_name();
auto const& progress_color = get_progress_bar_color(torrent);
auto const gstr_stat = torrent.get_short_status_text();
renderer_.get_padding(xpad, ypad);
auto fill_area = background_area;
@ -777,6 +452,7 @@ void TorrentCellRenderer::Impl::render_compact(
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_->property_text() = name;
@ -795,20 +471,11 @@ void TorrentCellRenderer::Impl::render_full(
Gtk::Requisition min_size;
Gtk::Requisition size;
auto* const tor = static_cast<tr_torrent*>(property_torrent_.get_value());
auto const* const st = tr_torrentStatCached(tor);
auto const total_size = tr_torrentTotalSize(tor);
bool const active = st->activity != TR_STATUS_STOPPED && st->activity != TR_STATUS_DOWNLOAD_WAIT &&
st->activity != TR_STATUS_SEED_WAIT;
auto const percent_done = get_percent_done(tor, st);
bool const sensitive = active || st->error != 0;
auto const& torrent = *property_torrent_.get_value();
auto const percent_done = static_cast<int>(torrent.get_percent_done() * 100);
bool const sensitive = torrent.get_sensitive();
if (st->activity == TR_STATUS_STOPPED)
{
flags |= TR_GTK_CELL_RENDERER_STATE(INSENSITIVE);
}
if (st->error != 0 && (flags & TR_GTK_CELL_RENDERER_STATE(SELECTED)) == Gtk::CellRendererState{})
if (torrent.get_error_code() != 0 && (flags & TR_GTK_CELL_RENDERER_STATE(SELECTED)) == Gtk::CellRendererState{})
{
text_renderer_->property_foreground() = "red";
}
@ -817,15 +484,11 @@ void TorrentCellRenderer::Impl::render_full(
text_renderer_->property_foreground_set() = false;
}
auto const icon = get_icon(tor);
auto const name = Glib::ustring(tr_torrentName(tor));
auto const& progress_color = get_progress_bar_color(*st);
auto const gstr_prog = getProgressString(tor, total_size, st);
auto const gstr_stat = getStatusString(
tor,
st,
property_upload_speed_KBps_.get_value(),
property_download_speed_KBps_.get_value());
auto const icon = torrent.get_icon();
auto const name = torrent.get_name();
auto const& progress_color = get_progress_bar_color(torrent);
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 */
@ -913,6 +576,7 @@ void TorrentCellRenderer::Impl::render_full(
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_->property_text() = gstr_prog;
@ -977,32 +641,23 @@ TorrentCellRenderer::Impl::Impl(TorrentCellRenderer& renderer)
: renderer_(renderer)
, property_torrent_(renderer, "torrent", nullptr)
, property_bar_height_(renderer, "bar-height", DefaultBarHeight)
, property_upload_speed_KBps_(renderer, "piece-upload-speed", 0)
, property_download_speed_KBps_(renderer, "piece-download-speed", 0)
, property_compact_(renderer, "compact", false)
, text_renderer_(Gtk::make_managed<Gtk::CellRendererText>())
, progress_renderer_(Gtk::make_managed<Gtk::CellRendererProgress>())
, icon_renderer_(Gtk::make_managed<Gtk::CellRendererPixbuf>())
{
renderer_.property_xpad() = GUI_PAD_SMALL;
renderer_.property_ypad() = GUI_PAD_SMALL;
text_renderer_->property_xpad() = 0;
text_renderer_->property_ypad() = 0;
}
Glib::PropertyProxy<gpointer> TorrentCellRenderer::property_torrent()
Glib::PropertyProxy<Torrent*> TorrentCellRenderer::property_torrent()
{
return impl_->property_torrent().get_proxy();
}
Glib::PropertyProxy<double> TorrentCellRenderer::property_piece_upload_speed()
{
return impl_->property_upload_speed_KBps().get_proxy();
}
Glib::PropertyProxy<double> TorrentCellRenderer::property_piece_download_speed()
{
return impl_->property_download_speed_KBps().get_proxy();
}
Glib::PropertyProxy<int> TorrentCellRenderer::property_bar_height()
{
return impl_->property_bar_height().get_proxy();

View File

@ -14,7 +14,7 @@
#include "Utils.h"
struct tr_torrent;
class Torrent;
class TorrentCellRenderer : public Gtk::CellRenderer
{
@ -26,16 +26,7 @@ public:
TR_DISABLE_COPY_MOVE(TorrentCellRenderer)
Glib::PropertyProxy<gpointer> property_torrent();
/* Use this instead of tr_stat.pieceUploadSpeed so that the model can
control when the speed displays get updated. This is done to keep
the individual torrents' speeds and the status bar's overall speed
in sync even if they refresh at slightly different times */
Glib::PropertyProxy<double> property_piece_upload_speed();
/* @see property_piece_upload_speed */
Glib::PropertyProxy<double> property_piece_download_speed();
Glib::PropertyProxy<Torrent*> property_torrent();
Glib::PropertyProxy<int> property_bar_height();
Glib::PropertyProxy<bool> property_compact();

276
gtk/TorrentFilter.cc Normal file
View File

@ -0,0 +1,276 @@
// This file Copyright © 2022 Mnemosyne LLC.
// It may be used under GPLv2 (SPDX: GPL-2.0-only), GPLv3 (SPDX: GPL-3.0-only),
// or any future license endorsed by Mnemosyne LLC.
// License text can be found in the licenses/ folder.
#include <libtransmission/transmission.h>
#include "Torrent.h"
#include "TorrentFilter.h"
#include "Utils.h"
void TorrentFilter::set_activity(Activity type)
{
if (activity_type_ == type)
{
return;
}
auto change = Change::DIFFERENT;
if (activity_type_ == Activity::ALL)
{
change = Change::MORE_STRICT;
}
else if (type == Activity::ALL)
{
change = Change::LESS_STRICT;
}
activity_type_ = type;
changed(change);
}
void TorrentFilter::set_tracker(Tracker type, Glib::ustring const& host)
{
if (tracker_type_ == type && tracker_host_ == host)
{
return;
}
auto change = Change::DIFFERENT;
if (tracker_type_ != type)
{
if (tracker_type_ == Tracker::ALL)
{
change = Change::MORE_STRICT;
}
else if (type == Tracker::ALL)
{
change = Change::LESS_STRICT;
}
}
else // tracker_host_ != host
{
if (tracker_host_.empty() || host.find(tracker_host_) != Glib::ustring::npos)
{
change = Change::MORE_STRICT;
}
else if (host.empty() || tracker_host_.find(host) != Glib::ustring::npos)
{
change = Change::LESS_STRICT;
}
}
tracker_type_ = type;
tracker_host_ = host;
changed(change);
}
void TorrentFilter::set_text(Glib::ustring const& text)
{
auto const normalized_text = gtr_str_strip(text.casefold());
if (text_ == normalized_text)
{
return;
}
auto change = Change::DIFFERENT;
if (text_.empty() || normalized_text.find(text_) != Glib::ustring::npos)
{
change = Change::MORE_STRICT;
}
else if (normalized_text.empty() || text_.find(normalized_text) != Glib::ustring::npos)
{
change = Change::LESS_STRICT;
}
text_ = normalized_text;
changed(change);
}
bool TorrentFilter::match_activity(Torrent const& torrent) const
{
return match_activity(torrent, activity_type_);
}
bool TorrentFilter::match_tracker(Torrent const& torrent) const
{
return match_tracker(torrent, tracker_type_, tracker_host_);
}
bool TorrentFilter::match_text(Torrent const& torrent) const
{
return match_text(torrent, text_);
}
bool TorrentFilter::match(Torrent const& torrent) const
{
return match_activity(torrent) && match_tracker(torrent) && match_text(torrent);
}
void TorrentFilter::update(Torrent::ChangeFlags changes)
{
using Flag = Torrent::ChangeFlag;
bool refilter_needed = false;
if (activity_type_ != Activity::ALL)
{
static auto const activity_flags = std::map<Activity, Torrent::ChangeFlags>({
{ Activity::DOWNLOADING, Flag::ACTIVITY },
{ Activity::SEEDING, Flag::ACTIVITY },
{ Activity::ACTIVE, Flag::ACTIVE_PEER_COUNT | Flag::ACTIVITY },
{ Activity::PAUSED, Flag::ACTIVITY },
{ Activity::FINISHED, Flag::FINISHED },
{ Activity::VERIFYING, Flag::ACTIVITY },
{ Activity::ERROR, Flag::ERROR_CODE },
});
auto const activity_flags_it = activity_flags.find(activity_type_);
refilter_needed = activity_flags_it != activity_flags.end() && changes.test(activity_flags_it->second);
}
if (!refilter_needed)
{
refilter_needed = tracker_type_ != Tracker::ALL && changes.test(Flag::TRACKERS);
}
if (!refilter_needed)
{
refilter_needed = !text_.empty() && changes.test(Flag::NAME);
}
if (refilter_needed)
{
changed(Change::DIFFERENT);
}
}
#if !GTKMM_CHECK_VERSION(4, 0, 0)
sigc::signal<void()>& TorrentFilter::signal_changed()
{
return signal_changed_;
}
#endif
Glib::RefPtr<TorrentFilter> TorrentFilter::create()
{
return Glib::make_refptr_for_instance(new TorrentFilter());
}
bool TorrentFilter::match_activity(Torrent const& torrent, Activity type)
{
auto activity = tr_torrent_activity();
switch (type)
{
case Activity::ALL:
return true;
case Activity::DOWNLOADING:
activity = torrent.get_activity();
return activity == TR_STATUS_DOWNLOAD || activity == TR_STATUS_DOWNLOAD_WAIT;
case Activity::SEEDING:
activity = torrent.get_activity();
return activity == TR_STATUS_SEED || activity == TR_STATUS_SEED_WAIT;
case Activity::ACTIVE:
return torrent.get_active_peer_count() > 0 || torrent.get_activity() == TR_STATUS_CHECK;
case Activity::PAUSED:
return torrent.get_activity() == TR_STATUS_STOPPED;
case Activity::FINISHED:
return torrent.get_finished();
case Activity::VERIFYING:
activity = torrent.get_activity();
return activity == TR_STATUS_CHECK || activity == TR_STATUS_CHECK_WAIT;
case Activity::ERROR:
return torrent.get_error_code() != 0;
default:
g_assert_not_reached();
return true;
}
}
bool TorrentFilter::match_tracker(Torrent const& torrent, Tracker type, Glib::ustring const& host)
{
if (type == Tracker::ALL)
{
return true;
}
g_assert(type == Tracker::HOST);
auto const& raw_torrent = torrent.get_underlying();
for (auto i = size_t{ 0 }, n = tr_torrentTrackerCount(&raw_torrent); i < n; ++i)
{
if (auto const tracker = tr_torrentTracker(&raw_torrent, i); std::data(tracker.sitename) == host)
{
return true;
}
}
return false;
}
bool TorrentFilter::match_text(Torrent const& torrent, Glib::ustring const& text)
{
bool ret = false;
if (text.empty())
{
ret = true;
}
else
{
auto const& raw_torrent = torrent.get_underlying();
/* test the torrent name... */
ret = torrent.get_name().casefold().find(text) != Glib::ustring::npos;
/* test the files... */
for (auto i = size_t{ 0 }, n = tr_torrentFileCount(&raw_torrent); i < n && !ret; ++i)
{
ret = Glib::ustring(tr_torrentFile(&raw_torrent, i).name).casefold().find(text) != Glib::ustring::npos;
}
}
return ret;
}
#if GTKMM_CHECK_VERSION(4, 0, 0)
bool TorrentFilter::match_vfunc(Glib::RefPtr<Glib::ObjectBase> const& item)
{
auto const torrent = gtr_ptr_dynamic_cast<Torrent>(item);
g_return_val_if_fail(torrent != nullptr, false);
return match(*torrent);
}
TorrentFilter::Match TorrentFilter::get_strictness_vfunc()
{
return activity_type_ == Activity::ALL && tracker_type_ == Tracker::ALL && text_.empty() ? Match::ALL : Match::SOME;
}
#else
void TorrentFilter::changed(Change /*change*/)
{
signal_changed_.emit();
}
#endif
TorrentFilter::TorrentFilter()
: Glib::ObjectBase(typeid(TorrentFilter))
{
}

86
gtk/TorrentFilter.h Normal file
View File

@ -0,0 +1,86 @@
// This file Copyright © 2022 Mnemosyne LLC.
// It may be used under GPLv2 (SPDX: GPL-2.0-only), GPLv3 (SPDX: GPL-3.0-only),
// or any future license endorsed by Mnemosyne LLC.
// License text can be found in the licenses/ folder.
#pragma once
#include <gtkmm.h>
#include "Utils.h"
class Torrent;
class TorrentFilter : public IF_GTKMM4(Gtk::Filter, Glib::Object)
{
#if !GTKMM_CHECK_VERSION(4, 0, 0)
enum class Change{
DIFFERENT,
LESS_STRICT,
MORE_STRICT,
};
#endif
public:
enum class Activity
{
ALL,
DOWNLOADING,
SEEDING,
ACTIVE,
PAUSED,
FINISHED,
VERIFYING,
ERROR,
};
enum class Tracker
{
ALL,
HOST,
};
public:
void set_activity(Activity type);
void set_tracker(Tracker type, Glib::ustring const& host);
void set_text(Glib::ustring const& text);
bool match_activity(Torrent const& torrent) const;
bool match_tracker(Torrent const& torrent) const;
bool match_text(Torrent const& torrent) const;
bool match(Torrent const& torrent) const;
void update(Torrent::ChangeFlags changes);
#if !GTKMM_CHECK_VERSION(4, 0, 0)
sigc::signal<void()>& signal_changed();
#endif
static Glib::RefPtr<TorrentFilter> create();
static bool match_activity(Torrent const& torrent, Activity type);
static bool match_tracker(Torrent const& torrent, Tracker type, Glib::ustring const& host);
static bool match_text(Torrent const& torrent, Glib::ustring const& text);
protected:
#if GTKMM_CHECK_VERSION(4, 0, 0)
bool match_vfunc(Glib::RefPtr<Glib::ObjectBase> const& item) override;
Match get_strictness_vfunc() override;
#else
void changed(Change change);
#endif
private:
TorrentFilter();
private:
Activity activity_type_ = Activity::ALL;
Tracker tracker_type_ = Tracker::ALL;
Glib::ustring tracker_host_;
Glib::ustring text_;
#if !GTKMM_CHECK_VERSION(4, 0, 0)
sigc::signal<void()> signal_changed_;
#endif
};

299
gtk/TorrentSorter.cc Normal file
View File

@ -0,0 +1,299 @@
// This file Copyright © 2022 Mnemosyne LLC.
// It may be used under GPLv2 (SPDX: GPL-2.0-only), GPLv3 (SPDX: GPL-3.0-only),
// or any future license endorsed by Mnemosyne LLC.
// License text can be found in the licenses/ folder.
#include <algorithm>
#include <libtransmission/transmission.h>
#include "Torrent.h"
#include "TorrentSorter.h"
using namespace std::string_view_literals;
namespace
{
constexpr bool is_valid_eta(time_t value)
{
return value != TR_ETA_NOT_AVAIL && value != TR_ETA_UNKNOWN;
}
// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
constexpr int compare_eta(time_t lhs, time_t rhs)
{
bool const lhs_valid = is_valid_eta(lhs);
bool const rhs_valid = is_valid_eta(rhs);
if (!lhs_valid && !rhs_valid)
{
return 0;
}
if (!lhs_valid)
{
return -1;
}
if (!rhs_valid)
{
return 1;
}
return -gtr_compare_generic(lhs, rhs);
}
// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
constexpr int compare_ratio(double lhs, double rhs)
{
if (static_cast<int>(lhs) == TR_RATIO_INF && static_cast<int>(rhs) == TR_RATIO_INF)
{
return 0;
}
if (static_cast<int>(lhs) == TR_RATIO_INF)
{
return 1;
}
if (static_cast<int>(rhs) == TR_RATIO_INF)
{
return -1;
}
return gtr_compare_generic(lhs, rhs);
}
// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
int compare_by_name(Torrent const& lhs, Torrent const& rhs)
{
return lhs.get_name_collated().compare(rhs.get_name_collated());
}
// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
int compare_by_queue(Torrent const& lhs, Torrent const& rhs)
{
return gtr_compare_generic(lhs.get_queue_position(), rhs.get_queue_position());
}
// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
int compare_by_ratio(Torrent const& lhs, Torrent const& rhs)
{
auto result = -compare_ratio(lhs.get_ratio(), rhs.get_ratio()); // default descending
if (result == 0)
{
result = compare_by_queue(lhs, rhs);
}
return result;
}
// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
int compare_by_activity(Torrent const& lhs, Torrent const& rhs)
{
auto result = -gtr_compare_generic(
lhs.get_speed_up() + lhs.get_speed_down(),
rhs.get_speed_up() + rhs.get_speed_down()); // default descending
if (result == 0)
{
result = -gtr_compare_generic(lhs.get_active_peer_count(), rhs.get_active_peer_count()); // default descending
}
if (result == 0)
{
result = compare_by_queue(lhs, rhs);
}
return result;
}
// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
int compare_by_age(Torrent const& lhs, Torrent const& rhs)
{
auto result = -gtr_compare_generic(lhs.get_added_date(), rhs.get_added_date()); // default descending
if (result == 0)
{
result = compare_by_name(lhs, rhs);
}
return result;
}
// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
int compare_by_size(Torrent const& lhs, Torrent const& rhs)
{
auto result = -gtr_compare_generic(lhs.get_total_size(), rhs.get_total_size()); // default descending
if (result == 0)
{
result = compare_by_name(lhs, rhs);
}
return result;
}
// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
int compare_by_progress(Torrent const& lhs, Torrent const& rhs)
{
auto result = -gtr_compare_generic(lhs.get_percent_complete(), rhs.get_percent_complete()); // default descending
if (result == 0)
{
result = -gtr_compare_generic(
lhs.get_seed_ratio_percent_done(),
rhs.get_seed_ratio_percent_done()); // default descending
}
if (result == 0)
{
result = compare_by_ratio(lhs, rhs);
}
return result;
}
// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
int compare_by_eta(Torrent const& lhs, Torrent const& rhs)
{
auto result = compare_eta(lhs.get_eta(), rhs.get_eta());
if (result == 0)
{
result = compare_by_name(lhs, rhs);
}
return result;
}
// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
int compare_by_state(Torrent const& lhs, Torrent const& rhs)
{
auto result = -gtr_compare_generic(lhs.get_activity(), rhs.get_activity());
if (result == 0)
{
result = compare_by_queue(lhs, rhs);
}
return result;
}
} // namespace
void TorrentSorter::set_mode(std::string_view mode)
{
static auto const compare_funcs = std::map<std::string_view, CompareFunc>({
{ "sort-by-activity"sv, &compare_by_activity },
{ "sort-by-age"sv, &compare_by_age },
{ "sort-by-name"sv, &compare_by_name },
{ "sort-by-progress"sv, &compare_by_progress },
{ "sort-by-queue"sv, &compare_by_queue },
{ "sort-by-ratio"sv, &compare_by_ratio },
{ "sort-by-size"sv, &compare_by_size },
{ "sort-by-state"sv, &compare_by_state },
{ "sort-by-time-left"sv, &compare_by_eta },
});
auto compare_func = &compare_by_name;
if (auto const compare_func_it = compare_funcs.find(mode); compare_func_it != compare_funcs.end())
{
compare_func = compare_func_it->second;
}
if (compare_func_ == compare_func)
{
return;
}
compare_func_ = compare_func;
changed(Change::DIFFERENT);
}
void TorrentSorter::set_reversed(bool is_reversed)
{
if (is_reversed_ == is_reversed)
{
return;
}
is_reversed_ = is_reversed;
changed(Change::INVERTED);
}
int TorrentSorter::compare(Torrent const& lhs, Torrent const& rhs) const
{
return compare_func_ != nullptr ? std::clamp(compare_func_(lhs, rhs), -1, 1) * (is_reversed_ ? -1 : 1) : 0;
}
void TorrentSorter::update(Torrent::ChangeFlags changes)
{
using Flag = Torrent::ChangeFlag;
static auto const compare_flags = std::map<CompareFunc, Torrent::ChangeFlags>({
{ &compare_by_activity, Flag::ACTIVE_PEER_COUNT | Flag::QUEUE_POSITION | Flag::SPEED_DOWN | Flag::SPEED_UP },
{ &compare_by_age, Flag::ADDED_DATE | Flag::NAME },
{ &compare_by_eta, Flag::ETA | Flag::NAME },
{ &compare_by_name, Flag::NAME },
{ &compare_by_progress, Flag::PERCENT_COMPLETE | Flag::QUEUE_POSITION | Flag::RATIO | Flag::SEED_RATIO_PERCENT_DONE },
{ &compare_by_queue, Flag::QUEUE_POSITION },
{ &compare_by_ratio, Flag::QUEUE_POSITION | Flag::RATIO },
{ &compare_by_size, Flag::NAME | Flag::TOTAL_SIZE },
{ &compare_by_state, Flag::ACTIVITY | Flag::QUEUE_POSITION },
});
if (auto const compare_flags_it = compare_flags.find(compare_func_);
compare_flags_it != compare_flags.end() && changes.test(compare_flags_it->second))
{
changed(Change::DIFFERENT);
}
}
#if !GTKMM_CHECK_VERSION(4, 0, 0)
sigc::signal<void()>& TorrentSorter::signal_changed()
{
return signal_changed_;
}
#endif
Glib::RefPtr<TorrentSorter> TorrentSorter::create()
{
return Glib::make_refptr_for_instance(new TorrentSorter());
}
#if GTKMM_CHECK_VERSION(4, 0, 0)
Gtk::Ordering TorrentSorter::compare_vfunc(gpointer lhs, gpointer rhs)
{
auto const* const lhs_torrent = dynamic_cast<Torrent const*>(Glib::wrap_auto(static_cast<GObject*>(lhs)));
g_return_val_if_fail(lhs_torrent != nullptr, Gtk::Ordering::SMALLER);
auto const* const rhs_torrent = dynamic_cast<Torrent const*>(Glib::wrap_auto(static_cast<GObject*>(rhs)));
g_return_val_if_fail(rhs_torrent != nullptr, Gtk::Ordering::LARGER);
return Gtk::Ordering{ compare(*lhs_torrent, *rhs_torrent) };
}
Gtk::Sorter::Order TorrentSorter::get_order_vfunc()
{
return Gtk::Sorter::Order::PARTIAL;
}
#else
void TorrentSorter::changed(Change /*change*/)
{
signal_changed_.emit();
}
#endif
TorrentSorter::TorrentSorter()
: Glib::ObjectBase(typeid(TorrentSorter))
{
}

60
gtk/TorrentSorter.h Normal file
View File

@ -0,0 +1,60 @@
// This file Copyright © 2022 Mnemosyne LLC.
// It may be used under GPLv2 (SPDX: GPL-2.0-only), GPLv3 (SPDX: GPL-3.0-only),
// or any future license endorsed by Mnemosyne LLC.
// License text can be found in the licenses/ folder.
#pragma once
#include <glibmm.h>
#include <gtkmm.h>
#include "Utils.h"
class Torrent;
class TorrentSorter : public IF_GTKMM4(Gtk::Sorter, Glib::Object)
{
using CompareFunc = int (*)(Torrent const&, Torrent const&);
#if !GTKMM_CHECK_VERSION(4, 0, 0)
enum class Change{
DIFFERENT,
INVERTED,
LESS_STRICT,
MORE_STRICT,
};
#endif
public:
void set_mode(std::string_view mode);
void set_reversed(bool is_reversed);
int compare(Torrent const& lhs, Torrent const& rhs) const;
void update(Torrent::ChangeFlags changes);
#if !GTKMM_CHECK_VERSION(4, 0, 0)
sigc::signal<void()>& signal_changed();
#endif
static Glib::RefPtr<TorrentSorter> create();
protected:
#if GTKMM_CHECK_VERSION(4, 0, 0)
Gtk::Ordering compare_vfunc(gpointer lhs, gpointer rhs) override;
Order get_order_vfunc() override;
#else
void changed(Change change);
#endif
private:
TorrentSorter();
private:
CompareFunc compare_func_ = nullptr;
bool is_reversed_ = false;
#if !GTKMM_CHECK_VERSION(4, 0, 0)
sigc::signal<void()> signal_changed_;
#endif
};

View File

@ -434,6 +434,38 @@ bool gtr_file_trash_or_remove(std::string const& filename, tr_error** error)
return result;
}
namespace
{
void object_signal_notify_callback(GObject* object, GParamSpec* /*param_spec*/, gpointer data)
{
if (object != nullptr && data != nullptr)
{
if (auto const* const slot = Glib::SignalProxyBase::data_to_slot(data); slot != nullptr)
{
(*static_cast<sigc::slot<TrObjectSignalNotifyCallback> const*>(slot))(Glib::wrap(object, true));
}
}
}
} // namespace
Glib::SignalProxy<TrObjectSignalNotifyCallback> gtr_object_signal_notify(Glib::ObjectBase& object)
{
static auto const object_signal_notify_info = Glib::SignalProxyInfo{
.signal_name = "notify",
.callback = reinterpret_cast<GCallback>(&object_signal_notify_callback),
.notify_callback = reinterpret_cast<GCallback>(&object_signal_notify_callback),
};
return { &object, &object_signal_notify_info };
}
void gtr_object_notify_emit(Glib::ObjectBase& object)
{
g_signal_emit_by_name(object.gobj(), "notify", nullptr);
}
Glib::ustring gtr_get_help_uri()
{
static auto const uri = fmt::format("https://transmissionbt.com/help/gtk/{}.{}x", MAJOR_VERSION, MINOR_VERSION / 10);

View File

@ -25,8 +25,6 @@
#include <libtransmission/transmission.h>
#include <libtransmission/tr-macros.h>
#include "Session.h"
/***
****
***/
@ -78,6 +76,7 @@
#define TR_GTK_SELECTION_MODE(Code) IF_GTKMM4(Gtk::SelectionMode::Code, Gtk::SELECTION_##Code)
#define TR_GTK_SORT_TYPE(Code) IF_GTKMM4(Gtk::SortType::Code, Gtk::SORT_##Code)
#define TR_GTK_STATE_FLAGS(Code) IF_GTKMM4(Gtk::StateFlags::Code, Gtk::STATE_FLAG_##Code)
#define TR_GTK_TREE_MODEL_FLAGS(Code) IF_GTKMM4(Gtk::TreeModel::Flags::Code, Gtk::TREE_MODEL_##Code)
#define TR_GTK_TREE_VIEW_COLUMN_SIZING(Code) IF_GTKMM4(Gtk::TreeViewColumn::Sizing::Code, Gtk::TREE_VIEW_COLUMN_##Code)
#define TR_GTK_TREE_MODEL_CHILD_ITER(Obj) IF_GTKMM4((Obj).get_iter(), (Obj))
@ -166,6 +165,11 @@ std::string tr_format_time(time_t timestamp);
****
***/
using TrObjectSignalNotifyCallback = void(Glib::RefPtr<Glib::ObjectBase const> const&);
Glib::SignalProxy<TrObjectSignalNotifyCallback> gtr_object_signal_notify(Glib::ObjectBase& object);
void gtr_object_notify_emit(Glib::ObjectBase& object);
void gtr_open_uri(Glib::ustring const& uri);
void gtr_open_file(std::string const& path);
@ -242,12 +246,31 @@ inline T gtr_str_strip(T const& text)
return new_begin == T::npos ? T() : text.substr(new_begin, new_end == T::npos ? new_end : new_end - new_begin + 1);
}
template<typename T>
// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
constexpr int gtr_compare_generic(T const& lhs, T const& rhs)
{
if (lhs < rhs)
{
return -1;
}
if (lhs > rhs)
{
return 1;
}
return 0;
}
std::string gtr_get_full_resource_path(std::string const& rel_path);
/***
****
***/
class Session;
extern size_t const max_recent_dirs;
std::list<std::string> gtr_get_recent_dirs(std::string const& pref);
void gtr_save_recent_dir(std::string const& pref, Glib::RefPtr<Session> const& core, std::string const& dir);