/* * Bittorrent Client using Qt and libtorrent. * Copyright (C) 2023 Vladimir Golovnev * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * * In addition, as a special exception, the copyright holders give permission to * link this program with the OpenSSL project's "OpenSSL" library (or with * modified versions of it that use the same license as the "OpenSSL" library), * and distribute the linked executables. You must obey the GNU General Public * License in all respects for all of the code used other than "OpenSSL". If you * modify file(s), you may extend this exception to your version of the file(s), * but you are not obligated to do so. If you do not wish to do so, delete this * exception statement from your version. */ #include "uithemedialog.h" #include #include #include #include #include #include #include #include #include #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 defaultColors = defaultUIThemeColors(); const QList 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 defaultIcons = defaultUIThemeIcons(); const QList iconIDs = std::invoke([](auto &&list) { list.sort(); return list; } , QList(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 defaultColors = defaultUIThemeColors(); const auto addColorOverrides = [this, &defaultColors, &userConfig](const ColorMode colorMode) { const QHash &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 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 &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; }