Merge pull request #6911 from tgregerson/tags

Add a Tags system to the GUI to expand torrent organization / filtering options. Closes #13.
This commit is contained in:
Vladimir Golovnev 2017-06-26 15:57:27 +03:00 committed by GitHub
commit 3181469d87
27 changed files with 1315 additions and 35 deletions

View file

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

View file

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

View file

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

View file

@ -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 &params)
: 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;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -56,6 +56,9 @@ shutdownconfirmdlg.h
speedlimitdlg.h
statsdialog.h
statusbar.h
tagfiltermodel.h
tagfilterproxymodel.h
tagfilterwidget.h
torrentcontentfiltermodel.h
torrentcontentmodel.h
torrentcontentmodelfile.h
@ -96,6 +99,9 @@ shutdownconfirmdlg.cpp
speedlimitdlg.cpp
statsdialog.cpp
statusbar.cpp
tagfiltermodel.cpp
tagfilterproxymodel.cpp
tagfilterwidget.cpp
torrentcontentfiltermodel.cpp
torrentcontentmodel.cpp
torrentcontentmodelfile.cpp

View file

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

View file

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

View file

@ -51,6 +51,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 \
@ -101,6 +104,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
View 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
View 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

View 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());
}

View 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
View 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 &current, 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
View 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 &current, 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

View file

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

View file

@ -62,6 +62,7 @@ public:
TR_ETA,
TR_RATIO,
TR_CATEGORY,
TR_TAGS,
TR_ADD_DATE,
TR_SEED_DATE,
TR_TRACKER,

View file

@ -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"
@ -77,11 +78,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
@ -113,7 +116,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);
@ -389,8 +393,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)
@ -577,21 +584,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);
@ -600,13 +626,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)
@ -657,3 +688,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());
}

View file

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

View file

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

View file

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

View file

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

View file

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