Add a Tags (multi-label) feature to the GUI. Closes #13.

See https://github.com/qbittorrent/qBittorrent/issues/13 for details.
This commit is contained in:
Tony Gregerson 2017-06-04 19:22:17 -05:00
parent ff80208534
commit 467e516801
27 changed files with 1315 additions and 35 deletions

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