" +
tr("Metadata received") + u" - " + tr("Torrent will stop after metadata is received.") +
- u" " + tr("Torrents that have metadata initially aren't affected.") + u"
" +
+ u" " + tr("Torrents that have metadata initially will be added as stopped.") + u"
" +
tr("Files checked") + u" - " + tr("Torrent will stop after files are initially checked.") +
u" " + tr("This will also download metadata if it wasn't there initially.") + u"
");
- m_ui->stopConditionComboBox->setItemData(0, QVariant::fromValue(BitTorrent::Torrent::StopCondition::None));
- m_ui->stopConditionComboBox->setItemData(1, QVariant::fromValue(BitTorrent::Torrent::StopCondition::MetadataReceived));
- m_ui->stopConditionComboBox->setItemData(2, QVariant::fromValue(BitTorrent::Torrent::StopCondition::FilesChecked));
- m_ui->stopConditionComboBox->setCurrentIndex(m_ui->stopConditionComboBox->findData(
- QVariant::fromValue(m_torrentParams.stopCondition.value_or(session->torrentStopCondition()))));
+ m_ui->stopConditionComboBox->addItem(tr("None"), QVariant::fromValue(BitTorrent::Torrent::StopCondition::None));
+ if (!hasMetadata())
+ m_ui->stopConditionComboBox->addItem(tr("Metadata received"), QVariant::fromValue(BitTorrent::Torrent::StopCondition::MetadataReceived));
+ m_ui->stopConditionComboBox->addItem(tr("Files checked"), QVariant::fromValue(BitTorrent::Torrent::StopCondition::FilesChecked));
+ const auto stopCondition = m_torrentParams.stopCondition.value_or(session->torrentStopCondition());
+ if (hasMetadata() && (stopCondition == BitTorrent::Torrent::StopCondition::MetadataReceived))
+ {
+ m_ui->startTorrentCheckBox->setChecked(false);
+ m_ui->stopConditionComboBox->setCurrentIndex(m_ui->stopConditionComboBox->findData(QVariant::fromValue(BitTorrent::Torrent::StopCondition::None)));
+ }
+ else
+ {
+ m_ui->startTorrentCheckBox->setChecked(!m_torrentParams.addPaused.value_or(session->isAddTorrentPaused()));
+ m_ui->stopConditionComboBox->setCurrentIndex(m_ui->stopConditionComboBox->findData(QVariant::fromValue(stopCondition)));
+ }
m_ui->stopConditionLabel->setEnabled(m_ui->startTorrentCheckBox->isChecked());
m_ui->stopConditionComboBox->setEnabled(m_ui->startTorrentCheckBox->isChecked());
connect(m_ui->startTorrentCheckBox, &QCheckBox::toggled, this, [this](const bool checked)
@@ -351,7 +401,7 @@ AddNewTorrentDialog::AddNewTorrentDialog(const BitTorrent::AddTorrentParams &inP
m_ui->checkBoxRememberLastSavePath->setChecked(m_storeRememberLastSavePath);
m_ui->contentLayoutComboBox->setCurrentIndex(
- static_cast(m_torrentParams.contentLayout.value_or(session->torrentContentLayout())));
+ static_cast(m_torrentParams.contentLayout.value_or(session->torrentContentLayout())));
connect(m_ui->contentLayoutComboBox, &QComboBox::currentIndexChanged, this, &AddNewTorrentDialog::contentLayoutChanged);
m_ui->sequentialCheckBox->setChecked(m_torrentParams.sequential);
@@ -471,6 +521,18 @@ void AddNewTorrentDialog::setSavePathHistoryLength(const int value)
, QStringList(settings()->loadValue(KEY_SAVEPATHHISTORY).mid(0, clampedValue)));
}
+#ifndef Q_OS_MACOS
+void AddNewTorrentDialog::setAttached(const bool value)
+{
+ settings()->storeValue(KEY_ATTACHED, value);
+}
+
+bool AddNewTorrentDialog::isAttached()
+{
+ return settings()->loadValue(KEY_ATTACHED, false);
+}
+#endif
+
void AddNewTorrentDialog::loadState()
{
if (const QSize dialogSize = m_storeDialogSize; dialogSize.isValid())
@@ -489,12 +551,27 @@ void AddNewTorrentDialog::saveState()
void AddNewTorrentDialog::show(const QString &source, const BitTorrent::AddTorrentParams &inParams, QWidget *parent)
{
- auto *dlg = new AddNewTorrentDialog(inParams, parent);
+ const auto *pref = Preferences::instance();
+#ifdef Q_OS_MACOS
+ const bool attached = false;
+#else
+ const bool attached = isAttached();
+#endif
+
+ // By not setting a parent to the "AddNewTorrentDialog", all those dialogs
+ // will be displayed on top and will not overlap with the main window.
+ auto *dlg = new AddNewTorrentDialog(inParams, (attached ? parent : nullptr));
+ // Qt::Window is required to avoid showing only two dialog on top (see #12852).
+ // Also improves the general convenience of adding multiple torrents.
+ if (!attached)
+ {
+ dlg->setWindowFlags(Qt::Window);
+ adjustDialogGeometry(dlg, parent);
+ }
dlg->setAttribute(Qt::WA_DeleteOnClose);
if (Net::DownloadManager::hasSupportedScheme(source))
{
- const auto *pref = Preferences::instance();
// Launch downloader
Net::DownloadManager::instance()->download(
Net::DownloadRequest(source).limit(pref->getTorrentFileSizeLimit())
@@ -742,7 +819,7 @@ void AddNewTorrentDialog::contentLayoutChanged()
const auto contentLayout = static_cast(m_ui->contentLayoutComboBox->currentIndex());
m_contentAdaptor->applyContentLayout(contentLayout);
- m_ui->contentTreeView->setContentHandler(m_contentAdaptor); // to cause reloading
+ m_ui->contentTreeView->setContentHandler(m_contentAdaptor.get()); // to cause reloading
}
void AddNewTorrentDialog::saveTorrentFile()
@@ -932,6 +1009,17 @@ void AddNewTorrentDialog::updateMetadata(const BitTorrent::TorrentInfo &metadata
setupTreeview();
setMetadataProgressIndicator(false, tr("Metadata retrieval complete"));
+ if (const auto stopCondition = m_ui->stopConditionComboBox->currentData().value()
+ ; stopCondition == BitTorrent::Torrent::StopCondition::MetadataReceived)
+ {
+ m_ui->startTorrentCheckBox->setChecked(false);
+
+ const auto index = m_ui->stopConditionComboBox->currentIndex();
+ m_ui->stopConditionComboBox->setCurrentIndex(m_ui->stopConditionComboBox->findData(
+ QVariant::fromValue(BitTorrent::Torrent::StopCondition::None)));
+ m_ui->stopConditionComboBox->removeItem(index);
+ }
+
m_ui->buttonSave->setVisible(true);
if (m_torrentInfo.infoHash().v2().isValid())
{
@@ -964,7 +1052,8 @@ void AddNewTorrentDialog::setupTreeview()
if (m_torrentParams.filePaths.isEmpty())
m_torrentParams.filePaths = m_torrentInfo.filePaths();
- m_contentAdaptor = new TorrentContentAdaptor(m_torrentInfo, m_torrentParams.filePaths, m_torrentParams.filePriorities);
+ m_contentAdaptor = std::make_unique(m_torrentInfo, m_torrentParams.filePaths
+ , m_torrentParams.filePriorities, [this] { updateDiskSpaceLabel(); });
const auto contentLayout = static_cast(m_ui->contentLayoutComboBox->currentIndex());
m_contentAdaptor->applyContentLayout(contentLayout);
@@ -985,7 +1074,7 @@ void AddNewTorrentDialog::setupTreeview()
m_contentAdaptor->prioritizeFiles(priorities);
}
- m_ui->contentTreeView->setContentHandler(m_contentAdaptor);
+ m_ui->contentTreeView->setContentHandler(m_contentAdaptor.get());
m_filterLine->blockSignals(false);
diff --git a/src/gui/addnewtorrentdialog.h b/src/gui/addnewtorrentdialog.h
index 2a830e14b..557df9b9e 100644
--- a/src/gui/addnewtorrentdialog.h
+++ b/src/gui/addnewtorrentdialog.h
@@ -39,6 +39,9 @@
#include "base/path.h"
#include "base/settingvalue.h"
+class LineEdit;
+class TorrentFileGuard;
+
namespace BitTorrent
{
class InfoHash;
@@ -54,9 +57,6 @@ namespace Ui
class AddNewTorrentDialog;
}
-class LineEdit;
-class TorrentFileGuard;
-
class AddNewTorrentDialog final : public QDialog
{
Q_OBJECT
@@ -74,6 +74,10 @@ public:
static void setTopLevel(bool value);
static int savePathHistoryLength();
static void setSavePathHistoryLength(int value);
+#ifndef Q_OS_MACOS
+ static bool isAttached();
+ static void setAttached(bool value);
+#endif
static void show(const QString &source, const BitTorrent::AddTorrentParams &inParams, QWidget *parent);
static void show(const QString &source, QWidget *parent);
@@ -112,7 +116,7 @@ private:
void showEvent(QShowEvent *event) override;
Ui::AddNewTorrentDialog *m_ui = nullptr;
- TorrentContentAdaptor *m_contentAdaptor = nullptr;
+ std::unique_ptr m_contentAdaptor;
BitTorrent::MagnetUri m_magnetURI;
BitTorrent::TorrentInfo m_torrentInfo;
int m_savePathIndex = -1;
diff --git a/src/gui/addnewtorrentdialog.ui b/src/gui/addnewtorrentdialog.ui
index 0c9a96e1f..9d2fc3e5f 100644
--- a/src/gui/addnewtorrentdialog.ui
+++ b/src/gui/addnewtorrentdialog.ui
@@ -261,24 +261,6 @@
-
- 0
-
-
-
- None
-
-
-
-
- Metadata received
-
-
-
-
- Files checked
-
-
diff --git a/src/gui/advancedsettings.cpp b/src/gui/advancedsettings.cpp
index 0d0ce6642..b116f2d4d 100644
--- a/src/gui/advancedsettings.cpp
+++ b/src/gui/advancedsettings.cpp
@@ -63,7 +63,7 @@ namespace
// qBittorrent section
QBITTORRENT_HEADER,
RESUME_DATA_STORAGE,
-#ifdef QBT_USES_LIBTORRENT2
+#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS)
MEMORY_WORKING_SET_LIMIT,
#endif
#if defined(Q_OS_WIN)
@@ -94,6 +94,7 @@ namespace
ENABLE_SPEED_WIDGET,
#ifndef Q_OS_MACOS
ENABLE_ICONS_IN_MENUS,
+ USE_ATTACHED_ADD_NEW_TORRENT_DIALOG,
#endif
// embedded tracker
TRACKER_STATUS,
@@ -194,7 +195,7 @@ void AdvancedSettings::saveAdvancedSettings() const
BitTorrent::Session *const session = BitTorrent::Session::instance();
session->setResumeDataStorageType(m_comboBoxResumeDataStorage.currentData().value());
-#ifdef QBT_USES_LIBTORRENT2
+#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS)
// Physical memory (RAM) usage limit
app()->setMemoryWorkingSetLimit(m_spinBoxMemoryWorkingSetLimit.value());
#endif
@@ -310,6 +311,7 @@ void AdvancedSettings::saveAdvancedSettings() const
pref->setSpeedWidgetEnabled(m_checkBoxSpeedWidgetEnabled.isChecked());
#ifndef Q_OS_MACOS
pref->setIconsInMenusEnabled(m_checkBoxIconsInMenusEnabled.isChecked());
+ AddNewTorrentDialog::setAttached(m_checkBoxAttachedAddNewTorrentDialog.isChecked());
#endif
// Tracker
@@ -449,7 +451,7 @@ void AdvancedSettings::loadAdvancedSettings()
m_comboBoxResumeDataStorage.setCurrentIndex(m_comboBoxResumeDataStorage.findData(QVariant::fromValue(session->resumeDataStorageType())));
addRow(RESUME_DATA_STORAGE, tr("Resume data storage type (requires restart)"), &m_comboBoxResumeDataStorage);
-#ifdef QBT_USES_LIBTORRENT2
+#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS)
// Physical memory (RAM) usage limit
m_spinBoxMemoryWorkingSetLimit.setMinimum(1);
m_spinBoxMemoryWorkingSetLimit.setMaximum(std::numeric_limits::max());
@@ -796,6 +798,9 @@ void AdvancedSettings::loadAdvancedSettings()
// Enable icons in menus
m_checkBoxIconsInMenusEnabled.setChecked(pref->iconsInMenusEnabled());
addRow(ENABLE_ICONS_IN_MENUS, tr("Enable icons in menus"), &m_checkBoxIconsInMenusEnabled);
+
+ m_checkBoxAttachedAddNewTorrentDialog.setChecked(AddNewTorrentDialog::isAttached());
+ addRow(USE_ATTACHED_ADD_NEW_TORRENT_DIALOG, tr("Attach \"Add new torrent\" dialog to main window"), &m_checkBoxAttachedAddNewTorrentDialog);
#endif
// Tracker State
m_checkBoxTrackerStatus.setChecked(session->isTrackerEnabled());
diff --git a/src/gui/advancedsettings.h b/src/gui/advancedsettings.h
index 1cb869e9e..3622fa787 100644
--- a/src/gui/advancedsettings.h
+++ b/src/gui/advancedsettings.h
@@ -30,6 +30,7 @@
#include
+#include
#include
#include
#include
@@ -88,7 +89,11 @@ private:
QCheckBox m_checkBoxCoalesceRW;
#else
QComboBox m_comboBoxDiskIOType;
- QSpinBox m_spinBoxMemoryWorkingSetLimit, m_spinBoxHashingThreads;
+ QSpinBox m_spinBoxHashingThreads;
+#endif
+
+#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS)
+ QSpinBox m_spinBoxMemoryWorkingSetLimit;
#endif
#if defined(QBT_USES_LIBTORRENT2) && TORRENT_USE_I2P
@@ -102,6 +107,7 @@ private:
#ifndef Q_OS_MACOS
QCheckBox m_checkBoxIconsInMenusEnabled;
+ QCheckBox m_checkBoxAttachedAddNewTorrentDialog;
#endif
#ifdef QBT_USES_DBUS
diff --git a/src/gui/desktopintegration.cpp b/src/gui/desktopintegration.cpp
index 2be2bc69d..466d62c8c 100644
--- a/src/gui/desktopintegration.cpp
+++ b/src/gui/desktopintegration.cpp
@@ -31,6 +31,7 @@
#include
+#include
#include
#include
@@ -286,17 +287,25 @@ void DesktopIntegration::createTrayIcon()
QIcon DesktopIntegration::getSystrayIcon() const
{
const TrayIcon::Style style = Preferences::instance()->trayIconStyle();
+ QIcon icon;
switch (style)
{
default:
case TrayIcon::Style::Normal:
- return UIThemeManager::instance()->getIcon(u"qbittorrent-tray"_s);
-
+ icon = UIThemeManager::instance()->getIcon(u"qbittorrent-tray"_s);
+ break;
case TrayIcon::Style::MonoDark:
- return UIThemeManager::instance()->getIcon(u"qbittorrent-tray-dark"_s);
-
+ icon = UIThemeManager::instance()->getIcon(u"qbittorrent-tray-dark"_s);
+ break;
case TrayIcon::Style::MonoLight:
- return UIThemeManager::instance()->getIcon(u"qbittorrent-tray-light"_s);
+ icon = UIThemeManager::instance()->getIcon(u"qbittorrent-tray-light"_s);
+ break;
}
+#ifdef Q_OS_UNIX
+ // Workaround for invisible tray icon in KDE, https://bugreports.qt.io/browse/QTBUG-53550
+ if (qEnvironmentVariable("XDG_CURRENT_DESKTOP").compare(u"KDE", Qt::CaseInsensitive) == 0)
+ return icon.pixmap(32);
+#endif
+ return icon;
}
#endif // Q_OS_MACOS
diff --git a/src/gui/downloadfromurldialog.cpp b/src/gui/downloadfromurldialog.cpp
index 183689b94..ac0737774 100644
--- a/src/gui/downloadfromurldialog.cpp
+++ b/src/gui/downloadfromurldialog.cpp
@@ -90,11 +90,12 @@ DownloadFromURLDialog::DownloadFromURLDialog(QWidget *parent)
urls << urlString;
}
- const QString text = urls.join(u'\n')
- + (!urls.isEmpty() ? u"\n" : u"");
-
- m_ui->textUrls->setText(text);
- m_ui->textUrls->moveCursor(QTextCursor::End);
+ if (!urls.isEmpty())
+ {
+ m_ui->textUrls->setText(urls.join(u'\n') + u"\n");
+ m_ui->textUrls->moveCursor(QTextCursor::End);
+ m_ui->buttonBox->setFocus();
+ }
if (const QSize dialogSize = m_storeDialogSize; dialogSize.isValid())
resize(dialogSize);
diff --git a/src/gui/fspathedit_p.cpp b/src/gui/fspathedit_p.cpp
index d6f4031c9..ba6efec12 100644
--- a/src/gui/fspathedit_p.cpp
+++ b/src/gui/fspathedit_p.cpp
@@ -1,7 +1,8 @@
/*
* Bittorrent Client using Qt and libtorrent.
- * Copyright (C) 2022 Mike Tzou (Chocobo1)
- * Copyright (C) 2016 Eugene Shalygin
+ * Copyright (C) 2024 Vladimir Golovnev
+ * Copyright (C) 2022 Mike Tzou (Chocobo1)
+ * Copyright (C) 2016 Eugene Shalygin
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@@ -32,6 +33,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -160,36 +162,34 @@ QValidator::State Private::FileSystemPathValidator::validate(QString &input, int
}
Private::FileLineEdit::FileLineEdit(QWidget *parent)
- : QLineEdit {parent}
- , m_completerModel {new QFileSystemModel(this)}
- , m_completer {new QCompleter(this)}
+ : QLineEdit(parent)
{
- m_iconProvider.setOptions(QFileIconProvider::DontUseCustomDirectoryIcons);
-
- m_completerModel->setIconProvider(&m_iconProvider);
- m_completerModel->setOptions(QFileSystemModel::DontWatchForChanges);
-
- m_completer->setModel(m_completerModel);
- setCompleter(m_completer);
-
+ setCompleter(new QCompleter(this));
connect(this, &QLineEdit::textChanged, this, &FileLineEdit::validateText);
}
Private::FileLineEdit::~FileLineEdit()
{
delete m_completerModel; // has to be deleted before deleting the m_iconProvider object
+ delete m_iconProvider;
}
void Private::FileLineEdit::completeDirectoriesOnly(const bool completeDirsOnly)
{
- const QDir::Filters filters = QDir::NoDotAndDotDot
- | (completeDirsOnly ? QDir::Dirs : QDir::AllEntries);
- m_completerModel->setFilter(filters);
+ m_completeDirectoriesOnly = completeDirsOnly;
+ if (m_completerModel)
+ {
+ const QDir::Filters filters = QDir::NoDotAndDotDot
+ | (completeDirsOnly ? QDir::Dirs : QDir::AllEntries);
+ m_completerModel->setFilter(filters);
+ }
}
void Private::FileLineEdit::setFilenameFilters(const QStringList &filters)
{
- m_completerModel->setNameFilters(filters);
+ m_filenameFilters = filters;
+ if (m_completerModel)
+ m_completerModel->setNameFilters(m_filenameFilters);
}
void Private::FileLineEdit::setBrowseAction(QAction *action)
@@ -223,6 +223,22 @@ void Private::FileLineEdit::keyPressEvent(QKeyEvent *e)
if ((e->key() == Qt::Key_Space) && (e->modifiers() == Qt::CTRL))
{
+ if (!m_completerModel)
+ {
+ m_iconProvider = new QFileIconProvider;
+ m_iconProvider->setOptions(QFileIconProvider::DontUseCustomDirectoryIcons);
+
+ m_completerModel = new QFileSystemModel(this);
+ m_completerModel->setIconProvider(m_iconProvider);
+ m_completerModel->setOptions(QFileSystemModel::DontWatchForChanges);
+ m_completerModel->setNameFilters(m_filenameFilters);
+ const QDir::Filters filters = QDir::NoDotAndDotDot
+ | (m_completeDirectoriesOnly ? QDir::Dirs : QDir::AllEntries);
+ m_completerModel->setFilter(filters);
+
+ completer()->setModel(m_completerModel);
+ }
+
m_completerModel->setRootPath(Path(text()).data());
showCompletionPopup();
}
@@ -244,8 +260,8 @@ void Private::FileLineEdit::contextMenuEvent(QContextMenuEvent *event)
void Private::FileLineEdit::showCompletionPopup()
{
- m_completer->setCompletionPrefix(text());
- m_completer->complete();
+ completer()->setCompletionPrefix(text());
+ completer()->complete();
}
void Private::FileLineEdit::validateText()
diff --git a/src/gui/fspathedit_p.h b/src/gui/fspathedit_p.h
index 69592281b..e52face0f 100644
--- a/src/gui/fspathedit_p.h
+++ b/src/gui/fspathedit_p.h
@@ -1,6 +1,7 @@
/*
* Bittorrent Client using Qt and libtorrent.
- * Copyright (C) 2016 Eugene Shalygin
+ * Copyright (C) 2024 Vladimir Golovnev
+ * Copyright (C) 2016 Eugene Shalygin
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@@ -29,7 +30,6 @@
#pragma once
#include
-#include
#include
#include
#include
@@ -37,8 +37,8 @@
#include "base/pathfwd.h"
class QAction;
-class QCompleter;
class QContextMenuEvent;
+class QFileIconProvider;
class QFileSystemModel;
class QKeyEvent;
@@ -140,10 +140,11 @@ namespace Private
static QString warningText(FileSystemPathValidator::TestResult result);
QFileSystemModel *m_completerModel = nullptr;
- QCompleter *m_completer = nullptr;
QAction *m_browseAction = nullptr;
QAction *m_warningAction = nullptr;
- QFileIconProvider m_iconProvider;
+ QFileIconProvider *m_iconProvider = nullptr;
+ bool m_completeDirectoriesOnly = false;
+ QStringList m_filenameFilters;
};
class FileComboEdit final : public QComboBox, public IFileEditorWithCompletion
diff --git a/src/gui/ipsubnetwhitelistoptionsdialog.cpp b/src/gui/ipsubnetwhitelistoptionsdialog.cpp
index 78af3610f..11556422c 100644
--- a/src/gui/ipsubnetwhitelistoptionsdialog.cpp
+++ b/src/gui/ipsubnetwhitelistoptionsdialog.cpp
@@ -50,7 +50,7 @@ IPSubnetWhitelistOptionsDialog::IPSubnetWhitelistOptionsDialog(QWidget *parent)
connect(m_ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
QStringList authSubnetWhitelistStringList;
- for (const Utils::Net::Subnet &subnet : asConst(Preferences::instance()->getWebUiAuthSubnetWhitelist()))
+ for (const Utils::Net::Subnet &subnet : asConst(Preferences::instance()->getWebUIAuthSubnetWhitelist()))
authSubnetWhitelistStringList << Utils::Net::subnetToString(subnet);
m_model = new QStringListModel(authSubnetWhitelistStringList, this);
@@ -81,7 +81,7 @@ void IPSubnetWhitelistOptionsDialog::on_buttonBox_accepted()
// Operate on the m_sortFilter to grab the strings in sorted order
for (int i = 0; i < m_sortFilter->rowCount(); ++i)
subnets.append(m_sortFilter->index(i, 0).data().toString());
- Preferences::instance()->setWebUiAuthSubnetWhitelist(subnets);
+ Preferences::instance()->setWebUIAuthSubnetWhitelist(subnets);
QDialog::accept();
}
else
diff --git a/src/gui/lineedit.cpp b/src/gui/lineedit.cpp
index b6ad0af76..08a5aeea5 100644
--- a/src/gui/lineedit.cpp
+++ b/src/gui/lineedit.cpp
@@ -29,20 +29,41 @@
#include "lineedit.h"
+#include
+
#include
#include
+#include
#include "base/global.h"
#include "uithememanager.h"
+using namespace std::chrono_literals;
+
+namespace
+{
+ const std::chrono::milliseconds FILTER_INPUT_DELAY {400};
+}
+
LineEdit::LineEdit(QWidget *parent)
: QLineEdit(parent)
+ , m_delayedTextChangedTimer {new QTimer(this)}
{
- auto *action = new QAction(UIThemeManager::instance()->getIcon(u"edit-find"_s), QString());
+ auto *action = new QAction(UIThemeManager::instance()->getIcon(u"edit-find"_s), QString(), this);
addAction(action, QLineEdit::LeadingPosition);
setClearButtonEnabled(true);
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
+
+ m_delayedTextChangedTimer->setSingleShot(true);
+ connect(m_delayedTextChangedTimer, &QTimer::timeout, this, [this]
+ {
+ emit textChanged(text());
+ });
+ connect(this, &QLineEdit::textChanged, this, [this]
+ {
+ m_delayedTextChangedTimer->start(FILTER_INPUT_DELAY);
+ });
}
void LineEdit::keyPressEvent(QKeyEvent *event)
diff --git a/src/gui/lineedit.h b/src/gui/lineedit.h
index 55ed3bd85..459740f86 100644
--- a/src/gui/lineedit.h
+++ b/src/gui/lineedit.h
@@ -31,6 +31,9 @@
#include
+class QKeyEvent;
+class QTimer;
+
class LineEdit final : public QLineEdit
{
Q_OBJECT
@@ -39,6 +42,11 @@ class LineEdit final : public QLineEdit
public:
explicit LineEdit(QWidget *parent = nullptr);
+signals:
+ void textChanged(const QString &text);
+
private:
void keyPressEvent(QKeyEvent *event) override;
+
+ QTimer *m_delayedTextChangedTimer = nullptr;
};
diff --git a/src/gui/mainwindow.cpp b/src/gui/mainwindow.cpp
index 6ecdc1972..fb01833f1 100644
--- a/src/gui/mainwindow.cpp
+++ b/src/gui/mainwindow.cpp
@@ -57,6 +57,7 @@
#include
#include
#include
+#include
#include
#include
@@ -174,7 +175,7 @@ MainWindow::MainWindow(IGUIApplication *app, WindowState initialState)
m_ui->menuLog->setIcon(UIThemeManager::instance()->getIcon(u"help-contents"_s));
m_ui->actionCheckForUpdates->setIcon(UIThemeManager::instance()->getIcon(u"view-refresh"_s));
- auto *lockMenu = new QMenu(this);
+ auto *lockMenu = new QMenu(m_ui->menuView);
lockMenu->addAction(tr("&Set Password"), this, &MainWindow::defineUILockPassword);
lockMenu->addAction(tr("&Clear Password"), this, &MainWindow::clearUILockPassword);
m_ui->actionLock->setMenu(lockMenu);
@@ -455,8 +456,6 @@ MainWindow::MainWindow(IGUIApplication *app, WindowState initialState)
}
#endif
- m_propertiesWidget->readSettings();
-
const bool isFiltersSidebarVisible = pref->isFiltersSidebarVisible();
m_ui->actionShowFiltersSidebar->setChecked(isFiltersSidebarVisible);
if (isFiltersSidebarVisible)
@@ -782,8 +781,11 @@ void MainWindow::saveSplitterSettings() const
void MainWindow::cleanup()
{
- saveSettings();
- saveSplitterSettings();
+ if (!m_neverShown)
+ {
+ saveSettings();
+ saveSplitterSettings();
+ }
// delete RSSWidget explicitly to avoid crash in
// handleRSSUnreadCountUpdated() at application shutdown
@@ -1092,6 +1094,12 @@ void MainWindow::showEvent(QShowEvent *e)
{
// preparations before showing the window
+ if (m_neverShown)
+ {
+ m_propertiesWidget->readSettings();
+ m_neverShown = false;
+ }
+
if (currentTabWidget() == m_transferListWidget)
m_propertiesWidget->loadDynamicData();
@@ -1178,7 +1186,7 @@ void MainWindow::closeEvent(QCloseEvent *e)
if (!isVisible())
show();
QMessageBox confirmBox(QMessageBox::Question, tr("Exiting qBittorrent"),
- // Split it because the last sentence is used in the Web UI
+ // Split it because the last sentence is used in the WebUI
tr("Some files are currently transferring.") + u'\n' + tr("Are you sure you want to quit qBittorrent?"),
QMessageBox::NoButton, this);
QPushButton *noBtn = confirmBox.addButton(tr("&No"), QMessageBox::NoRole);
diff --git a/src/gui/mainwindow.h b/src/gui/mainwindow.h
index 1c484fe7e..30923b166 100644
--- a/src/gui/mainwindow.h
+++ b/src/gui/mainwindow.h
@@ -42,6 +42,7 @@ class QCloseEvent;
class QComboBox;
class QFileSystemWatcher;
class QSplitter;
+class QString;
class QTabWidget;
class QTimer;
@@ -202,6 +203,7 @@ private:
QFileSystemWatcher *m_executableWatcher = nullptr;
// GUI related
bool m_posInitialized = false;
+ bool m_neverShown = true;
QPointer m_tabs;
QPointer m_statusBar;
QPointer m_options;
diff --git a/src/gui/optionsdialog.cpp b/src/gui/optionsdialog.cpp
index d043dfbb6..a9968cb56 100644
--- a/src/gui/optionsdialog.cpp
+++ b/src/gui/optionsdialog.cpp
@@ -69,13 +69,21 @@
#include "utils.h"
#include "watchedfolderoptionsdialog.h"
#include "watchedfoldersmodel.h"
+#include "webui/webui.h"
#ifndef DISABLE_WEBUI
#include "base/net/dnsupdater.h"
#endif
+#if defined Q_OS_MACOS || defined Q_OS_WIN
+#include "base/utils/os.h"
+#endif // defined Q_OS_MACOS || defined Q_OS_WIN
+
#define SETTINGS_KEY(name) u"OptionsDialog/" name
+const int WEBUI_MIN_USERNAME_LENGTH = 3;
+const int WEBUI_MIN_PASSWORD_LENGTH = 6;
+
namespace
{
QStringList translatedWeekdayNames()
@@ -102,6 +110,16 @@ namespace
}
};
+ bool isValidWebUIUsername(const QString &username)
+ {
+ return (username.length() >= WEBUI_MIN_USERNAME_LENGTH);
+ }
+
+ bool isValidWebUIPassword(const QString &password)
+ {
+ return (password.length() >= WEBUI_MIN_PASSWORD_LENGTH);
+ }
+
// Shortcuts for frequently used signals that have more than one overload. They would require
// type casts and that is why we declare required member pointer here instead.
void (QComboBox::*qComboBoxCurrentIndexChanged)(int) = &QComboBox::currentIndexChanged;
@@ -171,7 +189,11 @@ OptionsDialog::OptionsDialog(IGUIApplication *app, QWidget *parent)
// setup apply button
m_applyButton->setEnabled(false);
- connect(m_applyButton, &QPushButton::clicked, this, &OptionsDialog::applySettings);
+ connect(m_applyButton, &QPushButton::clicked, this, [this]
+ {
+ if (applySettings())
+ m_applyButton->setEnabled(false);
+ });
// disable mouse wheel event on widgets to avoid misselection
auto *wheelEventEater = new WheelEventEater(this);
@@ -284,15 +306,15 @@ void OptionsDialog::loadBehaviorTabOptions()
#ifdef Q_OS_WIN
m_ui->checkStartup->setChecked(pref->WinStartup());
- m_ui->checkAssociateTorrents->setChecked(Preferences::isTorrentFileAssocSet());
- m_ui->checkAssociateMagnetLinks->setChecked(Preferences::isMagnetLinkAssocSet());
+ m_ui->checkAssociateTorrents->setChecked(Utils::OS::isTorrentFileAssocSet());
+ m_ui->checkAssociateMagnetLinks->setChecked(Utils::OS::isMagnetLinkAssocSet());
#endif
#ifdef Q_OS_MACOS
m_ui->checkShowSystray->setVisible(false);
- m_ui->checkAssociateTorrents->setChecked(Preferences::isTorrentFileAssocSet());
+ m_ui->checkAssociateTorrents->setChecked(Utils::OS::isTorrentFileAssocSet());
m_ui->checkAssociateTorrents->setEnabled(!m_ui->checkAssociateTorrents->isChecked());
- m_ui->checkAssociateMagnetLinks->setChecked(Preferences::isMagnetLinkAssocSet());
+ m_ui->checkAssociateMagnetLinks->setChecked(Utils::OS::isMagnetLinkAssocSet());
m_ui->checkAssociateMagnetLinks->setEnabled(!m_ui->checkAssociateMagnetLinks->isChecked());
#endif
@@ -433,8 +455,8 @@ void OptionsDialog::saveBehaviorTabOptions() const
#ifdef Q_OS_WIN
pref->setWinStartup(WinStartup());
- Preferences::setTorrentFileAssoc(m_ui->checkAssociateTorrents->isChecked());
- Preferences::setMagnetLinkAssoc(m_ui->checkAssociateMagnetLinks->isChecked());
+ Utils::OS::setTorrentFileAssoc(m_ui->checkAssociateTorrents->isChecked());
+ Utils::OS::setMagnetLinkAssoc(m_ui->checkAssociateMagnetLinks->isChecked());
#endif
#ifndef Q_OS_MACOS
@@ -447,14 +469,14 @@ void OptionsDialog::saveBehaviorTabOptions() const
#ifdef Q_OS_MACOS
if (m_ui->checkAssociateTorrents->isChecked())
{
- Preferences::setTorrentFileAssoc();
- m_ui->checkAssociateTorrents->setChecked(Preferences::isTorrentFileAssocSet());
+ Utils::OS::setTorrentFileAssoc();
+ m_ui->checkAssociateTorrents->setChecked(Utils::OS::isTorrentFileAssocSet());
m_ui->checkAssociateTorrents->setEnabled(!m_ui->checkAssociateTorrents->isChecked());
}
if (m_ui->checkAssociateMagnetLinks->isChecked())
{
- Preferences::setMagnetLinkAssoc();
- m_ui->checkAssociateMagnetLinks->setChecked(Preferences::isMagnetLinkAssocSet());
+ Utils::OS::setMagnetLinkAssoc();
+ m_ui->checkAssociateMagnetLinks->setChecked(Utils::OS::isMagnetLinkAssocSet());
m_ui->checkAssociateMagnetLinks->setEnabled(!m_ui->checkAssociateMagnetLinks->isChecked());
}
#endif
@@ -494,7 +516,7 @@ void OptionsDialog::loadDownloadsTabOptions()
m_ui->stopConditionComboBox->setToolTip(
u"
" +
tr("Metadata received") + u" - " + tr("Torrent will stop after metadata is received.") +
- u" " + tr("Torrents that have metadata initially aren't affected.") + u"
" +
+ u" " + tr("Torrents that have metadata initially will be added as stopped.") + u"
" +
tr("Files checked") + u" - " + tr("Torrent will stop after files are initially checked.") +
u" " + tr("This will also download metadata if it wasn't there initially.") + u"
");
m_ui->stopConditionComboBox->setItemData(0, QVariant::fromValue(BitTorrent::Torrent::StopCondition::None));
@@ -1207,28 +1229,33 @@ void OptionsDialog::loadWebUITabOptions()
m_ui->textWebUIRootFolder->setMode(FileSystemPathEdit::Mode::DirectoryOpen);
m_ui->textWebUIRootFolder->setDialogCaption(tr("Choose Alternative UI files location"));
- m_ui->checkWebUi->setChecked(pref->isWebUiEnabled());
- m_ui->textWebUiAddress->setText(pref->getWebUiAddress());
- m_ui->spinWebUiPort->setValue(pref->getWebUiPort());
+ if (app()->webUI()->isErrored())
+ m_ui->labelWebUIError->setText(tr("WebUI configuration failed. Reason: %1").arg(app()->webUI()->errorMessage()));
+ else
+ m_ui->labelWebUIError->hide();
+
+ m_ui->checkWebUI->setChecked(pref->isWebUIEnabled());
+ m_ui->textWebUIAddress->setText(pref->getWebUIAddress());
+ m_ui->spinWebUIPort->setValue(pref->getWebUIPort());
m_ui->checkWebUIUPnP->setChecked(pref->useUPnPForWebUIPort());
- m_ui->checkWebUiHttps->setChecked(pref->isWebUiHttpsEnabled());
+ m_ui->checkWebUIHttps->setChecked(pref->isWebUIHttpsEnabled());
webUIHttpsCertChanged(pref->getWebUIHttpsCertificatePath());
webUIHttpsKeyChanged(pref->getWebUIHttpsKeyPath());
- m_ui->textWebUiUsername->setText(pref->getWebUiUsername());
- m_ui->checkBypassLocalAuth->setChecked(!pref->isWebUiLocalAuthEnabled());
- m_ui->checkBypassAuthSubnetWhitelist->setChecked(pref->isWebUiAuthSubnetWhitelistEnabled());
+ m_ui->textWebUIUsername->setText(pref->getWebUIUsername());
+ m_ui->checkBypassLocalAuth->setChecked(!pref->isWebUILocalAuthEnabled());
+ m_ui->checkBypassAuthSubnetWhitelist->setChecked(pref->isWebUIAuthSubnetWhitelistEnabled());
m_ui->IPSubnetWhitelistButton->setEnabled(m_ui->checkBypassAuthSubnetWhitelist->isChecked());
m_ui->spinBanCounter->setValue(pref->getWebUIMaxAuthFailCount());
m_ui->spinBanDuration->setValue(pref->getWebUIBanDuration().count());
m_ui->spinSessionTimeout->setValue(pref->getWebUISessionTimeout());
// Alternative UI
- m_ui->groupAltWebUI->setChecked(pref->isAltWebUiEnabled());
- m_ui->textWebUIRootFolder->setSelectedPath(pref->getWebUiRootFolder());
+ m_ui->groupAltWebUI->setChecked(pref->isAltWebUIEnabled());
+ m_ui->textWebUIRootFolder->setSelectedPath(pref->getWebUIRootFolder());
// Security
- m_ui->checkClickjacking->setChecked(pref->isWebUiClickjackingProtectionEnabled());
- m_ui->checkCSRFProtection->setChecked(pref->isWebUiCSRFProtectionEnabled());
- m_ui->checkSecureCookie->setEnabled(pref->isWebUiHttpsEnabled());
- m_ui->checkSecureCookie->setChecked(pref->isWebUiSecureCookieEnabled());
+ m_ui->checkClickjacking->setChecked(pref->isWebUIClickjackingProtectionEnabled());
+ m_ui->checkCSRFProtection->setChecked(pref->isWebUICSRFProtectionEnabled());
+ m_ui->checkSecureCookie->setEnabled(pref->isWebUIHttpsEnabled());
+ m_ui->checkSecureCookie->setChecked(pref->isWebUISecureCookieEnabled());
m_ui->groupHostHeaderValidation->setChecked(pref->isWebUIHostHeaderValidationEnabled());
m_ui->textServerDomains->setText(pref->getServerDomains());
// Custom HTTP headers
@@ -1244,18 +1271,18 @@ void OptionsDialog::loadWebUITabOptions()
m_ui->DNSUsernameTxt->setText(pref->getDynDNSUsername());
m_ui->DNSPasswordTxt->setText(pref->getDynDNSPassword());
- connect(m_ui->checkWebUi, &QGroupBox::toggled, this, &ThisType::enableApplyButton);
- connect(m_ui->textWebUiAddress, &QLineEdit::textChanged, this, &ThisType::enableApplyButton);
- connect(m_ui->spinWebUiPort, qSpinBoxValueChanged, this, &ThisType::enableApplyButton);
+ connect(m_ui->checkWebUI, &QGroupBox::toggled, this, &ThisType::enableApplyButton);
+ connect(m_ui->textWebUIAddress, &QLineEdit::textChanged, this, &ThisType::enableApplyButton);
+ connect(m_ui->spinWebUIPort, qSpinBoxValueChanged, this, &ThisType::enableApplyButton);
connect(m_ui->checkWebUIUPnP, &QAbstractButton::toggled, this, &ThisType::enableApplyButton);
- connect(m_ui->checkWebUiHttps, &QGroupBox::toggled, this, &ThisType::enableApplyButton);
+ connect(m_ui->checkWebUIHttps, &QGroupBox::toggled, this, &ThisType::enableApplyButton);
connect(m_ui->textWebUIHttpsCert, &FileSystemPathLineEdit::selectedPathChanged, this, &ThisType::enableApplyButton);
connect(m_ui->textWebUIHttpsCert, &FileSystemPathLineEdit::selectedPathChanged, this, &OptionsDialog::webUIHttpsCertChanged);
connect(m_ui->textWebUIHttpsKey, &FileSystemPathLineEdit::selectedPathChanged, this, &ThisType::enableApplyButton);
connect(m_ui->textWebUIHttpsKey, &FileSystemPathLineEdit::selectedPathChanged, this, &OptionsDialog::webUIHttpsKeyChanged);
- connect(m_ui->textWebUiUsername, &QLineEdit::textChanged, this, &ThisType::enableApplyButton);
- connect(m_ui->textWebUiPassword, &QLineEdit::textChanged, this, &ThisType::enableApplyButton);
+ connect(m_ui->textWebUIUsername, &QLineEdit::textChanged, this, &ThisType::enableApplyButton);
+ connect(m_ui->textWebUIPassword, &QLineEdit::textChanged, this, &ThisType::enableApplyButton);
connect(m_ui->checkBypassLocalAuth, &QAbstractButton::toggled, this, &ThisType::enableApplyButton);
connect(m_ui->checkBypassAuthSubnetWhitelist, &QAbstractButton::toggled, this, &ThisType::enableApplyButton);
@@ -1269,7 +1296,7 @@ void OptionsDialog::loadWebUITabOptions()
connect(m_ui->checkClickjacking, &QCheckBox::toggled, this, &ThisType::enableApplyButton);
connect(m_ui->checkCSRFProtection, &QCheckBox::toggled, this, &ThisType::enableApplyButton);
- connect(m_ui->checkWebUiHttps, &QGroupBox::toggled, m_ui->checkSecureCookie, &QWidget::setEnabled);
+ connect(m_ui->checkWebUIHttps, &QGroupBox::toggled, m_ui->checkSecureCookie, &QWidget::setEnabled);
connect(m_ui->checkSecureCookie, &QCheckBox::toggled, this, &ThisType::enableApplyButton);
connect(m_ui->groupHostHeaderValidation, &QGroupBox::toggled, this, &ThisType::enableApplyButton);
connect(m_ui->textServerDomains, &QLineEdit::textChanged, this, &ThisType::enableApplyButton);
@@ -1291,29 +1318,32 @@ void OptionsDialog::saveWebUITabOptions() const
{
auto *pref = Preferences::instance();
- pref->setWebUiEnabled(isWebUiEnabled());
- pref->setWebUiAddress(m_ui->textWebUiAddress->text());
- pref->setWebUiPort(m_ui->spinWebUiPort->value());
+ const bool webUIEnabled = isWebUIEnabled();
+
+ pref->setWebUIEnabled(webUIEnabled);
+ pref->setWebUIAddress(m_ui->textWebUIAddress->text());
+ pref->setWebUIPort(m_ui->spinWebUIPort->value());
pref->setUPnPForWebUIPort(m_ui->checkWebUIUPnP->isChecked());
- pref->setWebUiHttpsEnabled(m_ui->checkWebUiHttps->isChecked());
+ pref->setWebUIHttpsEnabled(m_ui->checkWebUIHttps->isChecked());
pref->setWebUIHttpsCertificatePath(m_ui->textWebUIHttpsCert->selectedPath());
pref->setWebUIHttpsKeyPath(m_ui->textWebUIHttpsKey->selectedPath());
pref->setWebUIMaxAuthFailCount(m_ui->spinBanCounter->value());
pref->setWebUIBanDuration(std::chrono::seconds {m_ui->spinBanDuration->value()});
pref->setWebUISessionTimeout(m_ui->spinSessionTimeout->value());
// Authentication
- pref->setWebUiUsername(webUiUsername());
- if (!webUiPassword().isEmpty())
- pref->setWebUIPassword(Utils::Password::PBKDF2::generate(webUiPassword()));
- pref->setWebUiLocalAuthEnabled(!m_ui->checkBypassLocalAuth->isChecked());
- pref->setWebUiAuthSubnetWhitelistEnabled(m_ui->checkBypassAuthSubnetWhitelist->isChecked());
+ if (const QString username = webUIUsername(); isValidWebUIUsername(username))
+ pref->setWebUIUsername(username);
+ if (const QString password = webUIPassword(); isValidWebUIPassword(password))
+ pref->setWebUIPassword(Utils::Password::PBKDF2::generate(password));
+ pref->setWebUILocalAuthEnabled(!m_ui->checkBypassLocalAuth->isChecked());
+ pref->setWebUIAuthSubnetWhitelistEnabled(m_ui->checkBypassAuthSubnetWhitelist->isChecked());
// Alternative UI
- pref->setAltWebUiEnabled(m_ui->groupAltWebUI->isChecked());
- pref->setWebUiRootFolder(m_ui->textWebUIRootFolder->selectedPath());
+ pref->setAltWebUIEnabled(m_ui->groupAltWebUI->isChecked());
+ pref->setWebUIRootFolder(m_ui->textWebUIRootFolder->selectedPath());
// Security
- pref->setWebUiClickjackingProtectionEnabled(m_ui->checkClickjacking->isChecked());
- pref->setWebUiCSRFProtectionEnabled(m_ui->checkCSRFProtection->isChecked());
- pref->setWebUiSecureCookieEnabled(m_ui->checkSecureCookie->isChecked());
+ pref->setWebUIClickjackingProtectionEnabled(m_ui->checkClickjacking->isChecked());
+ pref->setWebUICSRFProtectionEnabled(m_ui->checkCSRFProtection->isChecked());
+ pref->setWebUISecureCookieEnabled(m_ui->checkSecureCookie->isChecked());
pref->setWebUIHostHeaderValidationEnabled(m_ui->groupHostHeaderValidation->isChecked());
pref->setServerDomains(m_ui->textServerDomains->text());
// Custom HTTP headers
@@ -1513,53 +1543,37 @@ void OptionsDialog::on_buttonBox_accepted()
{
if (m_applyButton->isEnabled())
{
- if (!schedTimesOk())
- {
- m_ui->tabSelection->setCurrentRow(TAB_SPEED);
+ if (!applySettings())
return;
- }
-#ifndef DISABLE_WEBUI
- if (!webUIAuthenticationOk())
- {
- m_ui->tabSelection->setCurrentRow(TAB_WEBUI);
- return;
- }
- if (!isAlternativeWebUIPathValid())
- {
- m_ui->tabSelection->setCurrentRow(TAB_WEBUI);
- return;
- }
-#endif
m_applyButton->setEnabled(false);
- saveOptions();
}
accept();
}
-void OptionsDialog::applySettings()
+bool OptionsDialog::applySettings()
{
if (!schedTimesOk())
{
m_ui->tabSelection->setCurrentRow(TAB_SPEED);
- return;
+ return false;
}
#ifndef DISABLE_WEBUI
- if (!webUIAuthenticationOk())
+ if (isWebUIEnabled() && !webUIAuthenticationOk())
{
m_ui->tabSelection->setCurrentRow(TAB_WEBUI);
- return;
+ return false;
}
if (!isAlternativeWebUIPathValid())
{
m_ui->tabSelection->setCurrentRow(TAB_WEBUI);
- return;
+ return false;
}
#endif
- m_applyButton->setEnabled(false);
saveOptions();
+ return true;
}
void OptionsDialog::on_buttonBox_rejected()
@@ -1855,31 +1869,33 @@ void OptionsDialog::webUIHttpsKeyChanged(const Path &path)
(isKeyValid ? u"security-high"_s : u"security-low"_s), 24));
}
-bool OptionsDialog::isWebUiEnabled() const
+bool OptionsDialog::isWebUIEnabled() const
{
- return m_ui->checkWebUi->isChecked();
+ return m_ui->checkWebUI->isChecked();
}
-QString OptionsDialog::webUiUsername() const
+QString OptionsDialog::webUIUsername() const
{
- return m_ui->textWebUiUsername->text();
+ return m_ui->textWebUIUsername->text();
}
-QString OptionsDialog::webUiPassword() const
+QString OptionsDialog::webUIPassword() const
{
- return m_ui->textWebUiPassword->text();
+ return m_ui->textWebUIPassword->text();
}
bool OptionsDialog::webUIAuthenticationOk()
{
- if (webUiUsername().length() < 3)
+ if (!isValidWebUIUsername(webUIUsername()))
{
- QMessageBox::warning(this, tr("Length Error"), tr("The Web UI username must be at least 3 characters long."));
+ QMessageBox::warning(this, tr("Length Error"), tr("The WebUI username must be at least 3 characters long."));
return false;
}
- if (!webUiPassword().isEmpty() && (webUiPassword().length() < 6))
+
+ const bool dontChangePassword = webUIPassword().isEmpty() && !Preferences::instance()->getWebUIPassword().isEmpty();
+ if (!isValidWebUIPassword(webUIPassword()) && !dontChangePassword)
{
- QMessageBox::warning(this, tr("Length Error"), tr("The Web UI password must be at least 6 characters long."));
+ QMessageBox::warning(this, tr("Length Error"), tr("The WebUI password must be at least 6 characters long."));
return false;
}
return true;
@@ -1889,7 +1905,7 @@ bool OptionsDialog::isAlternativeWebUIPathValid()
{
if (m_ui->groupAltWebUI->isChecked() && m_ui->textWebUIRootFolder->selectedPath().isEmpty())
{
- QMessageBox::warning(this, tr("Location Error"), tr("The alternative Web UI files location cannot be blank."));
+ QMessageBox::warning(this, tr("Location Error"), tr("The alternative WebUI files location cannot be blank."));
return false;
}
return true;
diff --git a/src/gui/optionsdialog.h b/src/gui/optionsdialog.h
index dfb5a75c8..8bcb64744 100644
--- a/src/gui/optionsdialog.h
+++ b/src/gui/optionsdialog.h
@@ -88,7 +88,6 @@ private slots:
void adjustProxyOptions();
void on_buttonBox_accepted();
void on_buttonBox_rejected();
- void applySettings();
void enableApplyButton();
void toggleComboRatioLimitAct();
void changePage(QListWidgetItem *, QListWidgetItem *);
@@ -115,6 +114,7 @@ private:
void showEvent(QShowEvent *e) override;
// Methods
+ bool applySettings();
void saveOptions() const;
void loadBehaviorTabOptions();
@@ -184,9 +184,9 @@ private:
int getMaxActiveTorrents() const;
// WebUI
#ifndef DISABLE_WEBUI
- bool isWebUiEnabled() const;
- QString webUiUsername() const;
- QString webUiPassword() const;
+ bool isWebUIEnabled() const;
+ QString webUIUsername() const;
+ QString webUIPassword() const;
bool webUIAuthenticationOk();
bool isAlternativeWebUIPathValid();
#endif
diff --git a/src/gui/optionsdialog.ui b/src/gui/optionsdialog.ui
index 089cbb849..4f7e826b3 100644
--- a/src/gui/optionsdialog.ui
+++ b/src/gui/optionsdialog.ui
@@ -3223,8 +3223,8 @@ Disable encryption: Only connect to peers without protocol encryption
-
-
+
+ 0
@@ -3253,7 +3253,7 @@ Disable encryption: Only connect to peers without protocol encryption
-
+ Web User Interface (Remote control)
@@ -3264,17 +3264,29 @@ Disable encryption: Only connect to peers without protocol encryption
false
+
+
+
+
+ true
+
+
+
+
+
+
+
-
+ IP address:
-
+ IP address that the Web UI will bind to.
Specify an IPv4 or IPv6 address. You can specify "0.0.0.0" for any IPv4 address,
@@ -3283,14 +3295,14 @@ Specify an IPv4 or IPv6 address. You can specify "0.0.0.0" for any IPv
-
+ Port:
-
+ 1
@@ -3315,7 +3327,7 @@ Specify an IPv4 or IPv6 address. You can specify "0.0.0.0" for any IPv
-
+ &Use HTTPS instead of HTTP
@@ -3327,14 +3339,14 @@ Specify an IPv4 or IPv6 address. You can specify "0.0.0.0" for any IPv
-
+ Key:
-
+ Certificate:
@@ -3366,7 +3378,7 @@ Specify an IPv4 or IPv6 address. You can specify "0.0.0.0" for any IPv
-
+ Authentication
@@ -3374,24 +3386,24 @@ Specify an IPv4 or IPv6 address. You can specify "0.0.0.0" for any IPv
-
+ Username:
-
+
-
+ Password:
-
+ QLineEdit::Password
@@ -3819,13 +3831,13 @@ Use ';' to split multiple entries. Can use wildcard '*'.
stopConditionComboBoxspinPortcheckUPnP
- textWebUiUsername
- checkWebUi
+ textWebUIUsername
+ checkWebUItextSavePathscrollArea_7scrollArea_2
- spinWebUiPort
- textWebUiPassword
+ spinWebUIPort
+ textWebUIPasswordbuttonBoxtabSelectionscrollArea
@@ -3915,7 +3927,7 @@ Use ';' to split multiple entries. Can use wildcard '*'.
spinMaxActiveUploadsspinMaxActiveTorrentscheckWebUIUPnP
- checkWebUiHttps
+ checkWebUIHttpscheckBypassLocalAuthcheckBypassAuthSubnetWhitelistIPSubnetWhitelistButton
diff --git a/src/gui/powermanagement/powermanagement_x11.cpp b/src/gui/powermanagement/powermanagement_x11.cpp
index 55746da55..bfdb9da0b 100644
--- a/src/gui/powermanagement/powermanagement_x11.cpp
+++ b/src/gui/powermanagement/powermanagement_x11.cpp
@@ -105,7 +105,7 @@ void PowerManagementInhibitor::requestBusy()
args << 0u;
args << u"Active torrents are presented"_s;
if (m_useGSM)
- args << 8u;
+ args << 4u;
call.setArguments(args);
QDBusPendingCall pcall = QDBusConnection::sessionBus().asyncCall(call, 1000);
diff --git a/src/gui/previewlistdelegate.cpp b/src/gui/previewlistdelegate.cpp
index 89075bcfb..d89815756 100644
--- a/src/gui/previewlistdelegate.cpp
+++ b/src/gui/previewlistdelegate.cpp
@@ -1,5 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
+ * Copyright (C) 2023 Vladimir Golovnev
* Copyright (C) 2006 Christophe Dumez
*
* This program is free software; you can redistribute it and/or
@@ -30,13 +31,12 @@
#include
#include
-#include
-#include "base/utils/misc.h"
+#include "base/utils/string.h"
#include "previewselectdialog.h"
PreviewListDelegate::PreviewListDelegate(QObject *parent)
- : QItemDelegate(parent)
+ : QStyledItemDelegate(parent)
{
}
@@ -44,15 +44,8 @@ void PreviewListDelegate::paint(QPainter *painter, const QStyleOptionViewItem &o
{
painter->save();
- QStyleOptionViewItem opt = QItemDelegate::setOptions(index, option);
- drawBackground(painter, opt, index);
-
switch (index.column())
{
- case PreviewSelectDialog::SIZE:
- QItemDelegate::drawDisplay(painter, opt, option.rect, Utils::Misc::friendlyUnit(index.data().toLongLong()));
- break;
-
case PreviewSelectDialog::PROGRESS:
{
const qreal progress = (index.data().toReal() * 100);
@@ -65,7 +58,7 @@ void PreviewListDelegate::paint(QPainter *painter, const QStyleOptionViewItem &o
break;
default:
- QItemDelegate::paint(painter, option, index);
+ QStyledItemDelegate::paint(painter, option, index);
break;
}
diff --git a/src/gui/previewlistdelegate.h b/src/gui/previewlistdelegate.h
index ac0daab21..b9fc286f6 100644
--- a/src/gui/previewlistdelegate.h
+++ b/src/gui/previewlistdelegate.h
@@ -1,5 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
+ * Copyright (C) 2023 Vladimir Golovnev
* Copyright (C) 2006 Christophe Dumez
*
* This program is free software; you can redistribute it and/or
@@ -28,11 +29,11 @@
#pragma once
-#include
+#include
#include "progressbarpainter.h"
-class PreviewListDelegate final : public QItemDelegate
+class PreviewListDelegate final : public QStyledItemDelegate
{
Q_OBJECT
Q_DISABLE_COPY_MOVE(PreviewListDelegate)
diff --git a/src/gui/previewselectdialog.cpp b/src/gui/previewselectdialog.cpp
index 21546e081..f79ff54f4 100644
--- a/src/gui/previewselectdialog.cpp
+++ b/src/gui/previewselectdialog.cpp
@@ -1,5 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
+ * Copyright (C) 2023 Vladimir Golovnev
* Copyright (C) 2011 Christophe Dumez
*
* This program is free software; you can redistribute it and/or
@@ -70,16 +71,19 @@ PreviewSelectDialog::PreviewSelectDialog(QWidget *parent, const BitTorrent::Torr
const Preferences *pref = Preferences::instance();
// Preview list
- m_previewListModel = new QStandardItemModel(0, NB_COLUMNS, this);
- m_previewListModel->setHeaderData(NAME, Qt::Horizontal, tr("Name"));
- m_previewListModel->setHeaderData(SIZE, Qt::Horizontal, tr("Size"));
- m_previewListModel->setHeaderData(PROGRESS, Qt::Horizontal, tr("Progress"));
+ auto *previewListModel = new QStandardItemModel(0, NB_COLUMNS, this);
+ previewListModel->setHeaderData(NAME, Qt::Horizontal, tr("Name"));
+ previewListModel->setHeaderData(SIZE, Qt::Horizontal, tr("Size"));
+ previewListModel->setHeaderData(PROGRESS, Qt::Horizontal, tr("Progress"));
m_ui->previewList->setAlternatingRowColors(pref->useAlternatingRowColors());
- m_ui->previewList->setModel(m_previewListModel);
+ m_ui->previewList->setUniformRowHeights(true);
+ m_ui->previewList->setModel(previewListModel);
m_ui->previewList->hideColumn(FILE_INDEX);
- m_listDelegate = new PreviewListDelegate(this);
- m_ui->previewList->setItemDelegate(m_listDelegate);
+
+ auto *listDelegate = new PreviewListDelegate(this);
+ m_ui->previewList->setItemDelegate(listDelegate);
+
// Fill list in
const QVector fp = torrent->filesProgress();
for (int i = 0; i < torrent->filesCount(); ++i)
@@ -87,20 +91,20 @@ PreviewSelectDialog::PreviewSelectDialog(QWidget *parent, const BitTorrent::Torr
const Path filePath = torrent->filePath(i);
if (Utils::Misc::isPreviewable(filePath))
{
- int row = m_previewListModel->rowCount();
- m_previewListModel->insertRow(row);
- m_previewListModel->setData(m_previewListModel->index(row, NAME), filePath.filename());
- m_previewListModel->setData(m_previewListModel->index(row, SIZE), torrent->fileSize(i));
- m_previewListModel->setData(m_previewListModel->index(row, PROGRESS), fp[i]);
- m_previewListModel->setData(m_previewListModel->index(row, FILE_INDEX), i);
+ int row = previewListModel->rowCount();
+ previewListModel->insertRow(row);
+ previewListModel->setData(previewListModel->index(row, NAME), filePath.filename());
+ previewListModel->setData(previewListModel->index(row, SIZE), Utils::Misc::friendlyUnit(torrent->fileSize(i)));
+ previewListModel->setData(previewListModel->index(row, PROGRESS), fp[i]);
+ previewListModel->setData(previewListModel->index(row, FILE_INDEX), i);
}
}
- m_previewListModel->sort(NAME);
+ previewListModel->sort(NAME);
m_ui->previewList->header()->setContextMenuPolicy(Qt::CustomContextMenu);
m_ui->previewList->header()->setFirstSectionMovable(true);
m_ui->previewList->header()->setSortIndicator(0, Qt::AscendingOrder);
- m_ui->previewList->selectionModel()->select(m_previewListModel->index(0, NAME), QItemSelectionModel::Select | QItemSelectionModel::Rows);
+ m_ui->previewList->selectionModel()->select(previewListModel->index(0, NAME), QItemSelectionModel::Select | QItemSelectionModel::Rows);
connect(m_ui->previewList->header(), &QWidget::customContextMenuRequested, this, &PreviewSelectDialog::displayColumnHeaderMenu);
@@ -129,7 +133,7 @@ void PreviewSelectDialog::previewButtonClicked()
// File
if (!path.exists())
{
- const bool isSingleFile = (m_previewListModel->rowCount() == 1);
+ const bool isSingleFile = (m_ui->previewList->model()->rowCount() == 1);
QWidget *parent = isSingleFile ? this->parentWidget() : this;
QMessageBox::critical(parent, tr("Preview impossible")
, tr("Sorry, we can't preview this file: \"%1\".").arg(path.toString()));
@@ -199,6 +203,6 @@ void PreviewSelectDialog::showEvent(QShowEvent *event)
}
// Only one file, no choice
- if (m_previewListModel->rowCount() <= 1)
+ if (m_ui->previewList->model()->rowCount() <= 1)
previewButtonClicked();
}
diff --git a/src/gui/previewselectdialog.h b/src/gui/previewselectdialog.h
index 76a42bad7..7f4a1d985 100644
--- a/src/gui/previewselectdialog.h
+++ b/src/gui/previewselectdialog.h
@@ -1,5 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
+ * Copyright (C) 2023 Vladimir Golovnev
* Copyright (C) 2011 Christophe Dumez
*
* This program is free software; you can redistribute it and/or
@@ -33,8 +34,6 @@
#include "base/path.h"
#include "base/settingvalue.h"
-class QStandardItemModel;
-
namespace BitTorrent
{
class Torrent;
@@ -44,7 +43,6 @@ namespace Ui
{
class PreviewSelectDialog;
}
-class PreviewListDelegate;
class PreviewSelectDialog final : public QDialog
{
@@ -79,8 +77,6 @@ private:
void saveWindowState();
Ui::PreviewSelectDialog *m_ui = nullptr;
- QStandardItemModel *m_previewListModel = nullptr;
- PreviewListDelegate *m_listDelegate = nullptr;
const BitTorrent::Torrent *m_torrent = nullptr;
bool m_headerStateInitialized = false;
diff --git a/src/gui/programupdater.cpp b/src/gui/programupdater.cpp
index 5fa4abf18..c6ef71d7d 100644
--- a/src/gui/programupdater.cpp
+++ b/src/gui/programupdater.cpp
@@ -29,6 +29,9 @@
#include "programupdater.h"
+#include
+
+#include
#include
#if defined(Q_OS_WIN)
@@ -71,6 +74,22 @@ namespace
}
return (newVersion > currentVersion);
}
+
+ QString buildVariant()
+ {
+#if defined(Q_OS_MACOS)
+ const auto BASE_OS = u"Mac OS X"_s;
+#elif defined(Q_OS_WIN)
+ const auto BASE_OS = (::IsWindows7OrGreater() && QSysInfo::currentCpuArchitecture().endsWith(u"64"))
+ ? u"Windows x64"_s
+ : u"Windows"_s;
+#endif
+
+ if constexpr ((QT_VERSION_MAJOR == 6) && (LIBTORRENT_VERSION_MAJOR == 1))
+ return BASE_OS;
+
+ return u"%1 (qt%2 lt%3%4)"_s.arg(BASE_OS, QString::number(QT_VERSION_MAJOR), QString::number(LIBTORRENT_VERSION_MAJOR), QString::number(LIBTORRENT_VERSION_MINOR));
+ }
}
void ProgramUpdater::checkForUpdates() const
@@ -107,14 +126,7 @@ void ProgramUpdater::rssDownloadFinished(const Net::DownloadResult &result)
: QString {};
};
-#ifdef Q_OS_MACOS
- const QString OS_TYPE = u"Mac OS X"_s;
-#elif defined(Q_OS_WIN)
- const QString OS_TYPE = (::IsWindows7OrGreater() && QSysInfo::currentCpuArchitecture().endsWith(u"64"))
- ? u"Windows x64"_s
- : u"Windows"_s;
-#endif
-
+ const QString variant = buildVariant();
bool inItem = false;
QString version;
QString updateLink;
@@ -140,7 +152,7 @@ void ProgramUpdater::rssDownloadFinished(const Net::DownloadResult &result)
{
if (inItem && (xml.name() == u"item"))
{
- if (type.compare(OS_TYPE, Qt::CaseInsensitive) == 0)
+ if (type.compare(variant, Qt::CaseInsensitive) == 0)
{
qDebug("The last update available is %s", qUtf8Printable(version));
if (!version.isEmpty())
diff --git a/src/gui/properties/propertieswidget.cpp b/src/gui/properties/propertieswidget.cpp
index 572da74d9..9c74c9ed4 100644
--- a/src/gui/properties/propertieswidget.cpp
+++ b/src/gui/properties/propertieswidget.cpp
@@ -82,6 +82,7 @@ PropertiesWidget::PropertiesWidget(QWidget *parent)
m_ui->contentFilterLayout->insertWidget(3, m_contentFilterLine);
m_ui->filesList->setDoubleClickAction(TorrentContentWidget::DoubleClickAction::Open);
+ m_ui->filesList->setOpenByEnterKey(true);
// SIGNAL/SLOTS
connect(m_ui->selectAllButton, &QPushButton::clicked, m_ui->filesList, &TorrentContentWidget::checkAll);
@@ -290,6 +291,8 @@ void PropertiesWidget::loadTorrentInfos(BitTorrent::Torrent *const torrent)
// Info hashes
m_ui->labelInfohash1Val->setText(m_torrent->infoHash().v1().isValid() ? m_torrent->infoHash().v1().toString() : tr("N/A"));
m_ui->labelInfohash2Val->setText(m_torrent->infoHash().v2().isValid() ? m_torrent->infoHash().v2().toString() : tr("N/A"));
+ // URL seeds
+ loadUrlSeeds();
if (m_torrent->hasMetadata())
{
// Creation date
@@ -300,9 +303,6 @@ void PropertiesWidget::loadTorrentInfos(BitTorrent::Torrent *const torrent)
// Comment
m_ui->labelCommentVal->setText(Utils::Misc::parseHtmlLinks(m_torrent->comment().toHtmlEscaped()));
- // URL seeds
- loadUrlSeeds();
-
m_ui->labelCreatedByVal->setText(m_torrent->creator());
}
// Load dynamic data
diff --git a/src/gui/properties/propertieswidget.ui b/src/gui/properties/propertieswidget.ui
index 01123af83..2dcae04c2 100644
--- a/src/gui/properties/propertieswidget.ui
+++ b/src/gui/properties/propertieswidget.ui
@@ -192,7 +192,7 @@
-
+
@@ -382,7 +382,7 @@
-
+
diff --git a/src/gui/rss/articlelistwidget.cpp b/src/gui/rss/articlelistwidget.cpp
index 599a4d886..e20c0b1fd 100644
--- a/src/gui/rss/articlelistwidget.cpp
+++ b/src/gui/rss/articlelistwidget.cpp
@@ -104,7 +104,7 @@ void ArticleListWidget::handleArticleRead(RSS::Article *rssArticle)
const QBrush foregroundBrush {UIThemeManager::instance()->getColor(u"RSS.ReadArticle"_s)};
item->setData(Qt::ForegroundRole, foregroundBrush);
- item->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"loading"_s, u"sphere"_s));
+ item->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"rss_read_article"_s, u"sphere"_s));
checkInvariant();
}
@@ -131,13 +131,13 @@ QListWidgetItem *ArticleListWidget::createItem(RSS::Article *article) const
{
const QBrush foregroundBrush {UIThemeManager::instance()->getColor(u"RSS.ReadArticle"_s)};
item->setData(Qt::ForegroundRole, foregroundBrush);
- item->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"loading"_s, u"sphere"_s));
+ item->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"rss_read_article"_s, u"sphere"_s));
}
else
{
const QBrush foregroundBrush {UIThemeManager::instance()->getColor(u"RSS.UnreadArticle"_s)};
item->setData(Qt::ForegroundRole, foregroundBrush);
- item->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"loading"_s, u"sphere"_s));
+ item->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"rss_unread_article"_s, u"sphere"_s));
}
return item;
diff --git a/src/gui/search/searchjobwidget.cpp b/src/gui/search/searchjobwidget.cpp
index c3a8c1815..bddaefa21 100644
--- a/src/gui/search/searchjobwidget.cpp
+++ b/src/gui/search/searchjobwidget.cpp
@@ -128,9 +128,9 @@ SearchJobWidget::SearchJobWidget(SearchHandler *searchHandler, QWidget *parent)
m_lineEditSearchResultsFilter->setPlaceholderText(tr("Filter search results..."));
m_lineEditSearchResultsFilter->setContextMenuPolicy(Qt::CustomContextMenu);
connect(m_lineEditSearchResultsFilter, &QWidget::customContextMenuRequested, this, &SearchJobWidget::showFilterContextMenu);
+ connect(m_lineEditSearchResultsFilter, &LineEdit::textChanged, this, &SearchJobWidget::filterSearchResults);
m_ui->horizontalLayout->insertWidget(0, m_lineEditSearchResultsFilter);
- connect(m_lineEditSearchResultsFilter, &LineEdit::textChanged, this, &SearchJobWidget::filterSearchResults);
connect(m_ui->filterMode, qOverload(&QComboBox::currentIndexChanged)
, this, &SearchJobWidget::updateFilter);
connect(m_ui->minSeeds, &QAbstractSpinBox::editingFinished, this, &SearchJobWidget::updateFilter);
@@ -292,7 +292,7 @@ void SearchJobWidget::addTorrentToSession(const QString &source, const AddTorren
if (source.isEmpty()) return;
if ((option == AddTorrentOption::ShowDialog) || ((option == AddTorrentOption::Default) && AddNewTorrentDialog::isEnabled()))
- AddNewTorrentDialog::show(source, this);
+ AddNewTorrentDialog::show(source, window());
else
BitTorrent::Session::instance()->addTorrent(source);
}
diff --git a/src/gui/torrentcontentwidget.cpp b/src/gui/torrentcontentwidget.cpp
index 594fe76f1..0fbc2fb15 100644
--- a/src/gui/torrentcontentwidget.cpp
+++ b/src/gui/torrentcontentwidget.cpp
@@ -56,6 +56,19 @@
#include "gui/macutilities.h"
#endif
+namespace
+{
+ QList toPersistentIndexes(const QModelIndexList &indexes)
+ {
+ QList persistentIndexes;
+ persistentIndexes.reserve(indexes.size());
+ for (const QModelIndex &index : indexes)
+ persistentIndexes.append(index);
+
+ return persistentIndexes;
+ }
+}
+
TorrentContentWidget::TorrentContentWidget(QWidget *parent)
: QTreeView(parent)
{
@@ -89,10 +102,6 @@ TorrentContentWidget::TorrentContentWidget(QWidget *parent)
const auto *renameFileHotkey = new QShortcut(Qt::Key_F2, this, nullptr, nullptr, Qt::WidgetShortcut);
connect(renameFileHotkey, &QShortcut::activated, this, &TorrentContentWidget::renameSelectedFile);
- const auto *openFileHotkeyReturn = new QShortcut(Qt::Key_Return, this, nullptr, nullptr, Qt::WidgetShortcut);
- connect(openFileHotkeyReturn, &QShortcut::activated, this, &TorrentContentWidget::openSelectedFile);
- const auto *openFileHotkeyEnter = new QShortcut(Qt::Key_Enter, this, nullptr, nullptr, Qt::WidgetShortcut);
- connect(openFileHotkeyEnter, &QShortcut::activated, this, &TorrentContentWidget::openSelectedFile);
connect(model(), &QAbstractItemModel::modelReset, this, &TorrentContentWidget::expandRecursively);
}
@@ -118,6 +127,32 @@ void TorrentContentWidget::refresh()
setUpdatesEnabled(true);
}
+bool TorrentContentWidget::openByEnterKey() const
+{
+ return m_openFileHotkeyEnter;
+}
+
+void TorrentContentWidget::setOpenByEnterKey(const bool value)
+{
+ if (value == openByEnterKey())
+ return;
+
+ if (value)
+ {
+ m_openFileHotkeyReturn = new QShortcut(Qt::Key_Return, this, nullptr, nullptr, Qt::WidgetShortcut);
+ connect(m_openFileHotkeyReturn, &QShortcut::activated, this, &TorrentContentWidget::openSelectedFile);
+ m_openFileHotkeyEnter = new QShortcut(Qt::Key_Enter, this, nullptr, nullptr, Qt::WidgetShortcut);
+ connect(m_openFileHotkeyEnter, &QShortcut::activated, this, &TorrentContentWidget::openSelectedFile);
+ }
+ else
+ {
+ delete m_openFileHotkeyEnter;
+ m_openFileHotkeyEnter = nullptr;
+ delete m_openFileHotkeyReturn;
+ m_openFileHotkeyReturn = nullptr;
+ }
+}
+
TorrentContentWidget::DoubleClickAction TorrentContentWidget::doubleClickAction() const
{
return m_doubleClickAction;
@@ -197,9 +232,9 @@ void TorrentContentWidget::keyPressEvent(QKeyEvent *event)
const Qt::CheckState state = (static_cast(value.toInt()) == Qt::Checked)
? Qt::Unchecked : Qt::Checked;
- const QModelIndexList selection = selectionModel()->selectedRows(TorrentContentModelItem::COL_NAME);
+ const QList selection = toPersistentIndexes(selectionModel()->selectedRows(TorrentContentModelItem::COL_NAME));
- for (const QModelIndex &index : selection)
+ for (const QPersistentModelIndex &index : selection)
model()->setData(index, state, Qt::CheckStateRole);
}
@@ -226,10 +261,10 @@ void TorrentContentWidget::renameSelectedFile()
void TorrentContentWidget::applyPriorities(const BitTorrent::DownloadPriority priority)
{
- const QModelIndexList selectedRows = selectionModel()->selectedRows(0);
- for (const QModelIndex &index : selectedRows)
+ const QList selectedRows = toPersistentIndexes(selectionModel()->selectedRows(Priority));
+ for (const QPersistentModelIndex &index : selectedRows)
{
- model()->setData(index.sibling(index.row(), Priority), static_cast(priority));
+ model()->setData(index, static_cast(priority));
}
}
@@ -239,7 +274,7 @@ void TorrentContentWidget::applyPrioritiesByOrder()
// a download priority that will apply to each item. The number of groups depends on how
// many "download priority" are available to be assigned
- const QModelIndexList selectedRows = selectionModel()->selectedRows(0);
+ const QList selectedRows = toPersistentIndexes(selectionModel()->selectedRows(Priority));
const qsizetype priorityGroups = 3;
const auto priorityGroupSize = std::max((selectedRows.length() / priorityGroups), 1);
@@ -261,8 +296,8 @@ void TorrentContentWidget::applyPrioritiesByOrder()
break;
}
- const QModelIndex &index = selectedRows[i];
- model()->setData(index.sibling(index.row(), Priority), static_cast(priority));
+ const QPersistentModelIndex &index = selectedRows[i];
+ model()->setData(index, static_cast(priority));
}
}
diff --git a/src/gui/torrentcontentwidget.h b/src/gui/torrentcontentwidget.h
index 35620ee42..4baccb883 100644
--- a/src/gui/torrentcontentwidget.h
+++ b/src/gui/torrentcontentwidget.h
@@ -34,6 +34,8 @@
#include "base/bittorrent/downloadpriority.h"
#include "base/pathfwd.h"
+class QShortcut;
+
namespace BitTorrent
{
class Torrent;
@@ -78,6 +80,9 @@ public:
BitTorrent::TorrentContentHandler *contentHandler() const;
void refresh();
+ bool openByEnterKey() const;
+ void setOpenByEnterKey(bool value);
+
DoubleClickAction doubleClickAction() const;
void setDoubleClickAction(DoubleClickAction action);
@@ -118,4 +123,6 @@ private:
TorrentContentFilterModel *m_filterModel;
DoubleClickAction m_doubleClickAction = DoubleClickAction::Rename;
ColumnsVisibilityMode m_columnsVisibilityMode = ColumnsVisibilityMode::Editable;
+ QShortcut *m_openFileHotkeyEnter = nullptr;
+ QShortcut *m_openFileHotkeyReturn = nullptr;
};
diff --git a/src/gui/torrentoptionsdialog.cpp b/src/gui/torrentoptionsdialog.cpp
index 5d6ae2552..a618453da 100644
--- a/src/gui/torrentoptionsdialog.cpp
+++ b/src/gui/torrentoptionsdialog.cpp
@@ -285,7 +285,8 @@ TorrentOptionsDialog::TorrentOptionsDialog(QWidget *parent, const QVector
+ * Copyright (C) 2023-2024 Vladimir Golovnev
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@@ -37,6 +37,7 @@
#include "base/global.h"
#include "autoexpandabledialog.h"
#include "flowlayout.h"
+#include "utils.h"
#include "ui_torrenttagsdialog.h"
@@ -52,10 +53,10 @@ TorrentTagsDialog::TorrentTagsDialog(const TagSet &initialTags, QWidget *parent)
connect(m_ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(m_ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
- auto *tagsLayout = new FlowLayout(m_ui->scrollArea);
+ auto *tagsLayout = new FlowLayout(m_ui->scrollArea->widget());
for (const QString &tag : asConst(initialTags.united(BitTorrent::Session::instance()->tags())))
{
- auto *tagWidget = new QCheckBox(tag);
+ auto *tagWidget = new QCheckBox(Utils::Gui::tagToWidgetText(tag));
if (initialTags.contains(tag))
tagWidget->setChecked(true);
tagsLayout->addWidget(tagWidget);
@@ -78,12 +79,12 @@ TorrentTagsDialog::~TorrentTagsDialog()
TagSet TorrentTagsDialog::tags() const
{
TagSet tags;
- auto *layout = m_ui->scrollArea->layout();
+ auto *layout = m_ui->scrollArea->widget()->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());
+ tags.insert(Utils::Gui::widgetTextToTag(tagWidget->text()));
}
return tags;
@@ -111,9 +112,9 @@ void TorrentTagsDialog::addNewTag()
}
else
{
- auto *layout = m_ui->scrollArea->layout();
+ auto *layout = m_ui->scrollArea->widget()->layout();
auto *btn = layout->takeAt(layout->count() - 1);
- auto *tagWidget = new QCheckBox(tag);
+ auto *tagWidget = new QCheckBox(Utils::Gui::tagToWidgetText(tag));
tagWidget->setChecked(true);
layout->addWidget(tagWidget);
layout->addItem(btn);
diff --git a/src/gui/transferlistfilters/categoryfiltermodel.cpp b/src/gui/transferlistfilters/categoryfiltermodel.cpp
index 3ce7714c6..473aeddf0 100644
--- a/src/gui/transferlistfilters/categoryfiltermodel.cpp
+++ b/src/gui/transferlistfilters/categoryfiltermodel.cpp
@@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
- * Copyright (C) 2016 Vladimir Golovnev
+ * Copyright (C) 2016-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
@@ -38,6 +38,9 @@
class CategoryModelItem
{
public:
+ inline static const QString UID_ALL {QChar(1)};
+ inline static const QString UID_UNCATEGORIZED;
+
CategoryModelItem() = default;
CategoryModelItem(CategoryModelItem *parent, const QString &categoryName, const int torrentsCount = 0)
@@ -99,9 +102,21 @@ public:
int pos() const
{
- if (!m_parent) return -1;
+ if (!m_parent)
+ return -1;
- return m_parent->m_childUids.indexOf(m_name);
+ if (const int posByName = m_parent->m_childUids.indexOf(m_name); posByName >= 0)
+ return posByName;
+
+ // special cases
+ if (this == m_parent->m_children[UID_ALL])
+ return 0;
+
+ if (this == m_parent->m_children[UID_UNCATEGORIZED])
+ return 1;
+
+ Q_ASSERT(false);
+ return -1;
}
bool hasChild(const QString &name) const
@@ -202,7 +217,8 @@ int CategoryFilterModel::columnCount(const QModelIndex &) const
QVariant CategoryFilterModel::data(const QModelIndex &index, int role) const
{
- if (!index.isValid()) return {};
+ if (!index.isValid())
+ return {};
const auto *item = static_cast(index.internalPointer());
@@ -248,8 +264,8 @@ QModelIndex CategoryFilterModel::index(int row, int column, const QModelIndex &p
if (parent.isValid() && (parent.column() != 0))
return {};
- auto *parentItem = parent.isValid() ? static_cast(parent.internalPointer())
- : m_rootItem;
+ auto *parentItem = parent.isValid()
+ ? static_cast(parent.internalPointer()) : m_rootItem;
if (row < parentItem->childCount())
return createIndex(row, column, parentItem->childAt(row));
@@ -262,7 +278,8 @@ QModelIndex CategoryFilterModel::parent(const QModelIndex &index) const
return {};
auto *item = static_cast(index.internalPointer());
- if (!item) return {};
+ if (!item)
+ return {};
return this->index(item->parent());
}
@@ -276,7 +293,8 @@ int CategoryFilterModel::rowCount(const QModelIndex &parent) const
return m_rootItem->childCount();
auto *item = static_cast(parent.internalPointer());
- if (!item) return 0;
+ if (!item)
+ return 0;
return item->childCount();
}
@@ -288,13 +306,16 @@ QModelIndex CategoryFilterModel::index(const QString &categoryName) const
QString CategoryFilterModel::categoryName(const QModelIndex &index) const
{
- if (!index.isValid()) return {};
+ if (!index.isValid())
+ return {};
+
return static_cast(index.internalPointer())->fullName();
}
QModelIndex CategoryFilterModel::index(CategoryModelItem *item) const
{
- if (!item || !item->parent()) return {};
+ if (!item || !item->parent())
+ return {};
return index(item->pos(), 0, index(item->parent()));
}
@@ -337,8 +358,17 @@ void CategoryFilterModel::torrentsLoaded(const QVector &t
Q_ASSERT(item);
item->increaseTorrentsCount();
+ QModelIndex i = index(item);
+ while (i.isValid())
+ {
+ emit dataChanged(i, i);
+ i = parent(i);
+ }
+
m_rootItem->childAt(0)->increaseTorrentsCount();
}
+
+ emit dataChanged(index(0, 0), index(0, 0));
}
void CategoryFilterModel::torrentAboutToBeRemoved(BitTorrent::Torrent *const torrent)
@@ -347,18 +377,24 @@ void CategoryFilterModel::torrentAboutToBeRemoved(BitTorrent::Torrent *const tor
Q_ASSERT(item);
item->decreaseTorrentsCount();
+ QModelIndex i = index(item);
+ while (i.isValid())
+ {
+ emit dataChanged(i, i);
+ i = parent(i);
+ }
+
m_rootItem->childAt(0)->decreaseTorrentsCount();
+ emit dataChanged(index(0, 0), index(0, 0));
}
void CategoryFilterModel::torrentCategoryChanged(BitTorrent::Torrent *const torrent, const QString &oldCategory)
{
- QModelIndex i;
-
auto *item = findItem(oldCategory);
Q_ASSERT(item);
item->decreaseTorrentsCount();
- i = index(item);
+ QModelIndex i = index(item);
while (i.isValid())
{
emit dataChanged(i, i);
@@ -392,17 +428,16 @@ void CategoryFilterModel::populate()
const 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()));
+ m_rootItem->addChild(CategoryModelItem::UID_ALL
+ , new CategoryModelItem(nullptr, tr("All"), torrents.count()));
// Uncategorized torrents
using Torrent = BitTorrent::Torrent;
const int torrentsCount = std::count_if(torrents.begin(), torrents.end()
- , [](Torrent *torrent) { return torrent->category().isEmpty(); });
- m_rootItem->addChild(UID_UNCATEGORIZED, new CategoryModelItem(nullptr, tr("Uncategorized"), torrentsCount));
+ , [](Torrent *torrent) { return torrent->category().isEmpty(); });
+ m_rootItem->addChild(CategoryModelItem::UID_UNCATEGORIZED
+ , new CategoryModelItem(nullptr, tr("Uncategorized"), torrentsCount));
using BitTorrent::Torrent;
if (m_isSubcategoriesEnabled)
@@ -446,7 +481,9 @@ CategoryModelItem *CategoryFilterModel::findItem(const QString &fullName) const
for (const QString &subcat : asConst(BitTorrent::Session::expandCategory(fullName)))
{
const QString subcatName = shortName(subcat);
- if (!item->hasChild(subcatName)) return nullptr;
+ if (!item->hasChild(subcatName))
+ return nullptr;
+
item = item->child(subcatName);
}
diff --git a/src/gui/transferlistfilters/categoryfiltermodel.h b/src/gui/transferlistfilters/categoryfiltermodel.h
index 9576f094a..8aa3425cf 100644
--- a/src/gui/transferlistfilters/categoryfiltermodel.h
+++ b/src/gui/transferlistfilters/categoryfiltermodel.h
@@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
- * Copyright (C) 2016 Vladimir Golovnev
+ * Copyright (C) 2016-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
diff --git a/src/gui/transferlistfilters/statusfilterwidget.cpp b/src/gui/transferlistfilters/statusfilterwidget.cpp
index 99b00af76..ff77e034a 100644
--- a/src/gui/transferlistfilters/statusfilterwidget.cpp
+++ b/src/gui/transferlistfilters/statusfilterwidget.cpp
@@ -95,7 +95,7 @@ StatusFilterWidget::StatusFilterWidget(QWidget *parent, TransferListWidget *tran
connect(pref, &Preferences::changed, this, &StatusFilterWidget::configure);
const int storedRow = pref->getTransSelFilter();
- if (item((storedRow < count()) ? storedRow : 0)->isHidden())
+ if (item(((storedRow >= 0) && (storedRow < count())) ? storedRow : 0)->isHidden())
setCurrentRow(TorrentFilter::All, QItemSelectionModel::SelectCurrent);
else
setCurrentRow(storedRow, QItemSelectionModel::SelectCurrent);
@@ -235,10 +235,7 @@ void StatusFilterWidget::applyFilter(int row)
void StatusFilterWidget::handleTorrentsLoaded(const QVector &torrents)
{
- for (const BitTorrent::Torrent *torrent : torrents)
- updateTorrentStatus(torrent);
-
- updateTexts();
+ update(torrents);
}
void StatusFilterWidget::torrentAboutToBeDeleted(BitTorrent::Torrent *const torrent)
@@ -273,6 +270,12 @@ void StatusFilterWidget::torrentAboutToBeDeleted(BitTorrent::Torrent *const torr
m_nbStalled = m_nbStalledUploading + m_nbStalledDownloading;
updateTexts();
+
+ if (Preferences::instance()->getHideZeroStatusFilters())
+ {
+ hideZeroItems();
+ updateGeometry();
+ }
}
void StatusFilterWidget::configure()
diff --git a/src/gui/transferlistfilters/tagfiltermodel.cpp b/src/gui/transferlistfilters/tagfiltermodel.cpp
index 35c74a42c..92b1c294c 100644
--- a/src/gui/transferlistfilters/tagfiltermodel.cpp
+++ b/src/gui/transferlistfilters/tagfiltermodel.cpp
@@ -1,5 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
+ * Copyright (C) 2023 Vladimir Golovnev
* Copyright (C) 2017 Tony Gregerson
*
* This program is free software; you can redistribute it and/or
@@ -36,6 +37,9 @@
#include "base/global.h"
#include "gui/uithememanager.h"
+const int ROW_ALL = 0;
+const int ROW_UNTAGGED = 1;
+
namespace
{
QString getSpecialAllTag()
@@ -203,21 +207,29 @@ void TagFilterModel::tagRemoved(const QString &tag)
void TagFilterModel::torrentTagAdded(BitTorrent::Torrent *const torrent, const QString &tag)
{
if (torrent->tags().count() == 1)
+ {
untaggedItem()->decreaseTorrentsCount();
+ const QModelIndex i = index(ROW_UNTAGGED, 0);
+ emit dataChanged(i, i);
+ }
const int row = findRow(tag);
Q_ASSERT(isValidRow(row));
TagModelItem &item = m_tagItems[row];
item.increaseTorrentsCount();
- const QModelIndex i = index(row, 0, QModelIndex());
+ const QModelIndex i = index(row, 0);
emit dataChanged(i, i);
}
void TagFilterModel::torrentTagRemoved(BitTorrent::Torrent *const torrent, const QString &tag)
{
if (torrent->tags().empty())
+ {
untaggedItem()->increaseTorrentsCount();
+ const QModelIndex i = index(ROW_UNTAGGED, 0);
+ emit dataChanged(i, i);
+ }
const int row = findRow(tag);
if (row < 0)
@@ -225,7 +237,7 @@ void TagFilterModel::torrentTagRemoved(BitTorrent::Torrent *const torrent, const
m_tagItems[row].decreaseTorrentsCount();
- const QModelIndex i = index(row, 0, QModelIndex());
+ const QModelIndex i = index(row, 0);
emit dataChanged(i, i);
}
@@ -242,17 +254,39 @@ void TagFilterModel::torrentsLoaded(const QVector &torren
for (TagModelItem *item : items)
item->increaseTorrentsCount();
}
+
+ emit dataChanged(index(0, 0), index((rowCount() - 1), 0));
}
void TagFilterModel::torrentAboutToBeRemoved(BitTorrent::Torrent *const torrent)
{
allTagsItem()->decreaseTorrentsCount();
- if (torrent->tags().isEmpty())
- untaggedItem()->decreaseTorrentsCount();
+ {
+ const QModelIndex i = index(ROW_ALL, 0);
+ emit dataChanged(i, i);
+ }
- for (TagModelItem *item : asConst(findItems(torrent->tags())))
- item->decreaseTorrentsCount();
+ if (torrent->tags().isEmpty())
+ {
+ untaggedItem()->decreaseTorrentsCount();
+ const QModelIndex i = index(ROW_UNTAGGED, 0);
+ emit dataChanged(i, i);
+ }
+ else
+ {
+ for (const QString &tag : asConst(torrent->tags()))
+ {
+ const int row = findRow(tag);
+ Q_ASSERT(isValidRow(row));
+ if (Q_UNLIKELY(!isValidRow(row)))
+ continue;
+
+ m_tagItems[row].decreaseTorrentsCount();
+ const QModelIndex i = index(row, 0);
+ emit dataChanged(i, i);
+ }
+ }
}
QString TagFilterModel::tagDisplayName(const QString &tag)
@@ -299,11 +333,15 @@ void TagFilterModel::removeFromModel(int row)
int TagFilterModel::findRow(const QString &tag) const
{
+ if (!BitTorrent::Session::isValidTag(tag))
+ return -1;
+
for (int i = 0; i < m_tagItems.size(); ++i)
{
if (m_tagItems[i].tag() == tag)
return i;
}
+
return -1;
}
@@ -333,11 +371,11 @@ QVector TagFilterModel::findItems(const TagSet &tags)
TagModelItem *TagFilterModel::allTagsItem()
{
Q_ASSERT(!m_tagItems.isEmpty());
- return &m_tagItems[0];
+ return &m_tagItems[ROW_ALL];
}
TagModelItem *TagFilterModel::untaggedItem()
{
- Q_ASSERT(m_tagItems.size() > 1);
- return &m_tagItems[1];
+ Q_ASSERT(m_tagItems.size() > ROW_UNTAGGED);
+ return &m_tagItems[ROW_UNTAGGED];
}
diff --git a/src/gui/transferlistfilters/tagfiltermodel.h b/src/gui/transferlistfilters/tagfiltermodel.h
index 577530ac8..d7033941d 100644
--- a/src/gui/transferlistfilters/tagfiltermodel.h
+++ b/src/gui/transferlistfilters/tagfiltermodel.h
@@ -1,5 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
+ * Copyright (C) 2023 Vladimir Golovnev
* Copyright (C) 2017 Tony Gregerson
*
* This program is free software; you can redistribute it and/or
diff --git a/src/gui/transferlistmodel.cpp b/src/gui/transferlistmodel.cpp
index 9011a9d6f..c59604b2b 100644
--- a/src/gui/transferlistmodel.cpp
+++ b/src/gui/transferlistmodel.cpp
@@ -117,6 +117,7 @@ TransferListModel::TransferListModel(QObject *parent)
, m_completedIcon {UIThemeManager::instance()->getIcon(u"checked-completed"_s, u"completed"_s)}
, m_downloadingIcon {UIThemeManager::instance()->getIcon(u"downloading"_s)}
, m_errorIcon {UIThemeManager::instance()->getIcon(u"error"_s)}
+ , m_movingIcon {UIThemeManager::instance()->getIcon(u"set-location"_s)}
, m_pausedIcon {UIThemeManager::instance()->getIcon(u"stopped"_s, u"media-playback-pause"_s)}
, m_queuedIcon {UIThemeManager::instance()->getIcon(u"queued"_s)}
, m_stalledDLIcon {UIThemeManager::instance()->getIcon(u"stalledDL"_s)}
@@ -710,8 +711,9 @@ QIcon TransferListModel::getIconByState(const BitTorrent::TorrentState state) co
case BitTorrent::TorrentState::CheckingDownloading:
case BitTorrent::TorrentState::CheckingUploading:
case BitTorrent::TorrentState::CheckingResumeData:
- case BitTorrent::TorrentState::Moving:
return m_checkingIcon;
+ case BitTorrent::TorrentState::Moving:
+ return m_movingIcon;
case BitTorrent::TorrentState::Unknown:
case BitTorrent::TorrentState::MissingFiles:
case BitTorrent::TorrentState::Error:
diff --git a/src/gui/transferlistmodel.h b/src/gui/transferlistmodel.h
index f0d7e8ad1..e2da9f007 100644
--- a/src/gui/transferlistmodel.h
+++ b/src/gui/transferlistmodel.h
@@ -137,6 +137,7 @@ private:
QIcon m_completedIcon;
QIcon m_downloadingIcon;
QIcon m_errorIcon;
+ QIcon m_movingIcon;
QIcon m_pausedIcon;
QIcon m_queuedIcon;
QIcon m_stalledDLIcon;
diff --git a/src/gui/transferlistwidget.cpp b/src/gui/transferlistwidget.cpp
index c46b65a03..7fc717ece 100644
--- a/src/gui/transferlistwidget.cpp
+++ b/src/gui/transferlistwidget.cpp
@@ -1,5 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
+ * Copyright (C) 2023-2024 Vladimir Golovnev
* Copyright (C) 2006 Christophe Dumez
*
* This program is free software; you can redistribute it and/or
@@ -100,13 +101,15 @@ namespace
void openDestinationFolder(const BitTorrent::Torrent *const torrent)
{
+ const Path contentPath = torrent->contentPath();
+ const Path openedPath = (!contentPath.isEmpty() ? contentPath : torrent->savePath());
#ifdef Q_OS_MACOS
- MacUtils::openFiles({torrent->contentPath()});
+ MacUtils::openFiles({openedPath});
#else
if (torrent->filesCount() == 1)
- Utils::Gui::openFolderSelect(torrent->contentPath());
+ Utils::Gui::openFolderSelect(openedPath);
else
- Utils::Gui::openPath(torrent->contentPath());
+ Utils::Gui::openPath(openedPath);
#endif
}
@@ -253,6 +256,16 @@ QModelIndex TransferListWidget::mapToSource(const QModelIndex &index) const
return index;
}
+QModelIndexList TransferListWidget::mapToSource(const QModelIndexList &indexes) const
+{
+ QModelIndexList result;
+ result.reserve(indexes.size());
+ for (const QModelIndex &index : indexes)
+ result.append(mapToSource(index));
+
+ return result;
+}
+
QModelIndex TransferListWidget::mapFromSource(const QModelIndex &index) const
{
Q_ASSERT(index.isValid());
@@ -263,11 +276,13 @@ QModelIndex TransferListWidget::mapFromSource(const QModelIndex &index) const
void TransferListWidget::torrentDoubleClicked()
{
const QModelIndexList selectedIndexes = selectionModel()->selectedRows();
- if ((selectedIndexes.size() != 1) || !selectedIndexes.first().isValid()) return;
+ if ((selectedIndexes.size() != 1) || !selectedIndexes.first().isValid())
+ return;
const QModelIndex index = m_listModel->index(mapToSource(selectedIndexes.first()).row());
BitTorrent::Torrent *const torrent = m_listModel->torrentHandle(index);
- if (!torrent) return;
+ if (!torrent)
+ return;
int action;
if (torrent->isFinished())
@@ -575,21 +590,22 @@ void TransferListWidget::openSelectedTorrentsFolder() const
for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
{
const Path contentPath = torrent->contentPath();
- paths.insert(contentPath);
+ paths.insert(!contentPath.isEmpty() ? contentPath : torrent->savePath());
}
MacUtils::openFiles(PathList(paths.cbegin(), paths.cend()));
#else
for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
{
const Path contentPath = torrent->contentPath();
- if (!paths.contains(contentPath))
+ const Path openedPath = (!contentPath.isEmpty() ? contentPath : torrent->savePath());
+ if (!paths.contains(openedPath))
{
if (torrent->filesCount() == 1)
- Utils::Gui::openFolderSelect(contentPath);
+ Utils::Gui::openFolderSelect(openedPath);
else
- Utils::Gui::openPath(contentPath);
+ Utils::Gui::openPath(openedPath);
}
- paths.insert(contentPath);
+ paths.insert(openedPath);
}
#endif // Q_OS_MACOS
}
@@ -806,7 +822,8 @@ void TransferListWidget::exportTorrent()
bool hasError = false;
for (const BitTorrent::Torrent *torrent : torrents)
{
- const Path filePath = savePath / Path(torrent->name() + u".torrent");
+ const QString validName = Utils::Fs::toValidFileName(torrent->name(), u"_"_s);
+ const Path filePath = savePath / Path(validName + u".torrent");
if (filePath.exists())
{
LogMsg(errorMsg.arg(torrent->name(), filePath.toString(), tr("A file with the same name already exists")) , Log::WARNING);
@@ -871,9 +888,13 @@ QStringList TransferListWidget::askTagsForSelection(const QString &dialogTitle)
void TransferListWidget::applyToSelectedTorrents(const std::function &fn)
{
- for (const QModelIndex &index : asConst(selectionModel()->selectedRows()))
+ // Changing the data may affect the layout of the sort/filter model, which in turn may invalidate
+ // the indexes previously obtained from selection model before we process them all.
+ // Therefore, we must map all the selected indexes to source before start processing them.
+ const QModelIndexList sourceRows = mapToSource(selectionModel()->selectedRows());
+ for (const QModelIndex &index : sourceRows)
{
- BitTorrent::Torrent *const torrent = m_listModel->torrentHandle(mapToSource(index));
+ BitTorrent::Torrent *const torrent = m_listModel->torrentHandle(index);
Q_ASSERT(torrent);
fn(torrent);
}
@@ -882,11 +903,13 @@ void TransferListWidget::applyToSelectedTorrents(const std::functionselectedRows();
- if ((selectedIndexes.size() != 1) || !selectedIndexes.first().isValid()) return;
+ if ((selectedIndexes.size() != 1) || !selectedIndexes.first().isValid())
+ return;
const QModelIndex mi = m_listModel->index(mapToSource(selectedIndexes.first()).row(), TransferListModel::TR_NAME);
BitTorrent::Torrent *const torrent = m_listModel->torrentHandle(mi);
- if (!torrent) return;
+ if (!torrent)
+ return;
// Ask for a new Name
bool ok = false;
@@ -901,8 +924,7 @@ void TransferListWidget::renameSelectedTorrent()
void TransferListWidget::setSelectionCategory(const QString &category)
{
- for (const QModelIndex &index : asConst(selectionModel()->selectedRows()))
- m_listModel->setData(m_listModel->index(mapToSource(index).row(), TransferListModel::TR_CATEGORY), category, Qt::DisplayRole);
+ applyToSelectedTorrents([&category](BitTorrent::Torrent *torrent) { torrent->setCategory(category); });
}
void TransferListWidget::addSelectionTag(const QString &tag)
@@ -923,7 +945,8 @@ void TransferListWidget::clearSelectionTags()
void TransferListWidget::displayListMenu()
{
const QModelIndexList selectedIndexes = selectionModel()->selectedRows();
- if (selectedIndexes.isEmpty()) return;
+ if (selectedIndexes.isEmpty())
+ return;
auto *listMenu = new QMenu(this);
listMenu->setAttribute(Qt::WA_DeleteOnClose);
@@ -1169,7 +1192,7 @@ void TransferListWidget::displayListMenu()
for (const QString &tag : asConst(tags))
{
- auto *action = new TriStateAction(tag, tagsMenu);
+ auto *action = new TriStateAction(Utils::Gui::tagToWidgetText(tag), tagsMenu);
action->setCloseOnInteraction(false);
const Qt::CheckState initialState = tagsInAll.contains(tag) ? Qt::Checked
@@ -1308,9 +1331,10 @@ void TransferListWidget::applyFilter(const QString &name, const TransferListMode
m_sortFilterModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption));
}
-void TransferListWidget::applyStatusFilter(int f)
+void TransferListWidget::applyStatusFilter(const int filterIndex)
{
- m_sortFilterModel->setStatusFilter(static_cast(f));
+ const auto filterType = static_cast(filterIndex);
+ m_sortFilterModel->setStatusFilter(((filterType >= TorrentFilter::All) && (filterType < TorrentFilter::_Count)) ? filterType : TorrentFilter::All);
// Select first item if nothing is selected
if (selectionModel()->selectedRows(0).empty() && (m_sortFilterModel->rowCount() > 0))
{
diff --git a/src/gui/transferlistwidget.h b/src/gui/transferlistwidget.h
index 205b21246..84a965d4e 100644
--- a/src/gui/transferlistwidget.h
+++ b/src/gui/transferlistwidget.h
@@ -93,7 +93,7 @@ public slots:
void previewSelectedTorrents();
void hideQueuePosColumn(bool hide);
void applyFilter(const QString &name, const TransferListModel::Column &type);
- void applyStatusFilter(int f);
+ void applyStatusFilter(int filterIndex);
void applyCategoryFilter(const QString &category);
void applyTagFilter(const QString &tag);
void applyTrackerFilterAll();
@@ -119,6 +119,7 @@ private slots:
private:
void wheelEvent(QWheelEvent *event) override;
QModelIndex mapToSource(const QModelIndex &index) const;
+ QModelIndexList mapToSource(const QModelIndexList &indexes) const;
QModelIndex mapFromSource(const QModelIndex &index) const;
bool loadSettings();
QVector getSelectedTorrents() const;
diff --git a/src/gui/uithemecommon.h b/src/gui/uithemecommon.h
index 087357395..64c50d659 100644
--- a/src/gui/uithemecommon.h
+++ b/src/gui/uithemecommon.h
@@ -149,6 +149,8 @@ inline QSet defaultUIThemeIcons()
u"queued"_s,
u"ratio"_s,
u"reannounce"_s,
+ u"rss_read_article"_s,
+ u"rss_unread_article"_s,
u"security-high"_s,
u"security-low"_s,
u"set-location"_s,
diff --git a/src/gui/uithemesource.cpp b/src/gui/uithemesource.cpp
index 8565c6414..c4e4ef7f4 100644
--- a/src/gui/uithemesource.cpp
+++ b/src/gui/uithemesource.cpp
@@ -161,13 +161,13 @@ void DefaultThemeSource::loadColors()
return;
}
- const QByteArray configData = readResult.value();
+ const QByteArray &configData = readResult.value();
if (configData.isEmpty())
return;
const QJsonObject config = parseThemeConfig(configData);
- QHash lightModeColorOverrides = colorsFromJSON(config.value(KEY_COLORS_LIGHT).toObject());
+ const QHash lightModeColorOverrides = colorsFromJSON(config.value(KEY_COLORS_LIGHT).toObject());
for (auto overridesIt = lightModeColorOverrides.cbegin(); overridesIt != lightModeColorOverrides.cend(); ++overridesIt)
{
auto it = m_colors.find(overridesIt.key());
@@ -175,7 +175,7 @@ void DefaultThemeSource::loadColors()
it.value().light = overridesIt.value();
}
- QHash darkModeColorOverrides = colorsFromJSON(config.value(KEY_COLORS_DARK).toObject());
+ const QHash darkModeColorOverrides = colorsFromJSON(config.value(KEY_COLORS_DARK).toObject());
for (auto overridesIt = darkModeColorOverrides.cbegin(); overridesIt != darkModeColorOverrides.cend(); ++overridesIt)
{
auto it = m_colors.find(overridesIt.key());
@@ -184,6 +184,12 @@ void DefaultThemeSource::loadColors()
}
}
+CustomThemeSource::CustomThemeSource(const Path &themeRootPath)
+ : m_themeRootPath {themeRootPath}
+{
+ loadColors();
+}
+
QColor CustomThemeSource::getColor(const QString &colorId, const ColorMode colorMode) const
{
if (colorMode == ColorMode::Dark)
@@ -246,6 +252,11 @@ DefaultThemeSource *CustomThemeSource::defaultThemeSource() const
return m_defaultThemeSource.get();
}
+Path CustomThemeSource::themeRootPath() const
+{
+ return m_themeRootPath;
+}
+
void CustomThemeSource::loadColors()
{
const auto readResult = Utils::IO::readFile((themeRootPath() / Path(CONFIG_FILE_NAME)), FILE_MAX_SIZE, QIODevice::Text);
@@ -257,7 +268,7 @@ void CustomThemeSource::loadColors()
return;
}
- const QByteArray configData = readResult.value();
+ const QByteArray &configData = readResult.value();
if (configData.isEmpty())
return;
@@ -267,13 +278,9 @@ void CustomThemeSource::loadColors()
m_darkModeColors.insert(colorsFromJSON(config.value(KEY_COLORS_DARK).toObject()));
}
-Path QRCThemeSource::themeRootPath() const
-{
- return Path(u":/uitheme"_s);
-}
-
FolderThemeSource::FolderThemeSource(const Path &folderPath)
- : m_folder {folderPath}
+ : CustomThemeSource(folderPath)
+ , m_folder {folderPath}
{
}
@@ -285,10 +292,10 @@ QByteArray FolderThemeSource::readStyleSheet()
const QString stylesheetResourcesDir = u":/uitheme"_s;
QByteArray styleSheetData = CustomThemeSource::readStyleSheet();
- return styleSheetData.replace(stylesheetResourcesDir.toUtf8(), themeRootPath().data().toUtf8());
+ return styleSheetData.replace(stylesheetResourcesDir.toUtf8(), m_folder.data().toUtf8());
}
-Path FolderThemeSource::themeRootPath() const
+QRCThemeSource::QRCThemeSource()
+ : CustomThemeSource(Path(u":/uitheme"_s))
{
- return m_folder;
}
diff --git a/src/gui/uithemesource.h b/src/gui/uithemesource.h
index 4969c3cee..5c0920303 100644
--- a/src/gui/uithemesource.h
+++ b/src/gui/uithemesource.h
@@ -84,21 +84,24 @@ public:
QByteArray readStyleSheet() override;
protected:
- virtual Path themeRootPath() const = 0;
+ explicit CustomThemeSource(const Path &themeRootPath);
+
DefaultThemeSource *defaultThemeSource() const;
private:
+ Path themeRootPath() const;
void loadColors();
const std::unique_ptr m_defaultThemeSource = std::make_unique();
+ Path m_themeRootPath;
QHash m_colors;
QHash m_darkModeColors;
};
class QRCThemeSource final : public CustomThemeSource
{
-private:
- Path themeRootPath() const override;
+public:
+ QRCThemeSource();
};
class FolderThemeSource : public CustomThemeSource
@@ -109,7 +112,5 @@ public:
QByteArray readStyleSheet() override;
private:
- Path themeRootPath() const override;
-
const Path m_folder;
};
diff --git a/src/gui/utils.cpp b/src/gui/utils.cpp
index 9688c5f72..4a775689f 100644
--- a/src/gui/utils.cpp
+++ b/src/gui/utils.cpp
@@ -1,5 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
+ * Copyright (C) 2024 Vladimir Golovnev
* Copyright (C) 2017 Mike Tzou
*
* This program is free software; you can redistribute it and/or
@@ -218,3 +219,29 @@ void Utils::Gui::openFolderSelect(const Path &path)
openPath(path.parentPath());
#endif
}
+
+QString Utils::Gui::tagToWidgetText(const QString &tag)
+{
+ return QString(tag).replace(u'&', u"&&"_s);
+}
+
+QString Utils::Gui::widgetTextToTag(const QString &text)
+{
+ // replace pairs of '&' with single '&' and remove non-paired occurrences of '&'
+ QString cleanedText;
+ cleanedText.reserve(text.size());
+ bool amp = false;
+ for (const QChar c : text)
+ {
+ if (c == u'&')
+ {
+ amp = !amp;
+ if (amp)
+ continue;
+ }
+
+ cleanedText.append(c);
+ }
+
+ return cleanedText;
+}
diff --git a/src/gui/utils.h b/src/gui/utils.h
index 34cfd58ee..79e415c49 100644
--- a/src/gui/utils.h
+++ b/src/gui/utils.h
@@ -1,5 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
+ * Copyright (C) 2024 Vladimir Golovnev
* Copyright (C) 2017 Mike Tzou
*
* This program is free software; you can redistribute it and/or
@@ -34,6 +35,7 @@ class QIcon;
class QPixmap;
class QPoint;
class QSize;
+class QString;
class QWidget;
namespace Utils::Gui
@@ -51,4 +53,7 @@ namespace Utils::Gui
void openPath(const Path &path);
void openFolderSelect(const Path &path);
+
+ QString tagToWidgetText(const QString &tag);
+ QString widgetTextToTag(const QString &text);
}
diff --git a/src/icons/flags/ac.svg b/src/icons/flags/ac.svg
index 1a6d50805..b1ae9ac52 100644
--- a/src/icons/flags/ac.svg
+++ b/src/icons/flags/ac.svg
@@ -1,73 +1,686 @@
diff --git a/src/icons/flags/af.svg b/src/icons/flags/af.svg
index 6e755396f..417dd0476 100644
--- a/src/icons/flags/af.svg
+++ b/src/icons/flags/af.svg
@@ -14,7 +14,7 @@
-
+
@@ -61,7 +61,7 @@
-
+
diff --git a/src/icons/flags/ag.svg b/src/icons/flags/ag.svg
index 875f9753a..250b50126 100644
--- a/src/icons/flags/ag.svg
+++ b/src/icons/flags/ag.svg
@@ -1,14 +1,14 @@
diff --git a/src/icons/flags/ai.svg b/src/icons/flags/ai.svg
index cf91b39b9..81a857d5b 100644
--- a/src/icons/flags/ai.svg
+++ b/src/icons/flags/ai.svg
@@ -1,758 +1,29 @@
-