mirror of
https://github.com/d47081/qBittorrent.git
synced 2025-01-10 23:07:59 +00:00
Add a Tags (multi-label) feature to the GUI. Closes #13.
See https://github.com/qbittorrent/qBittorrent/issues/13 for details.
This commit is contained in:
parent
ff80208534
commit
467e516801
@ -28,6 +28,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QSet>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
|
||||
@ -39,6 +40,7 @@ namespace BitTorrent
|
||||
{
|
||||
QString name;
|
||||
QString category;
|
||||
QSet<QString> tags;
|
||||
QString savePath;
|
||||
bool disableTempPath = false; // e.g. for imported torrents
|
||||
bool sequential = false;
|
||||
|
@ -127,6 +127,43 @@ namespace
|
||||
return result;
|
||||
}
|
||||
|
||||
template <typename Entry>
|
||||
QSet<QString> entryListToSetImpl(const Entry &entry)
|
||||
{
|
||||
Q_ASSERT(entry.type() == Entry::list_t);
|
||||
QSet<QString> output;
|
||||
for (int i = 0; i < entry.list_size(); ++i) {
|
||||
const QString tag = QString::fromStdString(entry.list_string_value_at(i));
|
||||
if (Session::isValidTag(tag))
|
||||
output.insert(tag);
|
||||
else
|
||||
qWarning() << QString("Dropping invalid stored tag: %1").arg(tag);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
#if LIBTORRENT_VERSION_NUM < 10100
|
||||
bool isList(const libt::lazy_entry *entry)
|
||||
{
|
||||
return entry && (entry->type() == libt::lazy_entry::list_t);
|
||||
}
|
||||
|
||||
QSet<QString> entryListToSet(const libt::lazy_entry *entry)
|
||||
{
|
||||
return entry ? entryListToSetImpl(*entry) : QSet<QString>();
|
||||
}
|
||||
#else
|
||||
bool isList(const libt::bdecode_node &entry)
|
||||
{
|
||||
return entry.type() == libt::bdecode_node::list_t;
|
||||
}
|
||||
|
||||
QSet<QString> entryListToSet(const libt::bdecode_node &entry)
|
||||
{
|
||||
return entryListToSetImpl(entry);
|
||||
}
|
||||
#endif
|
||||
|
||||
QString normalizePath(const QString &path)
|
||||
{
|
||||
QString tmp = Utils::Fs::fromNativePath(path.trimmed());
|
||||
@ -260,6 +297,7 @@ Session::Session(QObject *parent)
|
||||
, m_isForceProxyEnabled(BITTORRENT_SESSION_KEY("ForceProxy"), true)
|
||||
, m_isProxyPeerConnectionsEnabled(BITTORRENT_SESSION_KEY("ProxyPeerConnections"), false)
|
||||
, m_storedCategories(BITTORRENT_SESSION_KEY("Categories"))
|
||||
, m_storedTags(BITTORRENT_SESSION_KEY("Tags"))
|
||||
, m_maxRatioAction(BITTORRENT_SESSION_KEY("MaxRatioAction"), Pause)
|
||||
, m_defaultSavePath(BITTORRENT_SESSION_KEY("DefaultSavePath"), specialFolderLocation(SpecialFolder::Downloads), normalizePath)
|
||||
, m_tempPath(BITTORRENT_SESSION_KEY("TempPath"), defaultSavePath() + "temp/", normalizePath)
|
||||
@ -400,6 +438,8 @@ Session::Session(QObject *parent)
|
||||
m_storedCategories = map_cast(m_categories);
|
||||
}
|
||||
|
||||
m_tags = QSet<QString>::fromList(m_storedTags.value());
|
||||
|
||||
m_refreshTimer = new QTimer(this);
|
||||
m_refreshTimer->setInterval(refreshInterval());
|
||||
connect(m_refreshTimer, SIGNAL(timeout()), SLOT(refresh()));
|
||||
@ -724,6 +764,47 @@ void Session::setSubcategoriesEnabled(bool value)
|
||||
emit subcategoriesSupportChanged();
|
||||
}
|
||||
|
||||
QSet<QString> Session::tags() const
|
||||
{
|
||||
return m_tags;
|
||||
}
|
||||
|
||||
bool Session::isValidTag(const QString &tag)
|
||||
{
|
||||
return (!tag.trimmed().isEmpty() && !tag.contains(','));
|
||||
}
|
||||
|
||||
bool Session::hasTag(const QString &tag) const
|
||||
{
|
||||
return m_tags.contains(tag);
|
||||
}
|
||||
|
||||
bool Session::addTag(const QString &tag)
|
||||
{
|
||||
if (!isValidTag(tag))
|
||||
return false;
|
||||
|
||||
if (!hasTag(tag)) {
|
||||
m_tags.insert(tag);
|
||||
m_storedTags = m_tags.toList();
|
||||
emit tagAdded(tag);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Session::removeTag(const QString &tag)
|
||||
{
|
||||
if (m_tags.remove(tag)) {
|
||||
foreach (TorrentHandle *const torrent, torrents())
|
||||
torrent->removeTag(tag);
|
||||
m_storedTags = m_tags.toList();
|
||||
emit tagRemoved(tag);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Session::isAutoTMMDisabledByDefault() const
|
||||
{
|
||||
return m_isAutoTMMDisabledByDefault;
|
||||
@ -2997,6 +3078,16 @@ void Session::handleTorrentCategoryChanged(TorrentHandle *const torrent, const Q
|
||||
emit torrentCategoryChanged(torrent, oldCategory);
|
||||
}
|
||||
|
||||
void Session::handleTorrentTagAdded(TorrentHandle *const torrent, const QString &tag)
|
||||
{
|
||||
emit torrentTagAdded(torrent, tag);
|
||||
}
|
||||
|
||||
void Session::handleTorrentTagRemoved(TorrentHandle *const torrent, const QString &tag)
|
||||
{
|
||||
emit torrentTagRemoved(torrent, tag);
|
||||
}
|
||||
|
||||
void Session::handleTorrentSavingModeChanged(TorrentHandle * const torrent)
|
||||
{
|
||||
emit torrentSavingModeChanged(torrent);
|
||||
@ -3930,6 +4021,10 @@ namespace
|
||||
if (torrentData.category.isEmpty())
|
||||
// **************************************************************************************
|
||||
torrentData.category = QString::fromStdString(fast.dict_find_string_value("qBt-category"));
|
||||
// auto because the return type depends on the #if above.
|
||||
const auto tagsEntry = fast.dict_find_list("qBt-tags");
|
||||
if (isList(tagsEntry))
|
||||
torrentData.tags = entryListToSet(tagsEntry);
|
||||
torrentData.name = QString::fromStdString(fast.dict_find_string_value("qBt-name"));
|
||||
torrentData.hasSeedStatus = fast.dict_find_int_value("qBt-seedStatus");
|
||||
torrentData.disableTempPath = fast.dict_find_int_value("qBt-tempPathDisabled");
|
||||
|
@ -44,6 +44,7 @@
|
||||
#endif
|
||||
#include <QNetworkConfigurationManager>
|
||||
#include <QPointer>
|
||||
#include <QSet>
|
||||
#include <QStringList>
|
||||
#include <QVector>
|
||||
#include <QWaitCondition>
|
||||
@ -223,6 +224,12 @@ namespace BitTorrent
|
||||
bool isSubcategoriesEnabled() const;
|
||||
void setSubcategoriesEnabled(bool value);
|
||||
|
||||
static bool isValidTag(const QString &tag);
|
||||
QSet<QString> tags() const;
|
||||
bool hasTag(const QString &tag) const;
|
||||
bool addTag(const QString &tag);
|
||||
bool removeTag(const QString &tag);
|
||||
|
||||
// Torrent Management Mode subsystem (TMM)
|
||||
//
|
||||
// Each torrent can be either in Manual mode or in Automatic mode
|
||||
@ -400,6 +407,8 @@ namespace BitTorrent
|
||||
void handleTorrentShareLimitChanged(TorrentHandle *const torrent);
|
||||
void handleTorrentSavePathChanged(TorrentHandle *const torrent);
|
||||
void handleTorrentCategoryChanged(TorrentHandle *const torrent, const QString &oldCategory);
|
||||
void handleTorrentTagAdded(TorrentHandle *const torrent, const QString &tag);
|
||||
void handleTorrentTagRemoved(TorrentHandle *const torrent, const QString &tag);
|
||||
void handleTorrentSavingModeChanged(TorrentHandle *const torrent);
|
||||
void handleTorrentMetadataReceived(TorrentHandle *const torrent);
|
||||
void handleTorrentPaused(TorrentHandle *const torrent);
|
||||
@ -431,6 +440,8 @@ namespace BitTorrent
|
||||
void torrentFinishedChecking(BitTorrent::TorrentHandle *const torrent);
|
||||
void torrentSavePathChanged(BitTorrent::TorrentHandle *const torrent);
|
||||
void torrentCategoryChanged(BitTorrent::TorrentHandle *const torrent, const QString &oldCategory);
|
||||
void torrentTagAdded(TorrentHandle *const torrent, const QString &tag);
|
||||
void torrentTagRemoved(TorrentHandle *const torrent, const QString &tag);
|
||||
void torrentSavingModeChanged(BitTorrent::TorrentHandle *const torrent);
|
||||
void allTorrentsFinished();
|
||||
void metadataLoaded(const BitTorrent::TorrentInfo &info);
|
||||
@ -452,6 +463,8 @@ namespace BitTorrent
|
||||
void categoryAdded(const QString &categoryName);
|
||||
void categoryRemoved(const QString &categoryName);
|
||||
void subcategoriesSupportChanged();
|
||||
void tagAdded(const QString &tag);
|
||||
void tagRemoved(const QString &tag);
|
||||
|
||||
private slots:
|
||||
void configureDeferred();
|
||||
@ -606,6 +619,7 @@ namespace BitTorrent
|
||||
CachedSettingValue<bool> m_isForceProxyEnabled;
|
||||
CachedSettingValue<bool> m_isProxyPeerConnectionsEnabled;
|
||||
CachedSettingValue<QVariantMap> m_storedCategories;
|
||||
CachedSettingValue<QStringList> m_storedTags;
|
||||
CachedSettingValue<int> m_maxRatioAction;
|
||||
CachedSettingValue<QString> m_defaultSavePath;
|
||||
CachedSettingValue<QString> m_tempPath;
|
||||
@ -650,6 +664,7 @@ namespace BitTorrent
|
||||
QHash<QString, AddTorrentParams> m_downloadedTorrents;
|
||||
TorrentStatusReport m_torrentStatusReport;
|
||||
QStringMap m_categories;
|
||||
QSet<QString> m_tags;
|
||||
|
||||
#if LIBTORRENT_VERSION_NUM < 10100
|
||||
QMutex m_alertsMutex;
|
||||
|
@ -68,6 +68,19 @@ const QString QB_EXT {".!qB"};
|
||||
namespace libt = libtorrent;
|
||||
using namespace BitTorrent;
|
||||
|
||||
namespace
|
||||
{
|
||||
using ListType = libt::entry::list_type;
|
||||
|
||||
ListType setToEntryList(const QSet<QString> &input)
|
||||
{
|
||||
ListType entryList;
|
||||
foreach (const QString &setValue, input)
|
||||
entryList.emplace_back(setValue.toStdString());
|
||||
return entryList;
|
||||
}
|
||||
}
|
||||
|
||||
// AddTorrentData
|
||||
|
||||
AddTorrentData::AddTorrentData()
|
||||
@ -89,6 +102,7 @@ AddTorrentData::AddTorrentData(const AddTorrentParams ¶ms)
|
||||
: resumed(false)
|
||||
, name(params.name)
|
||||
, category(params.category)
|
||||
, tags(params.tags)
|
||||
, savePath(params.savePath)
|
||||
, disableTempPath(params.disableTempPath)
|
||||
, sequential(params.sequential)
|
||||
@ -213,6 +227,7 @@ TorrentHandle::TorrentHandle(Session *session, const libtorrent::torrent_handle
|
||||
, m_name(data.name)
|
||||
, m_savePath(Utils::Fs::toNativePath(data.savePath))
|
||||
, m_category(data.category)
|
||||
, m_tags(data.tags)
|
||||
, m_hasSeedStatus(data.hasSeedStatus)
|
||||
, m_ratioLimit(data.ratioLimit)
|
||||
, m_seedingTimeLimit(data.seedingTimeLimit)
|
||||
@ -578,6 +593,50 @@ bool TorrentHandle::belongsToCategory(const QString &category) const
|
||||
return false;
|
||||
}
|
||||
|
||||
QSet<QString> TorrentHandle::tags() const
|
||||
{
|
||||
return m_tags;
|
||||
}
|
||||
|
||||
bool TorrentHandle::hasTag(const QString &tag) const
|
||||
{
|
||||
return m_tags.contains(tag);
|
||||
}
|
||||
|
||||
bool TorrentHandle::addTag(const QString &tag)
|
||||
{
|
||||
if (!Session::isValidTag(tag))
|
||||
return false;
|
||||
|
||||
if (!hasTag(tag)) {
|
||||
if (!m_session->hasTag(tag))
|
||||
if (!m_session->addTag(tag))
|
||||
return false;
|
||||
m_tags.insert(tag);
|
||||
m_session->handleTorrentTagAdded(this, tag);
|
||||
m_needSaveResumeData = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TorrentHandle::removeTag(const QString &tag)
|
||||
{
|
||||
if (m_tags.remove(tag)) {
|
||||
m_session->handleTorrentTagRemoved(this, tag);
|
||||
m_needSaveResumeData = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void TorrentHandle::removeAllTags()
|
||||
{
|
||||
// QT automatically copies the container in foreach, so it's safe to mutate it.
|
||||
foreach (const QString &tag, m_tags)
|
||||
removeTag(tag);
|
||||
}
|
||||
|
||||
QDateTime TorrentHandle::addedTime() const
|
||||
{
|
||||
return QDateTime::fromTime_t(m_nativeStatus.added_time);
|
||||
@ -1617,6 +1676,7 @@ void TorrentHandle::handleSaveResumeDataAlert(libtorrent::save_resume_data_alert
|
||||
resumeData["qBt-ratioLimit"] = QString::number(m_ratioLimit).toStdString();
|
||||
resumeData["qBt-seedingTimeLimit"] = QString::number(m_seedingTimeLimit).toStdString();
|
||||
resumeData["qBt-category"] = m_category.toStdString();
|
||||
resumeData["qBt-tags"] = setToEntryList(m_tags);
|
||||
resumeData["qBt-name"] = m_name.toStdString();
|
||||
resumeData["qBt-seedStatus"] = m_hasSeedStatus;
|
||||
resumeData["qBt-tempPathDisabled"] = m_tempPathDisabled;
|
||||
|
@ -30,12 +30,13 @@
|
||||
#ifndef BITTORRENT_TORRENTHANDLE_H
|
||||
#define BITTORRENT_TORRENTHANDLE_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QDateTime>
|
||||
#include <QQueue>
|
||||
#include <QVector>
|
||||
#include <QHash>
|
||||
#include <QObject>
|
||||
#include <QQueue>
|
||||
#include <QSet>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
|
||||
#include <libtorrent/torrent_handle.hpp>
|
||||
#include <libtorrent/version.hpp>
|
||||
@ -93,6 +94,7 @@ namespace BitTorrent
|
||||
// for both new and resumed torrents
|
||||
QString name;
|
||||
QString category;
|
||||
QSet<QString> tags;
|
||||
QString savePath;
|
||||
bool disableTempPath;
|
||||
bool sequential;
|
||||
@ -248,6 +250,12 @@ namespace BitTorrent
|
||||
bool belongsToCategory(const QString &category) const;
|
||||
bool setCategory(const QString &category);
|
||||
|
||||
QSet<QString> tags() const;
|
||||
bool hasTag(const QString &tag) const;
|
||||
bool addTag(const QString &tag);
|
||||
bool removeTag(const QString &tag);
|
||||
void removeAllTags();
|
||||
|
||||
bool hasRootFolder() const;
|
||||
|
||||
int filesCount() const;
|
||||
@ -445,6 +453,7 @@ namespace BitTorrent
|
||||
QString m_name;
|
||||
QString m_savePath;
|
||||
QString m_category;
|
||||
QSet<QString> m_tags;
|
||||
bool m_hasSeedStatus;
|
||||
qreal m_ratioLimit;
|
||||
int m_seedingTimeLimit;
|
||||
|
@ -1064,6 +1064,16 @@ void Preferences::setConfirmTorrentRecheck(bool enabled)
|
||||
setValue("Preferences/Advanced/confirmTorrentRecheck", enabled);
|
||||
}
|
||||
|
||||
bool Preferences::confirmRemoveAllTags() const
|
||||
{
|
||||
return value("Preferences/Advanced/confirmRemoveAllTags", true).toBool();
|
||||
}
|
||||
|
||||
void Preferences::setConfirmRemoveAllTags(bool enabled)
|
||||
{
|
||||
setValue("Preferences/Advanced/confirmRemoveAllTags", enabled);
|
||||
}
|
||||
|
||||
TrayIcon::Style Preferences::trayIconStyle() const
|
||||
{
|
||||
return TrayIcon::Style(value("Preferences/Advanced/TrayIconStyle", TrayIcon::NORMAL).toInt());
|
||||
@ -1327,6 +1337,16 @@ void Preferences::setCategoryFilterState(const bool checked)
|
||||
setValue("TransferListFilters/CategoryFilterState", checked);
|
||||
}
|
||||
|
||||
bool Preferences::getTagFilterState() const
|
||||
{
|
||||
return value("TransferListFilters/TagFilterState", true).toBool();
|
||||
}
|
||||
|
||||
void Preferences::setTagFilterState(const bool checked)
|
||||
{
|
||||
setValue("TransferListFilters/TagFilterState", checked);
|
||||
}
|
||||
|
||||
bool Preferences::getTrackerFilterState() const
|
||||
{
|
||||
return value("TransferListFilters/trackerFilterState", true).toBool();
|
||||
|
@ -260,6 +260,8 @@ public:
|
||||
void setConfirmTorrentDeletion(bool enabled);
|
||||
bool confirmTorrentRecheck() const;
|
||||
void setConfirmTorrentRecheck(bool enabled);
|
||||
bool confirmRemoveAllTags() const;
|
||||
void setConfirmRemoveAllTags(bool enabled);
|
||||
TrayIcon::Style trayIconStyle() const;
|
||||
void setTrayIconStyle(TrayIcon::Style style);
|
||||
|
||||
@ -313,6 +315,7 @@ public:
|
||||
void setTorImportGeometry(const QByteArray &geometry);
|
||||
bool getStatusFilterState() const;
|
||||
bool getCategoryFilterState() const;
|
||||
bool getTagFilterState() const;
|
||||
bool getTrackerFilterState() const;
|
||||
int getTransSelFilter() const;
|
||||
void setTransSelFilter(const int &index);
|
||||
@ -340,6 +343,7 @@ public:
|
||||
public slots:
|
||||
void setStatusFilterState(bool checked);
|
||||
void setCategoryFilterState(bool checked);
|
||||
void setTagFilterState(bool checked);
|
||||
void setTrackerFilterState(bool checked);
|
||||
|
||||
void apply();
|
||||
|
@ -31,6 +31,7 @@
|
||||
|
||||
const QString TorrentFilter::AnyCategory;
|
||||
const QStringSet TorrentFilter::AnyHash = (QStringSet() << QString());
|
||||
const QString TorrentFilter::AnyTag;
|
||||
|
||||
const TorrentFilter TorrentFilter::DownloadingTorrent(TorrentFilter::Downloading);
|
||||
const TorrentFilter TorrentFilter::SeedingTorrent(TorrentFilter::Seeding);
|
||||
@ -49,16 +50,18 @@ TorrentFilter::TorrentFilter()
|
||||
{
|
||||
}
|
||||
|
||||
TorrentFilter::TorrentFilter(const Type type, const QStringSet &hashSet, const QString &category)
|
||||
TorrentFilter::TorrentFilter(const Type type, const QStringSet &hashSet, const QString &category, const QString &tag)
|
||||
: m_type(type)
|
||||
, m_category(category)
|
||||
, m_tag(tag)
|
||||
, m_hashSet(hashSet)
|
||||
{
|
||||
}
|
||||
|
||||
TorrentFilter::TorrentFilter(const QString &filter, const QStringSet &hashSet, const QString &category)
|
||||
TorrentFilter::TorrentFilter(const QString &filter, const QStringSet &hashSet, const QString &category, const QString &tag)
|
||||
: m_type(All)
|
||||
, m_category(category)
|
||||
, m_tag(tag)
|
||||
, m_hashSet(hashSet)
|
||||
{
|
||||
setTypeByName(filter);
|
||||
@ -121,11 +124,24 @@ bool TorrentFilter::setCategory(const QString &category)
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TorrentFilter::setTag(const QString &tag)
|
||||
{
|
||||
// QString::operator==() doesn't distinguish between empty and null strings.
|
||||
if ((m_tag != tag)
|
||||
|| (m_tag.isNull() && !tag.isNull())
|
||||
|| (!m_tag.isNull() && tag.isNull())) {
|
||||
m_tag = tag;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TorrentFilter::match(TorrentHandle *const torrent) const
|
||||
{
|
||||
if (!torrent) return false;
|
||||
|
||||
return (matchState(torrent) && matchHash(torrent) && matchCategory(torrent));
|
||||
return (matchState(torrent) && matchHash(torrent) && matchCategory(torrent) && matchTag(torrent));
|
||||
}
|
||||
|
||||
bool TorrentFilter::matchState(BitTorrent::TorrentHandle *const torrent) const
|
||||
@ -165,3 +181,11 @@ bool TorrentFilter::matchCategory(BitTorrent::TorrentHandle *const torrent) cons
|
||||
if (m_category.isNull()) return true;
|
||||
else return (torrent->belongsToCategory(m_category));
|
||||
}
|
||||
|
||||
bool TorrentFilter::matchTag(BitTorrent::TorrentHandle *const torrent) const
|
||||
{
|
||||
// Empty tag is a special value to indicate we're filtering for untagged torrents.
|
||||
if (m_tag.isNull()) return true;
|
||||
else if (m_tag.isEmpty()) return torrent->tags().isEmpty();
|
||||
else return (torrent->hasTag(m_tag));
|
||||
}
|
||||
|
@ -58,8 +58,10 @@ public:
|
||||
Errored
|
||||
};
|
||||
|
||||
// These mean any permutation, including no category / tag.
|
||||
static const QString AnyCategory;
|
||||
static const QStringSet AnyHash;
|
||||
static const QString AnyTag;
|
||||
|
||||
static const TorrentFilter DownloadingTorrent;
|
||||
static const TorrentFilter SeedingTorrent;
|
||||
@ -71,14 +73,16 @@ public:
|
||||
static const TorrentFilter ErroredTorrent;
|
||||
|
||||
TorrentFilter();
|
||||
// category: pass empty string for "no category" or null string (QString()) for "any category"
|
||||
TorrentFilter(const Type type, const QStringSet &hashSet = AnyHash, const QString &category = AnyCategory);
|
||||
TorrentFilter(const QString &filter, const QStringSet &hashSet = AnyHash, const QString &category = AnyCategory);
|
||||
// category & tags: pass empty string for uncategorized / untagged torrents.
|
||||
// Pass null string (QString()) to disable filtering (i.e. all torrents).
|
||||
TorrentFilter(const Type type, const QStringSet &hashSet = AnyHash, const QString &category = AnyCategory, const QString &tag = AnyTag);
|
||||
TorrentFilter(const QString &filter, const QStringSet &hashSet = AnyHash, const QString &category = AnyCategory, const QString &tags = AnyTag);
|
||||
|
||||
bool setType(Type type);
|
||||
bool setTypeByName(const QString &filter);
|
||||
bool setHashSet(const QStringSet &hashSet);
|
||||
bool setCategory(const QString &category);
|
||||
bool setTag(const QString &tag);
|
||||
|
||||
bool match(BitTorrent::TorrentHandle *const torrent) const;
|
||||
|
||||
@ -86,9 +90,11 @@ private:
|
||||
bool matchState(BitTorrent::TorrentHandle *const torrent) const;
|
||||
bool matchHash(BitTorrent::TorrentHandle *const torrent) const;
|
||||
bool matchCategory(BitTorrent::TorrentHandle *const torrent) const;
|
||||
bool matchTag(BitTorrent::TorrentHandle *const torrent) const;
|
||||
|
||||
Type m_type;
|
||||
QString m_category;
|
||||
QString m_tag;
|
||||
QStringSet m_hashSet;
|
||||
};
|
||||
|
||||
|
@ -57,6 +57,9 @@ shutdownconfirmdlg.h
|
||||
speedlimitdlg.h
|
||||
statsdialog.h
|
||||
statusbar.h
|
||||
tagfiltermodel.h
|
||||
tagfilterproxymodel.h
|
||||
tagfilterwidget.h
|
||||
torrentcontentfiltermodel.h
|
||||
torrentcontentmodel.h
|
||||
torrentcontentmodelfile.h
|
||||
@ -98,6 +101,9 @@ shutdownconfirmdlg.cpp
|
||||
speedlimitdlg.cpp
|
||||
statsdialog.cpp
|
||||
statusbar.cpp
|
||||
tagfiltermodel.cpp
|
||||
tagfilterproxymodel.cpp
|
||||
tagfilterwidget.cpp
|
||||
torrentcontentfiltermodel.cpp
|
||||
torrentcontentmodel.cpp
|
||||
torrentcontentmodelfile.cpp
|
||||
|
@ -66,6 +66,7 @@ enum AdvSettingsRows
|
||||
RESOLVE_COUNTRIES,
|
||||
PROGRAM_NOTIFICATIONS,
|
||||
TORRENT_ADDED_NOTIFICATIONS,
|
||||
CONFIRM_REMOVE_ALL_TAGS,
|
||||
DOWNLOAD_TRACKER_FAVICON,
|
||||
#if (defined(Q_OS_UNIX) && !defined(Q_OS_MAC))
|
||||
USE_ICON_THEME,
|
||||
@ -185,6 +186,9 @@ void AdvancedSettings::saveAdvancedSettings()
|
||||
pref->useSystemIconTheme(cb_use_icon_theme.isChecked());
|
||||
#endif
|
||||
pref->setConfirmTorrentRecheck(cb_confirm_torrent_recheck.isChecked());
|
||||
|
||||
pref->setConfirmRemoveAllTags(cb_confirm_remove_all_tags.isChecked());
|
||||
|
||||
session->setAnnounceToAllTrackers(cb_announce_all_trackers.isChecked());
|
||||
}
|
||||
|
||||
@ -377,6 +381,11 @@ void AdvancedSettings::loadAdvancedSettings()
|
||||
// Torrent recheck confirmation
|
||||
cb_confirm_torrent_recheck.setChecked(pref->confirmTorrentRecheck());
|
||||
addRow(CONFIRM_RECHECK_TORRENT, tr("Confirm torrent recheck"), &cb_confirm_torrent_recheck);
|
||||
|
||||
// Remove all tags confirmation
|
||||
cb_confirm_remove_all_tags.setChecked(pref->confirmRemoveAllTags());
|
||||
addRow(CONFIRM_REMOVE_ALL_TAGS, tr("Confirm remove all tags"), &cb_confirm_remove_all_tags);
|
||||
|
||||
// Announce to all trackers
|
||||
cb_announce_all_trackers.setChecked(session->announceToAllTrackers());
|
||||
addRow(ANNOUNCE_ALL_TRACKERS, tr("Always announce to all trackers"), &cb_announce_all_trackers);
|
||||
|
@ -79,7 +79,7 @@ private:
|
||||
QSpinBox spin_cache, spin_save_resume_data_interval, outgoing_ports_min, outgoing_ports_max, spin_list_refresh, spin_maxhalfopen, spin_tracker_port, spin_cache_ttl;
|
||||
QCheckBox cb_os_cache, cb_recheck_completed, cb_resolve_countries, cb_resolve_hosts, cb_super_seeding,
|
||||
cb_program_notifications, cb_torrent_added_notifications, cb_tracker_favicon, cb_tracker_status,
|
||||
cb_confirm_torrent_recheck, cb_listen_ipv6, cb_announce_all_trackers;
|
||||
cb_confirm_torrent_recheck, cb_confirm_remove_all_tags, cb_listen_ipv6, cb_announce_all_trackers;
|
||||
QComboBox combo_iface, combo_iface_address;
|
||||
QLineEdit txtAnnounceIP;
|
||||
|
||||
|
@ -52,6 +52,9 @@ HEADERS += \
|
||||
$$PWD/categoryfiltermodel.h \
|
||||
$$PWD/categoryfilterproxymodel.h \
|
||||
$$PWD/categoryfilterwidget.h \
|
||||
$$PWD/tagfiltermodel.h \
|
||||
$$PWD/tagfilterproxymodel.h \
|
||||
$$PWD/tagfilterwidget.h \
|
||||
$$PWD/banlistoptions.h \
|
||||
$$PWD/rss/rsswidget.h \
|
||||
$$PWD/rss/articlelistwidget.h \
|
||||
@ -103,6 +106,9 @@ SOURCES += \
|
||||
$$PWD/categoryfiltermodel.cpp \
|
||||
$$PWD/categoryfilterproxymodel.cpp \
|
||||
$$PWD/categoryfilterwidget.cpp \
|
||||
$$PWD/tagfiltermodel.cpp \
|
||||
$$PWD/tagfilterproxymodel.cpp \
|
||||
$$PWD/tagfilterwidget.cpp \
|
||||
$$PWD/banlistoptions.cpp \
|
||||
$$PWD/rss/rsswidget.cpp \
|
||||
$$PWD/rss/articlelistwidget.cpp \
|
||||
|
337
src/gui/tagfiltermodel.cpp
Normal file
337
src/gui/tagfiltermodel.cpp
Normal file
@ -0,0 +1,337 @@
|
||||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2017 Tony Gregerson <tony.gregerson@gmail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link this program with the OpenSSL project's "OpenSSL" library (or with
|
||||
* modified versions of it that use the same license as the "OpenSSL" library),
|
||||
* and distribute the linked executables. You must obey the GNU General Public
|
||||
* License in all respects for all of the code used other than "OpenSSL". If you
|
||||
* modify file(s), you may extend this exception to your version of the file(s),
|
||||
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||
* exception statement from your version.
|
||||
*/
|
||||
|
||||
#include "tagfiltermodel.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QHash>
|
||||
#include <QIcon>
|
||||
|
||||
#include "base/bittorrent/session.h"
|
||||
#include "base/bittorrent/torrenthandle.h"
|
||||
#include "guiiconprovider.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
QString getSpecialAllTag()
|
||||
{
|
||||
static const QString *const ALL_TAG = new QString(" ");
|
||||
Q_ASSERT(!BitTorrent::Session::isValidTag(*ALL_TAG));
|
||||
return *ALL_TAG;
|
||||
}
|
||||
|
||||
QString getSpecialUntaggedTag()
|
||||
{
|
||||
static const QString *const UNTAGGED_TAG = new QString(" ");
|
||||
Q_ASSERT(!BitTorrent::Session::isValidTag(*UNTAGGED_TAG));
|
||||
return *UNTAGGED_TAG;
|
||||
}
|
||||
}
|
||||
|
||||
class TagModelItem
|
||||
{
|
||||
public:
|
||||
TagModelItem(const QString &tag, int torrentsCount = 0)
|
||||
: m_tag(tag)
|
||||
, m_torrentsCount(torrentsCount)
|
||||
{
|
||||
}
|
||||
|
||||
QString tag() const
|
||||
{
|
||||
return m_tag;
|
||||
}
|
||||
|
||||
int torrentsCount() const
|
||||
{
|
||||
return m_torrentsCount;
|
||||
}
|
||||
|
||||
void increaseTorrentsCount()
|
||||
{
|
||||
++m_torrentsCount;
|
||||
}
|
||||
|
||||
void decreaseTorrentsCount()
|
||||
{
|
||||
Q_ASSERT(m_torrentsCount > 0);
|
||||
--m_torrentsCount;
|
||||
}
|
||||
|
||||
private:
|
||||
const QString m_tag;
|
||||
int m_torrentsCount;
|
||||
};
|
||||
|
||||
TagFilterModel::TagFilterModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
using Session = BitTorrent::Session;
|
||||
auto session = Session::instance();
|
||||
|
||||
connect(session, &Session::tagAdded, this, &TagFilterModel::tagAdded);
|
||||
connect(session, &Session::tagRemoved, this, &TagFilterModel::tagRemoved);
|
||||
connect(session, &Session::torrentTagAdded, this, &TagFilterModel::torrentTagAdded);
|
||||
connect(session, &Session::torrentTagRemoved, this, &TagFilterModel::torrentTagRemoved);
|
||||
connect(session, &Session::torrentAdded, this, &TagFilterModel::torrentAdded);
|
||||
connect(session, &Session::torrentAboutToBeRemoved, this, &TagFilterModel::torrentAboutToBeRemoved);
|
||||
populate();
|
||||
}
|
||||
|
||||
TagFilterModel::~TagFilterModel() = default;
|
||||
|
||||
bool TagFilterModel::isSpecialItem(const QModelIndex &index)
|
||||
{
|
||||
// the first two items are special items: 'All' and 'Untagged'
|
||||
return (!index.parent().isValid() && (index.row() <= 1));
|
||||
}
|
||||
|
||||
QVariant TagFilterModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid() || index.column() != 0)
|
||||
return QVariant();
|
||||
|
||||
const int row = index.internalId();
|
||||
Q_ASSERT(isValidRow(row));
|
||||
const TagModelItem &item = m_tagItems[row];
|
||||
|
||||
switch (role) {
|
||||
case Qt::DecorationRole:
|
||||
return GuiIconProvider::instance()->getIcon("inode-directory");
|
||||
case Qt::DisplayRole:
|
||||
return QString(QLatin1String("%1 (%2)"))
|
||||
.arg(tagDisplayName(item.tag())).arg(item.torrentsCount());
|
||||
case Qt::UserRole:
|
||||
return item.torrentsCount();
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
|
||||
Qt::ItemFlags TagFilterModel::flags(const QModelIndex &index) const
|
||||
{
|
||||
if (!index.isValid())
|
||||
return 0;
|
||||
return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
|
||||
}
|
||||
|
||||
QVariant TagFilterModel::headerData(int section, Qt::Orientation orientation, int role) const
|
||||
{
|
||||
if ((orientation == Qt::Horizontal) && (role == Qt::DisplayRole))
|
||||
if (section == 0)
|
||||
return tr("Tags");
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
QModelIndex TagFilterModel::index(int row, int, const QModelIndex &) const
|
||||
{
|
||||
if (!isValidRow(row))
|
||||
return QModelIndex();
|
||||
return createIndex(row, 0, row);
|
||||
}
|
||||
|
||||
int TagFilterModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
if (!parent.isValid())
|
||||
return m_tagItems.count();
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool TagFilterModel::isValidRow(int row) const
|
||||
{
|
||||
return (row >= 0) && (row < m_tagItems.size());
|
||||
}
|
||||
|
||||
QModelIndex TagFilterModel::index(const QString &tag) const
|
||||
{
|
||||
const int row = findRow(tag);
|
||||
if (!isValidRow(row))
|
||||
return QModelIndex();
|
||||
return index(row, 0, QModelIndex());
|
||||
}
|
||||
|
||||
QString TagFilterModel::tag(const QModelIndex &index) const
|
||||
{
|
||||
if (!index.isValid())
|
||||
return QString();
|
||||
const int row = index.internalId();
|
||||
Q_ASSERT(isValidRow(row));
|
||||
return m_tagItems[row].tag();
|
||||
}
|
||||
|
||||
void TagFilterModel::tagAdded(const QString &tag)
|
||||
{
|
||||
const int row = m_tagItems.count();
|
||||
beginInsertRows(QModelIndex(), row, row);
|
||||
addToModel(tag, 0);
|
||||
endInsertRows();
|
||||
}
|
||||
|
||||
void TagFilterModel::tagRemoved(const QString &tag)
|
||||
{
|
||||
QModelIndex i = index(tag);
|
||||
beginRemoveRows(i.parent(), i.row(), i.row());
|
||||
removeFromModel(i.row());
|
||||
endRemoveRows();
|
||||
}
|
||||
|
||||
void TagFilterModel::torrentTagAdded(BitTorrent::TorrentHandle *const torrent, const QString &tag)
|
||||
{
|
||||
if (torrent->tags().count() == 1)
|
||||
untaggedItem()->decreaseTorrentsCount();
|
||||
|
||||
const int row = findRow(tag);
|
||||
Q_ASSERT(isValidRow(row));
|
||||
TagModelItem &item = m_tagItems[row];
|
||||
|
||||
item.increaseTorrentsCount();
|
||||
const QModelIndex i = index(row, 0, QModelIndex());
|
||||
emit dataChanged(i, i);
|
||||
}
|
||||
|
||||
void TagFilterModel::torrentTagRemoved(BitTorrent::TorrentHandle* const torrent, const QString &tag)
|
||||
{
|
||||
Q_ASSERT(torrent->tags().count() >= 0);
|
||||
if (torrent->tags().count() == 0)
|
||||
untaggedItem()->increaseTorrentsCount();
|
||||
|
||||
const int row = findRow(tag);
|
||||
Q_ASSERT(isValidRow(row));
|
||||
TagModelItem &item = m_tagItems[row];
|
||||
|
||||
item.decreaseTorrentsCount();
|
||||
const QModelIndex i = index(row, 0, QModelIndex());
|
||||
emit dataChanged(i, i);
|
||||
}
|
||||
|
||||
void TagFilterModel::torrentAdded(BitTorrent::TorrentHandle *const torrent)
|
||||
{
|
||||
allTagsItem()->increaseTorrentsCount();
|
||||
|
||||
const QVector<TagModelItem *> items = findItems(torrent->tags());
|
||||
if (items.isEmpty())
|
||||
untaggedItem()->increaseTorrentsCount();
|
||||
|
||||
foreach (TagModelItem *item, items)
|
||||
item->increaseTorrentsCount();
|
||||
}
|
||||
|
||||
void TagFilterModel::torrentAboutToBeRemoved(BitTorrent::TorrentHandle *const torrent)
|
||||
{
|
||||
allTagsItem()->decreaseTorrentsCount();
|
||||
|
||||
if (torrent->tags().isEmpty())
|
||||
untaggedItem()->decreaseTorrentsCount();
|
||||
|
||||
foreach (TagModelItem *item, findItems(torrent->tags()))
|
||||
item->decreaseTorrentsCount();
|
||||
}
|
||||
|
||||
QString TagFilterModel::tagDisplayName(const QString &tag)
|
||||
{
|
||||
if (tag == getSpecialAllTag())
|
||||
return tr("All");
|
||||
if (tag == getSpecialUntaggedTag())
|
||||
return tr("Untagged");
|
||||
return tag;
|
||||
}
|
||||
|
||||
void TagFilterModel::populate()
|
||||
{
|
||||
using Torrent = BitTorrent::TorrentHandle;
|
||||
|
||||
auto session = BitTorrent::Session::instance();
|
||||
auto torrents = session->torrents();
|
||||
|
||||
// All torrents
|
||||
addToModel(getSpecialAllTag(), torrents.count());
|
||||
|
||||
const int untaggedCount = std::count_if(torrents.begin(), torrents.end(),
|
||||
[](Torrent *torrent) { return torrent->tags().isEmpty(); });
|
||||
addToModel(getSpecialUntaggedTag(), untaggedCount);
|
||||
|
||||
foreach (const QString &tag, session->tags()) {
|
||||
const int count = std::count_if(torrents.begin(), torrents.end(),
|
||||
[tag](Torrent *torrent) { return torrent->hasTag(tag); });
|
||||
addToModel(tag, count);
|
||||
}
|
||||
}
|
||||
|
||||
void TagFilterModel::addToModel(const QString &tag, int count)
|
||||
{
|
||||
m_tagItems.append(TagModelItem(tag, count));
|
||||
}
|
||||
|
||||
void TagFilterModel::removeFromModel(int row)
|
||||
{
|
||||
Q_ASSERT(isValidRow(row));
|
||||
m_tagItems.removeAt(row);
|
||||
}
|
||||
|
||||
int TagFilterModel::findRow(const QString &tag) const
|
||||
{
|
||||
for (int i = 0; i < m_tagItems.size(); ++i) {
|
||||
if (m_tagItems[i].tag() == tag)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
TagModelItem *TagFilterModel::findItem(const QString &tag)
|
||||
{
|
||||
const int row = findRow(tag);
|
||||
if (!isValidRow(row))
|
||||
return nullptr;
|
||||
return &m_tagItems[row];
|
||||
}
|
||||
|
||||
QVector<TagModelItem *> TagFilterModel::findItems(const QSet<QString> &tags)
|
||||
{
|
||||
QVector<TagModelItem *> items;
|
||||
items.reserve(tags.size());
|
||||
foreach (const QString &tag, tags) {
|
||||
TagModelItem *item = findItem(tag);
|
||||
if (item)
|
||||
items.push_back(item);
|
||||
else
|
||||
qWarning() << QString("Requested tag '%1' missing from the model.").arg(tag);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
TagModelItem *TagFilterModel::allTagsItem()
|
||||
{
|
||||
Q_ASSERT(m_tagItems.size() > 0);
|
||||
return &m_tagItems[0];
|
||||
}
|
||||
|
||||
TagModelItem *TagFilterModel::untaggedItem()
|
||||
{
|
||||
Q_ASSERT(m_tagItems.size() > 1);
|
||||
return &m_tagItems[1];
|
||||
}
|
88
src/gui/tagfiltermodel.h
Normal file
88
src/gui/tagfiltermodel.h
Normal file
@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2017 Tony Gregerson <tony.gregerson@gmail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link this program with the OpenSSL project's "OpenSSL" library (or with
|
||||
* modified versions of it that use the same license as the "OpenSSL" library),
|
||||
* and distribute the linked executables. You must obey the GNU General Public
|
||||
* License in all respects for all of the code used other than "OpenSSL". If you
|
||||
* modify file(s), you may extend this exception to your version of the file(s),
|
||||
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||
* exception statement from your version.
|
||||
*/
|
||||
|
||||
#ifndef TAGFILTERMODEL_H
|
||||
#define TAGFILTERMODEL_H
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QHash>
|
||||
#include <QModelIndex>
|
||||
#include <QSet>
|
||||
#include <QVector>
|
||||
|
||||
namespace BitTorrent
|
||||
{
|
||||
class TorrentHandle;
|
||||
}
|
||||
|
||||
class TagModelItem;
|
||||
|
||||
class TagFilterModel: public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit TagFilterModel(QObject *parent = nullptr);
|
||||
~TagFilterModel();
|
||||
|
||||
static bool isSpecialItem(const QModelIndex &index);
|
||||
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
Qt::ItemFlags flags(const QModelIndex &index) const override;
|
||||
QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
|
||||
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
QModelIndex index(const QString &tag) const;
|
||||
QString tag(const QModelIndex &index) const;
|
||||
|
||||
private slots:
|
||||
void tagAdded(const QString &tag);
|
||||
void tagRemoved(const QString &tag);
|
||||
void torrentTagAdded(BitTorrent::TorrentHandle *const torrent, const QString &tag);
|
||||
void torrentTagRemoved(BitTorrent::TorrentHandle *const, const QString &tag);
|
||||
void torrentAdded(BitTorrent::TorrentHandle *const torrent);
|
||||
void torrentAboutToBeRemoved(BitTorrent::TorrentHandle *const torrent);
|
||||
|
||||
private:
|
||||
static QString tagDisplayName(const QString &tag);
|
||||
|
||||
void populate();
|
||||
void addToModel(const QString &tag, int count);
|
||||
void removeFromModel(int row);
|
||||
bool isValidRow(int row) const;
|
||||
int findRow(const QString &tag) const;
|
||||
TagModelItem *findItem(const QString &tag);
|
||||
QVector<TagModelItem *> findItems(const QSet<QString> &tags);
|
||||
TagModelItem *allTagsItem();
|
||||
TagModelItem *untaggedItem();
|
||||
|
||||
QList<TagModelItem> m_tagItems; // Index corresponds to its row
|
||||
};
|
||||
|
||||
#endif // TAGFILTERMODEL_H
|
56
src/gui/tagfilterproxymodel.cpp
Normal file
56
src/gui/tagfilterproxymodel.cpp
Normal file
@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2017 Tony Gregerson <tony.gregerson@gmail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link this program with the OpenSSL project's "OpenSSL" library (or with
|
||||
* modified versions of it that use the same license as the "OpenSSL" library),
|
||||
* and distribute the linked executables. You must obey the GNU General Public
|
||||
* License in all respects for all of the code used other than "OpenSSL". If you
|
||||
* modify file(s), you may extend this exception to your version of the file(s),
|
||||
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||
* exception statement from your version.
|
||||
*/
|
||||
|
||||
#include "tagfilterproxymodel.h"
|
||||
|
||||
#include "base/utils/string.h"
|
||||
#include "tagfiltermodel.h"
|
||||
|
||||
TagFilterProxyModel::TagFilterProxyModel(QObject *parent)
|
||||
: QSortFilterProxyModel(parent)
|
||||
{
|
||||
}
|
||||
|
||||
QModelIndex TagFilterProxyModel::index(const QString &tag) const
|
||||
{
|
||||
return mapFromSource(static_cast<TagFilterModel *>(sourceModel())->index(tag));
|
||||
}
|
||||
|
||||
QString TagFilterProxyModel::tag(const QModelIndex &index) const
|
||||
{
|
||||
return static_cast<TagFilterModel *>(sourceModel())->tag(mapToSource(index));
|
||||
}
|
||||
|
||||
bool TagFilterProxyModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
|
||||
{
|
||||
// "All" and "Untagged" must be left in place
|
||||
if (TagFilterModel::isSpecialItem(left) || TagFilterModel::isSpecialItem(right))
|
||||
return left.row() < right.row();
|
||||
return Utils::String::naturalCompareCaseInsensitive(
|
||||
left.data().toString(), right.data().toString());
|
||||
}
|
52
src/gui/tagfilterproxymodel.h
Normal file
52
src/gui/tagfilterproxymodel.h
Normal file
@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2017 Tony Gregerson <tony.gregerson@gmail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link this program with the OpenSSL project's "OpenSSL" library (or with
|
||||
* modified versions of it that use the same license as the "OpenSSL" library),
|
||||
* and distribute the linked executables. You must obey the GNU General Public
|
||||
* License in all respects for all of the code used other than "OpenSSL". If you
|
||||
* modify file(s), you may extend this exception to your version of the file(s),
|
||||
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||
* exception statement from your version.
|
||||
*/
|
||||
|
||||
#ifndef TAGFILTERPROXYMODEL_H
|
||||
#define TAGFILTERPROXYMODEL_H
|
||||
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QString>
|
||||
|
||||
class TagFilterProxyModel: public QSortFilterProxyModel
|
||||
{
|
||||
public:
|
||||
explicit TagFilterProxyModel(QObject *parent = nullptr);
|
||||
|
||||
// TagFilterModel methods which we need to relay
|
||||
QModelIndex index(const QString &tag) const;
|
||||
QString tag(const QModelIndex &index) const;
|
||||
|
||||
protected:
|
||||
bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
|
||||
|
||||
private:
|
||||
// we added another overload of index(), hence this using directive:
|
||||
using QSortFilterProxyModel::index;
|
||||
};
|
||||
|
||||
#endif // TAGFILTERPROXYMODEL_H
|
224
src/gui/tagfilterwidget.cpp
Normal file
224
src/gui/tagfilterwidget.cpp
Normal file
@ -0,0 +1,224 @@
|
||||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2017 Tony Gregerson <tony.gregerson@gmail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link this program with the OpenSSL project's "OpenSSL" library (or with
|
||||
* modified versions of it that use the same license as the "OpenSSL" library),
|
||||
* and distribute the linked executables. You must obey the GNU General Public
|
||||
* License in all respects for all of the code used other than "OpenSSL". If you
|
||||
* modify file(s), you may extend this exception to your version of the file(s),
|
||||
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||
* exception statement from your version.
|
||||
*/
|
||||
|
||||
#include "tagfilterwidget.h"
|
||||
|
||||
#include <QAction>
|
||||
#include <QDebug>
|
||||
#include <QHeaderView>
|
||||
#include <QLayout>
|
||||
#include <QMenu>
|
||||
#include <QMessageBox>
|
||||
|
||||
#include "base/bittorrent/session.h"
|
||||
#include "base/utils/misc.h"
|
||||
#include "autoexpandabledialog.h"
|
||||
#include "guiiconprovider.h"
|
||||
#include "tagfiltermodel.h"
|
||||
#include "tagfilterproxymodel.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
QString getTagFilter(const TagFilterProxyModel *const model, const QModelIndex &index)
|
||||
{
|
||||
QString tagFilter; // Defaults to All
|
||||
if (index.isValid()) {
|
||||
if (index.row() == 1)
|
||||
tagFilter = ""; // Untagged
|
||||
else if (index.row() > 1)
|
||||
tagFilter = model->tag(index);
|
||||
}
|
||||
return tagFilter;
|
||||
}
|
||||
}
|
||||
|
||||
TagFilterWidget::TagFilterWidget(QWidget *parent)
|
||||
: QTreeView(parent)
|
||||
{
|
||||
TagFilterProxyModel *proxyModel = new TagFilterProxyModel(this);
|
||||
proxyModel->setSortCaseSensitivity(Qt::CaseInsensitive);
|
||||
proxyModel->setSourceModel(new TagFilterModel(this));
|
||||
setModel(proxyModel);
|
||||
setFrameShape(QFrame::NoFrame);
|
||||
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||
setUniformRowHeights(true);
|
||||
setHeaderHidden(true);
|
||||
setIconSize(Utils::Misc::smallIconSize());
|
||||
#if defined(Q_OS_MAC)
|
||||
setAttribute(Qt::WA_MacShowFocusRect, false);
|
||||
#endif
|
||||
setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
sortByColumn(0, Qt::AscendingOrder);
|
||||
setCurrentIndex(model()->index(0, 0));
|
||||
|
||||
connect(this, &TagFilterWidget::collapsed, this, &TagFilterWidget::callUpdateGeometry);
|
||||
connect(this, &TagFilterWidget::expanded, this, &TagFilterWidget::callUpdateGeometry);
|
||||
connect(this, &TagFilterWidget::customContextMenuRequested, this, &TagFilterWidget::showMenu);
|
||||
connect(selectionModel(), &QItemSelectionModel::currentRowChanged, this
|
||||
, &TagFilterWidget::onCurrentRowChanged);
|
||||
connect(model(), &QAbstractItemModel::modelReset, this, &TagFilterWidget::callUpdateGeometry);
|
||||
}
|
||||
|
||||
QString TagFilterWidget::currentTag() const
|
||||
{
|
||||
QModelIndex current;
|
||||
auto selectedRows = selectionModel()->selectedRows();
|
||||
if (!selectedRows.isEmpty())
|
||||
current = selectedRows.first();
|
||||
|
||||
return getTagFilter(static_cast<TagFilterProxyModel *>(model()), current);
|
||||
}
|
||||
|
||||
void TagFilterWidget::onCurrentRowChanged(const QModelIndex ¤t, const QModelIndex &previous)
|
||||
{
|
||||
Q_UNUSED(previous);
|
||||
|
||||
emit tagChanged(getTagFilter(static_cast<TagFilterProxyModel *>(model()), current));
|
||||
}
|
||||
|
||||
void TagFilterWidget::showMenu(QPoint)
|
||||
{
|
||||
QMenu menu(this);
|
||||
|
||||
QAction *addAct = menu.addAction(
|
||||
GuiIconProvider::instance()->getIcon("list-add")
|
||||
, tr("Add tag..."));
|
||||
connect(addAct, &QAction::triggered, this, &TagFilterWidget::addTag);
|
||||
|
||||
auto selectedRows = selectionModel()->selectedRows();
|
||||
if (!selectedRows.empty() && !TagFilterModel::isSpecialItem(selectedRows.first())) {
|
||||
QAction *removeAct = menu.addAction(
|
||||
GuiIconProvider::instance()->getIcon("list-remove")
|
||||
, tr("Remove tag"));
|
||||
connect(removeAct, &QAction::triggered, this, &TagFilterWidget::removeTag);
|
||||
}
|
||||
|
||||
QAction *removeUnusedAct = menu.addAction(
|
||||
GuiIconProvider::instance()->getIcon("list-remove")
|
||||
, tr("Remove unused tags"));
|
||||
connect(removeUnusedAct, &QAction::triggered, this, &TagFilterWidget::removeUnusedTags);
|
||||
|
||||
menu.addSeparator();
|
||||
|
||||
QAction *startAct = menu.addAction(
|
||||
GuiIconProvider::instance()->getIcon("media-playback-start")
|
||||
, tr("Resume torrents"));
|
||||
connect(startAct, &QAction::triggered
|
||||
, this, &TagFilterWidget::actionResumeTorrentsTriggered);
|
||||
|
||||
QAction *pauseAct = menu.addAction(
|
||||
GuiIconProvider::instance()->getIcon("media-playback-pause")
|
||||
, tr("Pause torrents"));
|
||||
connect(pauseAct, &QAction::triggered, this
|
||||
, &TagFilterWidget::actionPauseTorrentsTriggered);
|
||||
|
||||
QAction *deleteTorrentsAct = menu.addAction(
|
||||
GuiIconProvider::instance()->getIcon("edit-delete")
|
||||
, tr("Delete torrents"));
|
||||
connect(deleteTorrentsAct, &QAction::triggered, this
|
||||
, &TagFilterWidget::actionDeleteTorrentsTriggered);
|
||||
|
||||
menu.exec(QCursor::pos());
|
||||
}
|
||||
|
||||
void TagFilterWidget::callUpdateGeometry()
|
||||
{
|
||||
updateGeometry();
|
||||
}
|
||||
|
||||
QSize TagFilterWidget::sizeHint() const
|
||||
{
|
||||
return viewportSizeHint();
|
||||
}
|
||||
|
||||
QSize TagFilterWidget::minimumSizeHint() const
|
||||
{
|
||||
QSize size = sizeHint();
|
||||
size.setWidth(6);
|
||||
return size;
|
||||
}
|
||||
|
||||
void TagFilterWidget::rowsInserted(const QModelIndex &parent, int start, int end)
|
||||
{
|
||||
QTreeView::rowsInserted(parent, start, end);
|
||||
updateGeometry();
|
||||
}
|
||||
|
||||
QString TagFilterWidget::askTagName()
|
||||
{
|
||||
bool ok = false;
|
||||
QString tag = "";
|
||||
bool invalid = true;
|
||||
while (invalid) {
|
||||
invalid = false;
|
||||
tag = AutoExpandableDialog::getText(
|
||||
this, tr("New Tag"), tr("Tag:"), QLineEdit::Normal, tag, &ok).trimmed();
|
||||
if (ok && !tag.isEmpty()) {
|
||||
if (!BitTorrent::Session::isValidTag(tag)) {
|
||||
QMessageBox::warning(
|
||||
this, tr("Invalid tag name")
|
||||
, tr("Tag name '%1' is invalid").arg(tag));
|
||||
invalid = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ok ? tag : QString();
|
||||
}
|
||||
|
||||
void TagFilterWidget::addTag()
|
||||
{
|
||||
const QString tag = askTagName();
|
||||
if (tag.isEmpty()) return;
|
||||
|
||||
if (BitTorrent::Session::instance()->tags().contains(tag))
|
||||
QMessageBox::warning(this, tr("Tag exists"), tr("Tag name already exists."));
|
||||
else
|
||||
BitTorrent::Session::instance()->addTag(tag);
|
||||
}
|
||||
|
||||
void TagFilterWidget::removeTag()
|
||||
{
|
||||
auto selectedRows = selectionModel()->selectedRows();
|
||||
if (!selectedRows.empty() && !TagFilterModel::isSpecialItem(selectedRows.first())) {
|
||||
BitTorrent::Session::instance()->removeTag(
|
||||
static_cast<TagFilterProxyModel *>(model())->tag(selectedRows.first()));
|
||||
updateGeometry();
|
||||
}
|
||||
}
|
||||
|
||||
void TagFilterWidget::removeUnusedTags()
|
||||
{
|
||||
auto session = BitTorrent::Session::instance();
|
||||
foreach (const QString &tag, session->tags())
|
||||
if (model()->data(static_cast<TagFilterProxyModel *>(model())->index(tag), Qt::UserRole) == 0)
|
||||
session->removeTag(tag);
|
||||
updateGeometry();
|
||||
}
|
64
src/gui/tagfilterwidget.h
Normal file
64
src/gui/tagfilterwidget.h
Normal file
@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2017 Tony Gregerson <tony.gregerson@gmail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link this program with the OpenSSL project's "OpenSSL" library (or with
|
||||
* modified versions of it that use the same license as the "OpenSSL" library),
|
||||
* and distribute the linked executables. You must obey the GNU General Public
|
||||
* License in all respects for all of the code used other than "OpenSSL". If you
|
||||
* modify file(s), you may extend this exception to your version of the file(s),
|
||||
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||
* exception statement from your version.
|
||||
*/
|
||||
|
||||
#ifndef TAGFILTERWIDGET_H
|
||||
#define TAGFILTERWIDGET_H
|
||||
|
||||
#include <QTreeView>
|
||||
|
||||
class TagFilterWidget: public QTreeView
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit TagFilterWidget(QWidget *parent = nullptr);
|
||||
|
||||
QString currentTag() const;
|
||||
|
||||
signals:
|
||||
void tagChanged(const QString &tag);
|
||||
void actionResumeTorrentsTriggered();
|
||||
void actionPauseTorrentsTriggered();
|
||||
void actionDeleteTorrentsTriggered();
|
||||
|
||||
private slots:
|
||||
void onCurrentRowChanged(const QModelIndex ¤t, const QModelIndex &previous);
|
||||
void showMenu(QPoint);
|
||||
void callUpdateGeometry();
|
||||
void addTag();
|
||||
void removeTag();
|
||||
void removeUnusedTags();
|
||||
|
||||
private:
|
||||
QSize sizeHint() const override;
|
||||
QSize minimumSizeHint() const override;
|
||||
void rowsInserted(const QModelIndex &parent, int start, int end) override;
|
||||
QString askTagName();
|
||||
};
|
||||
|
||||
#endif // TAGFILTERWIDGET_H
|
@ -105,6 +105,7 @@ QVariant TorrentModel::headerData(int section, Qt::Orientation orientation, int
|
||||
case TR_RATIO: return tr("Ratio", "Share ratio");
|
||||
case TR_ETA: return tr("ETA", "i.e: Estimated Time of Arrival / Time left");
|
||||
case TR_CATEGORY: return tr("Category");
|
||||
case TR_TAGS: return tr("Tags");
|
||||
case TR_ADD_DATE: return tr("Added On", "Torrent was added to transfer list on 01/01/2010 08:00");
|
||||
case TR_SEED_DATE: return tr("Completed On", "Torrent was completed on 01/01/2010 08:00");
|
||||
case TR_TRACKER: return tr("Tracker");
|
||||
@ -198,6 +199,11 @@ QVariant TorrentModel::data(const QModelIndex &index, int role) const
|
||||
return torrent->realRatio();
|
||||
case TR_CATEGORY:
|
||||
return torrent->category();
|
||||
case TR_TAGS: {
|
||||
QStringList tagsList = torrent->tags().toList();
|
||||
tagsList.sort();
|
||||
return tagsList.join(", ");
|
||||
}
|
||||
case TR_ADD_DATE:
|
||||
return torrent->addedTime();
|
||||
case TR_SEED_DATE:
|
||||
|
@ -62,6 +62,7 @@ public:
|
||||
TR_ETA,
|
||||
TR_RATIO,
|
||||
TR_CATEGORY,
|
||||
TR_TAGS,
|
||||
TR_ADD_DATE,
|
||||
TR_SEED_DATE,
|
||||
TR_TRACKER,
|
||||
|
@ -53,6 +53,7 @@
|
||||
#include "autoexpandabledialog.h"
|
||||
#include "categoryfilterwidget.h"
|
||||
#include "guiiconprovider.h"
|
||||
#include "tagfilterwidget.h"
|
||||
#include "torrentmodel.h"
|
||||
#include "transferlistdelegate.h"
|
||||
#include "transferlistwidget.h"
|
||||
@ -75,11 +76,13 @@ FiltersBase::FiltersBase(QWidget *parent, TransferListWidget *transferList)
|
||||
#endif
|
||||
|
||||
setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
connect(this, SIGNAL(customContextMenuRequested(QPoint)), SLOT(showMenu(QPoint)));
|
||||
connect(this, SIGNAL(currentRowChanged(int)), SLOT(applyFilter(int)));
|
||||
connect(this, &FiltersBase::customContextMenuRequested, this, &FiltersBase::showMenu);
|
||||
connect(this, &FiltersBase::currentRowChanged, this, &FiltersBase::applyFilter);
|
||||
|
||||
connect(BitTorrent::Session::instance(), SIGNAL(torrentAdded(BitTorrent::TorrentHandle *const)), SLOT(handleNewTorrent(BitTorrent::TorrentHandle *const)));
|
||||
connect(BitTorrent::Session::instance(), SIGNAL(torrentAboutToBeRemoved(BitTorrent::TorrentHandle *const)), SLOT(torrentAboutToBeDeleted(BitTorrent::TorrentHandle *const)));
|
||||
connect(BitTorrent::Session::instance(), &BitTorrent::Session::torrentAdded
|
||||
, this, &FiltersBase::handleNewTorrent);
|
||||
connect(BitTorrent::Session::instance(), &BitTorrent::Session::torrentAboutToBeRemoved
|
||||
, this, &FiltersBase::torrentAboutToBeDeleted);
|
||||
}
|
||||
|
||||
QSize FiltersBase::sizeHint() const
|
||||
@ -111,7 +114,8 @@ void FiltersBase::toggleFilter(bool checked)
|
||||
StatusFiltersWidget::StatusFiltersWidget(QWidget *parent, TransferListWidget *transferList)
|
||||
: FiltersBase(parent, transferList)
|
||||
{
|
||||
connect(BitTorrent::Session::instance(), SIGNAL(torrentsUpdated()), SLOT(updateTorrentNumbers()));
|
||||
connect(BitTorrent::Session::instance(), &BitTorrent::Session::torrentsUpdated
|
||||
, this, &StatusFiltersWidget::updateTorrentNumbers);
|
||||
|
||||
// Add status filters
|
||||
QListWidgetItem *all = new QListWidgetItem(this);
|
||||
@ -387,8 +391,11 @@ void TrackerFiltersList::downloadFavicon(const QString& url)
|
||||
{
|
||||
if (!m_downloadTrackerFavicon) return;
|
||||
Net::DownloadHandler *h = Net::DownloadManager::instance()->downloadUrl(url, true);
|
||||
connect(h, SIGNAL(downloadFinished(QString, QString)), this, SLOT(handleFavicoDownload(QString, QString)));
|
||||
connect(h, SIGNAL(downloadFailed(QString, QString)), this, SLOT(handleFavicoFailure(QString, QString)));
|
||||
using Func = void (Net::DownloadHandler::*)(const QString &, const QString &);
|
||||
connect(h, static_cast<Func>(&Net::DownloadHandler::downloadFinished), this
|
||||
, &TrackerFiltersList::handleFavicoDownload);
|
||||
connect(h, static_cast<Func>(&Net::DownloadHandler::downloadFailed), this
|
||||
, &TrackerFiltersList::handleFavicoFailure);
|
||||
}
|
||||
|
||||
void TrackerFiltersList::handleFavicoDownload(const QString& url, const QString& filePath)
|
||||
@ -568,21 +575,40 @@ TransferListFiltersWidget::TransferListFiltersWidget(QWidget *parent, TransferLi
|
||||
QCheckBox *categoryLabel = new QCheckBox(tr("Categories"), this);
|
||||
categoryLabel->setChecked(pref->getCategoryFilterState());
|
||||
categoryLabel->setFont(font);
|
||||
connect(categoryLabel, SIGNAL(toggled(bool)), SLOT(onCategoryFilterStateChanged(bool)));
|
||||
connect(categoryLabel, &QCheckBox::toggled, this
|
||||
, &TransferListFiltersWidget::onCategoryFilterStateChanged);
|
||||
frameLayout->addWidget(categoryLabel);
|
||||
|
||||
m_categoryFilterWidget = new CategoryFilterWidget(this);
|
||||
connect(m_categoryFilterWidget, SIGNAL(actionDeleteTorrentsTriggered())
|
||||
, transferList, SLOT(deleteVisibleTorrents()));
|
||||
connect(m_categoryFilterWidget, SIGNAL(actionPauseTorrentsTriggered())
|
||||
, transferList, SLOT(pauseVisibleTorrents()));
|
||||
connect(m_categoryFilterWidget, SIGNAL(actionResumeTorrentsTriggered())
|
||||
, transferList, SLOT(startVisibleTorrents()));
|
||||
connect(m_categoryFilterWidget, SIGNAL(categoryChanged(QString))
|
||||
, transferList, SLOT(applyCategoryFilter(QString)));
|
||||
connect(m_categoryFilterWidget, &CategoryFilterWidget::actionDeleteTorrentsTriggered
|
||||
, transferList, &TransferListWidget::deleteVisibleTorrents);
|
||||
connect(m_categoryFilterWidget, &CategoryFilterWidget::actionPauseTorrentsTriggered
|
||||
, transferList, &TransferListWidget::pauseVisibleTorrents);
|
||||
connect(m_categoryFilterWidget, &CategoryFilterWidget::actionResumeTorrentsTriggered
|
||||
, transferList, &TransferListWidget::startVisibleTorrents);
|
||||
connect(m_categoryFilterWidget, &CategoryFilterWidget::categoryChanged
|
||||
, transferList, &TransferListWidget::applyCategoryFilter);
|
||||
toggleCategoryFilter(pref->getCategoryFilterState());
|
||||
frameLayout->addWidget(m_categoryFilterWidget);
|
||||
|
||||
QCheckBox *tagsLabel = new QCheckBox(tr("Tags"), this);
|
||||
tagsLabel->setChecked(pref->getTagFilterState());
|
||||
tagsLabel->setFont(font);
|
||||
connect(tagsLabel, &QCheckBox::toggled, this, &TransferListFiltersWidget::onTagFilterStateChanged);
|
||||
frameLayout->addWidget(tagsLabel);
|
||||
|
||||
m_tagFilterWidget = new TagFilterWidget(this);
|
||||
connect(m_tagFilterWidget, &TagFilterWidget::actionDeleteTorrentsTriggered
|
||||
, transferList, &TransferListWidget::deleteVisibleTorrents);
|
||||
connect(m_tagFilterWidget, &TagFilterWidget::actionPauseTorrentsTriggered
|
||||
, transferList, &TransferListWidget::pauseVisibleTorrents);
|
||||
connect(m_tagFilterWidget, &TagFilterWidget::actionResumeTorrentsTriggered
|
||||
, transferList, &TransferListWidget::startVisibleTorrents);
|
||||
connect(m_tagFilterWidget, &TagFilterWidget::tagChanged
|
||||
, transferList, &TransferListWidget::applyTagFilter);
|
||||
toggleTagFilter(pref->getTagFilterState());
|
||||
frameLayout->addWidget(m_tagFilterWidget);
|
||||
|
||||
QCheckBox *trackerLabel = new QCheckBox(tr("Trackers"), this);
|
||||
trackerLabel->setChecked(pref->getTrackerFilterState());
|
||||
trackerLabel->setFont(font);
|
||||
@ -591,13 +617,18 @@ TransferListFiltersWidget::TransferListFiltersWidget(QWidget *parent, TransferLi
|
||||
m_trackerFilters = new TrackerFiltersList(this, transferList);
|
||||
frameLayout->addWidget(m_trackerFilters);
|
||||
|
||||
connect(statusLabel, SIGNAL(toggled(bool)), statusFilters, SLOT(toggleFilter(bool)));
|
||||
connect(statusLabel, SIGNAL(toggled(bool)), pref, SLOT(setStatusFilterState(const bool)));
|
||||
connect(trackerLabel, SIGNAL(toggled(bool)), m_trackerFilters, SLOT(toggleFilter(bool)));
|
||||
connect(trackerLabel, SIGNAL(toggled(bool)), pref, SLOT(setTrackerFilterState(const bool)));
|
||||
connect(this, SIGNAL(trackerSuccess(const QString &, const QString &)), m_trackerFilters, SLOT(trackerSuccess(const QString &, const QString &)));
|
||||
connect(this, SIGNAL(trackerError(const QString &, const QString &)), m_trackerFilters, SLOT(trackerError(const QString &, const QString &)));
|
||||
connect(this, SIGNAL(trackerWarning(const QString &, const QString &)), m_trackerFilters, SLOT(trackerWarning(const QString &, const QString &)));
|
||||
connect(statusLabel, &QCheckBox::toggled, statusFilters, &StatusFiltersWidget::toggleFilter);
|
||||
connect(statusLabel, &QCheckBox::toggled, pref, &Preferences::setStatusFilterState);
|
||||
connect(trackerLabel, &QCheckBox::toggled, m_trackerFilters, &TrackerFiltersList::toggleFilter);
|
||||
connect(trackerLabel, &QCheckBox::toggled, pref, &Preferences::setTrackerFilterState);
|
||||
|
||||
using Func = void (TransferListFiltersWidget::*)(const QString&, const QString&);
|
||||
connect(this, static_cast<Func>(&TransferListFiltersWidget::trackerSuccess)
|
||||
, m_trackerFilters, &TrackerFiltersList::trackerSuccess);
|
||||
connect(this, static_cast<Func>(&TransferListFiltersWidget::trackerError)
|
||||
, m_trackerFilters, &TrackerFiltersList::trackerError);
|
||||
connect(this, static_cast<Func>(&TransferListFiltersWidget::trackerWarning)
|
||||
, m_trackerFilters, &TrackerFiltersList::trackerWarning);
|
||||
}
|
||||
|
||||
void TransferListFiltersWidget::setDownloadTrackerFavicon(bool value)
|
||||
@ -648,3 +679,15 @@ void TransferListFiltersWidget::toggleCategoryFilter(bool enabled)
|
||||
m_categoryFilterWidget->setVisible(enabled);
|
||||
m_transferList->applyCategoryFilter(enabled ? m_categoryFilterWidget->currentCategory() : QString());
|
||||
}
|
||||
|
||||
void TransferListFiltersWidget::onTagFilterStateChanged(bool enabled)
|
||||
{
|
||||
toggleTagFilter(enabled);
|
||||
Preferences::instance()->setTagFilterState(enabled);
|
||||
}
|
||||
|
||||
void TransferListFiltersWidget::toggleTagFilter(bool enabled)
|
||||
{
|
||||
m_tagFilterWidget->setVisible(enabled);
|
||||
m_transferList->applyTagFilter(enabled ? m_tagFilterWidget->currentTag() : QString());
|
||||
}
|
||||
|
@ -136,6 +136,7 @@ private:
|
||||
};
|
||||
|
||||
class CategoryFilterWidget;
|
||||
class TagFilterWidget;
|
||||
|
||||
class TransferListFiltersWidget: public QFrame
|
||||
{
|
||||
@ -160,13 +161,16 @@ signals:
|
||||
|
||||
private slots:
|
||||
void onCategoryFilterStateChanged(bool enabled);
|
||||
void onTagFilterStateChanged(bool enabled);
|
||||
|
||||
private:
|
||||
void toggleCategoryFilter(bool enabled);
|
||||
void toggleTagFilter(bool enabled);
|
||||
|
||||
TransferListWidget *m_transferList;
|
||||
TrackerFiltersList *m_trackerFilters;
|
||||
CategoryFilterWidget *m_categoryFilterWidget;
|
||||
TagFilterWidget *m_tagFilterWidget;
|
||||
};
|
||||
|
||||
#endif // TRANSFERLISTFILTERSWIDGET_H
|
||||
|
@ -59,6 +59,18 @@ void TransferListSortModel::disableCategoryFilter()
|
||||
invalidateFilter();
|
||||
}
|
||||
|
||||
void TransferListSortModel::setTagFilter(const QString &tag)
|
||||
{
|
||||
if (m_filter.setTag(tag))
|
||||
invalidateFilter();
|
||||
}
|
||||
|
||||
void TransferListSortModel::disableTagFilter()
|
||||
{
|
||||
if (m_filter.setTag(TorrentFilter::AnyTag))
|
||||
invalidateFilter();
|
||||
}
|
||||
|
||||
void TransferListSortModel::setTrackerFilter(const QStringList &hashes)
|
||||
{
|
||||
if (m_filter.setHashSet(hashes.toSet()))
|
||||
@ -75,6 +87,7 @@ bool TransferListSortModel::lessThan(const QModelIndex &left, const QModelIndex
|
||||
{
|
||||
switch (sortColumn()) {
|
||||
case TorrentModel::TR_CATEGORY:
|
||||
case TorrentModel::TR_TAGS:
|
||||
case TorrentModel::TR_NAME: {
|
||||
QVariant vL = left.data();
|
||||
QVariant vR = right.data();
|
||||
|
@ -46,6 +46,8 @@ public:
|
||||
void setStatusFilter(TorrentFilter::Type filter);
|
||||
void setCategoryFilter(const QString &category);
|
||||
void disableCategoryFilter();
|
||||
void setTagFilter(const QString &tag);
|
||||
void disableTagFilter();
|
||||
void setTrackerFilter(const QStringList &hashes);
|
||||
void disableTrackerFilter();
|
||||
|
||||
|
@ -607,6 +607,62 @@ void TransferListWidget::askNewCategoryForSelection()
|
||||
} while(invalid);
|
||||
}
|
||||
|
||||
void TransferListWidget::askAddTagsForSelection()
|
||||
{
|
||||
const QStringList tags = askTagsForSelection(tr("Add Tags"));
|
||||
foreach (const QString &tag, tags)
|
||||
addSelectionTag(tag);
|
||||
}
|
||||
|
||||
void TransferListWidget::askRemoveTagsForSelection()
|
||||
{
|
||||
const QStringList tags = askTagsForSelection(tr("Remove Tags"));
|
||||
foreach (const QString &tag, tags)
|
||||
removeSelectionTag(tag);
|
||||
}
|
||||
|
||||
void TransferListWidget::confirmRemoveAllTagsForSelection()
|
||||
{
|
||||
QMessageBox::StandardButton response = QMessageBox::question(
|
||||
this, tr("Remove All Tags"), tr("Remove all tags from selected torrents?"),
|
||||
QMessageBox::Yes | QMessageBox::No);
|
||||
if (response == QMessageBox::Yes)
|
||||
clearSelectionTags();
|
||||
}
|
||||
|
||||
QStringList TransferListWidget::askTagsForSelection(const QString &dialogTitle)
|
||||
{
|
||||
QStringList tags;
|
||||
bool invalid = true;
|
||||
while (invalid) {
|
||||
bool ok = false;
|
||||
invalid = false;
|
||||
const QString tagsInput = AutoExpandableDialog::getText(
|
||||
this, dialogTitle, tr("Comma-separated tags:"), QLineEdit::Normal, "", &ok).trimmed();
|
||||
if (!ok || tagsInput.isEmpty())
|
||||
return QStringList();
|
||||
tags = tagsInput.split(',', QString::SkipEmptyParts);
|
||||
for (QString &tag : tags) {
|
||||
tag = tag.trimmed();
|
||||
if (!BitTorrent::Session::isValidTag(tag)) {
|
||||
QMessageBox::warning(this, tr("Invalid tag")
|
||||
, tr("Tag name: '%1' is invalid").arg(tag));
|
||||
invalid = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
void TransferListWidget::applyToSelectedTorrents(const std::function<void (BitTorrent::TorrentHandle *const)> &fn)
|
||||
{
|
||||
foreach (const QModelIndex &index, selectionModel()->selectedRows()) {
|
||||
BitTorrent::TorrentHandle *const torrent = listModel->torrentHandle(mapToSource(index));
|
||||
Q_ASSERT(torrent);
|
||||
fn(torrent);
|
||||
}
|
||||
}
|
||||
|
||||
void TransferListWidget::renameSelectedTorrent()
|
||||
{
|
||||
const QModelIndexList selectedIndexes = selectionModel()->selectedRows();
|
||||
@ -632,6 +688,21 @@ void TransferListWidget::setSelectionCategory(QString category)
|
||||
listModel->setData(listModel->index(mapToSource(index).row(), TorrentModel::TR_CATEGORY), category, Qt::DisplayRole);
|
||||
}
|
||||
|
||||
void TransferListWidget::addSelectionTag(const QString &tag)
|
||||
{
|
||||
applyToSelectedTorrents([&tag](BitTorrent::TorrentHandle *const torrent) { torrent->addTag(tag); });
|
||||
}
|
||||
|
||||
void TransferListWidget::removeSelectionTag(const QString &tag)
|
||||
{
|
||||
applyToSelectedTorrents([&tag](BitTorrent::TorrentHandle *const torrent) { torrent->removeTag(tag); });
|
||||
}
|
||||
|
||||
void TransferListWidget::clearSelectionTags()
|
||||
{
|
||||
applyToSelectedTorrents([](BitTorrent::TorrentHandle *const torrent) { torrent->removeAllTags(); });
|
||||
}
|
||||
|
||||
void TransferListWidget::displayListMenu(const QPoint&)
|
||||
{
|
||||
QModelIndexList selectedIndexes = selectionModel()->selectedRows();
|
||||
@ -701,6 +772,7 @@ void TransferListWidget::displayListMenu(const QPoint&)
|
||||
bool firstAutoTMM = false;
|
||||
QString firstCategory;
|
||||
bool first = true;
|
||||
QSet<QString> tagsInSelection;
|
||||
|
||||
BitTorrent::TorrentHandle *torrent;
|
||||
qDebug("Displaying menu");
|
||||
@ -715,6 +787,8 @@ void TransferListWidget::displayListMenu(const QPoint&)
|
||||
if (firstCategory != torrent->category())
|
||||
allSameCategory = false;
|
||||
|
||||
tagsInSelection.unite(torrent->tags());
|
||||
|
||||
if (first)
|
||||
firstAutoTMM = torrent->isAutoTMMEnabled();
|
||||
if (firstAutoTMM != torrent->isAutoTMMEnabled())
|
||||
@ -798,6 +872,25 @@ void TransferListWidget::displayListMenu(const QPoint&)
|
||||
categoryActions << cat;
|
||||
}
|
||||
|
||||
// Tag Menu
|
||||
QStringList tags(BitTorrent::Session::instance()->tags().toList());
|
||||
std::sort(tags.begin(), tags.end(), Utils::String::naturalCompareCaseInsensitive);
|
||||
QList<QAction *> tagsActions;
|
||||
QMenu *tagsMenu = listMenu.addMenu(GuiIconProvider::instance()->getIcon("view-categories"), tr("Tags"));
|
||||
tagsActions << tagsMenu->addAction(GuiIconProvider::instance()->getIcon("list-add"), tr("Add...", "Add / assign multiple tags..."));
|
||||
tagsActions << tagsMenu->addAction(GuiIconProvider::instance()->getIcon("edit-clear"), tr("Remove...", "Remove multiple tags..."));
|
||||
tagsActions << tagsMenu->addAction(GuiIconProvider::instance()->getIcon("edit-clear"), tr("Remove All", "Remove all tags"));
|
||||
tagsMenu->addSeparator();
|
||||
foreach (QString tag, tags) {
|
||||
const bool setChecked = tagsInSelection.contains(tag);
|
||||
tag.replace('&', "&&"); // avoid '&' becomes accelerator key
|
||||
QAction *tagSelection = new QAction(GuiIconProvider::instance()->getIcon("inode-directory"), tag, tagsMenu);
|
||||
tagSelection->setCheckable(true);
|
||||
tagSelection->setChecked(setChecked);
|
||||
tagsMenu->addAction(tagSelection);
|
||||
tagsActions << tagSelection;
|
||||
}
|
||||
|
||||
if (allSameAutoTMM) {
|
||||
actionAutoTMM.setChecked(firstAutoTMM);
|
||||
listMenu.addAction(&actionAutoTMM);
|
||||
@ -853,7 +946,7 @@ void TransferListWidget::displayListMenu(const QPoint&)
|
||||
QAction *act = 0;
|
||||
act = listMenu.exec(QCursor::pos());
|
||||
if (act) {
|
||||
// Parse category actions only (others have slots assigned)
|
||||
// Parse category & tag actions only (others have slots assigned)
|
||||
int i = categoryActions.indexOf(act);
|
||||
if (i >= 0) {
|
||||
// Category action
|
||||
@ -869,6 +962,29 @@ void TransferListWidget::displayListMenu(const QPoint&)
|
||||
setSelectionCategory(category);
|
||||
}
|
||||
}
|
||||
i = tagsActions.indexOf(act);
|
||||
if (i >= 0) {
|
||||
if (i == 0) {
|
||||
askAddTagsForSelection();
|
||||
}
|
||||
else if (i == 1) {
|
||||
askRemoveTagsForSelection();
|
||||
}
|
||||
else if (i == 2) {
|
||||
if (Preferences::instance()->confirmRemoveAllTags())
|
||||
confirmRemoveAllTagsForSelection();
|
||||
else
|
||||
clearSelectionTags();
|
||||
}
|
||||
else {
|
||||
// Individual tag toggles.
|
||||
const QString &tag = tags.at(i - 3);
|
||||
if (act->isChecked())
|
||||
addSelectionTag(tag);
|
||||
else
|
||||
removeSelectionTag(tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -892,6 +1008,14 @@ void TransferListWidget::applyCategoryFilter(QString category)
|
||||
nameFilterModel->setCategoryFilter(category);
|
||||
}
|
||||
|
||||
void TransferListWidget::applyTagFilter(const QString &tag)
|
||||
{
|
||||
if (tag.isNull())
|
||||
nameFilterModel->disableTagFilter();
|
||||
else
|
||||
nameFilterModel->setTagFilter(tag);
|
||||
}
|
||||
|
||||
void TransferListWidget::applyTrackerFilterAll()
|
||||
{
|
||||
nameFilterModel->disableTrackerFilter();
|
||||
|
@ -31,6 +31,7 @@
|
||||
#ifndef TRANSFERLISTWIDGET_H
|
||||
#define TRANSFERLISTWIDGET_H
|
||||
|
||||
#include <functional>
|
||||
#include <QTreeView>
|
||||
|
||||
namespace BitTorrent
|
||||
@ -60,6 +61,9 @@ public:
|
||||
|
||||
public slots:
|
||||
void setSelectionCategory(QString category);
|
||||
void addSelectionTag(const QString &tag);
|
||||
void removeSelectionTag(const QString &tag);
|
||||
void clearSelectionTags();
|
||||
void setSelectedTorrentsLocation();
|
||||
void pauseAllTorrents();
|
||||
void resumeAllTorrents();
|
||||
@ -89,6 +93,7 @@ public slots:
|
||||
void applyNameFilter(const QString& name);
|
||||
void applyStatusFilter(int f);
|
||||
void applyCategoryFilter(QString category);
|
||||
void applyTagFilter(const QString &tag);
|
||||
void applyTrackerFilterAll();
|
||||
void applyTrackerFilter(const QStringList &hashes);
|
||||
void previewFile(QString filePath);
|
||||
@ -116,6 +121,11 @@ signals:
|
||||
|
||||
private:
|
||||
void wheelEvent(QWheelEvent *event) override;
|
||||
void askAddTagsForSelection();
|
||||
void askRemoveTagsForSelection();
|
||||
void confirmRemoveAllTagsForSelection();
|
||||
QStringList askTagsForSelection(const QString &dialogTitle);
|
||||
void applyToSelectedTorrents(const std::function<void (BitTorrent::TorrentHandle *const)> &fn);
|
||||
|
||||
TransferListDelegate *listDelegate;
|
||||
TorrentModel *listModel;
|
||||
|
Loading…
Reference in New Issue
Block a user