1
0
Fork 0
mirror of https://github.com/transmission/transmission synced 2025-02-21 21:57:01 +00:00
transmission/gtk/DetailsDialog.cc
Mike Gelfand 586cff9506
Switch to list view for torrents list (GTK 4) (#5858)
* Add compat operator* for RefPtr

* Rename `*_tree_view_*` button handling helpers to `*_item_view_*`

* Move torrent item colors to CSS

* Switch to list view for torrents list (GTK 4)

* Bump Fedora image to 39 (current rawhide) for GTK 4.11

Enable deprecations as there're lots of them in 4.11 and I'm not keen on
fixing them all right now. Disable warnings as errors due to
-Warray-bounds issue somewhere in libfmt.
2023-08-06 04:26:29 +01:00

2653 lines
84 KiB
C++

// This file Copyright © 2007-2023 Mnemosyne LLC.
// It may be used under GPLv2 (SPDX: GPL-2.0-only), GPLv3 (SPDX: GPL-3.0-only),
// or any future license endorsed by Mnemosyne LLC.
// License text can be found in the licenses/ folder.
#include "DetailsDialog.h"
#include "Actions.h"
#include "FileList.h"
#include "GtkCompat.h"
#include "HigWorkarea.h" // GUI_PAD, GUI_PAD_BIG, GUI_PAD_SMALL
#include "Prefs.h"
#include "PrefsDialog.h"
#include "Session.h"
#include "Utils.h"
#include <libtransmission/utils.h>
#include <libtransmission/web-utils.h>
#include <gdkmm/pixbuf.h>
#include <glibmm/i18n.h>
#include <glibmm/main.h>
#include <glibmm/markup.h>
#include <glibmm/quark.h>
#include <glibmm/ustring.h>
#include <gtkmm/adjustment.h>
#include <gtkmm/button.h>
#include <gtkmm/cellrendererpixbuf.h>
#include <gtkmm/cellrendererprogress.h>
#include <gtkmm/cellrenderertext.h>
#include <gtkmm/checkbutton.h>
#include <gtkmm/combobox.h>
#include <gtkmm/entry.h>
#include <gtkmm/label.h>
#include <gtkmm/liststore.h>
#include <gtkmm/messagedialog.h>
#include <gtkmm/notebook.h>
#include <gtkmm/scrolledwindow.h>
#include <gtkmm/spinbutton.h>
#include <gtkmm/textbuffer.h>
#include <gtkmm/textview.h>
#include <gtkmm/tooltip.h>
#include <gtkmm/treemodel.h>
#include <gtkmm/treemodelfilter.h>
#include <gtkmm/treemodelsort.h>
#include <gtkmm/treerowreference.h>
#include <gtkmm/treeview.h>
#include <fmt/chrono.h>
#include <fmt/core.h>
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdlib> // abort()
#include <limits>
#include <memory>
#include <numeric>
#include <sstream>
#include <string>
#include <string_view>
#include <unordered_map>
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#else
#include <arpa/inet.h>
#include <sys/socket.h>
#endif
using namespace std::literals;
class DetailsDialog::Impl
{
public:
Impl(DetailsDialog& dialog, Glib::RefPtr<Gtk::Builder> const& builder, Glib::RefPtr<Session> const& core);
~Impl();
TR_DISABLE_COPY_MOVE(Impl)
void set_torrents(std::vector<tr_torrent_id_t> const& torrent_ids);
void refresh();
private:
void info_page_init(Glib::RefPtr<Gtk::Builder> const& builder);
void peer_page_init(Glib::RefPtr<Gtk::Builder> const& builder);
void tracker_page_init(Glib::RefPtr<Gtk::Builder> const& builder);
void options_page_init(Glib::RefPtr<Gtk::Builder> const& builder);
void on_details_window_size_allocated();
bool onPeerViewQueryTooltip(int x, int y, bool keyboard_tip, Glib::RefPtr<Gtk::Tooltip> const& tooltip);
void onMorePeerInfoToggled();
bool trackerVisibleFunc(Gtk::TreeModel::const_iterator const& iter);
void on_tracker_list_selection_changed();
void on_tracker_list_add_button_clicked();
void on_edit_trackers();
void on_tracker_list_remove_button_clicked();
void onScrapeToggled();
void onBackupToggled();
void torrent_set_bool(tr_quark key, bool value);
void torrent_set_int(tr_quark key, int value);
void torrent_set_real(tr_quark key, double value);
void refreshInfo(std::vector<tr_torrent*> const& torrents);
void refreshPeers(std::vector<tr_torrent*> const& torrents);
void refreshTracker(std::vector<tr_torrent*> const& torrents);
void refreshFiles(std::vector<tr_torrent*> const& torrents);
void refreshOptions(std::vector<tr_torrent*> const& torrents);
void refreshPeerList(std::vector<tr_torrent*> const& torrents);
void refreshWebseedList(std::vector<tr_torrent*> const& torrents);
tr_torrent_id_t tracker_list_get_current_torrent_id() const;
tr_torrent* tracker_list_get_current_torrent() const;
std::vector<tr_torrent*> getTorrents() const;
private:
DetailsDialog& dialog_;
Glib::RefPtr<Session> const core_;
Gtk::CheckButton* honor_limits_check_ = nullptr;
Gtk::CheckButton* up_limited_check_ = nullptr;
Gtk::SpinButton* up_limit_sping_ = nullptr;
Gtk::CheckButton* down_limited_check_ = nullptr;
Gtk::SpinButton* down_limit_spin_ = nullptr;
Gtk::ComboBox* bandwidth_combo_ = nullptr;
Gtk::ComboBox* ratio_combo_ = nullptr;
Gtk::SpinButton* ratio_spin_ = nullptr;
Gtk::ComboBox* idle_combo_ = nullptr;
Gtk::SpinButton* idle_spin_ = nullptr;
Gtk::SpinButton* max_peers_spin_ = nullptr;
sigc::connection honor_limits_check_tag_;
sigc::connection up_limited_check_tag_;
sigc::connection down_limited_check_tag_;
sigc::connection down_limit_spin_tag_;
sigc::connection up_limit_spin_tag_;
sigc::connection bandwidth_combo_tag_;
sigc::connection ratio_combo_tag_;
sigc::connection ratio_spin_tag_;
sigc::connection idle_combo_tag_;
sigc::connection idle_spin_tag_;
sigc::connection max_peers_spin_tag_;
Gtk::Label* added_lb_ = nullptr;
Gtk::Label* size_lb_ = nullptr;
Gtk::Label* state_lb_ = nullptr;
Gtk::Label* have_lb_ = nullptr;
Gtk::Label* dl_lb_ = nullptr;
Gtk::Label* ul_lb_ = nullptr;
Gtk::Label* error_lb_ = nullptr;
Gtk::Label* date_started_lb_ = nullptr;
Gtk::Label* eta_lb_ = nullptr;
Gtk::Label* last_activity_lb_ = nullptr;
Gtk::Label* hash_lb_ = nullptr;
Gtk::Label* privacy_lb_ = nullptr;
Gtk::Label* origin_lb_ = nullptr;
Gtk::Label* destination_lb_ = nullptr;
Glib::RefPtr<Gtk::TextBuffer> comment_buffer_;
std::unordered_map<std::string, Gtk::TreeRowReference> peer_hash_;
std::unordered_map<std::string, Gtk::TreeRowReference> webseed_hash_;
Glib::RefPtr<Gtk::ListStore> peer_store_;
Glib::RefPtr<Gtk::ListStore> webseed_store_;
Gtk::ScrolledWindow* webseed_view_ = nullptr;
Gtk::TreeView* peer_view_ = nullptr;
Gtk::CheckButton* more_peer_details_check_ = nullptr;
Glib::RefPtr<Gtk::ListStore> tracker_store_;
std::unordered_map<std::string, Gtk::TreeRowReference> tracker_hash_;
Glib::RefPtr<Gtk::TreeModelFilter> trackers_filtered_;
Gtk::Button* add_tracker_button_ = nullptr;
Gtk::Button* edit_trackers_button_ = nullptr;
Gtk::Button* remove_tracker_button_ = nullptr;
Gtk::TreeView* tracker_view_ = nullptr;
Gtk::CheckButton* scrape_check_ = nullptr;
Gtk::CheckButton* all_check_ = nullptr;
FileList* file_list_ = nullptr;
Gtk::Label* file_label_ = nullptr;
std::vector<tr_torrent_id_t> ids_;
sigc::connection periodic_refresh_tag_;
Glib::Quark const TORRENT_ID_KEY = Glib::Quark("tr-torrent-id-key");
Glib::Quark const TEXT_BUFFER_KEY = Glib::Quark("tr-text-buffer-key");
Glib::Quark const URL_ENTRY_KEY = Glib::Quark("tr-url-entry-key");
static guint last_page_;
};
guint DetailsDialog::Impl::last_page_ = 0;
std::vector<tr_torrent*> DetailsDialog::Impl::getTorrents() const
{
std::vector<tr_torrent*> torrents;
torrents.reserve(ids_.size());
for (auto const id : ids_)
{
if (auto* torrent = core_->find_torrent(id); torrent != nullptr)
{
torrents.push_back(torrent);
}
}
return torrents;
}
/****
*****
***** OPTIONS TAB
*****
****/
namespace
{
void set_togglebutton_if_different(Gtk::CheckButton* toggle, sigc::connection& tag, bool value)
{
bool const currentValue = toggle->get_active();
if (currentValue != value)
{
tag.block();
toggle->set_active(value);
tag.unblock();
}
}
void set_int_spin_if_different(Gtk::SpinButton* spin, sigc::connection& tag, int value)
{
int const currentValue = spin->get_value_as_int();
if (currentValue != value)
{
tag.block();
spin->set_value(value);
tag.unblock();
}
}
void set_double_spin_if_different(Gtk::SpinButton* spin, sigc::connection& tag, double value)
{
double const currentValue = spin->get_value();
if ((int)(currentValue * 100) != (int)(value * 100))
{
tag.block();
spin->set_value(value);
tag.unblock();
}
}
void unset_combo(Gtk::ComboBox* combobox, sigc::connection& tag)
{
tag.block();
combobox->set_active(-1);
tag.unblock();
}
} // namespace
void DetailsDialog::Impl::refreshOptions(std::vector<tr_torrent*> const& torrents)
{
/***
**** Options Page
***/
/* honor_limits_check */
if (!torrents.empty())
{
bool const baseline = tr_torrentUsesSessionLimits(torrents.front());
bool const is_uniform = std::all_of(
torrents.begin(),
torrents.end(),
[baseline](auto const* torrent) { return baseline == tr_torrentUsesSessionLimits(torrent); });
if (is_uniform)
{
set_togglebutton_if_different(honor_limits_check_, honor_limits_check_tag_, baseline);
}
}
/* down_limited_check */
if (!torrents.empty())
{
bool const baseline = tr_torrentUsesSpeedLimit(torrents.front(), TR_DOWN);
bool const is_uniform = std::all_of(
torrents.begin(),
torrents.end(),
[baseline](auto const* torrent) { return baseline == tr_torrentUsesSpeedLimit(torrent, TR_DOWN); });
if (is_uniform)
{
set_togglebutton_if_different(down_limited_check_, down_limited_check_tag_, baseline);
}
}
/* down_limit_spin */
if (!torrents.empty())
{
auto const baseline = tr_torrentGetSpeedLimit_KBps(torrents.front(), TR_DOWN);
bool const is_uniform = std::all_of(
torrents.begin(),
torrents.end(),
[baseline](auto const* torrent) { return baseline == tr_torrentGetSpeedLimit_KBps(torrent, TR_DOWN); });
if (is_uniform)
{
set_int_spin_if_different(down_limit_spin_, down_limit_spin_tag_, baseline);
}
}
/* up_limited_check */
if (!torrents.empty())
{
bool const baseline = tr_torrentUsesSpeedLimit(torrents.front(), TR_UP);
bool const is_uniform = std::all_of(
torrents.begin(),
torrents.end(),
[baseline](auto const* torrent) { return baseline == tr_torrentUsesSpeedLimit(torrent, TR_UP); });
if (is_uniform)
{
set_togglebutton_if_different(up_limited_check_, up_limited_check_tag_, baseline);
}
}
/* up_limit_sping */
if (!torrents.empty())
{
auto const baseline = tr_torrentGetSpeedLimit_KBps(torrents.front(), TR_UP);
bool const is_uniform = std::all_of(
torrents.begin(),
torrents.end(),
[baseline](auto const* torrent) { return baseline == tr_torrentGetSpeedLimit_KBps(torrent, TR_UP); });
if (is_uniform)
{
set_int_spin_if_different(up_limit_sping_, up_limit_spin_tag_, baseline);
}
}
/* bandwidth_combo */
if (!torrents.empty())
{
auto const baseline = tr_torrentGetPriority(torrents.front());
bool const is_uniform = std::all_of(
torrents.begin(),
torrents.end(),
[baseline](auto const* torrent) { return baseline == tr_torrentGetPriority(torrent); });
if (is_uniform)
{
bandwidth_combo_tag_.block();
gtr_combo_box_set_active_enum(*bandwidth_combo_, baseline);
bandwidth_combo_tag_.unblock();
}
else
{
unset_combo(bandwidth_combo_, bandwidth_combo_tag_);
}
}
/* ratio_combo */
if (!torrents.empty())
{
auto const baseline = tr_torrentGetRatioMode(torrents.front());
bool const is_uniform = std::all_of(
torrents.begin(),
torrents.end(),
[baseline](auto const* torrent) { return baseline == tr_torrentGetRatioMode(torrent); });
if (is_uniform)
{
ratio_combo_tag_.block();
gtr_combo_box_set_active_enum(*ratio_combo_, baseline);
gtr_widget_set_visible(*ratio_spin_, baseline == TR_RATIOLIMIT_SINGLE);
ratio_combo_tag_.unblock();
}
}
/* ratio_spin */
if (!torrents.empty())
{
auto const baseline = tr_torrentGetRatioLimit(torrents.front());
set_double_spin_if_different(ratio_spin_, ratio_spin_tag_, baseline);
}
/* idle_combo */
if (!torrents.empty())
{
auto const baseline = tr_torrentGetIdleMode(torrents.front());
bool const is_uniform = std::all_of(
torrents.begin(),
torrents.end(),
[baseline](auto const* torrent) { return baseline == tr_torrentGetIdleMode(torrent); });
if (is_uniform)
{
idle_combo_tag_.block();
gtr_combo_box_set_active_enum(*idle_combo_, baseline);
gtr_widget_set_visible(*idle_spin_, baseline == TR_IDLELIMIT_SINGLE);
idle_combo_tag_.unblock();
}
}
/* idle_spin */
if (!torrents.empty())
{
auto const baseline = tr_torrentGetIdleLimit(torrents.front());
set_int_spin_if_different(idle_spin_, idle_spin_tag_, baseline);
}
/* max_peers_spin */
if (!torrents.empty())
{
auto const baseline = tr_torrentGetPeerLimit(torrents.front());
set_int_spin_if_different(max_peers_spin_, max_peers_spin_tag_, baseline);
}
}
void DetailsDialog::Impl::torrent_set_bool(tr_quark key, bool value)
{
tr_variant top;
tr_variantInitDict(&top, 2);
tr_variantDictAddStrView(&top, TR_KEY_method, "torrent-set"sv);
tr_variant* const args = tr_variantDictAddDict(&top, TR_KEY_arguments, 2);
tr_variantDictAddBool(args, key, value);
tr_variant* const ids = tr_variantDictAddList(args, TR_KEY_ids, ids_.size());
for (auto const id : ids_)
{
tr_variantListAddInt(ids, id);
}
core_->exec(&top);
tr_variantClear(&top);
}
void DetailsDialog::Impl::torrent_set_int(tr_quark key, int value)
{
tr_variant top;
tr_variantInitDict(&top, 2);
tr_variantDictAddStrView(&top, TR_KEY_method, "torrent-set"sv);
tr_variant* const args = tr_variantDictAddDict(&top, TR_KEY_arguments, 2);
tr_variantDictAddInt(args, key, value);
tr_variant* const ids = tr_variantDictAddList(args, TR_KEY_ids, ids_.size());
for (auto const id : ids_)
{
tr_variantListAddInt(ids, id);
}
core_->exec(&top);
tr_variantClear(&top);
}
void DetailsDialog::Impl::torrent_set_real(tr_quark key, double value)
{
tr_variant top;
tr_variantInitDict(&top, 2);
tr_variantDictAddStrView(&top, TR_KEY_method, "torrent-set"sv);
tr_variant* const args = tr_variantDictAddDict(&top, TR_KEY_arguments, 2);
tr_variantDictAddReal(args, key, value);
tr_variant* const ids = tr_variantDictAddList(args, TR_KEY_ids, ids_.size());
for (auto const id : ids_)
{
tr_variantListAddInt(ids, id);
}
core_->exec(&top);
tr_variantClear(&top);
}
void DetailsDialog::Impl::options_page_init(Glib::RefPtr<Gtk::Builder> const& /*builder*/)
{
honor_limits_check_tag_ = honor_limits_check_->signal_toggled().connect(
[this]() { torrent_set_bool(TR_KEY_honorsSessionLimits, honor_limits_check_->get_active()); });
down_limited_check_->set_label(fmt::format(down_limited_check_->get_label().raw(), fmt::arg("speed_units", speed_K_str)));
down_limited_check_tag_ = down_limited_check_->signal_toggled().connect(
[this]() { torrent_set_bool(TR_KEY_downloadLimited, down_limited_check_->get_active()); });
down_limit_spin_->set_adjustment(Gtk::Adjustment::create(0, 0, std::numeric_limits<int>::max(), 5));
down_limit_spin_tag_ = down_limit_spin_->signal_value_changed().connect(
[this]() { torrent_set_int(TR_KEY_downloadLimit, down_limit_spin_->get_value_as_int()); });
up_limited_check_->set_label(fmt::format(up_limited_check_->get_label().raw(), fmt::arg("speed_units", speed_K_str)));
up_limited_check_tag_ = up_limited_check_->signal_toggled().connect(
[this]() { torrent_set_bool(TR_KEY_uploadLimited, up_limited_check_->get_active()); });
up_limit_sping_->set_adjustment(Gtk::Adjustment::create(0, 0, std::numeric_limits<int>::max(), 5));
up_limit_spin_tag_ = up_limit_sping_->signal_value_changed().connect(
[this]() { torrent_set_int(TR_KEY_uploadLimit, up_limit_sping_->get_value_as_int()); });
gtr_priority_combo_init(*bandwidth_combo_);
bandwidth_combo_tag_ = bandwidth_combo_->signal_changed().connect(
[this]() { torrent_set_int(TR_KEY_bandwidthPriority, gtr_combo_box_get_active_enum(*bandwidth_combo_)); });
gtr_combo_box_set_enum(
*ratio_combo_,
{
{ _("Use global settings"), TR_RATIOLIMIT_GLOBAL },
{ _("Seed regardless of ratio"), TR_RATIOLIMIT_UNLIMITED },
{ _("Stop seeding at ratio:"), TR_RATIOLIMIT_SINGLE },
});
ratio_combo_tag_ = ratio_combo_->signal_changed().connect(
[this]()
{
torrent_set_int(TR_KEY_seedRatioMode, gtr_combo_box_get_active_enum(*ratio_combo_));
refresh();
});
ratio_spin_->set_adjustment(Gtk::Adjustment::create(0, 0, 1000, .05));
ratio_spin_->set_width_chars(7);
ratio_spin_tag_ = ratio_spin_->signal_value_changed().connect(
[this]() { torrent_set_real(TR_KEY_seedRatioLimit, ratio_spin_->get_value()); });
gtr_combo_box_set_enum(
*idle_combo_,
{
{ _("Use global settings"), TR_IDLELIMIT_GLOBAL },
{ _("Seed regardless of activity"), TR_IDLELIMIT_UNLIMITED },
{ _("Stop seeding if idle for N minutes:"), TR_IDLELIMIT_SINGLE },
});
idle_combo_tag_ = idle_combo_->signal_changed().connect(
[this]()
{
torrent_set_int(TR_KEY_seedIdleMode, gtr_combo_box_get_active_enum(*idle_combo_));
refresh();
});
idle_spin_->set_adjustment(Gtk::Adjustment::create(1, 1, 40320, 5));
idle_spin_tag_ = idle_spin_->signal_value_changed().connect(
[this]() { torrent_set_int(TR_KEY_seedIdleLimit, idle_spin_->get_value_as_int()); });
max_peers_spin_->set_adjustment(Gtk::Adjustment::create(1, 1, 3000, 5));
max_peers_spin_tag_ = max_peers_spin_->signal_value_changed().connect(
[this]() { torrent_set_int(TR_KEY_peer_limit, max_peers_spin_->get_value_as_int()); });
}
/****
*****
***** INFO TAB
*****
****/
namespace
{
Glib::ustring activityString(int activity, bool finished)
{
switch (activity)
{
case TR_STATUS_CHECK_WAIT:
return _("Queued for verification");
case TR_STATUS_CHECK:
return _("Verifying local data");
case TR_STATUS_DOWNLOAD_WAIT:
return _("Queued for download");
case TR_STATUS_DOWNLOAD:
return C_("Verb", "Downloading");
case TR_STATUS_SEED_WAIT:
return _("Queued for seeding");
case TR_STATUS_SEED:
return C_("Verb", "Seeding");
case TR_STATUS_STOPPED:
return finished ? _("Finished") : _("Paused");
default:
g_assert_not_reached();
}
return {};
}
/* Only call gtk_text_buffer_set_text () if the new text differs from the old.
* This way if the user has text selected, refreshing won't deselect it */
void gtr_text_buffer_set_text(Glib::RefPtr<Gtk::TextBuffer> const& b, Glib::ustring const& str)
{
if (b->get_text() != str)
{
b->set_text(str);
}
}
[[nodiscard]] std::string get_date_string(time_t t)
{
return t == 0 ? _("N/A") : fmt::format(FMT_STRING("{:%x}"), fmt::localtime(t));
}
[[nodiscard]] std::string get_date_time_string(time_t t)
{
return t == 0 ? _("N/A") : fmt::format(FMT_STRING("{:%c}"), fmt::localtime(t));
}
} // namespace
void DetailsDialog::Impl::refreshInfo(std::vector<tr_torrent*> const& torrents)
{
auto const now = time(nullptr);
Glib::ustring str;
Glib::ustring const mixed = _("Mixed");
Glib::ustring const no_torrent = _("No Torrents Selected");
Glib::ustring stateString;
uint64_t sizeWhenDone = 0;
std::vector<tr_stat const*> stats;
std::vector<tr_torrent_view> infos;
stats.reserve(torrents.size());
infos.reserve(torrents.size());
for (auto* const torrent : torrents)
{
stats.push_back(tr_torrentStatCached(torrent));
infos.push_back(tr_torrentView(torrent));
}
/* privacy_lb */
if (infos.empty())
{
str = no_torrent;
}
else
{
bool const baseline = infos.front().is_private;
bool const is_uniform = std::all_of(
infos.begin(),
infos.end(),
[baseline](auto const& info) { return info.is_private == baseline; });
if (is_uniform)
{
str = baseline ? _("Private to this tracker -- DHT and PEX disabled") : _("Public torrent");
}
else
{
str = mixed;
}
}
privacy_lb_->set_text(str);
/* added_lb */
if (stats.empty())
{
str = no_torrent;
}
else
{
auto const baseline = stats.front()->addedDate;
bool const is_uniform = std::all_of(
stats.begin(),
stats.end(),
[baseline](auto const* stat) { return stat->addedDate == baseline; });
if (is_uniform)
{
str = get_date_time_string(baseline);
}
else
{
str = mixed;
}
}
added_lb_->set_text(str);
/* origin_lb */
if (infos.empty())
{
str = no_torrent;
}
else
{
auto const creator = tr_strv_strip(infos.front().creator != nullptr ? infos.front().creator : "");
auto const date = infos.front().date_created;
auto const datestr = get_date_string(date);
bool const mixed_creator = std::any_of(
infos.begin(),
infos.end(),
[&creator](auto const& info) { return creator != (info.creator != nullptr ? info.creator : ""); });
bool const mixed_date = std::any_of(
infos.begin(),
infos.end(),
[date](auto const& info) { return date != info.date_created; });
bool const empty_creator = std::empty(creator);
bool const empty_date = date == 0;
if (mixed_creator || mixed_date)
{
str = mixed;
}
else if (!empty_creator && !empty_date)
{
str = fmt::format(_("Created by {creator} on {date}"), fmt::arg("creator", creator), fmt::arg("date", datestr));
}
else if (!empty_creator)
{
str = fmt::format(_("Created by {creator}"), fmt::arg("creator", creator));
}
else if (!empty_date)
{
str = fmt::format(_("Created on {date}"), fmt::arg("date", datestr));
}
else
{
str = _("N/A");
}
}
origin_lb_->set_text(str);
/* comment_buffer */
if (infos.empty())
{
str.clear();
}
else
{
auto const baseline = Glib::ustring(infos.front().comment != nullptr ? infos.front().comment : "");
bool const is_uniform = std::all_of(
infos.begin(),
infos.end(),
[&baseline](auto const& info) { return baseline == (info.comment != nullptr ? info.comment : ""); });
str = is_uniform ? baseline : mixed;
}
gtr_text_buffer_set_text(comment_buffer_, str);
/* destination_lb */
if (torrents.empty())
{
str = no_torrent;
}
else
{
auto const baseline = Glib::ustring(tr_torrentGetDownloadDir(torrents.front()));
bool const is_uniform = std::all_of(
torrents.begin(),
torrents.end(),
[&baseline](auto const* torrent) { return baseline == tr_torrentGetDownloadDir(torrent); });
str = is_uniform ? baseline : mixed;
}
destination_lb_->set_text(str);
/* state_lb */
if (stats.empty())
{
str = no_torrent;
}
else
{
auto const activity = stats.front()->activity;
bool const is_uniform = std::all_of(
stats.begin(),
stats.end(),
[activity](auto const* st) { return activity == st->activity; });
bool const allFinished = std::all_of(stats.begin(), stats.end(), [](auto const* st) { return st->finished; });
str = is_uniform ? activityString(activity, allFinished) : mixed;
}
stateString = str;
state_lb_->set_text(str);
/* date started */
if (stats.empty())
{
str = no_torrent;
}
else
{
time_t const baseline = stats.front()->startDate;
bool const is_uniform = std::all_of(
stats.begin(),
stats.end(),
[baseline](auto const* st) { return baseline == st->startDate; });
if (!is_uniform)
{
str = mixed;
}
else if (baseline <= 0 || stats[0]->activity == TR_STATUS_STOPPED)
{
str = stateString;
}
else
{
str = tr_format_time(now - baseline);
}
}
date_started_lb_->set_text(str);
/* eta */
if (stats.empty())
{
str = no_torrent;
}
else
{
auto const baseline = stats.front()->eta;
auto const is_uniform = std::all_of(
stats.begin(),
stats.end(),
[baseline](auto const* st) { return baseline == st->eta; });
if (!is_uniform)
{
str = mixed;
}
else if (baseline < 0)
{
str = _("Unknown");
}
else
{
str = tr_format_time_left(baseline);
}
}
eta_lb_->set_text(str);
/* size_lb */
{
auto const piece_count = std::accumulate(
std::begin(infos),
std::end(infos),
uint64_t{},
[](auto sum, auto const& info) { return sum + info.n_pieces; });
if (piece_count == 0)
{
str.clear();
}
else
{
auto const total_size = std::accumulate(
std::begin(infos),
std::end(infos),
uint64_t{},
[](auto sum, auto const& info) { return sum + info.total_size; });
auto const file_count = std::accumulate(
std::begin(torrents),
std::end(torrents),
std::size_t{},
[](auto sum, auto const* tor) { return sum + tr_torrentFileCount(tor); });
str = fmt::format(
ngettext("{total_size} in {file_count:L} file", "{total_size} in {file_count:L} files", file_count),
fmt::arg("total_size", tr_strlsize(total_size)),
fmt::arg("file_count", file_count));
auto const piece_size = std::empty(infos) ? uint32_t{} : infos.front().piece_size;
auto const piece_size_is_uniform = std::all_of(
std::begin(infos),
std::end(infos),
[piece_size](auto const& info) { return info.piece_size == piece_size; });
if (piece_size_is_uniform)
{
str += ' ';
str += fmt::format(
ngettext(
"({piece_count} BitTorrent piece @ {piece_size})",
"({piece_count} BitTorrent pieces @ {piece_size})",
piece_count),
fmt::arg("piece_count", piece_count),
fmt::arg("piece_size", tr_formatter_mem_B(piece_size)));
}
}
size_lb_->set_text(str);
}
/* have_lb */
if (stats.empty())
{
str = no_torrent;
}
else
{
uint64_t leftUntilDone = 0;
uint64_t haveUnchecked = 0;
uint64_t haveValid = 0;
uint64_t available = 0;
for (auto const* const st : stats)
{
haveUnchecked += st->haveUnchecked;
haveValid += st->haveValid;
sizeWhenDone += st->sizeWhenDone;
leftUntilDone += st->leftUntilDone;
available += st->sizeWhenDone - st->leftUntilDone + st->haveUnchecked + st->desiredAvailable;
}
{
double const d = sizeWhenDone != 0 ? (100.0 * available) / sizeWhenDone : 0;
double const ratio = 100.0 * (sizeWhenDone != 0 ? (haveValid + haveUnchecked) / (double)sizeWhenDone : 1);
auto const avail = tr_strpercent(d);
auto const buf2 = tr_strpercent(ratio);
auto const total = tr_strlsize(haveUnchecked + haveValid);
auto const unver = tr_strlsize(haveUnchecked);
if (haveUnchecked == 0 && leftUntilDone == 0)
{
str = fmt::format(
_("{current_size} ({percent_done}%)"),
fmt::arg("current_size", total),
fmt::arg("percent_done", buf2));
}
else if (haveUnchecked == 0)
{
str = fmt::format(
// xgettext:no-c-format
_("{current_size} ({percent_done}% of {percent_available}% available)"),
fmt::arg("current_size", total),
fmt::arg("percent_done", buf2),
fmt::arg("percent_available", avail));
}
else
{
str = fmt::format(
// xgettext:no-c-format
_("{current_size} ({percent_done}% of {percent_available}% available; {unverified_size} unverified)"),
fmt::arg("current_size", total),
fmt::arg("percent_done", buf2),
fmt::arg("percent_available", avail),
fmt::arg("unverified_size", unver));
}
}
}
have_lb_->set_text(str);
// dl_lb
if (stats.empty())
{
str = no_torrent;
}
else
{
auto const downloaded_str = tr_strlsize(std::accumulate(
std::begin(stats),
std::end(stats),
uint64_t{ 0 },
[](auto sum, auto const* st) { return sum + st->downloadedEver; }));
auto const failed = std::accumulate(
std::begin(stats),
std::end(stats),
uint64_t{ 0 },
[](auto sum, auto const* st) { return sum + st->corruptEver; });
if (failed != 0)
{
str = fmt::format(
_("{downloaded_size} (+{discarded_size} discarded after failed checksum)"),
fmt::arg("downloaded_size", downloaded_str),
fmt::arg("discarded_size", tr_strlsize(failed)));
}
else
{
str = downloaded_str;
}
}
dl_lb_->set_text(str);
/* ul_lb */
if (stats.empty())
{
str = no_torrent;
}
else
{
auto const uploaded = std::accumulate(
std::begin(stats),
std::end(stats),
uint64_t{},
[](auto sum, auto const* st) { return sum + st->uploadedEver; });
auto const denominator = std::accumulate(
std::begin(stats),
std::end(stats),
uint64_t{},
[](auto sum, auto const* st) { return sum + st->sizeWhenDone; });
str = fmt::format(
_("{uploaded_size} (Ratio: {ratio})"),
fmt::arg("uploaded_size", tr_strlsize(uploaded)),
fmt::arg("ratio", tr_strlratio(tr_getRatio(uploaded, denominator))));
}
ul_lb_->set_text(str);
/* hash_lb */
if (infos.empty())
{
str = no_torrent;
}
else if (infos.size() == 1)
{
str = infos.front().hash_string;
}
else
{
str = mixed;
}
hash_lb_->set_text(str);
/* error */
if (stats.empty())
{
str = no_torrent;
}
else
{
auto const baseline = Glib::ustring(stats.front()->errorString);
bool const is_uniform = std::all_of(
stats.begin(),
stats.end(),
[&baseline](auto const* st) { return baseline == st->errorString; });
str = is_uniform ? baseline : mixed;
}
if (str.empty())
{
str = _("No errors");
}
error_lb_->set_text(str);
/* activity date */
if (stats.empty())
{
str = no_torrent;
}
else
{
time_t const latest = (*std::max_element(
stats.begin(),
stats.end(),
[](auto const* lhs, auto const* rhs) { return lhs->activityDate < rhs->activityDate; }))
->activityDate;
if (latest <= 0)
{
str = _("Never");
}
else if ((now - latest) < 5)
{
str = _("Active now");
}
else
{
str = tr_format_time_relative(now, latest);
}
}
last_activity_lb_->set_text(str);
}
void DetailsDialog::Impl::info_page_init(Glib::RefPtr<Gtk::Builder> const& builder)
{
comment_buffer_ = Gtk::TextBuffer::create();
auto* tw = gtr_get_widget<Gtk::TextView>(builder, "comment_value_view");
tw->set_buffer(comment_buffer_);
}
/****
*****
***** PEERS TAB
*****
****/
namespace
{
class WebseedModelColumns : public Gtk::TreeModelColumnRecord
{
public:
WebseedModelColumns() noexcept
{
add(key);
add(was_updated);
add(url);
add(download_rate_double);
add(download_rate_string);
}
Gtk::TreeModelColumn<std::string> key;
Gtk::TreeModelColumn<bool> was_updated;
Gtk::TreeModelColumn<Glib::ustring> url;
Gtk::TreeModelColumn<double> download_rate_double;
Gtk::TreeModelColumn<Glib::ustring> download_rate_string;
};
WebseedModelColumns const webseed_cols;
class PeerModelColumns : public Gtk::TreeModelColumnRecord
{
public:
PeerModelColumns() noexcept
{
add(key);
add(was_updated);
add(address);
add(address_collated);
add(download_rate_double);
add(download_rate_string);
add(upload_rate_double);
add(upload_rate_string);
add(client);
add(progress);
add(upload_request_count_number);
add(upload_request_count_string);
add(download_request_count_number);
add(download_request_count_string);
add(blocks_downloaded_count_number);
add(blocks_downloaded_count_string);
add(blocks_uploaded_count_number);
add(blocks_uploaded_count_string);
add(reqs_cancelled_by_client_count_number);
add(reqs_cancelled_by_client_count_string);
add(reqs_cancelled_by_peer_count_number);
add(reqs_cancelled_by_peer_count_string);
add(encryption_stock_id);
add(flags);
add(torrent_name);
}
Gtk::TreeModelColumn<std::string> key;
Gtk::TreeModelColumn<bool> was_updated;
Gtk::TreeModelColumn<Glib::ustring> address;
Gtk::TreeModelColumn<Glib::ustring> address_collated;
Gtk::TreeModelColumn<double> download_rate_double;
Gtk::TreeModelColumn<Glib::ustring> download_rate_string;
Gtk::TreeModelColumn<double> upload_rate_double;
Gtk::TreeModelColumn<Glib::ustring> upload_rate_string;
Gtk::TreeModelColumn<Glib::ustring> client;
Gtk::TreeModelColumn<int> progress;
Gtk::TreeModelColumn<decltype(tr_peer_stat::activeReqsToClient)> upload_request_count_number;
Gtk::TreeModelColumn<Glib::ustring> upload_request_count_string;
Gtk::TreeModelColumn<decltype(tr_peer_stat::activeReqsToPeer)> download_request_count_number;
Gtk::TreeModelColumn<Glib::ustring> download_request_count_string;
Gtk::TreeModelColumn<decltype(tr_peer_stat::blocksToClient)> blocks_downloaded_count_number;
Gtk::TreeModelColumn<Glib::ustring> blocks_downloaded_count_string;
Gtk::TreeModelColumn<decltype(tr_peer_stat::blocksToPeer)> blocks_uploaded_count_number;
Gtk::TreeModelColumn<Glib::ustring> blocks_uploaded_count_string;
Gtk::TreeModelColumn<decltype(tr_peer_stat::cancelsToPeer)> reqs_cancelled_by_client_count_number;
Gtk::TreeModelColumn<Glib::ustring> reqs_cancelled_by_client_count_string;
Gtk::TreeModelColumn<decltype(tr_peer_stat::cancelsToClient)> reqs_cancelled_by_peer_count_number;
Gtk::TreeModelColumn<Glib::ustring> reqs_cancelled_by_peer_count_string;
Gtk::TreeModelColumn<Glib::ustring> encryption_stock_id;
Gtk::TreeModelColumn<Glib::ustring> flags;
Gtk::TreeModelColumn<Glib::ustring> torrent_name;
};
PeerModelColumns const peer_cols;
void initPeerRow(
Gtk::TreeModel::iterator const& iter,
std::string_view key,
std::string_view torrent_name,
tr_peer_stat const* peer)
{
g_return_if_fail(peer != nullptr);
char const* client = peer->client;
if (client == nullptr || g_strcmp0(client, "Unknown Client") == 0)
{
client = "";
}
auto peer_addr4 = in_addr();
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
auto const* const peer_addr4_octets = reinterpret_cast<uint8_t const*>(&peer_addr4.s_addr);
auto const collated_name = inet_pton(AF_INET, std::data(peer->addr), &peer_addr4) != 1 ?
std::data(peer->addr) :
fmt::format(
"{:03}",
fmt::join(
peer_addr4_octets,
// NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic)
peer_addr4_octets + sizeof(peer_addr4.s_addr), // TODO(C++20): Use std::span
"."));
(*iter)[peer_cols.address] = std::data(peer->addr);
(*iter)[peer_cols.address_collated] = collated_name;
(*iter)[peer_cols.client] = client;
(*iter)[peer_cols.encryption_stock_id] = peer->isEncrypted ? "lock" : "";
(*iter)[peer_cols.key] = std::string(key);
(*iter)[peer_cols.torrent_name] = std::string(torrent_name);
}
void refreshPeerRow(Gtk::TreeModel::iterator const& iter, tr_peer_stat const* peer)
{
std::string up_speed;
std::string down_speed;
std::string up_count;
std::string down_count;
std::string blocks_to_peer;
std::string blocks_to_client;
std::string cancelled_by_peer;
std::string cancelled_by_client;
g_return_if_fail(peer != nullptr);
if (peer->rateToPeer_KBps > 0.01)
{
up_speed = tr_formatter_speed_KBps(peer->rateToPeer_KBps);
}
if (peer->rateToClient_KBps > 0)
{
down_speed = tr_formatter_speed_KBps(peer->rateToClient_KBps);
}
if (peer->activeReqsToPeer > 0)
{
down_count = std::to_string(peer->activeReqsToPeer);
}
if (peer->activeReqsToClient > 0)
{
up_count = std::to_string(peer->activeReqsToClient);
}
if (peer->blocksToPeer > 0)
{
blocks_to_peer = std::to_string(peer->blocksToPeer);
}
if (peer->blocksToClient > 0)
{
blocks_to_client = std::to_string(peer->blocksToClient);
}
if (peer->cancelsToPeer > 0)
{
cancelled_by_client = std::to_string(peer->cancelsToPeer);
}
if (peer->cancelsToClient > 0)
{
cancelled_by_peer = std::to_string(peer->cancelsToClient);
}
(*iter)[peer_cols.progress] = static_cast<int>(100.0 * peer->progress);
(*iter)[peer_cols.upload_request_count_number] = peer->activeReqsToClient;
(*iter)[peer_cols.upload_request_count_string] = up_count;
(*iter)[peer_cols.download_request_count_number] = peer->activeReqsToPeer;
(*iter)[peer_cols.download_request_count_string] = down_count;
(*iter)[peer_cols.download_rate_double] = peer->rateToClient_KBps;
(*iter)[peer_cols.download_rate_string] = down_speed;
(*iter)[peer_cols.upload_rate_double] = peer->rateToPeer_KBps;
(*iter)[peer_cols.upload_rate_string] = up_speed;
(*iter)[peer_cols.flags] = std::data(peer->flagStr);
(*iter)[peer_cols.was_updated] = true;
(*iter)[peer_cols.blocks_downloaded_count_number] = peer->blocksToClient;
(*iter)[peer_cols.blocks_downloaded_count_string] = blocks_to_client;
(*iter)[peer_cols.blocks_uploaded_count_number] = peer->blocksToPeer;
(*iter)[peer_cols.blocks_uploaded_count_string] = blocks_to_peer;
(*iter)[peer_cols.reqs_cancelled_by_client_count_number] = peer->cancelsToPeer;
(*iter)[peer_cols.reqs_cancelled_by_client_count_string] = cancelled_by_client;
(*iter)[peer_cols.reqs_cancelled_by_peer_count_number] = peer->cancelsToClient;
(*iter)[peer_cols.reqs_cancelled_by_peer_count_string] = cancelled_by_peer;
}
} // namespace
void DetailsDialog::Impl::refreshPeerList(std::vector<tr_torrent*> const& torrents)
{
auto& hash = peer_hash_;
auto const& store = peer_store_;
/* step 1: get all the peers */
std::vector<tr_peer_stat*> peers;
std::vector<size_t> peerCount;
peers.reserve(torrents.size());
peerCount.reserve(torrents.size());
for (auto const* const torrent : torrents)
{
size_t count = 0;
peers.push_back(tr_torrentPeers(torrent, &count));
peerCount.push_back(count);
}
/* step 2: mark all the peers in the list as not-updated */
for (auto& row : store->children())
{
row[peer_cols.was_updated] = false;
}
auto make_key = [](tr_torrent const* tor, tr_peer_stat const* ps)
{
return fmt::format(FMT_STRING("{:d}.{:s}"), tr_torrentId(tor), ps->addr);
};
/* step 3: add any new peers */
for (size_t i = 0; i < torrents.size(); ++i)
{
auto const* tor = torrents.at(i);
for (size_t j = 0; j < peerCount[i]; ++j)
{
auto const* s = &peers.at(i)[j];
auto const key = make_key(tor, s);
if (hash.find(key) == hash.end())
{
auto const iter = store->append();
initPeerRow(iter, key, tr_torrentName(tor), s);
hash.try_emplace(key, Gtk::TreeRowReference(store, store->get_path(iter)));
}
}
}
/* step 4: update the peers */
for (size_t i = 0; i < torrents.size(); ++i)
{
auto const* tor = torrents.at(i);
for (size_t j = 0; j < peerCount[i]; ++j)
{
auto const* s = &peers.at(i)[j];
auto const key = make_key(tor, s);
refreshPeerRow(store->get_iter(hash.at(key).get_path()), s);
}
}
/* step 5: remove peers that have disappeared */
if (auto iter = store->children().begin(); iter)
{
while (iter)
{
if (iter->get_value(peer_cols.was_updated))
{
++iter;
}
else
{
auto const key = iter->get_value(peer_cols.key);
hash.erase(key);
iter = store->erase(iter);
}
}
}
/* step 6: cleanup */
for (size_t i = 0; i < peers.size(); ++i)
{
tr_torrentPeersFree(peers[i], peerCount[i]);
}
}
void DetailsDialog::Impl::refreshWebseedList(std::vector<tr_torrent*> const& torrents)
{
auto has_any_webseeds = bool{ false };
auto& hash = webseed_hash_;
auto const& store = webseed_store_;
auto make_key = [](tr_torrent const* tor, char const* url)
{
return fmt::format(FMT_STRING("{:d}.{:s}"), tr_torrentId(tor), url);
};
/* step 1: mark all webseeds as not-updated */
for (auto& row : store->children())
{
row[webseed_cols.was_updated] = false;
}
/* step 2: add any new webseeds */
for (auto const* const tor : torrents)
{
for (size_t j = 0, n = tr_torrentWebseedCount(tor); j < n; ++j)
{
has_any_webseeds = true;
auto const* const url = tr_torrentWebseed(tor, j).url;
auto const key = make_key(tor, url);
if (hash.find(key) == hash.end())
{
auto const iter = store->append();
(*iter)[webseed_cols.url] = url;
(*iter)[webseed_cols.key] = key;
hash.try_emplace(key, Gtk::TreeRowReference(store, store->get_path(iter)));
}
}
}
/* step 3: update the webseeds */
for (auto const* const tor : torrents)
{
for (size_t j = 0, n = tr_torrentWebseedCount(tor); j < n; ++j)
{
auto const webseed = tr_torrentWebseed(tor, j);
auto const key = make_key(tor, webseed.url);
auto const iter = store->get_iter(hash.at(key).get_path());
auto const KBps = double(webseed.download_bytes_per_second) / speed_K;
auto const buf = webseed.is_downloading ? tr_formatter_speed_KBps(KBps) : std::string();
(*iter)[webseed_cols.download_rate_double] = KBps;
(*iter)[webseed_cols.download_rate_string] = buf;
(*iter)[webseed_cols.was_updated] = true;
}
}
/* step 4: remove webseeds that have disappeared */
if (auto iter = store->children().begin(); iter)
{
while (iter)
{
if (iter->get_value(webseed_cols.was_updated))
{
++iter;
}
else
{
auto const key = iter->get_value(webseed_cols.key);
hash.erase(key);
iter = store->erase(iter);
}
}
}
/* most of the time there are no webseeds...
don't waste space showing an empty list */
webseed_view_->set_visible(has_any_webseeds);
}
void DetailsDialog::Impl::refreshPeers(std::vector<tr_torrent*> const& torrents)
{
refreshPeerList(torrents);
refreshWebseedList(torrents);
}
bool DetailsDialog::Impl::onPeerViewQueryTooltip(int x, int y, bool keyboard_tip, Glib::RefPtr<Gtk::Tooltip> const& tooltip)
{
Gtk::TreeModel::iterator iter;
bool show_tip = false;
if (peer_view_->get_tooltip_context_iter(x, y, keyboard_tip, iter))
{
auto const name = iter->get_value(peer_cols.torrent_name);
auto const addr = iter->get_value(peer_cols.address);
auto const flagstr = iter->get_value(peer_cols.flags);
std::ostringstream gstr;
gstr << "<b>" << Glib::Markup::escape_text(name) << "</b>\n" << addr << "\n \n";
for (char const ch : flagstr)
{
char const* s = nullptr;
switch (ch)
{
case 'O':
s = _("Optimistic unchoke");
break;
case 'D':
s = _("Downloading from this peer");
break;
case 'd':
s = _("We would download from this peer if they would let us");
break;
case 'U':
s = _("Uploading to peer");
break;
case 'u':
s = _("We would upload to this peer if they asked");
break;
case 'K':
s = _("Peer has unchoked us, but we're not interested");
break;
case '?':
s = _("We unchoked this peer, but they're not interested");
break;
case 'E':
s = _("Encrypted connection");
break;
case 'X':
s = _("Peer was found through Peer Exchange (PEX)");
break;
case 'H':
s = _("Peer was found through DHT");
break;
case 'I':
s = _("Peer is an incoming connection");
break;
case 'T':
s = _("Peer is connected over µTP");
break;
default:
g_assert_not_reached();
}
if (s != nullptr)
{
gstr << ch << ": " << s << '\n';
}
}
auto str = gstr.str();
if (!str.empty()) /* remove the last linefeed */
{
str.resize(str.size() - 1);
}
tooltip->set_markup(str);
show_tip = true;
}
return show_tip;
}
namespace
{
void setPeerViewColumns(Gtk::TreeView* peer_view)
{
std::vector<Gtk::TreeModelColumnBase const*> view_columns;
Gtk::TreeViewColumn* c = nullptr;
bool const more = gtr_pref_flag_get(TR_KEY_show_extra_peer_details);
view_columns.push_back(&peer_cols.encryption_stock_id);
view_columns.push_back(&peer_cols.upload_rate_string);
if (more)
{
view_columns.push_back(&peer_cols.upload_request_count_string);
}
view_columns.push_back(&peer_cols.download_rate_string);
if (more)
{
view_columns.push_back(&peer_cols.download_request_count_string);
}
if (more)
{
view_columns.push_back(&peer_cols.blocks_downloaded_count_string);
}
if (more)
{
view_columns.push_back(&peer_cols.blocks_uploaded_count_string);
}
if (more)
{
view_columns.push_back(&peer_cols.reqs_cancelled_by_client_count_string);
}
if (more)
{
view_columns.push_back(&peer_cols.reqs_cancelled_by_peer_count_string);
}
view_columns.push_back(&peer_cols.progress);
view_columns.push_back(&peer_cols.flags);
view_columns.push_back(&peer_cols.address);
view_columns.push_back(&peer_cols.client);
/* remove any existing columns */
peer_view->remove_all_columns();
for (auto const* const col : view_columns)
{
auto const* sort_col = col;
if (*col == peer_cols.address)
{
auto* r = Gtk::make_managed<Gtk::CellRendererText>();
c = Gtk::make_managed<Gtk::TreeViewColumn>(_("Address"), *r);
c->add_attribute(r->property_text(), *col);
sort_col = &peer_cols.address_collated;
}
else if (*col == peer_cols.progress)
{
auto* r = Gtk::make_managed<Gtk::CellRendererProgress>();
// % is percent done
c = Gtk::make_managed<Gtk::TreeViewColumn>(_("%"), *r);
c->add_attribute(r->property_text(), *col);
}
else if (*col == peer_cols.encryption_stock_id)
{
auto* r = Gtk::make_managed<Gtk::CellRendererPixbuf>();
r->property_xalign() = 0.0F;
r->property_yalign() = 0.5F;
c = Gtk::make_managed<Gtk::TreeViewColumn>(Glib::ustring(), *r);
c->add_attribute(r->property_icon_name(), *col);
c->set_sizing(TR_GTK_TREE_VIEW_COLUMN_SIZING(FIXED));
c->set_fixed_width(20);
}
else if (*col == peer_cols.download_request_count_string)
{
auto* r = Gtk::make_managed<Gtk::CellRendererText>();
c = Gtk::make_managed<Gtk::TreeViewColumn>(_("Dn Reqs"), *r);
c->add_attribute(r->property_text(), *col);
sort_col = &peer_cols.download_request_count_number;
}
else if (*col == peer_cols.upload_request_count_string)
{
auto* r = Gtk::make_managed<Gtk::CellRendererText>();
c = Gtk::make_managed<Gtk::TreeViewColumn>(_("Up Reqs"), *r);
c->add_attribute(r->property_text(), *col);
sort_col = &peer_cols.upload_request_count_number;
}
else if (*col == peer_cols.blocks_downloaded_count_string)
{
auto* r = Gtk::make_managed<Gtk::CellRendererText>();
c = Gtk::make_managed<Gtk::TreeViewColumn>(_("Dn Blocks"), *r);
c->add_attribute(r->property_text(), *col);
sort_col = &peer_cols.blocks_downloaded_count_number;
}
else if (*col == peer_cols.blocks_uploaded_count_string)
{
auto* r = Gtk::make_managed<Gtk::CellRendererText>();
c = Gtk::make_managed<Gtk::TreeViewColumn>(_("Up Blocks"), *r);
c->add_attribute(r->property_text(), *col);
sort_col = &peer_cols.blocks_uploaded_count_number;
}
else if (*col == peer_cols.reqs_cancelled_by_client_count_string)
{
auto* r = Gtk::make_managed<Gtk::CellRendererText>();
c = Gtk::make_managed<Gtk::TreeViewColumn>(_("We Cancelled"), *r);
c->add_attribute(r->property_text(), *col);
sort_col = &peer_cols.reqs_cancelled_by_client_count_number;
}
else if (*col == peer_cols.reqs_cancelled_by_peer_count_string)
{
auto* r = Gtk::make_managed<Gtk::CellRendererText>();
c = Gtk::make_managed<Gtk::TreeViewColumn>(_("They Cancelled"), *r);
c->add_attribute(r->property_text(), *col);
sort_col = &peer_cols.reqs_cancelled_by_peer_count_number;
}
else if (*col == peer_cols.download_rate_string)
{
auto* r = Gtk::make_managed<Gtk::CellRendererText>();
r->property_xalign() = 1.0F;
c = Gtk::make_managed<Gtk::TreeViewColumn>(_("Down"), *r);
c->add_attribute(r->property_text(), *col);
sort_col = &peer_cols.download_rate_double;
}
else if (*col == peer_cols.upload_rate_string)
{
auto* r = Gtk::make_managed<Gtk::CellRendererText>();
r->property_xalign() = 1.0F;
c = Gtk::make_managed<Gtk::TreeViewColumn>(_("Up"), *r);
c->add_attribute(r->property_text(), *col);
sort_col = &peer_cols.upload_rate_double;
}
else if (*col == peer_cols.client)
{
auto* r = Gtk::make_managed<Gtk::CellRendererText>();
c = Gtk::make_managed<Gtk::TreeViewColumn>(_("Client"), *r);
c->add_attribute(r->property_text(), *col);
}
else if (*col == peer_cols.flags)
{
auto* r = Gtk::make_managed<Gtk::CellRendererText>();
c = Gtk::make_managed<Gtk::TreeViewColumn>(_("Flags"), *r);
c->add_attribute(r->property_text(), *col);
}
else
{
std::abort();
}
c->set_resizable(false);
c->set_sort_column(*sort_col);
peer_view->append_column(*c);
}
/* the 'expander' column has a 10-pixel margin on the left
that doesn't look quite correct in any of these columns...
so create a non-visible column and assign it as the
'expander column. */
c = Gtk::make_managed<Gtk::TreeViewColumn>();
c->set_visible(false);
peer_view->append_column(*c);
peer_view->set_expander_column(*c);
}
} // namespace
void DetailsDialog::Impl::onMorePeerInfoToggled()
{
tr_quark const key = TR_KEY_show_extra_peer_details;
bool const value = more_peer_details_check_->get_active();
core_->set_pref(key, value);
setPeerViewColumns(peer_view_);
}
void DetailsDialog::Impl::peer_page_init(Glib::RefPtr<Gtk::Builder> const& builder)
{
/* webseeds */
webseed_store_ = Gtk::ListStore::create(webseed_cols);
auto* v = gtr_get_widget<Gtk::TreeView>(builder, "webseeds_view");
v->set_model(webseed_store_);
setup_item_view_button_event_handling(
*v,
{},
[v](double view_x, double view_y) { return on_item_view_button_released(*v, view_x, view_y); });
{
auto* r = Gtk::make_managed<Gtk::CellRendererText>();
r->property_ellipsize() = TR_PANGO_ELLIPSIZE_MODE(END);
auto* c = Gtk::make_managed<Gtk::TreeViewColumn>(_("Web Seeds"), *r);
c->add_attribute(r->property_text(), webseed_cols.url);
c->set_expand(true);
c->set_sort_column(webseed_cols.url);
v->append_column(*c);
}
{
auto* r = Gtk::make_managed<Gtk::CellRendererText>();
auto* c = Gtk::make_managed<Gtk::TreeViewColumn>(_("Down"), *r);
c->add_attribute(r->property_text(), webseed_cols.download_rate_string);
c->set_sort_column(webseed_cols.download_rate_double);
v->append_column(*c);
}
/* peers */
peer_store_ = Gtk::ListStore::create(peer_cols);
auto m = Gtk::TreeModelSort::create(peer_store_);
m->set_sort_column(peer_cols.progress, TR_GTK_SORT_TYPE(DESCENDING));
peer_view_->set_model(m);
peer_view_->set_has_tooltip(true);
peer_view_->signal_query_tooltip().connect(sigc::mem_fun(*this, &Impl::onPeerViewQueryTooltip), false);
setup_item_view_button_event_handling(
*peer_view_,
{},
[this](double view_x, double view_y) { return on_item_view_button_released(*peer_view_, view_x, view_y); });
setPeerViewColumns(peer_view_);
more_peer_details_check_->set_active(gtr_pref_flag_get(TR_KEY_show_extra_peer_details));
more_peer_details_check_->signal_toggled().connect(sigc::mem_fun(*this, &Impl::onMorePeerInfoToggled));
}
/****
*****
***** TRACKER
*****
****/
namespace
{
auto constexpr ErrMarkupBegin = "<span color='red'>"sv;
auto constexpr ErrMarkupEnd = "</span>"sv;
auto constexpr TimeoutMarkupBegin = "<span color='#246'>"sv;
auto constexpr TimeoutMarkupEnd = "</span>"sv;
auto constexpr SuccessMarkupBegin = "<span color='#080'>"sv;
auto constexpr SuccessMarkupEnd = "</span>"sv;
std::array<std::string_view, 3> const text_dir_mark = { ""sv, "\u200E"sv, "\u200F"sv };
void appendAnnounceInfo(tr_tracker_view const& tracker, time_t const now, Gtk::TextDirection direction, std::ostream& gstr)
{
auto const dir_mark = text_dir_mark.at(static_cast<int>(direction));
if (tracker.hasAnnounced && tracker.announceState != TR_TRACKER_INACTIVE)
{
gstr << '\n';
gstr << dir_mark;
auto const time_span_ago = tr_format_time_relative(now, tracker.lastAnnounceTime);
if (tracker.lastAnnounceSucceeded)
{
gstr << fmt::format(
// {markup_begin} and {markup_end} should surround the peer text
ngettext(
"Got a list of {markup_begin}{peer_count} peer{markup_end} {time_span_ago}",
"Got a list of {markup_begin}{peer_count} peers{markup_end} {time_span_ago}",
tracker.lastAnnouncePeerCount),
fmt::arg("markup_begin", SuccessMarkupBegin),
fmt::arg("peer_count", tracker.lastAnnouncePeerCount),
fmt::arg("markup_end", SuccessMarkupEnd),
fmt::arg("time_span_ago", time_span_ago));
}
else if (tracker.lastAnnounceTimedOut)
{
gstr << fmt::format(
// {markup_begin} and {markup_end} should surround the time_span
_("Peer list request {markup_begin}timed out {time_span_ago}{markup_end}; will retry"),
fmt::arg("markup_begin", TimeoutMarkupBegin),
fmt::arg("time_span_ago", time_span_ago),
fmt::arg("markup_end", TimeoutMarkupEnd));
}
else
{
gstr << fmt::format(
// {markup_begin} and {markup_end} should surround the error
_("Got an error '{markup_begin}{error}{markup_end}' {time_span_ago}"),
fmt::arg("markup_begin", ErrMarkupBegin),
fmt::arg("error", Glib::Markup::escape_text(std::data(tracker.lastAnnounceResult))),
fmt::arg("markup_end", ErrMarkupEnd),
fmt::arg("time_span_ago", time_span_ago));
}
}
switch (tracker.announceState)
{
case TR_TRACKER_INACTIVE:
gstr << '\n';
gstr << dir_mark;
gstr << _("No updates scheduled");
break;
case TR_TRACKER_WAITING:
gstr << '\n';
gstr << dir_mark;
gstr << fmt::format(
_("Asking for more peers {time_span_from_now}"),
fmt::arg("time_span_from_now", tr_format_time_relative(now, tracker.nextAnnounceTime)));
break;
case TR_TRACKER_QUEUED:
gstr << '\n';
gstr << dir_mark;
gstr << _("Queued to ask for more peers");
break;
case TR_TRACKER_ACTIVE:
gstr << '\n';
gstr << dir_mark;
gstr << fmt::format(
// {markup_begin} and {markup_end} should surround time_span_ago
_("Asked for more peers {markup_begin}{time_span_ago}{markup_end}"),
fmt::arg("markup_begin", "<small>"),
fmt::arg("time_span_ago", tr_format_time_relative(now, tracker.lastAnnounceStartTime)),
fmt::arg("markup_end", "</small>"));
break;
default:
g_assert_not_reached();
}
}
void appendScrapeInfo(tr_tracker_view const& tracker, time_t const now, Gtk::TextDirection direction, std::ostream& gstr)
{
auto const dir_mark = text_dir_mark.at(static_cast<int>(direction));
if (tracker.hasScraped)
{
gstr << '\n';
gstr << dir_mark;
auto const time_span_ago = tr_format_time_relative(now, tracker.lastScrapeTime);
if (tracker.lastScrapeSucceeded)
{
gstr << fmt::format(
// {markup_begin} and {markup_end} should surround the seeder/leecher text
_("Tracker had {markup_begin}{seeder_count} {seeder_or_seeders} and {leecher_count} {leecher_or_leechers}{markup_end} {time_span_ago}"),
fmt::arg("seeder_count", tracker.seederCount),
fmt::arg("seeder_or_seeders", ngettext("seeder", "seeders", tracker.seederCount)),
fmt::arg("leecher_count", tracker.leecherCount),
fmt::arg("leecher_or_leechers", ngettext("leecher", "leechers", tracker.leecherCount)),
fmt::arg("time_span_ago", time_span_ago),
fmt::arg("markup_begin", SuccessMarkupBegin),
fmt::arg("markup_end", SuccessMarkupEnd));
}
else
{
gstr << fmt::format(
// {markup_begin} and {markup_end} should surround the error text
_("Got a scrape error '{markup_begin}{error}{markup_end}' {time_span_ago}"),
fmt::arg("error", Glib::Markup::escape_text(std::data(tracker.lastScrapeResult))),
fmt::arg("time_span_ago", time_span_ago),
fmt::arg("markup_begin", ErrMarkupBegin),
fmt::arg("markup_end", ErrMarkupEnd));
}
}
switch (tracker.scrapeState)
{
case TR_TRACKER_INACTIVE:
break;
case TR_TRACKER_WAITING:
gstr << '\n';
gstr << dir_mark;
gstr << fmt::format(
_("Asking for peer counts in {time_span_from_now}"),
fmt::arg("time_span_from_now", tr_format_time_relative(now, tracker.nextScrapeTime)));
break;
case TR_TRACKER_QUEUED:
gstr << '\n';
gstr << dir_mark;
gstr << _("Queued to ask for peer counts");
break;
case TR_TRACKER_ACTIVE:
gstr << '\n';
gstr << dir_mark;
gstr << fmt::format(
_("Asked for peer counts {markup_begin}{time_span_ago}{markup_end}"),
fmt::arg("markup_begin", "<small>"),
fmt::arg("time_span_ago", tr_format_time_relative(now, tracker.lastScrapeStartTime)),
fmt::arg("markup_end", "</small>"));
break;
default:
g_assert_not_reached();
}
}
void buildTrackerSummary(
std::ostream& gstr,
std::string const& key,
tr_tracker_view const& tracker,
bool showScrape,
Gtk::TextDirection direction)
{
// hostname
gstr << text_dir_mark.at(static_cast<int>(direction));
gstr << (tracker.isBackup ? "<i>" : "<b>");
gstr << Glib::Markup::escape_text(
!key.empty() ? fmt::format(FMT_STRING("{:s} - {:s}"), tracker.host_and_port, key) : tracker.host_and_port);
gstr << (tracker.isBackup ? "</i>" : "</b>");
if (!tracker.isBackup)
{
time_t const now = time(nullptr);
appendAnnounceInfo(tracker, now, direction, gstr);
if (showScrape)
{
appendScrapeInfo(tracker, now, direction, gstr);
}
}
}
class TrackerModelColumns : public Gtk::TreeModelColumnRecord
{
public:
TrackerModelColumns() noexcept
{
add(torrent_id);
add(text);
add(is_backup);
add(tracker_id);
add(favicon);
add(was_updated);
add(key);
}
Gtk::TreeModelColumn<tr_torrent_id_t> torrent_id;
Gtk::TreeModelColumn<Glib::ustring> text;
Gtk::TreeModelColumn<bool> is_backup;
Gtk::TreeModelColumn<int> tracker_id;
Gtk::TreeModelColumn<Glib::RefPtr<Gdk::Pixbuf>> favicon;
Gtk::TreeModelColumn<bool> was_updated;
Gtk::TreeModelColumn<std::string> key;
};
TrackerModelColumns const tracker_cols;
} // namespace
bool DetailsDialog::Impl::trackerVisibleFunc(Gtk::TreeModel::const_iterator const& iter)
{
/* show all */
if (all_check_->get_active())
{
return true;
}
/* don't show the backups... */
return !iter->get_value(tracker_cols.is_backup);
}
tr_torrent_id_t DetailsDialog::Impl::tracker_list_get_current_torrent_id() const
{
// if there's only one torrent in the dialog, always use it
if (ids_.size() == 1)
{
return ids_.front();
}
// otherwise, use the selected tracker's torrent
auto const sel = tracker_view_->get_selection();
if (auto const iter = sel->get_selected(); iter)
{
return iter->get_value(tracker_cols.torrent_id);
}
return -1;
}
tr_torrent* DetailsDialog::Impl::tracker_list_get_current_torrent() const
{
return core_->find_torrent(tracker_list_get_current_torrent_id());
}
namespace
{
void favicon_ready_cb(Glib::RefPtr<Gdk::Pixbuf> const* pixbuf, Gtk::TreeRowReference& reference)
{
if (pixbuf != nullptr && *pixbuf != nullptr)
{
auto const path = reference.get_path();
auto const model = reference.get_model();
if (auto const iter = model->get_iter(path); iter)
{
(*iter)[tracker_cols.favicon] = *pixbuf;
}
}
}
} // namespace
void DetailsDialog::Impl::refreshTracker(std::vector<tr_torrent*> const& torrents)
{
std::ostringstream gstr;
auto& hash = tracker_hash_;
auto const& store = tracker_store_;
bool const showScrape = scrape_check_->get_active();
/* step 1: get all the trackers */
auto trackers = std::multimap<tr_torrent const*, tr_tracker_view>{};
for (auto const* tor : torrents)
{
for (size_t i = 0, n = tr_torrentTrackerCount(tor); i < n; ++i)
{
trackers.emplace(tor, tr_torrentTracker(tor, i));
}
}
/* step 2: mark all the trackers in the list as not-updated */
for (auto& row : store->children())
{
row[tracker_cols.was_updated] = false;
}
/* step 3: add / update trackers */
for (auto const& [tor, tracker] : trackers)
{
auto const torrent_id = tr_torrentId(tor);
// build the key to find the row
gstr.str({});
gstr << torrent_id << '\t' << tracker.tier << '\t' << tracker.announce;
if (hash.find(gstr.str()) == hash.end())
{
// if we didn't have that row, add it
auto const iter = store->append();
(*iter)[tracker_cols.torrent_id] = torrent_id;
(*iter)[tracker_cols.tracker_id] = tracker.id;
(*iter)[tracker_cols.key] = gstr.str();
auto const p = store->get_path(iter);
hash.try_emplace(gstr.str(), Gtk::TreeRowReference(store, p));
core_->favicon_cache().load(
tracker.announce,
[ref = Gtk::TreeRowReference(store, p)](auto const* pixbuf_refptr) mutable
{ favicon_ready_cb(pixbuf_refptr, ref); });
}
}
/* step 4: update the rows */
auto const summary_name = std::string(std::size(torrents) == 1 ? tr_torrentName(torrents.front()) : "");
for (auto const& [tor, tracker] : trackers)
{
auto const torrent_id = tr_torrentId(tor);
// build the key to find the row
gstr.str({});
gstr << torrent_id << '\t' << tracker.tier << '\t' << tracker.announce;
auto const iter = store->get_iter(hash.at(gstr.str()).get_path());
// update the row
gstr.str({});
buildTrackerSummary(gstr, summary_name, tracker, showScrape, dialog_.get_direction());
(*iter)[tracker_cols.text] = gstr.str();
(*iter)[tracker_cols.is_backup] = tracker.isBackup;
(*iter)[tracker_cols.tracker_id] = tracker.id;
(*iter)[tracker_cols.was_updated] = true;
}
/* step 5: remove trackers that have disappeared */
if (auto iter = store->children().begin(); iter)
{
while (iter)
{
if (iter->get_value(tracker_cols.was_updated))
{
++iter;
}
else
{
auto const key = iter->get_value(tracker_cols.key);
hash.erase(key);
iter = store->erase(iter);
}
}
}
edit_trackers_button_->set_sensitive(tracker_list_get_current_torrent_id() > 0);
}
void DetailsDialog::Impl::refreshFiles(std::vector<tr_torrent*> const& torrents)
{
if (torrents.size() == 1)
{
file_list_->set_torrent(tr_torrentId(torrents.front()));
file_list_->show();
file_label_->hide();
}
else
{
file_list_->clear();
file_list_->hide();
file_label_->show();
}
}
void DetailsDialog::Impl::onScrapeToggled()
{
tr_quark const key = TR_KEY_show_tracker_scrapes;
bool const value = scrape_check_->get_active();
core_->set_pref(key, value);
refresh();
}
void DetailsDialog::Impl::onBackupToggled()
{
tr_quark const key = TR_KEY_show_backup_trackers;
bool const value = all_check_->get_active();
core_->set_pref(key, value);
refresh();
}
namespace
{
class EditTrackersDialog : public Gtk::Dialog
{
public:
EditTrackersDialog(
BaseObjectType* cast_item,
Glib::RefPtr<Gtk::Builder> const& builder,
DetailsDialog& parent,
Glib::RefPtr<Session> const& core,
tr_torrent const* torrent);
~EditTrackersDialog() override = default;
TR_DISABLE_COPY_MOVE(EditTrackersDialog)
static std::unique_ptr<EditTrackersDialog> create(
DetailsDialog& parent,
Glib::RefPtr<Session> const& core,
tr_torrent const* tor);
private:
void on_response(int response) override;
private:
DetailsDialog& parent_;
Glib::RefPtr<Session> const core_;
tr_torrent_id_t const torrent_id_;
Gtk::TextView* const urls_view_;
};
EditTrackersDialog::EditTrackersDialog(
BaseObjectType* cast_item,
Glib::RefPtr<Gtk::Builder> const& builder,
DetailsDialog& parent,
Glib::RefPtr<Session> const& core,
tr_torrent const* torrent)
: Gtk::Dialog(cast_item)
, parent_(parent)
, core_(core)
, torrent_id_(tr_torrentId(torrent))
, urls_view_(gtr_get_widget<Gtk::TextView>(builder, "urls_view"))
{
set_title(fmt::format(_("{torrent_name} - Edit Trackers"), fmt::arg("torrent_name", tr_torrentName(torrent))));
set_transient_for(parent);
urls_view_->get_buffer()->set_text(tr_torrentGetTrackerList(torrent));
}
std::unique_ptr<EditTrackersDialog> EditTrackersDialog::create(
DetailsDialog& parent,
Glib::RefPtr<Session> const& core,
tr_torrent const* torrent)
{
auto const builder = Gtk::Builder::create_from_resource(gtr_get_full_resource_path("EditTrackersDialog.ui"));
return std::unique_ptr<EditTrackersDialog>(
gtr_get_widget_derived<EditTrackersDialog>(builder, "EditTrackersDialog", parent, core, torrent));
}
void EditTrackersDialog::on_response(int response)
{
bool do_destroy = true;
if (response == TR_GTK_RESPONSE_TYPE(ACCEPT))
{
auto const text_buffer = urls_view_->get_buffer();
if (auto* const tor = core_->find_torrent(torrent_id_); tor != nullptr)
{
if (tr_torrentSetTrackerList(tor, text_buffer->get_text(false).c_str()))
{
parent_.refresh();
}
else
{
auto w = std::make_shared<Gtk::MessageDialog>(
*this,
_("List contains invalid URLs"),
false,
TR_GTK_MESSAGE_TYPE(ERROR),
TR_GTK_BUTTONS_TYPE(CLOSE),
true);
w->set_secondary_text(_("Please correct the errors and try again."));
w->signal_response().connect([w](int /*response*/) mutable { w.reset(); });
w->show();
do_destroy = false;
}
}
}
if (do_destroy)
{
close();
}
}
} // namespace
void DetailsDialog::Impl::on_edit_trackers()
{
if (auto const* const tor = tracker_list_get_current_torrent(); tor != nullptr)
{
auto d = std::shared_ptr<EditTrackersDialog>(EditTrackersDialog::create(dialog_, core_, tor));
gtr_window_on_close(*d, [d]() mutable { d.reset(); });
d->show();
}
}
void DetailsDialog::Impl::on_tracker_list_selection_changed()
{
int const n = tracker_view_->get_selection()->count_selected_rows();
auto const* const tor = tracker_list_get_current_torrent();
remove_tracker_button_->set_sensitive(n > 0);
add_tracker_button_->set_sensitive(tor != nullptr);
edit_trackers_button_->set_sensitive(tor != nullptr);
}
namespace
{
class AddTrackerDialog : public Gtk::Dialog
{
public:
AddTrackerDialog(
BaseObjectType* cast_item,
Glib::RefPtr<Gtk::Builder> const& builder,
DetailsDialog& parent,
Glib::RefPtr<Session> const& core,
tr_torrent const* torrent);
~AddTrackerDialog() override = default;
TR_DISABLE_COPY_MOVE(AddTrackerDialog)
static std::unique_ptr<AddTrackerDialog> create(
DetailsDialog& parent,
Glib::RefPtr<Session> const& core,
tr_torrent const* tor);
private:
void on_response(int response) override;
private:
DetailsDialog& parent_;
Glib::RefPtr<Session> const core_;
tr_torrent_id_t const torrent_id_;
Gtk::Entry* const url_entry_;
};
AddTrackerDialog::AddTrackerDialog(
BaseObjectType* cast_item,
Glib::RefPtr<Gtk::Builder> const& builder,
DetailsDialog& parent,
Glib::RefPtr<Session> const& core,
tr_torrent const* torrent)
: Gtk::Dialog(cast_item)
, parent_(parent)
, core_(core)
, torrent_id_(tr_torrentId(torrent))
, url_entry_(gtr_get_widget<Gtk::Entry>(builder, "url_entry"))
{
set_title(fmt::format(_("{torrent_name} - Add Tracker"), fmt::arg("torrent_name", tr_torrentName(torrent))));
set_transient_for(parent);
gtr_paste_clipboard_url_into_entry(*url_entry_);
}
std::unique_ptr<AddTrackerDialog> AddTrackerDialog::create(
DetailsDialog& parent,
Glib::RefPtr<Session> const& core,
tr_torrent const* torrent)
{
auto const builder = Gtk::Builder::create_from_resource(gtr_get_full_resource_path("AddTrackerDialog.ui"));
return std::unique_ptr<AddTrackerDialog>(
gtr_get_widget_derived<AddTrackerDialog>(builder, "AddTrackerDialog", parent, core, torrent));
}
void AddTrackerDialog::on_response(int response)
{
bool destroy = true;
if (response == TR_GTK_RESPONSE_TYPE(ACCEPT))
{
auto const url = gtr_str_strip(url_entry_->get_text());
if (!url.empty())
{
if (tr_urlIsValidTracker(url.c_str()))
{
tr_variant top;
tr_variantInitDict(&top, 2);
tr_variantDictAddStrView(&top, TR_KEY_method, "torrent-set"sv);
auto* const args = tr_variantDictAddDict(&top, TR_KEY_arguments, 2);
tr_variantDictAddInt(args, TR_KEY_id, torrent_id_);
auto* const trackers = tr_variantDictAddList(args, TR_KEY_trackerAdd, 1);
tr_variantListAddStr(trackers, url.raw());
core_->exec(&top);
parent_.refresh();
tr_variantClear(&top);
}
else
{
gtr_unrecognized_url_dialog(*this, url);
destroy = false;
}
}
}
if (destroy)
{
close();
}
}
} // namespace
void DetailsDialog::Impl::on_tracker_list_add_button_clicked()
{
if (auto const* const tor = tracker_list_get_current_torrent(); tor != nullptr)
{
auto d = std::shared_ptr<AddTrackerDialog>(AddTrackerDialog::create(dialog_, core_, tor));
gtr_window_on_close(*d, [d]() mutable { d.reset(); });
d->show();
}
}
void DetailsDialog::Impl::on_tracker_list_remove_button_clicked()
{
auto* v = tracker_view_;
auto sel = v->get_selection();
if (auto const iter = sel->get_selected(); iter)
{
auto const torrent_id = iter->get_value(tracker_cols.torrent_id);
auto const tracker_id = iter->get_value(tracker_cols.tracker_id);
tr_variant top;
tr_variantInitDict(&top, 2);
tr_variantDictAddStrView(&top, TR_KEY_method, "torrent-set"sv);
auto* const args = tr_variantDictAddDict(&top, TR_KEY_arguments, 2);
tr_variantDictAddInt(args, TR_KEY_id, torrent_id);
auto* const trackers = tr_variantDictAddList(args, TR_KEY_trackerRemove, 1);
tr_variantListAddInt(trackers, tracker_id);
core_->exec(&top);
refresh();
tr_variantClear(&top);
}
}
void DetailsDialog::Impl::tracker_page_init(Glib::RefPtr<Gtk::Builder> const& /*builder*/)
{
int const pad = (GUI_PAD + GUI_PAD_BIG) / 2;
tracker_store_ = Gtk::ListStore::create(tracker_cols);
trackers_filtered_ = Gtk::TreeModelFilter::create(tracker_store_);
trackers_filtered_->set_visible_func(sigc::mem_fun(*this, &Impl::trackerVisibleFunc));
tracker_view_->set_model(trackers_filtered_);
setup_item_view_button_event_handling(
*tracker_view_,
[this](guint /*button*/, TrGdkModifierType /*state*/, double view_x, double view_y, bool context_menu_requested)
{ return on_item_view_button_pressed(*tracker_view_, view_x, view_y, context_menu_requested); },
[this](double view_x, double view_y) { return on_item_view_button_released(*tracker_view_, view_x, view_y); });
auto sel = tracker_view_->get_selection();
sel->signal_changed().connect(sigc::mem_fun(*this, &Impl::on_tracker_list_selection_changed));
auto* c = Gtk::make_managed<Gtk::TreeViewColumn>();
c->set_title(_("Trackers"));
tracker_view_->append_column(*c);
{
auto* r = Gtk::make_managed<Gtk::CellRendererPixbuf>();
r->property_width() = 20 + (GUI_PAD_SMALL * 2);
r->property_xpad() = GUI_PAD_SMALL;
r->property_ypad() = pad;
r->property_yalign() = 0.0F;
c->pack_start(*r, false);
c->add_attribute(r->property_pixbuf(), tracker_cols.favicon);
}
{
auto* r = Gtk::make_managed<Gtk::CellRendererText>();
r->property_ellipsize() = TR_PANGO_ELLIPSIZE_MODE(END);
r->property_xpad() = GUI_PAD_SMALL;
r->property_ypad() = pad;
c->pack_start(*r, true);
c->add_attribute(r->property_markup(), tracker_cols.text);
}
add_tracker_button_->signal_clicked().connect(sigc::mem_fun(*this, &Impl::on_tracker_list_add_button_clicked));
edit_trackers_button_->signal_clicked().connect(sigc::mem_fun(*this, &Impl::on_edit_trackers));
remove_tracker_button_->signal_clicked().connect(sigc::mem_fun(*this, &Impl::on_tracker_list_remove_button_clicked));
scrape_check_->set_active(gtr_pref_flag_get(TR_KEY_show_tracker_scrapes));
scrape_check_->signal_toggled().connect(sigc::mem_fun(*this, &Impl::onScrapeToggled));
all_check_->set_active(gtr_pref_flag_get(TR_KEY_show_backup_trackers));
all_check_->signal_toggled().connect(sigc::mem_fun(*this, &Impl::onBackupToggled));
}
/****
***** DIALOG
****/
void DetailsDialog::Impl::refresh()
{
auto const torrents = getTorrents();
refreshInfo(torrents);
refreshPeers(torrents);
refreshTracker(torrents);
refreshFiles(torrents);
refreshOptions(torrents);
if (torrents.empty())
{
dialog_.response(TR_GTK_RESPONSE_TYPE(CLOSE));
}
}
void DetailsDialog::Impl::on_details_window_size_allocated()
{
int w = 0;
int h = 0;
#if GTKMM_CHECK_VERSION(4, 0, 0)
dialog_.get_default_size(w, h);
#else
dialog_.get_size(w, h);
#endif
gtr_pref_int_set(TR_KEY_details_window_width, w);
gtr_pref_int_set(TR_KEY_details_window_height, h);
}
DetailsDialog::Impl::~Impl()
{
periodic_refresh_tag_.disconnect();
}
std::unique_ptr<DetailsDialog> DetailsDialog::create(Gtk::Window& parent, Glib::RefPtr<Session> const& core)
{
auto const builder = Gtk::Builder::create_from_resource(gtr_get_full_resource_path("DetailsDialog.ui"));
return std::unique_ptr<DetailsDialog>(gtr_get_widget_derived<DetailsDialog>(builder, "DetailsDialog", parent, core));
}
DetailsDialog::DetailsDialog(
BaseObjectType* cast_item,
Glib::RefPtr<Gtk::Builder> const& builder,
Gtk::Window& parent,
Glib::RefPtr<Session> const& core)
: Gtk::Dialog(cast_item)
, impl_(std::make_unique<Impl>(*this, builder, core))
{
set_transient_for(parent);
}
DetailsDialog::~DetailsDialog() = default;
DetailsDialog::Impl::Impl(DetailsDialog& dialog, Glib::RefPtr<Gtk::Builder> const& builder, Glib::RefPtr<Session> const& core)
: dialog_(dialog)
, core_(core)
, honor_limits_check_(gtr_get_widget<Gtk::CheckButton>(builder, "honor_limits_check"))
, up_limited_check_(gtr_get_widget<Gtk::CheckButton>(builder, "upload_limit_check"))
, up_limit_sping_(gtr_get_widget<Gtk::SpinButton>(builder, "upload_limit_spin"))
, down_limited_check_(gtr_get_widget<Gtk::CheckButton>(builder, "download_limit_check"))
, down_limit_spin_(gtr_get_widget<Gtk::SpinButton>(builder, "download_limit_spin"))
, bandwidth_combo_(gtr_get_widget<Gtk::ComboBox>(builder, "priority_combo"))
, ratio_combo_(gtr_get_widget<Gtk::ComboBox>(builder, "ratio_limit_combo"))
, ratio_spin_(gtr_get_widget<Gtk::SpinButton>(builder, "ratio_limit_spin"))
, idle_combo_(gtr_get_widget<Gtk::ComboBox>(builder, "idle_limit_combo"))
, idle_spin_(gtr_get_widget<Gtk::SpinButton>(builder, "idle_limit_spin"))
, max_peers_spin_(gtr_get_widget<Gtk::SpinButton>(builder, "max_peers_spin"))
, added_lb_(gtr_get_widget<Gtk::Label>(builder, "added_value_label"))
, size_lb_(gtr_get_widget<Gtk::Label>(builder, "torrent_size_value_label"))
, state_lb_(gtr_get_widget<Gtk::Label>(builder, "state_value_label"))
, have_lb_(gtr_get_widget<Gtk::Label>(builder, "have_value_label"))
, dl_lb_(gtr_get_widget<Gtk::Label>(builder, "downloaded_value_label"))
, ul_lb_(gtr_get_widget<Gtk::Label>(builder, "uploaded_value_label"))
, error_lb_(gtr_get_widget<Gtk::Label>(builder, "error_value_label"))
, date_started_lb_(gtr_get_widget<Gtk::Label>(builder, "running_time_value_label"))
, eta_lb_(gtr_get_widget<Gtk::Label>(builder, "remaining_time_value_label"))
, last_activity_lb_(gtr_get_widget<Gtk::Label>(builder, "last_activity_value_label"))
, hash_lb_(gtr_get_widget<Gtk::Label>(builder, "hash_value_label"))
, privacy_lb_(gtr_get_widget<Gtk::Label>(builder, "privacy_value_label"))
, origin_lb_(gtr_get_widget<Gtk::Label>(builder, "origin_value_label"))
, destination_lb_(gtr_get_widget<Gtk::Label>(builder, "location_value_label"))
, webseed_view_(gtr_get_widget<Gtk::ScrolledWindow>(builder, "webseeds_view_scroll"))
, peer_view_(gtr_get_widget<Gtk::TreeView>(builder, "peers_view"))
, more_peer_details_check_(gtr_get_widget<Gtk::CheckButton>(builder, "more_peer_details_check"))
, add_tracker_button_(gtr_get_widget<Gtk::Button>(builder, "add_tracker_button"))
, edit_trackers_button_(gtr_get_widget<Gtk::Button>(builder, "edit_tracker_button"))
, remove_tracker_button_(gtr_get_widget<Gtk::Button>(builder, "remove_tracker_button"))
, tracker_view_(gtr_get_widget<Gtk::TreeView>(builder, "trackers_view"))
, scrape_check_(gtr_get_widget<Gtk::CheckButton>(builder, "more_tracker_details_check"))
, all_check_(gtr_get_widget<Gtk::CheckButton>(builder, "backup_trackers_check"))
, file_list_(gtr_get_widget_derived<FileList>(builder, "files_view_scroll", "files_view", core, 0))
, file_label_(gtr_get_widget<Gtk::Label>(builder, "files_label"))
{
/* return saved window size */
auto const width = (int)gtr_pref_int_get(TR_KEY_details_window_width);
auto const height = (int)gtr_pref_int_get(TR_KEY_details_window_height);
#if GTKMM_CHECK_VERSION(4, 0, 0)
dialog_.set_default_size(width, height);
dialog_.property_default_width().signal_changed().connect(sigc::mem_fun(*this, &Impl::on_details_window_size_allocated));
dialog_.property_default_height().signal_changed().connect(sigc::mem_fun(*this, &Impl::on_details_window_size_allocated));
#else
dialog_.resize(width, height);
dialog_.signal_size_allocate().connect(sigc::hide<0>(sigc::mem_fun(*this, &Impl::on_details_window_size_allocated)));
#endif
dialog_.signal_response().connect(sigc::hide<0>(sigc::mem_fun(dialog_, &DetailsDialog::close)));
info_page_init(builder);
peer_page_init(builder);
tracker_page_init(builder);
options_page_init(builder);
periodic_refresh_tag_ = Glib::signal_timeout().connect_seconds(
[this]() { return refresh(), true; },
SECONDARY_WINDOW_REFRESH_INTERVAL_SECONDS);
auto* const n = gtr_get_widget<Gtk::Notebook>(builder, "dialog_pages");
n->set_current_page(last_page_);
n->signal_switch_page().connect([](Gtk::Widget* /*page*/, guint page_number) { last_page_ = page_number; });
}
void DetailsDialog::set_torrents(std::vector<tr_torrent_id_t> const& ids)
{
impl_->set_torrents(ids);
}
void DetailsDialog::refresh()
{
impl_->refresh();
}
void DetailsDialog::Impl::set_torrents(std::vector<tr_torrent_id_t> const& ids)
{
Glib::ustring title;
auto const len = ids.size();
ids_ = ids;
if (len == 1)
{
int const id = ids.front();
auto const* tor = core_->find_torrent(id);
title = fmt::format(_("{torrent_name} Properties"), fmt::arg("torrent_name", tr_torrentName(tor)));
}
else
{
title = fmt::format(
ngettext("Properties - {torrent_count:L} Torrent", "Properties - {torrent_count:L} Torrents", len),
fmt::arg("torrent_count", len));
}
dialog_.set_title(title);
refresh();
}