Implement torrent tags editing dialog

PR #18797.
This commit is contained in:
Vladimir Golovnev 2023-04-03 10:36:28 +03:00 committed by GitHub
parent b55d4b1733
commit d40be79c69
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 615 additions and 4 deletions

View file

@ -1,5 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2021 Mike Tzou (Chocobo1)
*
* This program is free software; you can redistribute it and/or
@ -100,9 +101,18 @@ public:
return (BaseType::erase(value) > 0);
}
ThisType &unite(const ThisType &other)
template <typename Set>
ThisType &unite(const Set &other)
{
BaseType::insert(other.cbegin(), other.cend());
return *this;
}
template <typename Set>
ThisType united(const Set &other) const
{
ThisType result = *this;
result.unite(other);
return result;
}
};

View file

@ -36,6 +36,7 @@ qt_wrap_ui(UI_HEADERS
torrentcategorydialog.ui
torrentcreatordialog.ui
torrentoptionsdialog.ui
torrenttagsdialog.ui
trackerentriesdialog.ui
uithemedialog.ui
watchedfolderoptionsdialog.ui
@ -58,6 +59,7 @@ add_library(qbt_gui STATIC
desktopintegration.h
downloadfromurldialog.h
executionlogwidget.h
flowlayout.h
fspathedit.h
fspathedit_p.h
guiapplicationcomponent.h
@ -114,6 +116,7 @@ add_library(qbt_gui STATIC
torrentcontentwidget.h
torrentcreatordialog.h
torrentoptionsdialog.h
torrenttagsdialog.h
trackerentriesdialog.h
transferlistdelegate.h
transferlistfilterswidget.h
@ -146,6 +149,7 @@ add_library(qbt_gui STATIC
desktopintegration.cpp
downloadfromurldialog.cpp
executionlogwidget.cpp
flowlayout.cpp
fspathedit.cpp
fspathedit_p.cpp
guiapplicationcomponent.cpp
@ -201,6 +205,7 @@ add_library(qbt_gui STATIC
torrentcontentwidget.cpp
torrentcreatordialog.cpp
torrentoptionsdialog.cpp
torrenttagsdialog.cpp
trackerentriesdialog.cpp
transferlistdelegate.cpp
transferlistfilterswidget.cpp

View file

@ -60,6 +60,7 @@
#include "base/utils/misc.h"
#include "lineedit.h"
#include "raisedmessagebox.h"
#include "torrenttagsdialog.h"
#include "ui_addnewtorrentdialog.h"
#include "uithememanager.h"
@ -368,10 +369,23 @@ AddNewTorrentDialog::AddNewTorrentDialog(const BitTorrent::AddTorrentParams &inP
for (const QString &category : asConst(categories))
{
if (category != defaultCategory && category != m_torrentParams.category)
if ((category != defaultCategory) && (category != m_torrentParams.category))
m_ui->categoryComboBox->addItem(category);
}
m_ui->tagsLineEdit->setText(m_torrentParams.tags.join(u", "_qs));
connect(m_ui->tagsEditButton, &QAbstractButton::clicked, this, [this]
{
auto *dlg = new TorrentTagsDialog(m_torrentParams.tags, this);
dlg->setAttribute(Qt::WA_DeleteOnClose);
connect(dlg, &TorrentTagsDialog::accepted, this, [this, dlg]
{
m_torrentParams.tags = dlg->tags();
m_ui->tagsLineEdit->setText(m_torrentParams.tags.join(u", "_qs));
});
dlg->open();
});
// Torrent content filtering
m_filterLine->setPlaceholderText(tr("Filter files..."));
m_filterLine->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);

View file

@ -201,6 +201,46 @@
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="tagsLayout" stretch="0,0,0">
<item>
<widget class="QLabel" name="tagsLabel">
<property name="text">
<string>Tags:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="tagsLineEdit">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
<property name="placeholderText">
<string>Click [...] button to add/remove tags.</string>
</property>
<property name="clearButtonEnabled">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="tagsEditButton">
<property name="toolTip">
<string>Add/remove tags</string>
</property>
<property name="text">
<string>...</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">

195
src/gui/flowlayout.cpp Normal file
View file

@ -0,0 +1,195 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2016 The Qt Company Ltd.
*
* 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 "flowlayout.h"
#include <QWidget>
#include "base/global.h"
FlowLayout::FlowLayout(QWidget *parent, const int margin, const int hSpacing, const int vSpacing)
: QLayout(parent)
, m_hSpace {hSpacing}
, m_vSpace {vSpacing}
{
setContentsMargins(margin, margin, margin, margin);
}
FlowLayout::FlowLayout(const int margin, const int hSpacing, const int vSpacing)
: m_hSpace {hSpacing}
, m_vSpace {vSpacing}
{
setContentsMargins(margin, margin, margin, margin);
}
FlowLayout::~FlowLayout()
{
QLayoutItem *item;
while ((item = takeAt(0)))
delete item;
}
void FlowLayout::addItem(QLayoutItem *item)
{
m_itemList.append(item);
}
int FlowLayout::horizontalSpacing() const
{
if (m_hSpace >= 0)
return m_hSpace;
return smartSpacing(QStyle::PM_LayoutHorizontalSpacing);
}
int FlowLayout::verticalSpacing() const
{
if (m_vSpace >= 0)
return m_vSpace;
return smartSpacing(QStyle::PM_LayoutVerticalSpacing);
}
int FlowLayout::count() const
{
return m_itemList.size();
}
QLayoutItem *FlowLayout::itemAt(const int index) const
{
return m_itemList.value(index);
}
QLayoutItem *FlowLayout::takeAt(const int index)
{
if ((index >= 0) && (index < m_itemList.size()))
return m_itemList.takeAt(index);
return nullptr;
}
Qt::Orientations FlowLayout::expandingDirections() const
{
return {};
}
bool FlowLayout::hasHeightForWidth() const
{
return true;
}
int FlowLayout::heightForWidth(const int width) const
{
const int height = doLayout(QRect(0, 0, width, 0), true);
return height;
}
void FlowLayout::setGeometry(const QRect &rect)
{
QLayout::setGeometry(rect);
doLayout(rect, false);
}
QSize FlowLayout::sizeHint() const
{
return minimumSize();
}
QSize FlowLayout::minimumSize() const
{
QSize size;
for (const QLayoutItem *item : asConst(m_itemList))
size = size.expandedTo(item->minimumSize());
const QMargins margins = contentsMargins();
size += QSize(margins.left() + margins.right(), margins.top() + margins.bottom());
return size;
}
int FlowLayout::doLayout(const QRect &rect, const bool testOnly) const
{
int left, top, right, bottom;
getContentsMargins(&left, &top, &right, &bottom);
const QRect effectiveRect = rect.adjusted(+left, +top, -right, -bottom);
int x = effectiveRect.x();
int y = effectiveRect.y();
int lineHeight = 0;
for (QLayoutItem *item : asConst(m_itemList))
{
const QWidget *wid = item->widget();
int spaceX = horizontalSpacing();
if (spaceX == -1)
{
spaceX = wid->style()->layoutSpacing(
QSizePolicy::PushButton, QSizePolicy::PushButton, Qt::Horizontal);
}
int spaceY = verticalSpacing();
if (spaceY == -1)
{
spaceY = wid->style()->layoutSpacing(
QSizePolicy::PushButton, QSizePolicy::PushButton, Qt::Vertical);
}
int nextX = x + item->sizeHint().width() + spaceX;
if (((nextX - spaceX) > effectiveRect.right()) && (lineHeight > 0))
{
x = effectiveRect.x();
y = y + lineHeight + spaceY;
nextX = x + item->sizeHint().width() + spaceX;
lineHeight = 0;
}
if (!testOnly)
item->setGeometry(QRect(QPoint(x, y), item->sizeHint()));
x = nextX;
lineHeight = std::max(lineHeight, item->sizeHint().height());
}
return y + lineHeight - rect.y() + bottom;
}
int FlowLayout::smartSpacing(const QStyle::PixelMetric pm) const
{
QObject *parent = this->parent();
if (!parent)
return -1;
if (parent->isWidgetType())
{
auto *pw = static_cast<QWidget *>(parent);
return pw->style()->pixelMetric(pm, nullptr, pw);
}
return static_cast<QLayout *>(parent)->spacing();
}

63
src/gui/flowlayout.h Normal file
View file

@ -0,0 +1,63 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2016 The Qt Company Ltd.
*
* 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.
*/
#pragma once
#include <QLayout>
#include <QRect>
#include <QStyle>
class FlowLayout final : public QLayout
{
public:
explicit FlowLayout(QWidget *parent, int margin = -1, int hSpacing = -1, int vSpacing = -1);
explicit FlowLayout(int margin = -1, int hSpacing = -1, int vSpacing = -1);
~FlowLayout() override;
void addItem(QLayoutItem *item) override;
int horizontalSpacing() const;
int verticalSpacing() const;
Qt::Orientations expandingDirections() const override;
bool hasHeightForWidth() const override;
int heightForWidth(int) const override;
int count() const override;
QLayoutItem *itemAt(int index) const override;
QSize minimumSize() const override;
void setGeometry(const QRect &rect) override;
QSize sizeHint() const override;
QLayoutItem *takeAt(int index) override;
private:
int doLayout(const QRect &rect, bool testOnly) const;
int smartSpacing(QStyle::PixelMetric pm) const;
QList<QLayoutItem *> m_itemList;
int m_hSpace;
int m_vSpace;
};

View file

@ -16,6 +16,7 @@ HEADERS += \
$$PWD/desktopintegration.h \
$$PWD/downloadfromurldialog.h \
$$PWD/executionlogwidget.h \
$$PWD/flowlayout.h \
$$PWD/fspathedit.h \
$$PWD/fspathedit_p.h \
$$PWD/guiapplicationcomponent.h \
@ -72,6 +73,7 @@ HEADERS += \
$$PWD/torrentcontentwidget.h \
$$PWD/torrentcreatordialog.h \
$$PWD/torrentoptionsdialog.h \
$$PWD/torrenttagsdialog.h \
$$PWD/trackerentriesdialog.h \
$$PWD/transferlistdelegate.h \
$$PWD/transferlistfilterswidget.h \
@ -104,6 +106,7 @@ SOURCES += \
$$PWD/desktopintegration.cpp \
$$PWD/downloadfromurldialog.cpp \
$$PWD/executionlogwidget.cpp \
$$PWD/flowlayout.cpp \
$$PWD/fspathedit.cpp \
$$PWD/fspathedit_p.cpp \
$$PWD/guiapplicationcomponent.cpp \
@ -159,6 +162,7 @@ SOURCES += \
$$PWD/torrentcontentwidget.cpp \
$$PWD/torrentcreatordialog.cpp \
$$PWD/torrentoptionsdialog.cpp \
$$PWD/torrenttagsdialog.cpp \
$$PWD/trackerentriesdialog.cpp \
$$PWD/transferlistdelegate.cpp \
$$PWD/transferlistfilterswidget.cpp \
@ -202,6 +206,7 @@ FORMS += \
$$PWD/torrentcategorydialog.ui \
$$PWD/torrentcreatordialog.ui \
$$PWD/torrentoptionsdialog.ui \
$$PWD/torrenttagsdialog.ui \
$$PWD/trackerentriesdialog.ui \
$$PWD/uithemedialog.ui \
$$PWD/watchedfolderoptionsdialog.ui

View file

@ -0,0 +1,121 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru>
*
* 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 "torrenttagsdialog.h"
#include <QCheckBox>
#include <QMessageBox>
#include <QPushButton>
#include <QSet>
#include "base/bittorrent/session.h"
#include "base/global.h"
#include "autoexpandabledialog.h"
#include "flowlayout.h"
#include "ui_torrenttagsdialog.h"
#define SETTINGS_KEY(name) u"GUI/TorrentTagsDialog/" name
TorrentTagsDialog::TorrentTagsDialog(const TagSet &initialTags, QWidget *parent)
: QDialog(parent)
, m_ui {new Ui::TorrentTagsDialog}
, m_storeDialogSize {SETTINGS_KEY(u"Size"_qs)}
{
m_ui->setupUi(this);
auto *tagsLayout = new FlowLayout(m_ui->scrollArea);
for (const QString &tag : asConst(initialTags.united(BitTorrent::Session::instance()->tags())))
{
auto *tagWidget = new QCheckBox(tag);
if (initialTags.contains(tag))
tagWidget->setChecked(true);
tagsLayout->addWidget(tagWidget);
}
auto *addTagButton = new QPushButton(u"+"_qs);
connect(addTagButton, &QPushButton::clicked, this, &TorrentTagsDialog::addNewTag);
tagsLayout->addWidget(addTagButton);
if (const QSize dialogSize = m_storeDialogSize; dialogSize.isValid())
resize(dialogSize);
}
TorrentTagsDialog::~TorrentTagsDialog()
{
m_storeDialogSize = size();
delete m_ui;
}
TagSet TorrentTagsDialog::tags() const
{
TagSet tags;
auto *layout = m_ui->scrollArea->layout();
for (int i = 0; i < (layout->count() - 1); ++i)
{
const auto *tagWidget = static_cast<QCheckBox *>(layout->itemAt(i)->widget());
if (tagWidget->isChecked())
tags.insert(tagWidget->text());
}
return tags;
}
void TorrentTagsDialog::addNewTag()
{
bool done = false;
QString tag;
while (!done)
{
bool ok = false;
tag = AutoExpandableDialog::getText(this
, tr("New Tag"), tr("Tag:"), QLineEdit::Normal, tag, &ok).trimmed();
if (!ok || tag.isEmpty())
break;
if (!BitTorrent::Session::isValidTag(tag))
{
QMessageBox::warning(this, tr("Invalid tag name"), tr("Tag name '%1' is invalid.").arg(tag));
}
else if (BitTorrent::Session::instance()->tags().contains(tag))
{
QMessageBox::warning(this, tr("Tag exists"), tr("Tag name already exists."));
}
else
{
auto *layout = m_ui->scrollArea->layout();
auto *btn = layout->takeAt(layout->count() - 1);
auto *tagWidget = new QCheckBox(tag);
tagWidget->setChecked(true);
layout->addWidget(tagWidget);
layout->addItem(btn);
done = true;
}
}
}

View file

@ -0,0 +1,57 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru>
*
* 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.
*/
#pragma once
#include <QDialog>
#include "base/settingvalue.h"
#include "base/tagset.h"
namespace Ui
{
class TorrentTagsDialog;
}
class TorrentTagsDialog final : public QDialog
{
Q_OBJECT
Q_DISABLE_COPY_MOVE(TorrentTagsDialog)
public:
explicit TorrentTagsDialog(const TagSet &initialTags, QWidget *parent = nullptr);
~TorrentTagsDialog() override;
TagSet tags() const;
private:
void addNewTag();
Ui::TorrentTagsDialog *m_ui;
SettingValue<QSize> m_storeDialogSize;
};

View file

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TorrentTagsDialog</class>
<widget class="QDialog" name="TorrentTagsDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>484</width>
<height>313</height>
</rect>
</property>
<property name="windowTitle">
<string>Torrent Tags</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QScrollArea" name="scrollArea">
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>464</width>
<height>263</height>
</rect>
</property>
</widget>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>TorrentTagsDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>241</x>
<y>291</y>
</hint>
<hint type="destinationlabel">
<x>241</x>
<y>156</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>TorrentTagsDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>241</x>
<y>291</y>
</hint>
<hint type="destinationlabel">
<x>241</x>
<y>156</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View file

@ -1,5 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2022 Mike Tzou (Chocobo1)
*
* This program is free software; you can redistribute it and/or
@ -26,6 +27,7 @@
* exception statement from your version.
*/
#include <QSet>
#include <QTest>
#include "base/global.h"
@ -114,16 +116,34 @@ private slots:
{
const OrderedSet<QString> newData1 {u"z"_qs};
const OrderedSet<QString> newData2 {u"y"_qs};
const QSet<QString> newData3 {u"c"_qs, u"d"_qs, u"e"_qs};
OrderedSet<QString> set {u"a"_qs, u"b"_qs, u"c"_qs};
set.unite(newData1);
QCOMPARE(set.join(u","_qs), u"a,b,c,z"_qs);
set.unite(newData2);
QCOMPARE(set.join(u","_qs), u"a,b,c,y,z"_qs);
set.unite(newData3);
QCOMPARE(set.join(u","_qs), u"a,b,c,d,e,y,z"_qs);
OrderedSet<QString> emptySet;
emptySet.unite(newData1).unite(newData2);
QCOMPARE(emptySet.join(u","_qs), u"y,z"_qs);
emptySet.unite(newData1).unite(newData2).unite(newData3);
QCOMPARE(emptySet.join(u","_qs), u"c,d,e,y,z"_qs);
}
void testUnited() const
{
const OrderedSet<QString> newData1 {u"z"_qs};
const OrderedSet<QString> newData2 {u"y"_qs};
const QSet<QString> newData3 {u"c"_qs, u"d"_qs, u"e"_qs};
OrderedSet<QString> set {u"a"_qs, u"b"_qs, u"c"_qs};
QCOMPARE(set.united(newData1).join(u","_qs), u"a,b,c,z"_qs);
QCOMPARE(set.united(newData2).join(u","_qs), u"a,b,c,y"_qs);
QCOMPARE(set.united(newData3).join(u","_qs), u"a,b,c,d,e"_qs);
QCOMPARE(OrderedSet<QString>().united(newData1).united(newData2).united(newData3).join(u","_qs), u"c,d,e,y,z"_qs);
}
};