mirror of
https://github.com/transmission/transmission.git
synced 2025-12-12 20:35:49 +01:00
* chore: savepoint * chore: code style * refactor: add std::string_view constructor for NativeIcon::Spec * chore: add TODO comment * feat: honor per-desktop HIG on when to show menu icons * chore: remove Faenza system-run icon unused sinceb58e95910b* chore: remove Faenza view-refresh icon not needed due tob58e95910b* chore: remove Faenza media-playback-pause icon not needed due tob58e95910b* chore: remove Faenza media-playback-start icon not needed due tob58e95910b* chore: add a safeguard against merging with incomplete TODO items * feat: add more icons refactor: remove some tracer cerr statements * refactor: remove IconCache use from MainWindow * chore: remove Faenza icon set * chore: re-enable remote session network icon * fix: FTBFS on Windows * refactor: use symbolic names for Segoe icons * docs: add links to Segoe MDL2 Assets icon list * chore: savepoint segoe icons work still a WIP; includes test code that should not ship * feat: use segoe::FastForward for action_StartNow feat: use segoe::Move for action_SetLocation refactor: make it easier for devs to force a font at compile time for development work segoe license does not allow bundling but does allow dev work chore: code_style.sh * refactor: remove unused addEmblem() * docs: add code comment on how to force an icon font * fix: Win 10, 11 icons play nicely with dark mode * chore: savepoint add draft of SF Symbol -> QPixmap loader * chore: remove dangling font reference from qrc file * fix: FTBFS * refactor: use bribri code for NSImage -> QPixmap * feat: support dark, light mode when rendering SF Symbol monochrome icons * fixup! feat: support dark, light mode when rendering SF Symbol monochrome icons fix: fail gracefully on macOS 11 * chore: code style * chore: tweak some SF Symbol icon choices * chore: consistent uppercase for hex segoe QChars * chore: undefine DEV_FORCE_FONT_FAMILY and DEV_FORCE_FONT_RESOURCE * chore: savepoint * refactor: clean up NativeIcon impl * refactor: remove unused MenuMode::Other * refactor: DRY in FilterBar::createActivityCombo() * chore: remove obsolete code comment * refactor: rename icons::Facet as icons::Type * fix: oops * refactor: minor cleanup * fix: tyop * chore: remove unused #includes * fix: add modes for some icons * refactor: tweak some icon choices on macOS * fix: ensure icons are visible on File, Help menus fix: remove unused local variable * refactor: tweak some icon choices for XDG * refactor: remove the fallback QStyle::StandardPixmaps These interfere with deciding whether an icon is well-defined and unambiguous as per the macOS and Windows HIG guidelines. If a standard or unambiguous icon exists in the native icon sets, specify it with an SF Symbols name, a Segoe codepoint, or XDG standard icon name. Otherwise, leave those fields blank. * refactor: remove unused #includes * docs: add "choosing icons" section in NativeIcons.cc * refactor: simplify icons::shouldBeShownInMenu() * refactor: reduce unnecessary code shear from main * refactor: make TorrentDelegate::warning_emblem_ const * refactor: extract-method MainWindow::updateActionIcons() * feat: update MainWindow icons when light/dark theme changes * feat: restore the QStyle::StandardPixmaps as fallbacks Can be used on older Windows / macOS if Segoe or SF Symbols are unavailable * refactor: add button text for add/edit/remove tracker buttons QStyle::StandardPixmap doesn't have good icons for these, so let's ensure that these buttons have visible text. * fix: building NativeIconMac.mm on mac even if not clang * chore: iwyu in new code * docs: tweak the "Choosing Icons" comments again * fix: handle changed QStyles in icons::icon() do not cache point_sizes set between calls refactor: const correctness * fixup! refactor: simplify icons::shouldBeShownInMenu() refactor: minor code tweak, declare vars in order that they are used
384 lines
11 KiB
C++
384 lines
11 KiB
C++
// This file Copyright © 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 "FilterBar.h"
|
|
|
|
#include <cstdint> // uint64_t
|
|
#include <unordered_map>
|
|
#include <utility>
|
|
|
|
#include <QHBoxLayout>
|
|
#include <QLabel>
|
|
#include <QLineEdit>
|
|
#include <QStandardItemModel>
|
|
|
|
#include "Application.h"
|
|
#include "FilterBarComboBox.h"
|
|
#include "FilterBarComboBoxDelegate.h"
|
|
#include "Filters.h"
|
|
#include "IconCache.h"
|
|
#include "NativeIcon.h"
|
|
#include "Prefs.h"
|
|
#include "Torrent.h"
|
|
#include "TorrentFilter.h"
|
|
#include "TorrentModel.h"
|
|
#include "Utils.h"
|
|
|
|
enum
|
|
{
|
|
ACTIVITY_ROLE = FilterBarComboBox::UserRole,
|
|
TRACKER_ROLE
|
|
};
|
|
|
|
/***
|
|
****
|
|
***/
|
|
|
|
FilterBarComboBox* FilterBar::createActivityCombo()
|
|
{
|
|
auto* c = new FilterBarComboBox{ this };
|
|
auto* delegate = new FilterBarComboBoxDelegate{ this, c };
|
|
c->setItemDelegate(delegate);
|
|
|
|
auto* model = new QStandardItemModel{ this };
|
|
|
|
auto* row = new QStandardItem{ tr("All") };
|
|
row->setData(FilterMode::SHOW_ALL, ACTIVITY_ROLE);
|
|
model->appendRow(row);
|
|
|
|
model->appendRow(new QStandardItem{}); // separator
|
|
FilterBarComboBoxDelegate::setSeparator(model, model->index(1, 0));
|
|
|
|
auto add_row = [model](auto const filter_mode, QString label, std::optional<icons::Type> const type)
|
|
{
|
|
auto* row = type ? new QStandardItem{ icons::icon(*type), label } : new QStandardItem{ label };
|
|
row->setData(filter_mode, ACTIVITY_ROLE);
|
|
model->appendRow(row);
|
|
};
|
|
add_row(FilterMode::SHOW_ACTIVE, tr("Active"), icons::Type::TorrentStateActive);
|
|
add_row(FilterMode::SHOW_SEEDING, tr("Seeding"), icons::Type::TorrentStateSeeding);
|
|
add_row(FilterMode::SHOW_DOWNLOADING, tr("Downloading"), icons::Type::TorrentStateDownloading);
|
|
add_row(FilterMode::SHOW_PAUSED, tr("Paused"), icons::Type::TorrentStatePaused);
|
|
add_row(FilterMode::SHOW_FINISHED, tr("Finished"), {});
|
|
add_row(FilterMode::SHOW_VERIFYING, tr("Verifying"), icons::Type::TorrentStateVerifying);
|
|
add_row(FilterMode::SHOW_ERROR, tr("Error"), icons::Type::TorrentStateError);
|
|
|
|
c->setModel(model);
|
|
return c;
|
|
}
|
|
|
|
/***
|
|
****
|
|
***/
|
|
|
|
namespace
|
|
{
|
|
|
|
[[nodiscard]] auto getCountString(size_t n)
|
|
{
|
|
return QStringLiteral("%L1").arg(n);
|
|
}
|
|
|
|
Torrent::fields_t constexpr TrackerFields = {
|
|
static_cast<uint64_t>(1) << Torrent::TRACKER_STATS,
|
|
};
|
|
|
|
[[nodiscard]] auto displayName(QString const& sitename)
|
|
{
|
|
auto name = sitename;
|
|
|
|
if (!name.isEmpty())
|
|
{
|
|
name.front() = name.front().toTitleCase();
|
|
}
|
|
|
|
return name;
|
|
}
|
|
|
|
auto constexpr ActivityFields = FilterMode::TorrentFields;
|
|
|
|
} // namespace
|
|
|
|
void FilterBar::refreshTrackers()
|
|
{
|
|
enum
|
|
{
|
|
ROW_TOTALS = 0,
|
|
ROW_SEPARATOR,
|
|
ROW_FIRST_TRACKER
|
|
};
|
|
|
|
auto torrents_per_sitename = std::unordered_map<QString, int>{};
|
|
for (auto const& tor : torrents_.torrents())
|
|
{
|
|
for (auto const& sitename : tor->sitenames())
|
|
{
|
|
++torrents_per_sitename[sitename];
|
|
}
|
|
}
|
|
|
|
// update the "All" row
|
|
auto const num_trackers = torrents_per_sitename.size();
|
|
auto* item = tracker_model_->item(ROW_TOTALS);
|
|
item->setData(static_cast<int>(num_trackers), FilterBarComboBox::CountRole);
|
|
item->setData(getCountString(num_trackers), FilterBarComboBox::CountStringRole);
|
|
|
|
auto update_tracker_item = [](QStandardItem* i, auto const& it)
|
|
{
|
|
auto const& [sitename, count] = *it;
|
|
auto const display_name = displayName(sitename);
|
|
|
|
i->setData(display_name, Qt::DisplayRole);
|
|
i->setData(display_name, TRACKER_ROLE);
|
|
i->setData(getCountString(static_cast<size_t>(count)), FilterBarComboBox::CountStringRole);
|
|
i->setData(trApp->find_favicon(sitename), Qt::DecorationRole);
|
|
i->setData(static_cast<int>(count), FilterBarComboBox::CountRole);
|
|
|
|
return i;
|
|
};
|
|
|
|
auto new_trackers = small::map<QString, int>{ torrents_per_sitename.begin(), torrents_per_sitename.end() };
|
|
auto old_it = sitename_counts_.cbegin();
|
|
auto new_it = new_trackers.cbegin();
|
|
auto const old_end = sitename_counts_.cend();
|
|
auto const new_end = new_trackers.cend();
|
|
bool any_added = false;
|
|
int row = ROW_FIRST_TRACKER;
|
|
|
|
while ((old_it != old_end) || (new_it != new_end))
|
|
{
|
|
if ((old_it == old_end) || ((new_it != new_end) && (old_it->first > new_it->first)))
|
|
{
|
|
tracker_model_->insertRow(row, update_tracker_item(new QStandardItem{ 1 }, new_it));
|
|
any_added = true;
|
|
++new_it;
|
|
++row;
|
|
}
|
|
else if ((new_it == new_end) || ((old_it != old_end) && (old_it->first < new_it->first)))
|
|
{
|
|
tracker_model_->removeRow(row);
|
|
++old_it;
|
|
}
|
|
else // update
|
|
{
|
|
update_tracker_item(tracker_model_->item(row), new_it);
|
|
++old_it;
|
|
++new_it;
|
|
++row;
|
|
}
|
|
}
|
|
|
|
if (any_added) // the one added might match our filter...
|
|
{
|
|
refreshPref(Prefs::FILTER_TRACKERS);
|
|
}
|
|
|
|
sitename_counts_.swap(new_trackers);
|
|
}
|
|
|
|
FilterBarComboBox* FilterBar::createTrackerCombo(QStandardItemModel* model)
|
|
{
|
|
auto* c = new FilterBarComboBox{ this };
|
|
auto* delegate = new FilterBarComboBoxDelegate{ this, c };
|
|
c->setItemDelegate(delegate);
|
|
|
|
auto* row = new QStandardItem{ tr("All") };
|
|
row->setData(QString{}, TRACKER_ROLE);
|
|
int const count = torrents_.rowCount();
|
|
row->setData(count, FilterBarComboBox::CountRole);
|
|
row->setData(getCountString(static_cast<size_t>(count)), FilterBarComboBox::CountStringRole);
|
|
model->appendRow(row);
|
|
|
|
model->appendRow(new QStandardItem{}); // separator
|
|
FilterBarComboBoxDelegate::setSeparator(model, model->index(1, 0));
|
|
|
|
c->setModel(model);
|
|
return c;
|
|
}
|
|
|
|
/***
|
|
****
|
|
***/
|
|
|
|
FilterBar::FilterBar(Prefs& prefs, TorrentModel const& torrents, TorrentFilter const& filter, QWidget* parent)
|
|
: QWidget{ parent }
|
|
, prefs_{ prefs }
|
|
, torrents_{ torrents }
|
|
, filter_{ filter }
|
|
, count_label_{ new QLabel{ tr("Show:"), this } }
|
|
, is_bootstrapping_{ true }
|
|
{
|
|
auto* h = new QHBoxLayout{ this };
|
|
h->setContentsMargins(3, 3, 3, 3);
|
|
|
|
h->addWidget(count_label_);
|
|
h->addWidget(activity_combo_);
|
|
h->addWidget(tracker_combo_);
|
|
h->addStretch();
|
|
h->addWidget(line_edit_, 1);
|
|
|
|
line_edit_->setClearButtonEnabled(true);
|
|
line_edit_->setPlaceholderText(tr("Search…"));
|
|
line_edit_->setMaximumWidth(250);
|
|
connect(line_edit_, &QLineEdit::textChanged, this, &FilterBar::onTextChanged);
|
|
|
|
// listen for changes from the other players
|
|
connect(&prefs_, &Prefs::changed, this, &FilterBar::refreshPref);
|
|
connect(activity_combo_, qOverload<int>(&QComboBox::currentIndexChanged), this, &FilterBar::onActivityIndexChanged);
|
|
connect(tracker_combo_, qOverload<int>(&QComboBox::currentIndexChanged), this, &FilterBar::onTrackerIndexChanged);
|
|
connect(&torrents_, &TorrentModel::modelReset, this, &FilterBar::recountAllSoon);
|
|
connect(&torrents_, &TorrentModel::rowsInserted, this, &FilterBar::recountAllSoon);
|
|
connect(&torrents_, &TorrentModel::rowsRemoved, this, &FilterBar::recountAllSoon);
|
|
connect(&torrents_, &TorrentModel::torrentsChanged, this, &FilterBar::onTorrentsChanged);
|
|
connect(&recount_timer_, &QTimer::timeout, this, &FilterBar::recount);
|
|
connect(trApp, &Application::faviconsChanged, this, &FilterBar::recountTrackersSoon);
|
|
|
|
recountAllSoon();
|
|
is_bootstrapping_ = false; // NOLINT cppcoreguidelines-prefer-member-initializer
|
|
|
|
// initialize our state
|
|
for (int const key : { Prefs::FILTER_MODE, Prefs::FILTER_TRACKERS })
|
|
{
|
|
refreshPref(key);
|
|
}
|
|
}
|
|
|
|
/***
|
|
****
|
|
***/
|
|
|
|
void FilterBar::clear()
|
|
{
|
|
activity_combo_->setCurrentIndex(0);
|
|
tracker_combo_->setCurrentIndex(0);
|
|
line_edit_->clear();
|
|
}
|
|
|
|
/***
|
|
****
|
|
***/
|
|
|
|
void FilterBar::refreshPref(int key)
|
|
{
|
|
switch (key)
|
|
{
|
|
case Prefs::FILTER_MODE:
|
|
{
|
|
auto const m = prefs_.get<FilterMode>(key);
|
|
QAbstractItemModel const* const model = activity_combo_->model();
|
|
QModelIndexList indices = model->match(model->index(0, 0), ACTIVITY_ROLE, m.mode());
|
|
activity_combo_->setCurrentIndex(indices.isEmpty() ? 0 : indices.first().row());
|
|
break;
|
|
}
|
|
|
|
case Prefs::FILTER_TRACKERS:
|
|
{
|
|
auto const display_name = prefs_.getString(key);
|
|
|
|
if (auto rows = tracker_model_->findItems(display_name); !rows.isEmpty())
|
|
{
|
|
tracker_combo_->setCurrentIndex(rows.front()->row());
|
|
}
|
|
else // hm, we don't seem to have this tracker anymore...
|
|
{
|
|
bool const is_bootstrapping = tracker_model_->rowCount() <= 2;
|
|
|
|
if (!is_bootstrapping)
|
|
{
|
|
prefs_.set(key, QString{});
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void FilterBar::onTorrentsChanged(torrent_ids_t const& ids, Torrent::fields_t const& changed_fields)
|
|
{
|
|
Q_UNUSED(ids)
|
|
|
|
if ((changed_fields & TrackerFields).any())
|
|
{
|
|
recountTrackersSoon();
|
|
}
|
|
|
|
if ((changed_fields & ActivityFields).any())
|
|
{
|
|
recountActivitySoon();
|
|
}
|
|
}
|
|
|
|
void FilterBar::onTextChanged(QString const& str)
|
|
{
|
|
if (!is_bootstrapping_)
|
|
{
|
|
prefs_.set(Prefs::FILTER_TEXT, str.trimmed());
|
|
}
|
|
}
|
|
|
|
void FilterBar::onTrackerIndexChanged(int i)
|
|
{
|
|
if (!is_bootstrapping_)
|
|
{
|
|
auto const display_name = tracker_combo_->itemData(i, TRACKER_ROLE).toString();
|
|
prefs_.set(Prefs::FILTER_TRACKERS, display_name);
|
|
}
|
|
}
|
|
|
|
void FilterBar::onActivityIndexChanged(int i)
|
|
{
|
|
if (!is_bootstrapping_)
|
|
{
|
|
auto const mode = FilterMode(activity_combo_->itemData(i, ACTIVITY_ROLE).toInt());
|
|
prefs_.set(Prefs::FILTER_MODE, mode);
|
|
}
|
|
}
|
|
|
|
/***
|
|
****
|
|
***/
|
|
|
|
void FilterBar::recountSoon(Pending const& fields)
|
|
{
|
|
pending_ |= fields;
|
|
|
|
if (!recount_timer_.isActive())
|
|
{
|
|
recount_timer_.setSingleShot(true);
|
|
recount_timer_.start(800);
|
|
}
|
|
}
|
|
|
|
void FilterBar::recount()
|
|
{
|
|
QAbstractItemModel* model = activity_combo_->model();
|
|
|
|
decltype(pending_) pending = {};
|
|
std::swap(pending_, pending);
|
|
|
|
if (pending[ACTIVITY])
|
|
{
|
|
auto const torrents_per_mode = filter_.countTorrentsPerMode();
|
|
|
|
for (int row = 0, n = model->rowCount(); row < n; ++row)
|
|
{
|
|
auto const index = model->index(row, 0);
|
|
auto const mode = index.data(ACTIVITY_ROLE).toInt();
|
|
auto const count = torrents_per_mode[mode];
|
|
model->setData(index, count, FilterBarComboBox::CountRole);
|
|
model->setData(index, getCountString(static_cast<size_t>(count)), FilterBarComboBox::CountStringRole);
|
|
}
|
|
}
|
|
|
|
if (pending[TRACKERS])
|
|
{
|
|
refreshTrackers();
|
|
}
|
|
}
|