From c002f3084837615ec7b8bf1ea7a08c9f63418cde Mon Sep 17 00:00:00 2001 From: "Vladimir Golovnev (Glassez)" Date: Fri, 29 Jul 2016 12:45:50 +0300 Subject: [PATCH] Implement category filter widget Show categories in tree mode when subcategories are enabled. --- src/gui/categoryfiltermodel.cpp | 442 ++++++++++++++++++++++++++ src/gui/categoryfiltermodel.h | 79 +++++ src/gui/categoryfilterwidget.cpp | 269 ++++++++++++++++ src/gui/categoryfilterwidget.h | 60 ++++ src/gui/gui.pri | 8 +- src/gui/transferlistfilterswidget.cpp | 336 +++----------------- src/gui/transferlistfilterswidget.h | 43 +-- src/src.pro | 2 + 8 files changed, 912 insertions(+), 327 deletions(-) create mode 100644 src/gui/categoryfiltermodel.cpp create mode 100644 src/gui/categoryfiltermodel.h create mode 100644 src/gui/categoryfilterwidget.cpp create mode 100644 src/gui/categoryfilterwidget.h diff --git a/src/gui/categoryfiltermodel.cpp b/src/gui/categoryfiltermodel.cpp new file mode 100644 index 000000000..4ceb7693e --- /dev/null +++ b/src/gui/categoryfiltermodel.cpp @@ -0,0 +1,442 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2016 Vladimir Golovnev + * + * 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 "categoryfiltermodel.h" + +#include +#include + +#include "base/bittorrent/torrenthandle.h" +#include "base/bittorrent/session.h" +#include "guiiconprovider.h" + +class CategoryModelItem +{ +public: + CategoryModelItem() + : m_parent(nullptr) + , m_torrentsCount(0) + { + } + + CategoryModelItem(CategoryModelItem *parent, QString categoryName, int torrentsCount = 0) + : m_parent(nullptr) + , m_name(categoryName) + , m_torrentsCount(torrentsCount) + { + if (parent) + parent->addChild(m_name, this); + } + + ~CategoryModelItem() + { + clear(); + if (m_parent) { + m_parent->m_torrentsCount -= m_torrentsCount; + const QString uid = m_parent->m_children.key(this); + m_parent->m_children.remove(uid); + m_parent->m_childUids.removeOne(uid); + } + } + + QString name() const + { + return m_name; + } + + QString fullName() const + { + if (!m_parent || m_parent->name().isEmpty()) + return m_name; + + return QString("%1/%2").arg(m_parent->fullName()).arg(m_name); + } + + CategoryModelItem *parent() const + { + return m_parent; + } + + int torrentsCount() const + { + return m_torrentsCount; + } + + void increaseTorrentsCount() + { + ++m_torrentsCount; + if (m_parent) + m_parent->increaseTorrentsCount(); + } + + void decreaseTorrentsCount() + { + --m_torrentsCount; + if (m_parent) + m_parent->decreaseTorrentsCount(); + } + + int pos() const + { + if (!m_parent) return -1; + + return m_parent->m_childUids.indexOf(m_name); + } + + bool hasChild(const QString &name) const + { + return m_children.contains(name); + } + + int childCount() const + { + return m_children.count(); + } + + CategoryModelItem *child(const QString &uid) const + { + return m_children.value(uid); + } + + CategoryModelItem *childAt(int index) const + { + if ((index < 0) || (index >= m_childUids.count())) + return nullptr; + + return m_children[m_childUids[index]]; + } + + void addChild(const QString &uid, CategoryModelItem *item) + { + Q_ASSERT(item); + Q_ASSERT(!item->parent()); + Q_ASSERT(!m_children.contains(uid)); + + item->m_parent = this; + m_children[uid] = item; + auto pos = std::lower_bound(m_childUids.begin(), m_childUids.end(), uid); + m_childUids.insert(pos, uid); + m_torrentsCount += item->torrentsCount(); + } + + void clear() + { + // use copy of m_children for qDeleteAll + // to avoid collision when child removes + // itself from parent children + qDeleteAll(decltype(m_children)(m_children)); + } + +private: + CategoryModelItem *m_parent; + QString m_name; + int m_torrentsCount; + QHash m_children; + QStringList m_childUids; +}; + +namespace +{ + QString shortName(const QString &fullName) + { + int pos = fullName.lastIndexOf(QLatin1Char('/')); + if (pos >= 0) + return fullName.mid(pos + 1); + return fullName; + } +} + +CategoryFilterModel::CategoryFilterModel(QObject *parent) + : QAbstractItemModel(parent) + , m_rootItem(new CategoryModelItem) +{ + auto session = BitTorrent::Session::instance(); + + connect(session, SIGNAL(categoryAdded(QString)), SLOT(categoryAdded(QString))); + connect(session, SIGNAL(categoryRemoved(QString)), SLOT(categoryRemoved(QString))); + connect(session, SIGNAL(torrentCategoryChanged(BitTorrent::TorrentHandle *const, QString)) + , SLOT(torrentCategoryChanged(BitTorrent::TorrentHandle *const, QString))); + connect(session, SIGNAL(subcategoriesSupportChanged()), SLOT(subcategoriesSupportChanged())); + connect(session, SIGNAL(torrentAdded(BitTorrent::TorrentHandle *const)) + , SLOT(torrentAdded(BitTorrent::TorrentHandle *const))); + connect(session, SIGNAL(torrentAboutToBeRemoved(BitTorrent::TorrentHandle *const)) + , SLOT(torrentAboutToBeRemoved(BitTorrent::TorrentHandle *const))); + + populate(); +} + +CategoryFilterModel::~CategoryFilterModel() +{ + delete m_rootItem; +} + +int CategoryFilterModel::columnCount(const QModelIndex &) const +{ + return 1; +} + +QVariant CategoryFilterModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) return QVariant(); + + auto item = static_cast(index.internalPointer()); + + if ((index.column() == 0) && (role == Qt::DecorationRole)) { + return GuiIconProvider::instance()->getIcon("inode-directory"); + } + + if ((index.column() == 0) && (role == Qt::DisplayRole)) { + return QString(QStringLiteral("%1 (%2)")) + .arg(item->name()).arg(item->torrentsCount()); + } + + if ((index.column() == 0) && (role == Qt::UserRole)) { + return item->torrentsCount(); + } + + return QVariant(); +} + +Qt::ItemFlags CategoryFilterModel::flags(const QModelIndex &index) const +{ + if (!index.isValid()) return 0; + + return Qt::ItemIsEnabled | Qt::ItemIsSelectable; +} + +QVariant CategoryFilterModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if ((orientation == Qt::Horizontal) && (role == Qt::DisplayRole)) + if (section == 0) + return tr("Categories"); + + return QVariant(); +} + +QModelIndex CategoryFilterModel::index(int row, int column, const QModelIndex &parent) const +{ + if (column > 0) + return QModelIndex(); + + if (parent.isValid() && (parent.column() != 0)) + return QModelIndex(); + + auto parentItem = parent.isValid() ? static_cast(parent.internalPointer()) + : m_rootItem; + if (row < parentItem->childCount()) + return createIndex(row, column, parentItem->childAt(row)); + + return QModelIndex(); +} + +QModelIndex CategoryFilterModel::parent(const QModelIndex &index) const +{ + if (!index.isValid()) + return QModelIndex(); + + auto item = static_cast(index.internalPointer()); + if (!item) return QModelIndex(); + + return this->index(item->parent()); +} + +int CategoryFilterModel::rowCount(const QModelIndex &parent) const +{ + if (parent.column() > 0) + return 0; + + if (!parent.isValid()) + return m_rootItem->childCount(); + + auto item = static_cast(parent.internalPointer()); + if (!item) return 0; + + return item->childCount(); +} + +QModelIndex CategoryFilterModel::index(const QString &categoryName) const +{ + return index(findItem(categoryName)); +} + +QString CategoryFilterModel::categoryName(const QModelIndex &index) const +{ + if (!index.isValid()) return QString(); + return static_cast(index.internalPointer())->fullName(); +} + +QModelIndex CategoryFilterModel::index(CategoryModelItem *item) const +{ + if (!item || !item->parent()) return QModelIndex(); + + return index(item->pos(), 0, index(item->parent())); +} + +void CategoryFilterModel::categoryAdded(const QString &categoryName) +{ + CategoryModelItem *parent = m_rootItem; + + if (m_isSubcategoriesEnabled) { + QStringList expanded = BitTorrent::Session::expandCategory(categoryName); + if (expanded.count() > 1) + parent = findItem(expanded[expanded.count() - 2]); + } + + auto item = new CategoryModelItem( + parent, m_isSubcategoriesEnabled ? shortName(categoryName) : categoryName); + + QModelIndex i = index(item); + beginInsertRows(i.parent(), i.row(), i.row()); + endInsertRows(); +} + +void CategoryFilterModel::categoryRemoved(const QString &categoryName) +{ + auto item = findItem(categoryName); + if (item) { + QModelIndex i = index(item); + beginRemoveRows(i.parent(), i.row(), i.row()); + delete item; + endRemoveRows(); + } +} + +void CategoryFilterModel::torrentAdded(BitTorrent::TorrentHandle *const torrent) +{ + CategoryModelItem *item = findItem(torrent->category()); + Q_ASSERT(item); + + item->increaseTorrentsCount(); + m_rootItem->childAt(0)->increaseTorrentsCount(); +} + +void CategoryFilterModel::torrentAboutToBeRemoved(BitTorrent::TorrentHandle *const torrent) +{ + CategoryModelItem *item = findItem(torrent->category()); + Q_ASSERT(item); + + item->decreaseTorrentsCount(); + m_rootItem->childAt(0)->decreaseTorrentsCount(); +} + +void CategoryFilterModel::torrentCategoryChanged(BitTorrent::TorrentHandle *const torrent, const QString &oldCategory) +{ + QModelIndex i; + + auto item = findItem(oldCategory); + Q_ASSERT(item); + + item->decreaseTorrentsCount(); + i = index(item); + while (i.isValid()) { + emit dataChanged(i, i); + i = parent(i); + } + + item = findItem(torrent->category()); + Q_ASSERT(item); + + item->increaseTorrentsCount(); + i = index(item); + while (i.isValid()) { + emit dataChanged(i, i); + i = parent(i); + } +} + +void CategoryFilterModel::subcategoriesSupportChanged() +{ + beginResetModel(); + populate(); + endResetModel(); +} + +void CategoryFilterModel::populate() +{ + m_rootItem->clear(); + + auto session = BitTorrent::Session::instance(); + auto torrents = session->torrents(); + m_isSubcategoriesEnabled = session->isSubcategoriesEnabled(); + + const QString UID_ALL; + const QString UID_UNCATEGORIZED(QChar(1)); + + // All torrents + m_rootItem->addChild(UID_ALL, new CategoryModelItem(nullptr, tr("All"), torrents.count())); + + // Uncategorized torrents + using Torrent = BitTorrent::TorrentHandle; + m_rootItem->addChild( + UID_UNCATEGORIZED + , new CategoryModelItem( + nullptr, tr("Uncategorized") + , std::count_if(torrents.begin(), torrents.end() + , [](Torrent *torrent) { return torrent->category().isEmpty(); }))); + + using Torrent = BitTorrent::TorrentHandle; + foreach (const QString &category, session->categories()) { + if (m_isSubcategoriesEnabled) { + CategoryModelItem *parent = m_rootItem; + foreach (const QString &subcat, session->expandCategory(category)) { + const QString subcatName = shortName(subcat); + if (!parent->hasChild(subcatName)) { + new CategoryModelItem( + parent, subcatName + , std::count_if(torrents.begin(), torrents.end() + , [subcat](Torrent *torrent) { return torrent->category() == subcat; })); + } + parent = parent->child(subcatName); + } + } + else { + new CategoryModelItem( + m_rootItem, category + , std::count_if(torrents.begin(), torrents.end() + , [category](Torrent *torrent) { return torrent->belongsToCategory(category); })); + } + } +} + +CategoryModelItem *CategoryFilterModel::findItem(const QString &fullName) const +{ + if (fullName.isEmpty()) + return m_rootItem->childAt(1); // "Uncategorized" item + + if (!m_isSubcategoriesEnabled) + return m_rootItem->child(fullName); + + CategoryModelItem *item = m_rootItem; + foreach (const QString &subcat, BitTorrent::Session::expandCategory(fullName)) { + const QString subcatName = shortName(subcat); + if (!item->hasChild(subcatName)) return nullptr; + item = item->child(subcatName); + } + + return item; +} diff --git a/src/gui/categoryfiltermodel.h b/src/gui/categoryfiltermodel.h new file mode 100644 index 000000000..df92eab57 --- /dev/null +++ b/src/gui/categoryfiltermodel.h @@ -0,0 +1,79 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2016 Vladimir Golovnev + * + * 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 CATEGORYFILTERMODEL_H +#define CATEGORYFILTERMODEL_H + +#include +#include +#include + +namespace BitTorrent +{ + class TorrentHandle; +} + +class CategoryModelItem; + +class CategoryFilterModel: public QAbstractItemModel +{ + Q_OBJECT + +public: + explicit CategoryFilterModel(QObject *parent = nullptr); + ~CategoryFilterModel(); + + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + 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; + QModelIndex parent(const QModelIndex &index) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + + QModelIndex index(const QString &categoryName) const; + QString categoryName(const QModelIndex &index) const; + +private slots: + void categoryAdded(const QString &categoryName); + void categoryRemoved(const QString &categoryName); + void torrentAdded(BitTorrent::TorrentHandle *const torrent); + void torrentAboutToBeRemoved(BitTorrent::TorrentHandle *const torrent); + void torrentCategoryChanged(BitTorrent::TorrentHandle *const torrent, const QString &oldCategory); + void subcategoriesSupportChanged(); + +private: + void populate(); + QModelIndex index(CategoryModelItem *item) const; + CategoryModelItem *findItem(const QString &fullName) const; + + bool m_isSubcategoriesEnabled; + CategoryModelItem *m_rootItem; +}; + +#endif // CATEGORYFILTERMODEL_H diff --git a/src/gui/categoryfilterwidget.cpp b/src/gui/categoryfilterwidget.cpp new file mode 100644 index 000000000..536086fe6 --- /dev/null +++ b/src/gui/categoryfilterwidget.cpp @@ -0,0 +1,269 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2016 Vladimir Golovnev + * + * 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 "categoryfilterwidget.h" + +#include +#include +#include +#include +#include + +#include "base/bittorrent/session.h" +#include "base/utils/misc.h" +#include "autoexpandabledialog.h" +#include "categoryfiltermodel.h" +#include "guiiconprovider.h" + +namespace +{ + QString getCategoryFilter(const CategoryFilterModel *const model, const QModelIndex &index) + { + QString categoryFilter; // Defaults to All + if (index.isValid()) { + if (!index.parent().isValid() && (index.row() == 1)) + categoryFilter = ""; // Uncategorized + else if (index.parent().isValid() || (index.row() > 1)) + categoryFilter = model->categoryName(index); + } + + return categoryFilter; + } + + bool isSpecialItem(const QModelIndex &index) + { + // the first two items at first level are special items: + // 'All' and 'Uncategorized' + return (!index.parent().isValid() && (index.row() <= 1)); + } +} + +CategoryFilterWidget::CategoryFilterWidget(QWidget *parent) + : QTreeView(parent) +{ + setModel(new CategoryFilterModel(this)); + 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); + + connect(this, SIGNAL(collapsed(QModelIndex)), SLOT(callUpdateGeometry())); + connect(this, SIGNAL(expanded(QModelIndex)), SLOT(callUpdateGeometry())); + connect(this, SIGNAL(customContextMenuRequested(QPoint)), SLOT(showMenu(QPoint))); + connect(selectionModel(), SIGNAL(currentRowChanged(QModelIndex,QModelIndex)) + , SLOT(onCurrentRowChanged(QModelIndex,QModelIndex))); + connect(model(), SIGNAL(modelReset()), SLOT(callUpdateGeometry())); +} + +QString CategoryFilterWidget::currentCategory() const +{ + QModelIndex current; + auto selectedRows = selectionModel()->selectedRows(); + if (!selectedRows.isEmpty()) + current = selectedRows.first(); + + return getCategoryFilter(static_cast(model()), current); +} + +void CategoryFilterWidget::onCurrentRowChanged(const QModelIndex ¤t, const QModelIndex &previous) +{ + Q_UNUSED(previous); + + emit categoryChanged(getCategoryFilter(static_cast(model()), current)); +} + +void CategoryFilterWidget::showMenu(QPoint) +{ + QMenu menu(this); + + QAction *addAct = menu.addAction( + GuiIconProvider::instance()->getIcon("list-add") + , tr("Add category...")); + connect(addAct, SIGNAL(triggered()), SLOT(addCategory())); + + auto selectedRows = selectionModel()->selectedRows(); + if (!selectedRows.empty() && !isSpecialItem(selectedRows.first())) { + if (BitTorrent::Session::instance()->isSubcategoriesEnabled()) { + QAction *addSubAct = menu.addAction( + GuiIconProvider::instance()->getIcon("list-add") + , tr("Add subcategory...")); + connect(addSubAct, SIGNAL(triggered()), SLOT(addSubcategory())); + } + + QAction *removeAct = menu.addAction( + GuiIconProvider::instance()->getIcon("list-remove") + , tr("Remove category")); + connect(removeAct, SIGNAL(triggered()), SLOT(removeCategory())); + } + + QAction *removeUnusedAct = menu.addAction( + GuiIconProvider::instance()->getIcon("list-remove") + , tr("Remove unused categories")); + connect(removeUnusedAct, SIGNAL(triggered()), SLOT(removeUnusedCategories())); + + menu.addSeparator(); + + QAction *startAct = menu.addAction( + GuiIconProvider::instance()->getIcon("media-playback-start") + , tr("Resume torrents")); + connect(startAct, SIGNAL(triggered()), SIGNAL(actionResumeTorrentsTriggered())); + + QAction *pauseAct = menu.addAction( + GuiIconProvider::instance()->getIcon("media-playback-pause") + , tr("Pause torrents")); + connect(pauseAct, SIGNAL(triggered()), SIGNAL(actionPauseTorrentsTriggered())); + + QAction *deleteTorrentsAct = menu.addAction( + GuiIconProvider::instance()->getIcon("edit-delete") + , tr("Delete torrents")); + connect(deleteTorrentsAct, SIGNAL(triggered()), SIGNAL(actionDeleteTorrentsTriggered())); + + menu.exec(QCursor::pos()); +} + +void CategoryFilterWidget::callUpdateGeometry() +{ + updateGeometry(); +} + +QSize CategoryFilterWidget::sizeHint() const +{ +#ifdef QBT_USES_QT5 + return viewportSizeHint(); +#else + int lastRow = model()->rowCount() - 1; + QModelIndex last = model()->index(lastRow, 0); + while ((lastRow >= 0) && isExpanded(last)) { + lastRow = model()->rowCount(last) - 1; + last = model()->index(lastRow, 0, last); + } + const QRect deepestRect = visualRect(last); + + if (!deepestRect.isValid()) + return viewport()->sizeHint(); + + return QSize(header()->length(), deepestRect.bottom() + 1); +#endif +} + +QSize CategoryFilterWidget::minimumSizeHint() const +{ + QSize size = sizeHint(); + size.setWidth(6); + return size; +} + +void CategoryFilterWidget::rowsInserted(const QModelIndex &parent, int start, int end) +{ + QTreeView::rowsInserted(parent, start, end); + + // Expand all parents if the parent(s) of the node are not expanded. + QModelIndex p = parent; + while (p.isValid()) { + if (!isExpanded(p)) + expand(p); + p = model()->parent(p); + } + + updateGeometry(); +} + +QString CategoryFilterWidget::askCategoryName() +{ + bool ok; + QString category = ""; + bool invalid; + do { + invalid = false; + category = AutoExpandableDialog::getText( + this, tr("New Category"), tr("Category:"), QLineEdit::Normal, category, &ok); + if (ok && !category.isEmpty()) { + if (!BitTorrent::Session::isValidCategoryName(category)) { + QMessageBox::warning( + this, tr("Invalid category name") + , tr("Category name must not contain '\\'.\n" + "Category name must not start/end with '/'.\n" + "Category name must not contain '//' sequence.")); + invalid = true; + } + } + } while (invalid); + + return ok ? category : QString(); +} + +void CategoryFilterWidget::addCategory() +{ + const QString category = askCategoryName(); + if (category.isEmpty()) return; + + if (BitTorrent::Session::instance()->categories().contains(category)) + QMessageBox::warning(this, tr("Category exists"), tr("Category name already exists.")); + else + BitTorrent::Session::instance()->addCategory(category); +} + +void CategoryFilterWidget::addSubcategory() +{ + const QString subcat = askCategoryName(); + if (subcat.isEmpty()) return; + + const QString category = QString(QStringLiteral("%1/%2")).arg(currentCategory()).arg(subcat); + + if (BitTorrent::Session::instance()->categories().contains(category)) + QMessageBox::warning(this, tr("Category exists") + , tr("Subcategory name already exists in selected category.")); + else + BitTorrent::Session::instance()->addCategory(category); +} + +void CategoryFilterWidget::removeCategory() +{ + auto selectedRows = selectionModel()->selectedRows(); + if (!selectedRows.empty() && !isSpecialItem(selectedRows.first())) { + BitTorrent::Session::instance()->removeCategory( + static_cast(model())->categoryName(selectedRows.first())); + updateGeometry(); + } +} + +void CategoryFilterWidget::removeUnusedCategories() +{ + auto session = BitTorrent::Session::instance(); + foreach (const QString &category, session->categories()) + if (model()->data(static_cast(model())->index(category), Qt::UserRole) == 0) + session->removeCategory(category); + updateGeometry(); +} diff --git a/src/gui/categoryfilterwidget.h b/src/gui/categoryfilterwidget.h new file mode 100644 index 000000000..3cfe64e2f --- /dev/null +++ b/src/gui/categoryfilterwidget.h @@ -0,0 +1,60 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2016 Vladimir Golovnev + * + * 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 + +class CategoryFilterWidget: public QTreeView +{ + Q_OBJECT + +public: + explicit CategoryFilterWidget(QWidget *parent = nullptr); + + QString currentCategory() const; + +signals: + void categoryChanged(const QString &categoryName); + void actionResumeTorrentsTriggered(); + void actionPauseTorrentsTriggered(); + void actionDeleteTorrentsTriggered(); + +private slots: + void onCurrentRowChanged(const QModelIndex ¤t, const QModelIndex &previous); + void showMenu(QPoint); + void callUpdateGeometry(); + void addCategory(); + void addSubcategory(); + void removeCategory(); + void removeUnusedCategories(); + +private: + QSize sizeHint() const override; + QSize minimumSizeHint() const override; + void rowsInserted(const QModelIndex &parent, int start, int end) override; + QString askCategoryName(); +}; diff --git a/src/gui/gui.pri b/src/gui/gui.pri index c55e74fe3..62ad185eb 100644 --- a/src/gui/gui.pri +++ b/src/gui/gui.pri @@ -49,7 +49,9 @@ HEADERS += \ $$PWD/search/searchlistdelegate.h \ $$PWD/search/searchsortmodel.h \ $$PWD/cookiesmodel.h \ - $$PWD/cookiesdialog.h + $$PWD/cookiesdialog.h \ + $$PWD/categoryfiltermodel.h \ + $$PWD/categoryfilterwidget.h SOURCES += \ $$PWD/mainwindow.cpp \ @@ -89,7 +91,9 @@ SOURCES += \ $$PWD/search/searchlistdelegate.cpp \ $$PWD/search/searchsortmodel.cpp \ $$PWD/cookiesmodel.cpp \ - $$PWD/cookiesdialog.cpp + $$PWD/cookiesdialog.cpp \ + $$PWD/categoryfiltermodel.cpp \ + $$PWD/categoryfilterwidget.cpp win32|macx { HEADERS += $$PWD/programupdater.h diff --git a/src/gui/transferlistfilterswidget.cpp b/src/gui/transferlistfilterswidget.cpp index 319fc36ec..b3c86b002 100644 --- a/src/gui/transferlistfilterswidget.cpp +++ b/src/gui/transferlistfilterswidget.cpp @@ -30,31 +30,32 @@ #include "transferlistfilterswidget.h" +#include #include -#include #include -#include +#include #include #include -#include #include +#include -#include "transferlistdelegate.h" -#include "transferlistwidget.h" -#include "base/preferences.h" -#include "torrentmodel.h" -#include "guiiconprovider.h" -#include "base/utils/fs.h" -#include "base/utils/string.h" -#include "autoexpandabledialog.h" -#include "base/torrentfilter.h" -#include "base/bittorrent/trackerentry.h" #include "base/bittorrent/session.h" #include "base/bittorrent/torrenthandle.h" +#include "base/bittorrent/trackerentry.h" +#include "base/logger.h" #include "base/net/downloadmanager.h" #include "base/net/downloadhandler.h" +#include "base/preferences.h" +#include "base/torrentfilter.h" +#include "base/utils/fs.h" #include "base/utils/misc.h" -#include "base/logger.h" +#include "base/utils/string.h" +#include "autoexpandabledialog.h" +#include "categoryfilterwidget.h" +#include "guiiconprovider.h" +#include "torrentmodel.h" +#include "transferlistdelegate.h" +#include "transferlistwidget.h" FiltersBase::FiltersBase(QWidget *parent, TransferListWidget *transferList) : QListWidget(parent) @@ -177,266 +178,6 @@ void StatusFiltersWidget::handleNewTorrent(BitTorrent::TorrentHandle *const) {} void StatusFiltersWidget::torrentAboutToBeDeleted(BitTorrent::TorrentHandle *const) {} -CategoryFiltersList::CategoryFiltersList(QWidget *parent, TransferListWidget *transferList) - : FiltersBase(parent, transferList) -{ - connect(BitTorrent::Session::instance(), SIGNAL(torrentCategoryChanged(BitTorrent::TorrentHandle *const, QString)), SLOT(torrentCategoryChanged(BitTorrent::TorrentHandle *const, QString))); - connect(BitTorrent::Session::instance(), SIGNAL(categoryAdded(QString)), SLOT(addItem(QString))); - connect(BitTorrent::Session::instance(), SIGNAL(categoryRemoved(QString)), SLOT(categoryRemoved(QString))); - connect(BitTorrent::Session::instance(), SIGNAL(subcategoriesSupportChanged()), SLOT(subcategoriesSupportChanged())); - - refresh(); - toggleFilter(Preferences::instance()->getCategoryFilterState()); -} - -void CategoryFiltersList::refresh() -{ - clear(); - m_categories.clear(); - m_totalTorrents = 0; - m_totalCategorized = 0; - - QListWidgetItem *allCategories = new QListWidgetItem(this); - allCategories->setData(Qt::DisplayRole, QVariant(tr("All (0)", "this is for the category filter"))); - allCategories->setData(Qt::DecorationRole, GuiIconProvider::instance()->getIcon("inode-directory")); - QListWidgetItem *noCategory = new QListWidgetItem(this); - noCategory->setData(Qt::DisplayRole, QVariant(tr("Uncategorized (0)"))); - noCategory->setData(Qt::DecorationRole, GuiIconProvider::instance()->getIcon("inode-directory")); - - foreach (const QString &category, BitTorrent::Session::instance()->categories()) - addItem(category, false); - - foreach (BitTorrent::TorrentHandle *const torrent, BitTorrent::Session::instance()->torrents()) - handleNewTorrent(torrent); - - setCurrentRow(0, QItemSelectionModel::SelectCurrent); -} - -void CategoryFiltersList::addItem(const QString &category, bool hasTorrent) -{ - if (category.isEmpty()) return; - - int torrentsInCategory = 0; - QListWidgetItem *categoryItem = 0; - - bool exists = m_categories.contains(category); - if (exists) { - torrentsInCategory = m_categories.value(category); - categoryItem = item(rowFromCategory(category)); - } - else { - categoryItem = new QListWidgetItem(); - categoryItem->setData(Qt::DecorationRole, GuiIconProvider::instance()->getIcon("inode-directory")); - } - - if (hasTorrent) - ++torrentsInCategory; - - m_categories.insert(category, torrentsInCategory); - categoryItem->setText(QString("%1 (%2)").arg(category).arg(torrentsInCategory)); - if (exists) return; - - Q_ASSERT(count() >= 2); - int insPos = count(); - for (int i = 2; i < count(); ++i) { - if (Utils::String::naturalCompareCaseSensitive(category, item(i)->text())) { - insPos = i; - break; - } - } - QListWidget::insertItem(insPos, categoryItem); - updateGeometry(); -} - -void CategoryFiltersList::removeItem(const QString &category) -{ - if (category.isEmpty()) return; - - int torrentsInCategory = m_categories.value(category) - 1; - int row = rowFromCategory(category); - if (row < 2) return; - - QListWidgetItem *categoryItem = item(row); - categoryItem->setText(QString("%1 (%2)").arg(category).arg(torrentsInCategory)); - m_categories.insert(category, torrentsInCategory); -} - -void CategoryFiltersList::removeSelectedCategory() -{ - QList items = selectedItems(); - if (items.size() == 0) return; - - const int categoryRow = row(items.first()); - if (categoryRow < 2) return; - - BitTorrent::Session::instance()->removeCategory(categoryFromRow(categoryRow)); - updateGeometry(); -} - -void CategoryFiltersList::removeUnusedCategories() -{ - foreach (const QString &category, m_categories.keys()) - if (m_categories[category] == 0) - BitTorrent::Session::instance()->removeCategory(category); - updateGeometry(); -} - -void CategoryFiltersList::torrentCategoryChanged(BitTorrent::TorrentHandle *const torrent, const QString &oldCategory) -{ - qDebug() << "Torrent category changed from" << oldCategory << "to" << torrent->category(); - - if (torrent->category().isEmpty() && !oldCategory.isEmpty()) - --m_totalCategorized; - else if (!torrent->category().isEmpty() && oldCategory.isEmpty()) - ++m_totalCategorized; - - item(1)->setText(tr("Uncategorized (%1)").arg(m_totalTorrents - m_totalCategorized)); - - if (BitTorrent::Session::instance()->isSubcategoriesEnabled()) { - foreach (const QString &subcategory, BitTorrent::Session::expandCategory(oldCategory)) - removeItem(subcategory); - foreach (const QString &subcategory, BitTorrent::Session::expandCategory(torrent->category())) - addItem(subcategory, true); - } - else { - removeItem(oldCategory); - addItem(torrent->category(), true); - } -} - -void CategoryFiltersList::categoryRemoved(const QString &category) -{ - m_categories.remove(category); - delete takeItem(rowFromCategory(category)); -} - -void CategoryFiltersList::subcategoriesSupportChanged() -{ - refresh(); -} - -void CategoryFiltersList::showMenu(QPoint) -{ - QMenu menu(this); - QAction *addAct = menu.addAction(GuiIconProvider::instance()->getIcon("list-add"), tr("Add category...")); - QAction *removeAct = 0; - QAction *removeUnusedAct = 0; - if (!selectedItems().empty() && row(selectedItems().first()) > 1) - removeAct = menu.addAction(GuiIconProvider::instance()->getIcon("list-remove"), tr("Remove category")); - removeUnusedAct = menu.addAction(GuiIconProvider::instance()->getIcon("list-remove"), tr("Remove unused categories")); - menu.addSeparator(); - QAction *startAct = menu.addAction(GuiIconProvider::instance()->getIcon("media-playback-start"), tr("Resume torrents")); - QAction *pauseAct = menu.addAction(GuiIconProvider::instance()->getIcon("media-playback-pause"), tr("Pause torrents")); - QAction *deleteTorrentsAct = menu.addAction(GuiIconProvider::instance()->getIcon("edit-delete"), tr("Delete torrents")); - QAction *act = 0; - act = menu.exec(QCursor::pos()); - if (!act) - return; - - if (act == removeAct) { - removeSelectedCategory(); - } - else if (act == removeUnusedAct) { - removeUnusedCategories(); - } - else if (act == deleteTorrentsAct) { - transferList->deleteVisibleTorrents(); - } - else if (act == startAct) { - transferList->startVisibleTorrents(); - } - else if (act == pauseAct) { - transferList->pauseVisibleTorrents(); - } - else if (act == addAct) { - bool ok; - QString category = ""; - bool invalid; - do { - invalid = false; - category = AutoExpandableDialog::getText(this, tr("New Category"), tr("Category:"), QLineEdit::Normal, category, &ok); - if (ok && !category.isEmpty()) { - if (!BitTorrent::Session::isValidCategoryName(category)) { - QMessageBox::warning(this, tr("Invalid category name"), - tr("Category name must not contain '\\'.\n" - "Category name must not start/end with '/'.\n" - "Category name must not contain '//' sequence.")); - invalid = true; - } - else { - BitTorrent::Session::instance()->addCategory(category); - } - } - } while (invalid); - } -} - -void CategoryFiltersList::applyFilter(int row) -{ - if (row >= 0) - transferList->applyCategoryFilter(categoryFromRow(row)); -} - -void CategoryFiltersList::handleNewTorrent(BitTorrent::TorrentHandle *const torrent) -{ - Q_ASSERT(torrent); - - ++m_totalTorrents; - if (!torrent->category().isEmpty()) - ++m_totalCategorized; - - item(0)->setText(tr("All (%1)", "this is for the category filter").arg(m_totalTorrents)); - item(1)->setText(tr("Uncategorized (%1)").arg(m_totalTorrents - m_totalCategorized)); - - if (BitTorrent::Session::instance()->isSubcategoriesEnabled()) { - foreach (const QString &subcategory, BitTorrent::Session::expandCategory(torrent->category())) - addItem(subcategory, true); - } - else { - addItem(torrent->category(), true); - } -} - -void CategoryFiltersList::torrentAboutToBeDeleted(BitTorrent::TorrentHandle *const torrent) -{ - Q_ASSERT(torrent); - - --m_totalTorrents; - if (!torrent->category().isEmpty()) - --m_totalCategorized; - - item(0)->setText(tr("All (%1)", "this is for the category filter").arg(m_totalTorrents)); - item(1)->setText(tr("Uncategorized (%1)").arg(m_totalTorrents - m_totalCategorized)); - - if (BitTorrent::Session::instance()->isSubcategoriesEnabled()) { - foreach (const QString &subcategory, BitTorrent::Session::expandCategory(torrent->category())) - removeItem(subcategory); - } - else { - removeItem(torrent->category()); - } -} - -QString CategoryFiltersList::categoryFromRow(int row) const -{ - if (row == 0) return QString(); // All - if (row == 1) return QLatin1String(""); // Uncategorized - - const QString &category = item(row)->text(); - QStringList parts = category.split(" "); - Q_ASSERT(parts.size() >= 2); - parts.removeLast(); // Remove trailing number - return parts.join(" "); -} - -int CategoryFiltersList::rowFromCategory(const QString &category) const -{ - Q_ASSERT(!category.isEmpty()); - for (int i = 2; isetText(tr("%1 (%2)", "openbittorrent.com (10)").arg(host).arg(tmp.size())); + trackerItem->setText(QString("%1 (%2)").arg(host).arg(tmp.size())); } else { row = 1; @@ -787,7 +528,8 @@ QStringList TrackerFiltersList::getHashes(int row) TransferListFiltersWidget::TransferListFiltersWidget(QWidget *parent, TransferListWidget *transferList) : QFrame(parent) - , trackerFilters(0) + , m_transferList(transferList) + , m_trackerFilters(0) { Preferences* const pref = Preferences::instance(); @@ -826,50 +568,58 @@ 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))); frameLayout->addWidget(categoryLabel); - CategoryFiltersList *categoryFilters = new CategoryFiltersList(this, transferList); - frameLayout->addWidget(categoryFilters); + 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))); + onCategoryFilterStateChanged(pref->getCategoryFilterState()); + frameLayout->addWidget(m_categoryFilterWidget); QCheckBox *trackerLabel = new QCheckBox(tr("Trackers"), this); trackerLabel->setChecked(pref->getTrackerFilterState()); trackerLabel->setFont(font); frameLayout->addWidget(trackerLabel); - trackerFilters = new TrackerFiltersList(this, transferList); - frameLayout->addWidget(trackerFilters); + 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(categoryLabel, SIGNAL(toggled(bool)), categoryFilters, SLOT(toggleFilter(bool))); - connect(categoryLabel, SIGNAL(toggled(bool)), pref, SLOT(setCategoryFilterState(const bool))); - connect(trackerLabel, SIGNAL(toggled(bool)), trackerFilters, SLOT(toggleFilter(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 &)), trackerFilters, SLOT(trackerSuccess(const QString &, const QString &))); - connect(this, SIGNAL(trackerError(const QString &, const QString &)), trackerFilters, SLOT(trackerError(const QString &, const QString &))); - connect(this, SIGNAL(trackerWarning(const QString &, const QString &)), trackerFilters, SLOT(trackerWarning(const QString &, const QString &))); + 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 &))); } void TransferListFiltersWidget::setDownloadTrackerFavicon(bool value) { - trackerFilters->setDownloadTrackerFavicon(value); + m_trackerFilters->setDownloadTrackerFavicon(value); } void TransferListFiltersWidget::addTrackers(BitTorrent::TorrentHandle *const torrent, const QList &trackers) { foreach (const BitTorrent::TrackerEntry &tracker, trackers) - trackerFilters->addItem(tracker.url(), torrent->hash()); + m_trackerFilters->addItem(tracker.url(), torrent->hash()); } void TransferListFiltersWidget::removeTrackers(BitTorrent::TorrentHandle *const torrent, const QList &trackers) { foreach (const BitTorrent::TrackerEntry &tracker, trackers) - trackerFilters->removeItem(tracker.url(), torrent->hash()); + m_trackerFilters->removeItem(tracker.url(), torrent->hash()); } void TransferListFiltersWidget::changeTrackerless(BitTorrent::TorrentHandle *const torrent, bool trackerless) { - trackerFilters->changeTrackerless(trackerless, torrent->hash()); + m_trackerFilters->changeTrackerless(trackerless, torrent->hash()); } void TransferListFiltersWidget::trackerSuccess(BitTorrent::TorrentHandle *const torrent, const QString &tracker) @@ -886,3 +636,9 @@ void TransferListFiltersWidget::trackerError(BitTorrent::TorrentHandle *const to { emit trackerError(torrent->hash(), tracker); } + +void TransferListFiltersWidget::onCategoryFilterStateChanged(bool enabled) +{ + m_categoryFilterWidget->setVisible(enabled); + m_transferList->applyCategoryFilter(enabled ? m_categoryFilterWidget->currentCategory() : QString()); +} diff --git a/src/gui/transferlistfilterswidget.h b/src/gui/transferlistfilterswidget.h index 3ea54260a..8f1638673 100644 --- a/src/gui/transferlistfilterswidget.h +++ b/src/gui/transferlistfilterswidget.h @@ -90,40 +90,6 @@ private: virtual void torrentAboutToBeDeleted(BitTorrent::TorrentHandle *const); }; -class CategoryFiltersList: public FiltersBase -{ - Q_OBJECT - -public: - CategoryFiltersList(QWidget *parent, TransferListWidget *transferList); - -private slots: - // Redefine addItem() to make sure the list stays sorted - void addItem(const QString &category, bool hasTorrent = false); - void removeItem(const QString &category); - void removeSelectedCategory(); - void removeUnusedCategories(); - void torrentCategoryChanged(BitTorrent::TorrentHandle *const torrent, const QString &oldCategory); - void categoryRemoved(const QString &category); - void subcategoriesSupportChanged(); - -private: - // These 4 methods are virtual slots in the base class. - // No need to redeclare them here as slots. - virtual void showMenu(QPoint); - virtual void applyFilter(int row); - virtual void handleNewTorrent(BitTorrent::TorrentHandle *const torrent); - virtual void torrentAboutToBeDeleted(BitTorrent::TorrentHandle *const torrent); - QString categoryFromRow(int row) const; - int rowFromCategory(const QString &category) const; - void refresh(); - -private: - QHash m_categories; - int m_totalTorrents; - int m_totalCategorized; -}; - class TrackerFiltersList: public FiltersBase { Q_OBJECT @@ -169,6 +135,8 @@ private: bool m_downloadTrackerFavicon; }; +class CategoryFilterWidget; + class TransferListFiltersWidget: public QFrame { Q_OBJECT @@ -190,8 +158,13 @@ signals: void trackerError(const QString &hash, const QString &tracker); void trackerWarning(const QString &hash, const QString &tracker); +private slots: + void onCategoryFilterStateChanged(bool enabled); + private: - TrackerFiltersList *trackerFilters; + TransferListWidget *m_transferList; + TrackerFiltersList *m_trackerFilters; + CategoryFilterWidget *m_categoryFilterWidget; }; #endif // TRANSFERLISTFILTERSWIDGET_H diff --git a/src/src.pro b/src/src.pro index fe3bd2a5a..8c516a599 100644 --- a/src/src.pro +++ b/src/src.pro @@ -7,6 +7,8 @@ CONFIG += c++11 DEFINES += BOOST_NO_CXX11_RVALUE_REFERENCES greaterThan(QT_MAJOR_VERSION, 4): greaterThan(QT_MINOR_VERSION, 1): DEFINES += QBT_USES_QT5 +lessThan(QT_MAJOR_VERSION, 5): DEFINES += QStringLiteral=QLatin1String + # Windows specific configuration win32: include(../winconf.pri)