mirror of
https://github.com/qbittorrent/qBittorrent.git
synced 2025-01-05 06:37:26 +03:00
d41a77841d
PR #18709.
395 lines
12 KiB
C++
395 lines
12 KiB
C++
/*
|
|
* Bittorrent Client using Qt and libtorrent.
|
|
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru>
|
|
*
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation; either version 2
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
*
|
|
* In addition, as a special exception, the copyright holders give permission to
|
|
* link this program with the OpenSSL project's "OpenSSL" library (or with
|
|
* modified versions of it that use the same license as the "OpenSSL" library),
|
|
* and distribute the linked executables. You must obey the GNU General Public
|
|
* License in all respects for all of the code used other than "OpenSSL". If you
|
|
* modify file(s), you may extend this exception to your version of the file(s),
|
|
* but you are not obligated to do so. If you do not wish to do so, delete this
|
|
* exception statement from your version.
|
|
*/
|
|
|
|
#include "uithemedialog.h"
|
|
|
|
#include <QColor>
|
|
#include <QColorDialog>
|
|
#include <QFile>
|
|
#include <QFileDialog>
|
|
#include <QJsonDocument>
|
|
#include <QJsonObject>
|
|
#include <QLabel>
|
|
#include <QMenu>
|
|
#include <QMessageBox>
|
|
|
|
#include "base/3rdparty/expected.hpp"
|
|
#include "base/global.h"
|
|
#include "base/logger.h"
|
|
#include "base/path.h"
|
|
#include "base/profile.h"
|
|
#include "base/utils/fs.h"
|
|
#include "base/utils/io.h"
|
|
#include "uithemecommon.h"
|
|
#include "utils.h"
|
|
|
|
#include "ui_uithemedialog.h"
|
|
|
|
#define SETTINGS_KEY(name) u"GUI/UIThemeDialog/" name
|
|
|
|
namespace
|
|
{
|
|
Path userConfigPath()
|
|
{
|
|
return specialFolderLocation(SpecialFolder::Config) / Path(u"themes/default"_qs);
|
|
}
|
|
|
|
Path defaultIconPath(const QString &iconID, [[maybe_unused]] const ColorMode colorMode)
|
|
{
|
|
return Path(u":icons"_qs) / Path(iconID + u".svg");
|
|
}
|
|
}
|
|
|
|
class ColorWidget final : public QFrame
|
|
{
|
|
Q_DISABLE_COPY_MOVE(ColorWidget)
|
|
|
|
public:
|
|
explicit ColorWidget(const QColor ¤tColor, const QColor &defaultColor, QWidget *parent = nullptr)
|
|
: QFrame(parent)
|
|
, m_defaultColor {defaultColor}
|
|
{
|
|
setObjectName(u"colorWidget"_qs);
|
|
setFrameShape(QFrame::Box);
|
|
setFrameShadow(QFrame::Plain);
|
|
|
|
setCurrentColor(currentColor);
|
|
}
|
|
|
|
QColor currentColor() const
|
|
{
|
|
return m_currentColor;
|
|
}
|
|
|
|
private:
|
|
void mouseDoubleClickEvent([[maybe_unused]] QMouseEvent *event) override
|
|
{
|
|
showColorDialog();
|
|
}
|
|
|
|
void contextMenuEvent([[maybe_unused]] QContextMenuEvent *event) override
|
|
{
|
|
QMenu *menu = new QMenu(this);
|
|
menu->setAttribute(Qt::WA_DeleteOnClose);
|
|
|
|
menu->addAction(tr("Edit..."), this, &ColorWidget::showColorDialog);
|
|
menu->addAction(tr("Reset"), this, &ColorWidget::resetColor);
|
|
|
|
menu->popup(QCursor::pos());
|
|
}
|
|
|
|
void setCurrentColor(const QColor &color)
|
|
{
|
|
if (m_currentColor == color)
|
|
return;
|
|
|
|
m_currentColor = color;
|
|
applyColor(m_currentColor);
|
|
}
|
|
|
|
void resetColor()
|
|
{
|
|
setCurrentColor(m_defaultColor);
|
|
}
|
|
|
|
void applyColor(const QColor &color)
|
|
{
|
|
setStyleSheet(u"#colorWidget { background-color: %1; }"_qs.arg(color.name()));
|
|
}
|
|
|
|
void showColorDialog()
|
|
{
|
|
auto dialog = new QColorDialog(m_currentColor, this);
|
|
dialog->setAttribute(Qt::WA_DeleteOnClose);
|
|
connect(dialog, &QDialog::accepted, this, [this, dialog]
|
|
{
|
|
setCurrentColor(dialog->currentColor());
|
|
});
|
|
|
|
dialog->open();
|
|
}
|
|
|
|
const QColor m_defaultColor;
|
|
QColor m_currentColor;
|
|
};
|
|
|
|
class IconWidget final : public QLabel
|
|
{
|
|
Q_DISABLE_COPY_MOVE(IconWidget)
|
|
|
|
public:
|
|
explicit IconWidget(const Path ¤tPath, const Path &defaultPath, QWidget *parent = nullptr)
|
|
: QLabel(parent)
|
|
, m_defaultPath {defaultPath}
|
|
{
|
|
setObjectName(u"iconWidget"_qs);
|
|
setAlignment(Qt::AlignCenter);
|
|
|
|
setCurrentPath(currentPath);
|
|
}
|
|
|
|
Path currentPath() const
|
|
{
|
|
return m_currentPath;
|
|
}
|
|
|
|
private:
|
|
void mouseDoubleClickEvent([[maybe_unused]] QMouseEvent *event) override
|
|
{
|
|
showFileDialog();
|
|
}
|
|
|
|
void contextMenuEvent([[maybe_unused]] QContextMenuEvent *event) override
|
|
{
|
|
QMenu *menu = new QMenu(this);
|
|
menu->setAttribute(Qt::WA_DeleteOnClose);
|
|
|
|
menu->addAction(tr("Browse..."), this, &IconWidget::showFileDialog);
|
|
menu->addAction(tr("Reset"), this, &IconWidget::resetIcon);
|
|
|
|
menu->popup(QCursor::pos());
|
|
}
|
|
|
|
void setCurrentPath(const Path &path)
|
|
{
|
|
if (m_currentPath == path)
|
|
return;
|
|
|
|
m_currentPath = path;
|
|
showIcon(m_currentPath);
|
|
}
|
|
|
|
void resetIcon()
|
|
{
|
|
setCurrentPath(m_defaultPath);
|
|
}
|
|
|
|
void showIcon(const Path &iconPath)
|
|
{
|
|
const QIcon icon {iconPath.data()};
|
|
setPixmap(icon.pixmap(Utils::Gui::smallIconSize()));
|
|
}
|
|
|
|
void showFileDialog()
|
|
{
|
|
auto *dialog = new QFileDialog(this, tr("Select icon")
|
|
, QDir::homePath(), (tr("Supported image files") + u" (*.svg *.png)"));
|
|
dialog->setFileMode(QFileDialog::ExistingFile);
|
|
dialog->setAttribute(Qt::WA_DeleteOnClose);
|
|
connect(dialog, &QDialog::accepted, this, [this, dialog]
|
|
{
|
|
const Path iconPath {dialog->selectedFiles().value(0)};
|
|
setCurrentPath(iconPath);
|
|
});
|
|
|
|
dialog->open();
|
|
}
|
|
|
|
const Path m_defaultPath;
|
|
Path m_currentPath;
|
|
};
|
|
|
|
UIThemeDialog::UIThemeDialog(QWidget *parent)
|
|
: QDialog(parent)
|
|
, m_ui {new Ui::UIThemeDialog}
|
|
, m_storeDialogSize {SETTINGS_KEY(u"Size"_qs)}
|
|
{
|
|
m_ui->setupUi(this);
|
|
|
|
loadColors();
|
|
loadIcons();
|
|
|
|
if (const QSize dialogSize = m_storeDialogSize; dialogSize.isValid())
|
|
resize(dialogSize);
|
|
}
|
|
|
|
UIThemeDialog::~UIThemeDialog()
|
|
{
|
|
m_storeDialogSize = size();
|
|
delete m_ui;
|
|
}
|
|
|
|
void UIThemeDialog::accept()
|
|
{
|
|
QDialog::accept();
|
|
|
|
bool hasError = false;
|
|
if (!storeColors())
|
|
hasError = true;
|
|
if (!storeIcons())
|
|
hasError = true;
|
|
|
|
if (hasError)
|
|
{
|
|
QMessageBox::critical(this, tr("UI Theme Configuration.")
|
|
, tr("The UI Theme changes could not be fully applied. The details can be found in the Log."));
|
|
}
|
|
}
|
|
|
|
void UIThemeDialog::loadColors()
|
|
{
|
|
const QHash<QString, UIThemeColor> defaultColors = defaultUIThemeColors();
|
|
const QList<QString> colorIDs = std::invoke([](auto &&list) { list.sort(); return list; }, defaultColors.keys());
|
|
int row = 2;
|
|
for (const QString &id : colorIDs)
|
|
{
|
|
m_ui->colorsLayout->addWidget(new QLabel(id), row, 0);
|
|
|
|
const UIThemeColor &defaultColor = defaultColors.value(id);
|
|
|
|
auto *lightColorWidget = new ColorWidget(m_defaultThemeSource.getColor(id, ColorMode::Light), defaultColor.light, this);
|
|
m_lightColorWidgets.insert(id, lightColorWidget);
|
|
m_ui->colorsLayout->addWidget(lightColorWidget, row, 2);
|
|
|
|
auto *darkColorWidget = new ColorWidget(m_defaultThemeSource.getColor(id, ColorMode::Dark), defaultColor.dark, this);
|
|
m_darkColorWidgets.insert(id, darkColorWidget);
|
|
m_ui->colorsLayout->addWidget(darkColorWidget, row, 4);
|
|
|
|
++row;
|
|
}
|
|
}
|
|
|
|
void UIThemeDialog::loadIcons()
|
|
{
|
|
const QSet<QString> defaultIcons = defaultUIThemeIcons();
|
|
const QList<QString> iconIDs = std::invoke([](auto &&list) { list.sort(); return list; }
|
|
, QList<QString>(defaultIcons.cbegin(), defaultIcons.cend()));
|
|
int row = 2;
|
|
for (const QString &id : iconIDs)
|
|
{
|
|
m_ui->iconsLayout->addWidget(new QLabel(id), row, 0);
|
|
|
|
auto *lightIconWidget = new IconWidget(m_defaultThemeSource.getIconPath(id, ColorMode::Light)
|
|
, defaultIconPath(id, ColorMode::Light), this);
|
|
m_lightIconWidgets.insert(id, lightIconWidget);
|
|
m_ui->iconsLayout->addWidget(lightIconWidget, row, 2);
|
|
|
|
auto *darkIconWidget = new IconWidget(m_defaultThemeSource.getIconPath(id, ColorMode::Dark)
|
|
, defaultIconPath(id, ColorMode::Dark), this);
|
|
m_darkIconWidgets.insert(id, darkIconWidget);
|
|
m_ui->iconsLayout->addWidget(darkIconWidget, row, 4);
|
|
|
|
++row;
|
|
}
|
|
}
|
|
|
|
bool UIThemeDialog::storeColors()
|
|
{
|
|
QJsonObject userConfig;
|
|
userConfig.insert(u"version", 2);
|
|
|
|
const QHash<QString, UIThemeColor> defaultColors = defaultUIThemeColors();
|
|
const auto addColorOverrides = [this, &defaultColors, &userConfig](const ColorMode colorMode)
|
|
{
|
|
const QHash<QString, ColorWidget *> &colorWidgets = (colorMode == ColorMode::Light)
|
|
? m_lightColorWidgets : m_darkColorWidgets;
|
|
|
|
QJsonObject colors;
|
|
for (auto it = colorWidgets.cbegin(); it != colorWidgets.cend(); ++it)
|
|
{
|
|
const QString &colorID = it.key();
|
|
const QColor &defaultColor = (colorMode == ColorMode::Light)
|
|
? defaultColors.value(colorID).light : defaultColors.value(colorID).dark;
|
|
const QColor &color = it.value()->currentColor();
|
|
if (color != defaultColor)
|
|
colors.insert(it.key(), color.name());
|
|
}
|
|
|
|
if (!colors.isEmpty())
|
|
userConfig.insert(((colorMode == ColorMode::Light) ? KEY_COLORS_LIGHT : KEY_COLORS_DARK), colors);
|
|
};
|
|
|
|
addColorOverrides(ColorMode::Light);
|
|
addColorOverrides(ColorMode::Dark);
|
|
|
|
const QByteArray configData = QJsonDocument(userConfig).toJson();
|
|
const nonstd::expected<void, QString> result = Utils::IO::saveToFile((userConfigPath() / Path(CONFIG_FILE_NAME)), configData);
|
|
if (!result)
|
|
{
|
|
const QString error = tr("Couldn't save UI Theme configuration. Reason: %1").arg(result.error());
|
|
LogMsg(error, Log::WARNING);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool UIThemeDialog::storeIcons()
|
|
{
|
|
bool hasError = false;
|
|
|
|
const auto updateIcons = [this, &hasError](const ColorMode colorMode)
|
|
{
|
|
const QHash<QString, IconWidget *> &iconWidgets = (colorMode == ColorMode::Light)
|
|
? m_lightIconWidgets : m_darkIconWidgets;
|
|
const Path subdirPath = (colorMode == ColorMode::Light)
|
|
? Path(u"icons/light"_qs) : Path(u"icons/dark"_qs);
|
|
|
|
for (auto it = iconWidgets.cbegin(); it != iconWidgets.cend(); ++it)
|
|
{
|
|
const QString &id = it.key();
|
|
const Path &path = it.value()->currentPath();
|
|
if (path == m_defaultThemeSource.getIconPath(id, colorMode))
|
|
continue;
|
|
|
|
const Path &userIconPathBase = userConfigPath() / subdirPath / Path(id);
|
|
|
|
if (const Path oldIconPath = userIconPathBase + u".svg"
|
|
; path.exists() && !Utils::Fs::removeFile(oldIconPath))
|
|
{
|
|
const QString error = tr("Couldn't remove icon file. File: %1.").arg(oldIconPath.toString());
|
|
LogMsg(error, Log::WARNING);
|
|
hasError = true;
|
|
continue;
|
|
}
|
|
|
|
if (const Path oldIconPath = userIconPathBase + u".png"
|
|
; path.exists() && !Utils::Fs::removeFile(oldIconPath))
|
|
{
|
|
const QString error = tr("Couldn't remove icon file. File: %1.").arg(oldIconPath.toString());
|
|
LogMsg(error, Log::WARNING);
|
|
hasError = true;
|
|
continue;
|
|
}
|
|
|
|
if (const Path targetPath = userIconPathBase + path.extension()
|
|
; !Utils::Fs::copyFile(path, targetPath))
|
|
{
|
|
const QString error = tr("Couldn't copy icon file. Source: %1. Destination: %2.")
|
|
.arg(path.toString(), targetPath.toString());
|
|
LogMsg(error, Log::WARNING);
|
|
hasError = true;
|
|
}
|
|
}
|
|
};
|
|
|
|
updateIcons(ColorMode::Light);
|
|
updateIcons(ColorMode::Dark);
|
|
|
|
return !hasError;
|
|
}
|