diff --git a/src/base/orderedset.h b/src/base/orderedset.h index f3266690e..4a7730a6b 100644 --- a/src/base/orderedset.h +++ b/src/base/orderedset.h @@ -1,5 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2023 Vladimir Golovnev * 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 + ThisType &unite(const Set &other) { BaseType::insert(other.cbegin(), other.cend()); return *this; } + + template + ThisType united(const Set &other) const + { + ThisType result = *this; + result.unite(other); + return result; + } }; diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 443021a48..f0ebcdc91 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -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 diff --git a/src/gui/addnewtorrentdialog.cpp b/src/gui/addnewtorrentdialog.cpp index faa492989..97cadb3b2 100644 --- a/src/gui/addnewtorrentdialog.cpp +++ b/src/gui/addnewtorrentdialog.cpp @@ -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); diff --git a/src/gui/addnewtorrentdialog.ui b/src/gui/addnewtorrentdialog.ui index f28a71032..b2d05cd8c 100644 --- a/src/gui/addnewtorrentdialog.ui +++ b/src/gui/addnewtorrentdialog.ui @@ -201,6 +201,46 @@ + + + + + + Tags: + + + + + + + + 0 + 0 + + + + true + + + Click [...] button to add/remove tags. + + + false + + + + + + + Add/remove tags + + + ... + + + + + diff --git a/src/gui/flowlayout.cpp b/src/gui/flowlayout.cpp new file mode 100644 index 000000000..6cd3bfeab --- /dev/null +++ b/src/gui/flowlayout.cpp @@ -0,0 +1,195 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2023 Vladimir Golovnev + * 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 + +#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(parent); + return pw->style()->pixelMetric(pm, nullptr, pw); + } + + return static_cast(parent)->spacing(); +} diff --git a/src/gui/flowlayout.h b/src/gui/flowlayout.h new file mode 100644 index 000000000..888e8262f --- /dev/null +++ b/src/gui/flowlayout.h @@ -0,0 +1,63 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2023 Vladimir Golovnev + * 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 +#include +#include + +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 m_itemList; + int m_hSpace; + int m_vSpace; +}; diff --git a/src/gui/gui.pri b/src/gui/gui.pri index f41193456..c25d5800e 100644 --- a/src/gui/gui.pri +++ b/src/gui/gui.pri @@ -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 diff --git a/src/gui/torrenttagsdialog.cpp b/src/gui/torrenttagsdialog.cpp new file mode 100644 index 000000000..8d5feb63f --- /dev/null +++ b/src/gui/torrenttagsdialog.cpp @@ -0,0 +1,121 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2023 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 "torrenttagsdialog.h" + +#include +#include +#include +#include + +#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(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; + } + } +} diff --git a/src/gui/torrenttagsdialog.h b/src/gui/torrenttagsdialog.h new file mode 100644 index 000000000..b8cf9edde --- /dev/null +++ b/src/gui/torrenttagsdialog.h @@ -0,0 +1,57 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2023 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. + */ + +#pragma once + +#include + +#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 m_storeDialogSize; +}; diff --git a/src/gui/torrenttagsdialog.ui b/src/gui/torrenttagsdialog.ui new file mode 100644 index 000000000..86ce8c0a4 --- /dev/null +++ b/src/gui/torrenttagsdialog.ui @@ -0,0 +1,81 @@ + + + TorrentTagsDialog + + + + 0 + 0 + 484 + 313 + + + + Torrent Tags + + + + + + Qt::ScrollBarAlwaysOff + + + true + + + + + 0 + 0 + 464 + 263 + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + TorrentTagsDialog + accept() + + + 241 + 291 + + + 241 + 156 + + + + + buttonBox + rejected() + TorrentTagsDialog + reject() + + + 241 + 291 + + + 241 + 156 + + + + + diff --git a/test/testorderedset.cpp b/test/testorderedset.cpp index a99687b7a..f0315ca23 100644 --- a/test/testorderedset.cpp +++ b/test/testorderedset.cpp @@ -1,5 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2023 Vladimir Golovnev * 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 #include #include "base/global.h" @@ -114,16 +116,34 @@ private slots: { const OrderedSet newData1 {u"z"_qs}; const OrderedSet newData2 {u"y"_qs}; + const QSet newData3 {u"c"_qs, u"d"_qs, u"e"_qs}; OrderedSet 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 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 newData1 {u"z"_qs}; + const OrderedSet newData2 {u"y"_qs}; + const QSet newData3 {u"c"_qs, u"d"_qs, u"e"_qs}; + + OrderedSet 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().united(newData1).united(newData2).united(newData3).join(u","_qs), u"c,d,e,y,z"_qs); } };