Vladimir Golovnev
2 years ago
committed by
GitHub
15 changed files with 1381 additions and 380 deletions
@ -0,0 +1,181 @@ |
|||||||
|
/*
|
||||||
|
* Bittorrent Client using Qt and libtorrent. |
||||||
|
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru> |
||||||
|
* |
||||||
|
* This program is free software; you can redistribute it and/or |
||||||
|
* modify it under the terms of the GNU General Public License |
||||||
|
* as published by the Free Software Foundation; either version 2 |
||||||
|
* of the License, or (at your option) any later version. |
||||||
|
* |
||||||
|
* This program is distributed in the hope that it will be useful, |
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
* GNU General Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU General Public License |
||||||
|
* along with this program; if not, write to the Free Software |
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, |
||||||
|
* USA. |
||||||
|
* |
||||||
|
* In addition, as a special exception, the copyright holders give permission to |
||||||
|
* link this program with the OpenSSL project's "OpenSSL" library (or with |
||||||
|
* modified versions of it that use the same license as the "OpenSSL" library), |
||||||
|
* and distribute the linked executables. You must obey the GNU General Public |
||||||
|
* License in all respects for all of the code used other than "OpenSSL". If |
||||||
|
* you modify file(s), you may extend this exception to your version of the |
||||||
|
* file(s), but you are not obligated to do so. If you do not wish to do so, |
||||||
|
* delete this exception statement from your version. |
||||||
|
*/ |
||||||
|
|
||||||
|
#pragma once |
||||||
|
|
||||||
|
#include <QtGlobal> |
||||||
|
#include <QApplication> |
||||||
|
#include <QColor> |
||||||
|
#include <QHash> |
||||||
|
#include <QPalette> |
||||||
|
#include <QSet> |
||||||
|
#include <QString> |
||||||
|
|
||||||
|
#include "base/global.h" |
||||||
|
#include "color.h" |
||||||
|
|
||||||
|
inline const QString CONFIG_FILE_NAME = u"config.json"_qs; |
||||||
|
inline const QString STYLESHEET_FILE_NAME = u"stylesheet.qss"_qs; |
||||||
|
inline const QString KEY_COLORS = u"colors"_qs; |
||||||
|
inline const QString KEY_COLORS_LIGHT = u"colors.light"_qs; |
||||||
|
inline const QString KEY_COLORS_DARK = u"colors.dark"_qs; |
||||||
|
|
||||||
|
struct UIThemeColor |
||||||
|
{ |
||||||
|
QColor light; |
||||||
|
QColor dark; |
||||||
|
}; |
||||||
|
|
||||||
|
inline QHash<QString, UIThemeColor> defaultUIThemeColors() |
||||||
|
{ |
||||||
|
const QPalette palette = QApplication::palette(); |
||||||
|
return { |
||||||
|
{u"Log.TimeStamp"_qs, {Color::Primer::Light::fgSubtle, Color::Primer::Dark::fgSubtle}}, |
||||||
|
{u"Log.Normal"_qs, {palette.color(QPalette::Active, QPalette::WindowText), palette.color(QPalette::Active, QPalette::WindowText)}}, |
||||||
|
{u"Log.Info"_qs, {Color::Primer::Light::accentFg, Color::Primer::Dark::accentFg}}, |
||||||
|
{u"Log.Warning"_qs, {Color::Primer::Light::severeFg, Color::Primer::Dark::severeFg}}, |
||||||
|
{u"Log.Critical"_qs, {Color::Primer::Light::dangerFg, Color::Primer::Dark::dangerFg}}, |
||||||
|
{u"Log.BannedPeer"_qs, {Color::Primer::Light::dangerFg, Color::Primer::Dark::dangerFg}}, |
||||||
|
|
||||||
|
{u"RSS.ReadArticle"_qs, {palette.color(QPalette::Inactive, QPalette::WindowText), palette.color(QPalette::Inactive, QPalette::WindowText)}}, |
||||||
|
{u"RSS.UnreadArticle"_qs, {palette.color(QPalette::Active, QPalette::Link), palette.color(QPalette::Active, QPalette::Link)}}, |
||||||
|
|
||||||
|
{u"TransferList.Downloading"_qs, {Color::Primer::Light::successFg, Color::Primer::Dark::successFg}}, |
||||||
|
{u"TransferList.StalledDownloading"_qs, {Color::Primer::Light::successEmphasis, Color::Primer::Dark::successEmphasis}}, |
||||||
|
{u"TransferList.DownloadingMetadata"_qs, {Color::Primer::Light::successFg, Color::Primer::Dark::successFg}}, |
||||||
|
{u"TransferList.ForcedDownloadingMetadata"_qs, {Color::Primer::Light::successFg, Color::Primer::Dark::successFg}}, |
||||||
|
{u"TransferList.ForcedDownloading"_qs, {Color::Primer::Light::successFg, Color::Primer::Dark::successFg}}, |
||||||
|
{u"TransferList.Uploading"_qs, {Color::Primer::Light::accentFg, Color::Primer::Dark::accentFg}}, |
||||||
|
{u"TransferList.StalledUploading"_qs, {Color::Primer::Light::accentEmphasis, Color::Primer::Dark::accentEmphasis}}, |
||||||
|
{u"TransferList.ForcedUploading"_qs, {Color::Primer::Light::accentFg, Color::Primer::Dark::accentFg}}, |
||||||
|
{u"TransferList.QueuedDownloading"_qs, {Color::Primer::Light::scaleYellow6, Color::Primer::Dark::scaleYellow6}}, |
||||||
|
{u"TransferList.QueuedUploading"_qs, {Color::Primer::Light::scaleYellow6, Color::Primer::Dark::scaleYellow6}}, |
||||||
|
{u"TransferList.CheckingDownloading"_qs, {Color::Primer::Light::successFg, Color::Primer::Dark::successFg}}, |
||||||
|
{u"TransferList.CheckingUploading"_qs, {Color::Primer::Light::successFg, Color::Primer::Dark::successFg}}, |
||||||
|
{u"TransferList.CheckingResumeData"_qs, {Color::Primer::Light::successFg, Color::Primer::Dark::successFg}}, |
||||||
|
{u"TransferList.PausedDownloading"_qs, {Color::Primer::Light::fgMuted, Color::Primer::Dark::fgMuted}}, |
||||||
|
{u"TransferList.PausedUploading"_qs, {Color::Primer::Light::doneFg, Color::Primer::Dark::doneFg}}, |
||||||
|
{u"TransferList.Moving"_qs, {Color::Primer::Light::successFg, Color::Primer::Dark::successFg}}, |
||||||
|
{u"TransferList.MissingFiles"_qs, {Color::Primer::Light::dangerFg, Color::Primer::Dark::dangerFg}}, |
||||||
|
{u"TransferList.Error"_qs, {Color::Primer::Light::dangerFg, Color::Primer::Dark::dangerFg}} |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
inline QSet<QString> defaultUIThemeIcons() |
||||||
|
{ |
||||||
|
return { |
||||||
|
u"application-exit"_qs, |
||||||
|
u"application-rss"_qs, |
||||||
|
u"application-url"_qs, |
||||||
|
u"browser-cookies"_qs, |
||||||
|
u"chart-line"_qs, |
||||||
|
u"checked-completed"_qs, |
||||||
|
u"configure"_qs, |
||||||
|
u"connected"_qs, |
||||||
|
u"dialog-warning"_qs, |
||||||
|
u"directory"_qs, |
||||||
|
u"disconnected"_qs, |
||||||
|
u"download"_qs, |
||||||
|
u"downloading"_qs, |
||||||
|
u"edit-clear"_qs, |
||||||
|
u"edit-copy"_qs, |
||||||
|
u"edit-find"_qs, |
||||||
|
u"edit-rename"_qs, |
||||||
|
u"error"_qs, |
||||||
|
u"fileicon"_qs, |
||||||
|
u"filter-active"_qs, |
||||||
|
u"filter-all"_qs, |
||||||
|
u"filter-inactive"_qs, |
||||||
|
u"filter-stalled"_qs, |
||||||
|
u"firewalled"_qs, |
||||||
|
u"folder-documents"_qs, |
||||||
|
u"folder-new"_qs, |
||||||
|
u"folder-remote"_qs, |
||||||
|
u"force-recheck"_qs, |
||||||
|
u"go-bottom"_qs, |
||||||
|
u"go-down"_qs, |
||||||
|
u"go-top"_qs, |
||||||
|
u"go-up"_qs, |
||||||
|
u"hash"_qs, |
||||||
|
u"help-about"_qs, |
||||||
|
u"help-contents"_qs, |
||||||
|
u"insert-link"_qs, |
||||||
|
u"ip-blocked"_qs, |
||||||
|
u"list-add"_qs, |
||||||
|
u"list-remove"_qs, |
||||||
|
u"loading"_qs, |
||||||
|
u"mail-inbox"_qs, |
||||||
|
u"name"_qs, |
||||||
|
u"network-connect"_qs, |
||||||
|
u"network-server"_qs, |
||||||
|
u"object-locked"_qs, |
||||||
|
u"peers"_qs, |
||||||
|
u"peers-add"_qs, |
||||||
|
u"peers-remove"_qs, |
||||||
|
u"plugins"_qs, |
||||||
|
u"preferences-advanced"_qs, |
||||||
|
u"preferences-bittorrent"_qs, |
||||||
|
u"preferences-desktop"_qs, |
||||||
|
u"preferences-webui"_qs, |
||||||
|
u"qbittorrent-tray"_qs, |
||||||
|
u"qbittorrent-tray-dark"_qs, |
||||||
|
u"qbittorrent-tray-light"_qs, |
||||||
|
u"queued"_qs, |
||||||
|
u"ratio"_qs, |
||||||
|
u"reannounce"_qs, |
||||||
|
u"security-high"_qs, |
||||||
|
u"security-low"_qs, |
||||||
|
u"set-location"_qs, |
||||||
|
u"slow"_qs, |
||||||
|
u"slow_off"_qs, |
||||||
|
u"speedometer"_qs, |
||||||
|
u"stalledDL"_qs, |
||||||
|
u"stalledUP"_qs, |
||||||
|
u"stopped"_qs, |
||||||
|
u"system-log-out"_qs, |
||||||
|
u"tags"_qs, |
||||||
|
u"task-complete"_qs, |
||||||
|
u"task-reject"_qs, |
||||||
|
u"torrent-creator"_qs, |
||||||
|
u"torrent-magnet"_qs, |
||||||
|
u"torrent-start"_qs, |
||||||
|
u"torrent-start-forced"_qs, |
||||||
|
u"torrent-stop"_qs, |
||||||
|
u"tracker-error"_qs, |
||||||
|
u"tracker-warning"_qs, |
||||||
|
u"trackerless"_qs, |
||||||
|
u"trackers"_qs, |
||||||
|
u"upload"_qs, |
||||||
|
u"view-categories"_qs, |
||||||
|
u"view-preview"_qs, |
||||||
|
u"view-refresh"_qs, |
||||||
|
u"view-statistics"_qs, |
||||||
|
u"wallet-open"_qs |
||||||
|
}; |
||||||
|
} |
@ -0,0 +1,388 @@ |
|||||||
|
/*
|
||||||
|
* 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" |
||||||
|
|
||||||
|
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_ui->setupUi(this); |
||||||
|
|
||||||
|
loadColors(); |
||||||
|
loadIcons(); |
||||||
|
} |
||||||
|
|
||||||
|
UIThemeDialog::~UIThemeDialog() |
||||||
|
{ |
||||||
|
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; |
||||||
|
} |
@ -0,0 +1,69 @@ |
|||||||
|
/*
|
||||||
|
* Bittorrent Client using Qt and libtorrent. |
||||||
|
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru> |
||||||
|
* |
||||||
|
* This program is free software; you can redistribute it and/or |
||||||
|
* modify it under the terms of the GNU General Public License |
||||||
|
* as published by the Free Software Foundation; either version 2 |
||||||
|
* of the License, or (at your option) any later version. |
||||||
|
* |
||||||
|
* This program is distributed in the hope that it will be useful, |
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
* GNU General Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU General Public License |
||||||
|
* along with this program; if not, write to the Free Software |
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||||
|
* |
||||||
|
* In addition, as a special exception, the copyright holders give permission to |
||||||
|
* link this program with the OpenSSL project's "OpenSSL" library (or with |
||||||
|
* modified versions of it that use the same license as the "OpenSSL" library), |
||||||
|
* and distribute the linked executables. You must obey the GNU General Public |
||||||
|
* License in all respects for all of the code used other than "OpenSSL". If you |
||||||
|
* modify file(s), you may extend this exception to your version of the file(s), |
||||||
|
* but you are not obligated to do so. If you do not wish to do so, delete this |
||||||
|
* exception statement from your version. |
||||||
|
*/ |
||||||
|
|
||||||
|
#pragma once |
||||||
|
|
||||||
|
#include <QDialog> |
||||||
|
#include <QHash> |
||||||
|
#include <QString> |
||||||
|
|
||||||
|
#include "uithemesource.h" |
||||||
|
|
||||||
|
namespace Ui |
||||||
|
{ |
||||||
|
class UIThemeDialog; |
||||||
|
} |
||||||
|
|
||||||
|
class ColorWidget; |
||||||
|
class IconWidget; |
||||||
|
|
||||||
|
class UIThemeDialog final : public QDialog |
||||||
|
{ |
||||||
|
Q_OBJECT |
||||||
|
Q_DISABLE_COPY_MOVE(UIThemeDialog) |
||||||
|
|
||||||
|
public: |
||||||
|
explicit UIThemeDialog(QWidget *parent = nullptr); |
||||||
|
~UIThemeDialog() override; |
||||||
|
|
||||||
|
void accept() override; |
||||||
|
|
||||||
|
private: |
||||||
|
void loadColors(); |
||||||
|
void loadIcons(); |
||||||
|
bool storeColors(); |
||||||
|
bool storeIcons(); |
||||||
|
|
||||||
|
Ui::UIThemeDialog *m_ui; |
||||||
|
|
||||||
|
DefaultThemeSource m_defaultThemeSource; |
||||||
|
QHash<QString, ColorWidget *> m_lightColorWidgets; |
||||||
|
QHash<QString, ColorWidget *> m_darkColorWidgets; |
||||||
|
QHash<QString, IconWidget *> m_lightIconWidgets; |
||||||
|
QHash<QString, IconWidget *> m_darkIconWidgets; |
||||||
|
}; |
@ -0,0 +1,286 @@ |
|||||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||||
|
<ui version="4.0"> |
||||||
|
<class>UIThemeDialog</class> |
||||||
|
<widget class="QDialog" name="UIThemeDialog"> |
||||||
|
<property name="geometry"> |
||||||
|
<rect> |
||||||
|
<x>0</x> |
||||||
|
<y>0</y> |
||||||
|
<width>451</width> |
||||||
|
<height>348</height> |
||||||
|
</rect> |
||||||
|
</property> |
||||||
|
<property name="windowTitle"> |
||||||
|
<string>UI Theme Configuration</string> |
||||||
|
</property> |
||||||
|
<layout class="QVBoxLayout" name="verticalLayout"> |
||||||
|
<item> |
||||||
|
<widget class="QTabWidget" name="tabWidget"> |
||||||
|
<property name="sizePolicy"> |
||||||
|
<sizepolicy hsizetype="Expanding" vsizetype="Preferred"> |
||||||
|
<horstretch>0</horstretch> |
||||||
|
<verstretch>0</verstretch> |
||||||
|
</sizepolicy> |
||||||
|
</property> |
||||||
|
<property name="currentIndex"> |
||||||
|
<number>0</number> |
||||||
|
</property> |
||||||
|
<property name="tabBarAutoHide"> |
||||||
|
<bool>true</bool> |
||||||
|
</property> |
||||||
|
<widget class="QWidget" name="colorsTab"> |
||||||
|
<attribute name="title"> |
||||||
|
<string>Colors</string> |
||||||
|
</attribute> |
||||||
|
<layout class="QVBoxLayout" name="verticalLayout_2"> |
||||||
|
<property name="leftMargin"> |
||||||
|
<number>0</number> |
||||||
|
</property> |
||||||
|
<property name="topMargin"> |
||||||
|
<number>0</number> |
||||||
|
</property> |
||||||
|
<property name="rightMargin"> |
||||||
|
<number>0</number> |
||||||
|
</property> |
||||||
|
<property name="bottomMargin"> |
||||||
|
<number>0</number> |
||||||
|
</property> |
||||||
|
<item> |
||||||
|
<widget class="QScrollArea" name="colorsScrollArea"> |
||||||
|
<property name="sizePolicy"> |
||||||
|
<sizepolicy hsizetype="Expanding" vsizetype="Preferred"> |
||||||
|
<horstretch>0</horstretch> |
||||||
|
<verstretch>0</verstretch> |
||||||
|
</sizepolicy> |
||||||
|
</property> |
||||||
|
<property name="frameShape"> |
||||||
|
<enum>QFrame::NoFrame</enum> |
||||||
|
</property> |
||||||
|
<property name="sizeAdjustPolicy"> |
||||||
|
<enum>QAbstractScrollArea::AdjustToContentsOnFirstShow</enum> |
||||||
|
</property> |
||||||
|
<property name="widgetResizable"> |
||||||
|
<bool>true</bool> |
||||||
|
</property> |
||||||
|
<widget class="QWidget" name="scrollAreaWidgetContents"> |
||||||
|
<property name="geometry"> |
||||||
|
<rect> |
||||||
|
<x>0</x> |
||||||
|
<y>0</y> |
||||||
|
<width>427</width> |
||||||
|
<height>271</height> |
||||||
|
</rect> |
||||||
|
</property> |
||||||
|
<layout class="QVBoxLayout" name="verticalLayout_3"> |
||||||
|
<item> |
||||||
|
<layout class="QGridLayout" name="colorsLayout" columnstretch="1,0,0,0,0" columnminimumwidth="0,15,0,15,0"> |
||||||
|
<item row="0" column="0"> |
||||||
|
<widget class="QLabel" name="colorIDLabel"> |
||||||
|
<property name="font"> |
||||||
|
<font> |
||||||
|
<bold>true</bold> |
||||||
|
</font> |
||||||
|
</property> |
||||||
|
<property name="text"> |
||||||
|
<string>Color ID</string> |
||||||
|
</property> |
||||||
|
</widget> |
||||||
|
</item> |
||||||
|
<item row="0" column="2"> |
||||||
|
<widget class="QLabel" name="lightModeColorLabel"> |
||||||
|
<property name="font"> |
||||||
|
<font> |
||||||
|
<bold>true</bold> |
||||||
|
</font> |
||||||
|
</property> |
||||||
|
<property name="text"> |
||||||
|
<string>Light Mode</string> |
||||||
|
</property> |
||||||
|
</widget> |
||||||
|
</item> |
||||||
|
<item row="0" column="4"> |
||||||
|
<widget class="QLabel" name="darkModeColorLabel"> |
||||||
|
<property name="font"> |
||||||
|
<font> |
||||||
|
<bold>true</bold> |
||||||
|
</font> |
||||||
|
</property> |
||||||
|
<property name="text"> |
||||||
|
<string>Dark Mode</string> |
||||||
|
</property> |
||||||
|
</widget> |
||||||
|
</item> |
||||||
|
</layout> |
||||||
|
</item> |
||||||
|
<item> |
||||||
|
<spacer name="colorsSpacer"> |
||||||
|
<property name="orientation"> |
||||||
|
<enum>Qt::Vertical</enum> |
||||||
|
</property> |
||||||
|
<property name="sizeHint" stdset="0"> |
||||||
|
<size> |
||||||
|
<width>20</width> |
||||||
|
<height>203</height> |
||||||
|
</size> |
||||||
|
</property> |
||||||
|
</spacer> |
||||||
|
</item> |
||||||
|
</layout> |
||||||
|
</widget> |
||||||
|
</widget> |
||||||
|
</item> |
||||||
|
</layout> |
||||||
|
</widget> |
||||||
|
<widget class="QWidget" name="iconsTab"> |
||||||
|
<attribute name="title"> |
||||||
|
<string>Icons</string> |
||||||
|
</attribute> |
||||||
|
<layout class="QVBoxLayout" name="verticalLayout_5"> |
||||||
|
<property name="leftMargin"> |
||||||
|
<number>0</number> |
||||||
|
</property> |
||||||
|
<property name="topMargin"> |
||||||
|
<number>0</number> |
||||||
|
</property> |
||||||
|
<property name="rightMargin"> |
||||||
|
<number>0</number> |
||||||
|
</property> |
||||||
|
<property name="bottomMargin"> |
||||||
|
<number>0</number> |
||||||
|
</property> |
||||||
|
<item> |
||||||
|
<widget class="QScrollArea" name="iconsScrollArea"> |
||||||
|
<property name="sizePolicy"> |
||||||
|
<sizepolicy hsizetype="Expanding" vsizetype="Preferred"> |
||||||
|
<horstretch>0</horstretch> |
||||||
|
<verstretch>0</verstretch> |
||||||
|
</sizepolicy> |
||||||
|
</property> |
||||||
|
<property name="frameShape"> |
||||||
|
<enum>QFrame::NoFrame</enum> |
||||||
|
</property> |
||||||
|
<property name="sizeAdjustPolicy"> |
||||||
|
<enum>QAbstractScrollArea::AdjustToContentsOnFirstShow</enum> |
||||||
|
</property> |
||||||
|
<property name="widgetResizable"> |
||||||
|
<bool>true</bool> |
||||||
|
</property> |
||||||
|
<widget class="QWidget" name="scrollAreaWidgetContents_2"> |
||||||
|
<property name="geometry"> |
||||||
|
<rect> |
||||||
|
<x>0</x> |
||||||
|
<y>0</y> |
||||||
|
<width>427</width> |
||||||
|
<height>271</height> |
||||||
|
</rect> |
||||||
|
</property> |
||||||
|
<layout class="QVBoxLayout" name="verticalLayout_4"> |
||||||
|
<item> |
||||||
|
<layout class="QGridLayout" name="iconsLayout" columnstretch="1,0,0,0,0" columnminimumwidth="0,15,0,15,0"> |
||||||
|
<item row="0" column="0"> |
||||||
|
<widget class="QLabel" name="iconIDLabel"> |
||||||
|
<property name="font"> |
||||||
|
<font> |
||||||
|
<bold>true</bold> |
||||||
|
</font> |
||||||
|
</property> |
||||||
|
<property name="text"> |
||||||
|
<string>Icon ID</string> |
||||||
|
</property> |
||||||
|
</widget> |
||||||
|
</item> |
||||||
|
<item row="0" column="2"> |
||||||
|
<widget class="QLabel" name="lightModeIconLabel"> |
||||||
|
<property name="font"> |
||||||
|
<font> |
||||||
|
<bold>true</bold> |
||||||
|
</font> |
||||||
|
</property> |
||||||
|
<property name="text"> |
||||||
|
<string>Light Mode</string> |
||||||
|
</property> |
||||||
|
</widget> |
||||||
|
</item> |
||||||
|
<item row="0" column="4"> |
||||||
|
<widget class="QLabel" name="darkModeIconLabel"> |
||||||
|
<property name="font"> |
||||||
|
<font> |
||||||
|
<bold>true</bold> |
||||||
|
</font> |
||||||
|
</property> |
||||||
|
<property name="text"> |
||||||
|
<string>Dark Mode</string> |
||||||
|
</property> |
||||||
|
</widget> |
||||||
|
</item> |
||||||
|
</layout> |
||||||
|
</item> |
||||||
|
<item> |
||||||
|
<spacer name="iconsSpacer"> |
||||||
|
<property name="orientation"> |
||||||
|
<enum>Qt::Vertical</enum> |
||||||
|
</property> |
||||||
|
<property name="sizeHint" stdset="0"> |
||||||
|
<size> |
||||||
|
<width>20</width> |
||||||
|
<height>203</height> |
||||||
|
</size> |
||||||
|
</property> |
||||||
|
</spacer> |
||||||
|
</item> |
||||||
|
</layout> |
||||||
|
</widget> |
||||||
|
</widget> |
||||||
|
</item> |
||||||
|
</layout> |
||||||
|
</widget> |
||||||
|
</widget> |
||||||
|
</item> |
||||||
|
<item> |
||||||
|
<widget class="QDialogButtonBox" name="buttonBox"> |
||||||
|
<property name="orientation"> |
||||||
|
<enum>Qt::Horizontal</enum> |
||||||
|
</property> |
||||||
|
<property name="standardButtons"> |
||||||
|
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> |
||||||
|
</property> |
||||||
|
</widget> |
||||||
|
</item> |
||||||
|
</layout> |
||||||
|
</widget> |
||||||
|
<resources/> |
||||||
|
<connections> |
||||||
|
<connection> |
||||||
|
<sender>buttonBox</sender> |
||||||
|
<signal>accepted()</signal> |
||||||
|
<receiver>UIThemeDialog</receiver> |
||||||
|
<slot>accept()</slot> |
||||||
|
<hints> |
||||||
|
<hint type="sourcelabel"> |
||||||
|
<x>248</x> |
||||||
|
<y>254</y> |
||||||
|
</hint> |
||||||
|
<hint type="destinationlabel"> |
||||||
|
<x>157</x> |
||||||
|
<y>274</y> |
||||||
|
</hint> |
||||||
|
</hints> |
||||||
|
</connection> |
||||||
|
<connection> |
||||||
|
<sender>buttonBox</sender> |
||||||
|
<signal>rejected()</signal> |
||||||
|
<receiver>UIThemeDialog</receiver> |
||||||
|
<slot>reject()</slot> |
||||||
|
<hints> |
||||||
|
<hint type="sourcelabel"> |
||||||
|
<x>316</x> |
||||||
|
<y>260</y> |
||||||
|
</hint> |
||||||
|
<hint type="destinationlabel"> |
||||||
|
<x>286</x> |
||||||
|
<y>274</y> |
||||||
|
</hint> |
||||||
|
</hints> |
||||||
|
</connection> |
||||||
|
</connections> |
||||||
|
</ui> |
@ -0,0 +1,280 @@ |
|||||||
|
/*
|
||||||
|
* Bittorrent Client using Qt and libtorrent. |
||||||
|
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru> |
||||||
|
* Copyright (C) 2019, 2021 Prince Gupta <jagannatharjun11@gmail.com> |
||||||
|
* |
||||||
|
* 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 "uithemesource.h" |
||||||
|
|
||||||
|
#include <QFile> |
||||||
|
#include <QJsonDocument> |
||||||
|
#include <QJsonObject> |
||||||
|
|
||||||
|
#include "base/global.h" |
||||||
|
#include "base/logger.h" |
||||||
|
#include "base/profile.h" |
||||||
|
|
||||||
|
namespace |
||||||
|
{ |
||||||
|
QByteArray readFile(const Path &filePath) |
||||||
|
{ |
||||||
|
QFile file {filePath.data()}; |
||||||
|
if (!file.exists()) |
||||||
|
return {}; |
||||||
|
|
||||||
|
if (file.open(QIODevice::ReadOnly | QIODevice::Text)) |
||||||
|
return file.readAll(); |
||||||
|
|
||||||
|
LogMsg(UIThemeSource::tr("UITheme - Failed to open \"%1\". Reason: %2") |
||||||
|
.arg(filePath.filename(), file.errorString()) |
||||||
|
, Log::WARNING); |
||||||
|
return {}; |
||||||
|
} |
||||||
|
|
||||||
|
QJsonObject parseThemeConfig(const QByteArray &data) |
||||||
|
{ |
||||||
|
if (data.isEmpty()) |
||||||
|
return {}; |
||||||
|
|
||||||
|
QJsonParseError jsonError; |
||||||
|
const QJsonDocument configJsonDoc = QJsonDocument::fromJson(data, &jsonError); |
||||||
|
if (jsonError.error != QJsonParseError::NoError) |
||||||
|
{ |
||||||
|
LogMsg(UIThemeSource::tr("Couldn't parse UI Theme configuration file. Reason: %1") |
||||||
|
.arg(jsonError.errorString()), Log::WARNING); |
||||||
|
return {}; |
||||||
|
} |
||||||
|
|
||||||
|
if (!configJsonDoc.isObject()) |
||||||
|
{ |
||||||
|
LogMsg(UIThemeSource::tr("UI Theme configuration file has invalid format. Reason: %1") |
||||||
|
.arg(UIThemeSource::tr("Root JSON value is not an object")), Log::WARNING); |
||||||
|
return {}; |
||||||
|
} |
||||||
|
|
||||||
|
return configJsonDoc.object(); |
||||||
|
} |
||||||
|
|
||||||
|
QHash<QString, QColor> colorsFromJSON(const QJsonObject &jsonObj) |
||||||
|
{ |
||||||
|
QHash<QString, QColor> colors; |
||||||
|
for (auto colorNode = jsonObj.constBegin(); colorNode != jsonObj.constEnd(); ++colorNode) |
||||||
|
{ |
||||||
|
const QColor color {colorNode.value().toString()}; |
||||||
|
if (!color.isValid()) |
||||||
|
{ |
||||||
|
LogMsg(UIThemeSource::tr("Invalid color for ID \"%1\" is provided by theme") |
||||||
|
.arg(colorNode.key()), Log::WARNING); |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
colors.insert(colorNode.key(), color); |
||||||
|
} |
||||||
|
|
||||||
|
return colors; |
||||||
|
} |
||||||
|
|
||||||
|
Path findIcon(const QString &iconId, const Path &dir) |
||||||
|
{ |
||||||
|
const Path pathSvg = dir / Path(iconId + u".svg"); |
||||||
|
if (pathSvg.exists()) |
||||||
|
return pathSvg; |
||||||
|
|
||||||
|
const Path pathPng = dir / Path(iconId + u".png"); |
||||||
|
if (pathPng.exists()) |
||||||
|
return pathPng; |
||||||
|
|
||||||
|
return {}; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
DefaultThemeSource::DefaultThemeSource() |
||||||
|
: m_defaultPath {u":"_qs} |
||||||
|
, m_userPath {specialFolderLocation(SpecialFolder::Config) / Path(u"themes/default"_qs)} |
||||||
|
, m_colors {defaultUIThemeColors()} |
||||||
|
{ |
||||||
|
loadColors(); |
||||||
|
} |
||||||
|
|
||||||
|
QByteArray DefaultThemeSource::readStyleSheet() |
||||||
|
{ |
||||||
|
return {}; |
||||||
|
} |
||||||
|
|
||||||
|
QColor DefaultThemeSource::getColor(const QString &colorId, const ColorMode colorMode) const |
||||||
|
{ |
||||||
|
return (colorMode == ColorMode::Light) |
||||||
|
? m_colors.value(colorId).light : m_colors.value(colorId).dark; |
||||||
|
} |
||||||
|
|
||||||
|
Path DefaultThemeSource::getIconPath(const QString &iconId, const ColorMode colorMode) const |
||||||
|
{ |
||||||
|
const Path iconsPath {u"icons"_qs}; |
||||||
|
const Path lightModeIconsPath = iconsPath / Path(u"light"_qs); |
||||||
|
const Path darkModeIconsPath = iconsPath / Path(u"dark"_qs); |
||||||
|
|
||||||
|
if (colorMode == ColorMode::Dark) |
||||||
|
{ |
||||||
|
if (const Path iconPath = findIcon(iconId, (m_userPath / darkModeIconsPath)) |
||||||
|
; !iconPath.isEmpty()) |
||||||
|
{ |
||||||
|
return iconPath; |
||||||
|
} |
||||||
|
|
||||||
|
if (const Path iconPath = findIcon(iconId, (m_defaultPath / darkModeIconsPath)) |
||||||
|
; !iconPath.isEmpty()) |
||||||
|
{ |
||||||
|
return iconPath; |
||||||
|
} |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
if (const Path iconPath = findIcon(iconId, (m_userPath / lightModeIconsPath)) |
||||||
|
; !iconPath.isEmpty()) |
||||||
|
{ |
||||||
|
return iconPath; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return findIcon(iconId, (m_defaultPath / iconsPath)); |
||||||
|
} |
||||||
|
|
||||||
|
void DefaultThemeSource::loadColors() |
||||||
|
{ |
||||||
|
const QByteArray configData = readFile(m_userPath / Path(CONFIG_FILE_NAME)); |
||||||
|
if (configData.isEmpty()) |
||||||
|
return; |
||||||
|
|
||||||
|
const QJsonObject config = parseThemeConfig(configData); |
||||||
|
|
||||||
|
QHash<QString, QColor> lightModeColorOverrides = colorsFromJSON(config.value(KEY_COLORS_LIGHT).toObject()); |
||||||
|
for (auto overridesIt = lightModeColorOverrides.cbegin(); overridesIt != lightModeColorOverrides.cend(); ++overridesIt) |
||||||
|
{ |
||||||
|
auto it = m_colors.find(overridesIt.key()); |
||||||
|
if (it != m_colors.end()) |
||||||
|
it.value().light = overridesIt.value(); |
||||||
|
} |
||||||
|
|
||||||
|
QHash<QString, QColor> darkModeColorOverrides = colorsFromJSON(config.value(KEY_COLORS_DARK).toObject()); |
||||||
|
for (auto overridesIt = darkModeColorOverrides.cbegin(); overridesIt != darkModeColorOverrides.cend(); ++overridesIt) |
||||||
|
{ |
||||||
|
auto it = m_colors.find(overridesIt.key()); |
||||||
|
if (it != m_colors.end()) |
||||||
|
it.value().dark = overridesIt.value(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
QColor CustomThemeSource::getColor(const QString &colorId, const ColorMode colorMode) const |
||||||
|
{ |
||||||
|
if (colorMode == ColorMode::Dark) |
||||||
|
{ |
||||||
|
if (const QColor color = m_darkModeColors.value(colorId) |
||||||
|
; color.isValid()) |
||||||
|
{ |
||||||
|
return color; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (const QColor color = m_colors.value(colorId) |
||||||
|
; color.isValid()) |
||||||
|
{ |
||||||
|
return color; |
||||||
|
} |
||||||
|
|
||||||
|
return defaultThemeSource()->getColor(colorId, colorMode); |
||||||
|
} |
||||||
|
|
||||||
|
Path CustomThemeSource::getIconPath(const QString &iconId, const ColorMode colorMode) const |
||||||
|
{ |
||||||
|
const Path iconsPath {u"icons"_qs}; |
||||||
|
const Path darkModeIconsPath = iconsPath / Path(u"dark"_qs); |
||||||
|
|
||||||
|
if (colorMode == ColorMode::Dark) |
||||||
|
{ |
||||||
|
if (const Path iconPath = findIcon(iconId, (themeRootPath() / darkModeIconsPath)) |
||||||
|
; !iconPath.isEmpty()) |
||||||
|
{ |
||||||
|
return iconPath; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (const Path iconPath = findIcon(iconId, (themeRootPath() / iconsPath)) |
||||||
|
; !iconPath.isEmpty()) |
||||||
|
{ |
||||||
|
return iconPath; |
||||||
|
} |
||||||
|
|
||||||
|
return defaultThemeSource()->getIconPath(iconId, colorMode); |
||||||
|
} |
||||||
|
|
||||||
|
QByteArray CustomThemeSource::readStyleSheet() |
||||||
|
{ |
||||||
|
return readFile(themeRootPath() / Path(STYLESHEET_FILE_NAME)); |
||||||
|
} |
||||||
|
|
||||||
|
DefaultThemeSource *CustomThemeSource::defaultThemeSource() const |
||||||
|
{ |
||||||
|
return m_defaultThemeSource.get(); |
||||||
|
} |
||||||
|
|
||||||
|
void CustomThemeSource::loadColors() |
||||||
|
{ |
||||||
|
const QByteArray configData = readFile(themeRootPath() / Path(CONFIG_FILE_NAME)); |
||||||
|
if (configData.isEmpty()) |
||||||
|
return; |
||||||
|
|
||||||
|
const QJsonObject config = parseThemeConfig(configData); |
||||||
|
|
||||||
|
m_colors.insert(colorsFromJSON(config.value(KEY_COLORS).toObject())); |
||||||
|
m_darkModeColors.insert(colorsFromJSON(config.value(KEY_COLORS_DARK).toObject())); |
||||||
|
} |
||||||
|
|
||||||
|
Path QRCThemeSource::themeRootPath() const |
||||||
|
{ |
||||||
|
return Path(u":/uitheme"_qs); |
||||||
|
} |
||||||
|
|
||||||
|
FolderThemeSource::FolderThemeSource(const Path &folderPath) |
||||||
|
: m_folder {folderPath} |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
QByteArray FolderThemeSource::readStyleSheet() |
||||||
|
{ |
||||||
|
// Directory used by stylesheet to reference internal resources
|
||||||
|
// for example `icon: url(:/uitheme/file.svg)` will be expected to
|
||||||
|
// point to a file `file.svg` in root directory of CONFIG_FILE_NAME
|
||||||
|
const QString stylesheetResourcesDir = u":/uitheme"_qs; |
||||||
|
|
||||||
|
QByteArray styleSheetData = CustomThemeSource::readStyleSheet(); |
||||||
|
return styleSheetData.replace(stylesheetResourcesDir.toUtf8(), themeRootPath().data().toUtf8()); |
||||||
|
} |
||||||
|
|
||||||
|
Path FolderThemeSource::themeRootPath() const |
||||||
|
{ |
||||||
|
return m_folder; |
||||||
|
} |
@ -0,0 +1,115 @@ |
|||||||
|
/*
|
||||||
|
* Bittorrent Client using Qt and libtorrent. |
||||||
|
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru> |
||||||
|
* Copyright (C) 2019 Prince Gupta <jagannatharjun11@gmail.com> |
||||||
|
* |
||||||
|
* This program is free software; you can redistribute it and/or |
||||||
|
* modify it under the terms of the GNU General Public License |
||||||
|
* as published by the Free Software Foundation; either version 2 |
||||||
|
* of the License, or (at your option) any later version. |
||||||
|
* |
||||||
|
* This program is distributed in the hope that it will be useful, |
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
* GNU General Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU General Public License |
||||||
|
* along with this program; if not, write to the Free Software |
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, |
||||||
|
* USA. |
||||||
|
* |
||||||
|
* In addition, as a special exception, the copyright holders give permission to |
||||||
|
* link this program with the OpenSSL project's "OpenSSL" library (or with |
||||||
|
* modified versions of it that use the same license as the "OpenSSL" library), |
||||||
|
* and distribute the linked executables. You must obey the GNU General Public |
||||||
|
* License in all respects for all of the code used other than "OpenSSL". If |
||||||
|
* you modify file(s), you may extend this exception to your version of the |
||||||
|
* file(s), but you are not obligated to do so. If you do not wish to do so, |
||||||
|
* delete this exception statement from your version. |
||||||
|
*/ |
||||||
|
|
||||||
|
#pragma once |
||||||
|
|
||||||
|
#include <memory> |
||||||
|
|
||||||
|
#include <QtGlobal> |
||||||
|
#include <QColor> |
||||||
|
#include <QHash> |
||||||
|
#include <QIcon> |
||||||
|
#include <QString> |
||||||
|
|
||||||
|
#include "base/path.h" |
||||||
|
#include "uithemecommon.h" |
||||||
|
|
||||||
|
enum class ColorMode |
||||||
|
{ |
||||||
|
Light, |
||||||
|
Dark |
||||||
|
}; |
||||||
|
|
||||||
|
class UIThemeSource |
||||||
|
{ |
||||||
|
Q_DECLARE_TR_FUNCTIONS(UIThemeSource) |
||||||
|
|
||||||
|
public: |
||||||
|
virtual ~UIThemeSource() = default; |
||||||
|
|
||||||
|
virtual QColor getColor(const QString &colorId, const ColorMode colorMode) const = 0; |
||||||
|
virtual Path getIconPath(const QString &iconId, const ColorMode colorMode) const = 0; |
||||||
|
virtual QByteArray readStyleSheet() = 0; |
||||||
|
}; |
||||||
|
|
||||||
|
class DefaultThemeSource final : public UIThemeSource |
||||||
|
{ |
||||||
|
public: |
||||||
|
DefaultThemeSource(); |
||||||
|
|
||||||
|
QByteArray readStyleSheet() override; |
||||||
|
QColor getColor(const QString &colorId, const ColorMode colorMode) const override; |
||||||
|
Path getIconPath(const QString &iconId, const ColorMode colorMode) const override; |
||||||
|
|
||||||
|
private: |
||||||
|
void loadColors(); |
||||||
|
|
||||||
|
const Path m_defaultPath; |
||||||
|
const Path m_userPath; |
||||||
|
QHash<QString, UIThemeColor> m_colors; |
||||||
|
}; |
||||||
|
|
||||||
|
class CustomThemeSource : public UIThemeSource |
||||||
|
{ |
||||||
|
public: |
||||||
|
QColor getColor(const QString &colorId, const ColorMode colorMode) const override; |
||||||
|
Path getIconPath(const QString &iconId, const ColorMode colorMode) const override; |
||||||
|
QByteArray readStyleSheet() override; |
||||||
|
|
||||||
|
protected: |
||||||
|
virtual Path themeRootPath() const = 0; |
||||||
|
DefaultThemeSource *defaultThemeSource() const; |
||||||
|
|
||||||
|
private: |
||||||
|
void loadColors(); |
||||||
|
|
||||||
|
const std::unique_ptr<DefaultThemeSource> m_defaultThemeSource = std::make_unique<DefaultThemeSource>(); |
||||||
|
QHash<QString, QColor> m_colors; |
||||||
|
QHash<QString, QColor> m_darkModeColors; |
||||||
|
}; |
||||||
|
|
||||||
|
class QRCThemeSource final : public CustomThemeSource |
||||||
|
{ |
||||||
|
private: |
||||||
|
Path themeRootPath() const override; |
||||||
|
}; |
||||||
|
|
||||||
|
class FolderThemeSource : public CustomThemeSource |
||||||
|
{ |
||||||
|
public: |
||||||
|
explicit FolderThemeSource(const Path &folderPath); |
||||||
|
|
||||||
|
QByteArray readStyleSheet() override; |
||||||
|
|
||||||
|
private: |
||||||
|
Path themeRootPath() const override; |
||||||
|
|
||||||
|
const Path m_folder; |
||||||
|
}; |
Loading…
Reference in new issue