|
|
@ -1,5 +1,6 @@ |
|
|
|
/*
|
|
|
|
/*
|
|
|
|
* Bittorrent Client using Qt and libtorrent. |
|
|
|
* Bittorrent Client using Qt and libtorrent. |
|
|
|
|
|
|
|
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru> |
|
|
|
* Copyright (C) 2019, 2021 Prince Gupta <jagannatharjun11@gmail.com> |
|
|
|
* Copyright (C) 2019, 2021 Prince Gupta <jagannatharjun11@gmail.com> |
|
|
|
* |
|
|
|
* |
|
|
|
* This program is free software; you can redistribute it and/or |
|
|
|
* This program is free software; you can redistribute it and/or |
|
|
@ -38,25 +39,84 @@ |
|
|
|
#include <QPixmapCache> |
|
|
|
#include <QPixmapCache> |
|
|
|
#include <QResource> |
|
|
|
#include <QResource> |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#include "base/algorithm.h" |
|
|
|
#include "base/global.h" |
|
|
|
#include "base/global.h" |
|
|
|
#include "base/logger.h" |
|
|
|
#include "base/logger.h" |
|
|
|
#include "base/path.h" |
|
|
|
#include "base/path.h" |
|
|
|
#include "base/preferences.h" |
|
|
|
#include "base/preferences.h" |
|
|
|
#include "base/profile.h" |
|
|
|
#include "base/profile.h" |
|
|
|
#include "base/utils/fs.h" |
|
|
|
#include "base/utils/fs.h" |
|
|
|
|
|
|
|
#include "color.h" |
|
|
|
|
|
|
|
|
|
|
|
namespace |
|
|
|
namespace |
|
|
|
{ |
|
|
|
{ |
|
|
|
const Path DEFAULT_ICONS_DIR {u":icons"_qs}; |
|
|
|
|
|
|
|
const QString CONFIG_FILE_NAME = u"config.json"_qs; |
|
|
|
const QString CONFIG_FILE_NAME = u"config.json"_qs; |
|
|
|
const QString STYLESHEET_FILE_NAME = u"stylesheet.qss"_qs; |
|
|
|
const QString STYLESHEET_FILE_NAME = u"stylesheet.qss"_qs; |
|
|
|
|
|
|
|
|
|
|
|
// Directory used by stylesheet to reference internal resources
|
|
|
|
bool isDarkTheme() |
|
|
|
// 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 QPalette palette = qApp->palette(); |
|
|
|
const QString STYLESHEET_RESOURCES_DIR = u":/uitheme"_qs; |
|
|
|
const QColor &color = palette.color(QPalette::Active, QPalette::Base); |
|
|
|
|
|
|
|
return (color.lightness() < 127); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
QByteArray readFile(const Path &filePath) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
QFile file {filePath.data()}; |
|
|
|
|
|
|
|
if (!file.exists()) |
|
|
|
|
|
|
|
return {}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (file.open(QIODevice::ReadOnly | QIODevice::Text)) |
|
|
|
|
|
|
|
return file.readAll(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
LogMsg(UIThemeManager::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(UIThemeManager::tr("Couldn't parse UI Theme configuration file. Reason: %1") |
|
|
|
|
|
|
|
.arg(jsonError.errorString()), Log::WARNING); |
|
|
|
|
|
|
|
return {}; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!configJsonDoc.isObject()) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
LogMsg(UIThemeManager::tr("UI Theme configuration file has invalid format. Reason: %1") |
|
|
|
|
|
|
|
.arg(UIThemeManager::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(UIThemeManager::tr("Invalid color for ID \"%1\" is provided by theme") |
|
|
|
|
|
|
|
.arg(colorNode.key()), Log::WARNING); |
|
|
|
|
|
|
|
continue; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
colors.insert(colorNode.key(), color); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const Path THEME_ICONS_DIR {u"icons"_qs}; |
|
|
|
return colors; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
Path findIcon(const QString &iconId, const Path &dir) |
|
|
|
Path findIcon(const QString &iconId, const Path &dir) |
|
|
|
{ |
|
|
|
{ |
|
|
@ -71,88 +131,269 @@ namespace |
|
|
|
return {}; |
|
|
|
return {}; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
QByteArray readFile(const Path &filePath) |
|
|
|
class DefaultThemeSource final : public UIThemeSource |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
public: |
|
|
|
|
|
|
|
DefaultThemeSource() |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
loadColors(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
QByteArray readStyleSheet() override |
|
|
|
{ |
|
|
|
{ |
|
|
|
QFile file {filePath.data()}; |
|
|
|
|
|
|
|
if (!file.exists()) |
|
|
|
|
|
|
|
return {}; |
|
|
|
return {}; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (file.open(QIODevice::ReadOnly | QIODevice::Text)) |
|
|
|
QColor getColor(const QString &colorId, const ColorMode colorMode) const override |
|
|
|
return file.readAll(); |
|
|
|
{ |
|
|
|
|
|
|
|
if (colorMode == ColorMode::Dark) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
if (const QColor color = m_darkModeColors.value(colorId) |
|
|
|
|
|
|
|
; color.isValid()) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
return color; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
LogMsg(UIThemeManager::tr("UITheme - Failed to open \"%1\". Reason: %2") |
|
|
|
return m_colors.value(colorId); |
|
|
|
.arg(filePath.filename(), file.errorString()) |
|
|
|
|
|
|
|
, Log::WARNING); |
|
|
|
|
|
|
|
return {}; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
class QRCThemeSource final : public UIThemeSource |
|
|
|
Path getIconPath(const QString &iconId, const ColorMode colorMode) const override |
|
|
|
{ |
|
|
|
{ |
|
|
|
public: |
|
|
|
const Path iconsPath {u"icons"_qs}; |
|
|
|
QByteArray readStyleSheet() override |
|
|
|
const Path darkModeIconsPath = iconsPath / Path(u"dark"_qs); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (colorMode == ColorMode::Dark) |
|
|
|
{ |
|
|
|
{ |
|
|
|
return readFile(m_qrcThemeDir / Path(STYLESHEET_FILE_NAME)); |
|
|
|
if (const Path iconPath = findIcon(iconId, (m_userPath / darkModeIconsPath)) |
|
|
|
|
|
|
|
; !iconPath.isEmpty()) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
return iconPath; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
QByteArray readConfig() override |
|
|
|
if (const Path iconPath = findIcon(iconId, (m_defaultPath / darkModeIconsPath)) |
|
|
|
|
|
|
|
; !iconPath.isEmpty()) |
|
|
|
{ |
|
|
|
{ |
|
|
|
return readFile(m_qrcThemeDir / Path(CONFIG_FILE_NAME)); |
|
|
|
return iconPath; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
Path iconPath(const QString &iconId) const override |
|
|
|
if (const Path iconPath = findIcon(iconId, (m_userPath / iconsPath)) |
|
|
|
|
|
|
|
; !iconPath.isEmpty()) |
|
|
|
{ |
|
|
|
{ |
|
|
|
return findIcon(iconId, m_qrcIconsDir); |
|
|
|
return iconPath; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return findIcon(iconId, (m_defaultPath / iconsPath)); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private: |
|
|
|
private: |
|
|
|
const Path m_qrcThemeDir {u":/uitheme"_qs}; |
|
|
|
void loadColors() |
|
|
|
const Path m_qrcIconsDir = m_qrcThemeDir / THEME_ICONS_DIR; |
|
|
|
{ |
|
|
|
|
|
|
|
m_colors = { |
|
|
|
|
|
|
|
{u"Log.TimeStamp"_qs, Color::Primer::Light::fgSubtle}, |
|
|
|
|
|
|
|
{u"Log.Normal"_qs, QApplication::palette().color(QPalette::Active, QPalette::WindowText)}, |
|
|
|
|
|
|
|
{u"Log.Info"_qs, Color::Primer::Light::accentFg}, |
|
|
|
|
|
|
|
{u"Log.Warning"_qs, Color::Primer::Light::severeFg}, |
|
|
|
|
|
|
|
{u"Log.Critical"_qs, Color::Primer::Light::dangerFg}, |
|
|
|
|
|
|
|
{u"Log.BannedPeer"_qs, Color::Primer::Light::dangerFg}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{u"RSS.ReadArticle"_qs, QApplication::palette().color(QPalette::Inactive, QPalette::WindowText)}, |
|
|
|
|
|
|
|
{u"RSS.UnreadArticle"_qs, QApplication::palette().color(QPalette::Active, QPalette::Link)}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{u"TransferList.Downloading"_qs, Color::Primer::Light::successFg}, |
|
|
|
|
|
|
|
{u"TransferList.StalledDownloading"_qs, Color::Primer::Light::successEmphasis}, |
|
|
|
|
|
|
|
{u"TransferList.DownloadingMetadata"_qs, Color::Primer::Light::successFg}, |
|
|
|
|
|
|
|
{u"TransferList.ForcedDownloadingMetadata"_qs, Color::Primer::Light::successFg}, |
|
|
|
|
|
|
|
{u"TransferList.ForcedDownloading"_qs, Color::Primer::Light::successFg}, |
|
|
|
|
|
|
|
{u"TransferList.Uploading"_qs, Color::Primer::Light::accentFg}, |
|
|
|
|
|
|
|
{u"TransferList.StalledUploading"_qs, Color::Primer::Light::accentEmphasis}, |
|
|
|
|
|
|
|
{u"TransferList.ForcedUploading"_qs, Color::Primer::Light::accentFg}, |
|
|
|
|
|
|
|
{u"TransferList.QueuedDownloading"_qs, Color::Primer::Light::scaleYellow6}, |
|
|
|
|
|
|
|
{u"TransferList.QueuedUploading"_qs, Color::Primer::Light::scaleYellow6}, |
|
|
|
|
|
|
|
{u"TransferList.CheckingDownloading"_qs, Color::Primer::Light::successFg}, |
|
|
|
|
|
|
|
{u"TransferList.CheckingUploading"_qs, Color::Primer::Light::successFg}, |
|
|
|
|
|
|
|
{u"TransferList.CheckingResumeData"_qs, Color::Primer::Light::successFg}, |
|
|
|
|
|
|
|
{u"TransferList.PausedDownloading"_qs, Color::Primer::Light::fgMuted}, |
|
|
|
|
|
|
|
{u"TransferList.PausedUploading"_qs, Color::Primer::Light::doneFg}, |
|
|
|
|
|
|
|
{u"TransferList.Moving"_qs, Color::Primer::Light::successFg}, |
|
|
|
|
|
|
|
{u"TransferList.MissingFiles"_qs, Color::Primer::Light::dangerFg}, |
|
|
|
|
|
|
|
{u"TransferList.Error"_qs, Color::Primer::Light::dangerFg} |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
m_darkModeColors = { |
|
|
|
|
|
|
|
{u"Log.TimeStamp"_qs, Color::Primer::Dark::fgSubtle}, |
|
|
|
|
|
|
|
{u"Log.Normal"_qs, QApplication::palette().color(QPalette::Active, QPalette::WindowText)}, |
|
|
|
|
|
|
|
{u"Log.Info"_qs, Color::Primer::Dark::accentFg}, |
|
|
|
|
|
|
|
{u"Log.Warning"_qs, Color::Primer::Dark::severeFg}, |
|
|
|
|
|
|
|
{u"Log.Critical"_qs, Color::Primer::Dark::dangerFg}, |
|
|
|
|
|
|
|
{u"Log.BannedPeer"_qs, Color::Primer::Dark::dangerFg}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{u"RSS.ReadArticle"_qs, QApplication::palette().color(QPalette::Inactive, QPalette::WindowText)}, |
|
|
|
|
|
|
|
{u"RSS.UnreadArticle"_qs, QApplication::palette().color(QPalette::Active, QPalette::Link)}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{u"TransferList.Downloading"_qs, Color::Primer::Dark::successFg}, |
|
|
|
|
|
|
|
{u"TransferList.StalledDownloading"_qs, Color::Primer::Dark::successEmphasis}, |
|
|
|
|
|
|
|
{u"TransferList.DownloadingMetadata"_qs, Color::Primer::Dark::successFg}, |
|
|
|
|
|
|
|
{u"TransferList.ForcedDownloadingMetadata"_qs, Color::Primer::Dark::successFg}, |
|
|
|
|
|
|
|
{u"TransferList.ForcedDownloading"_qs, Color::Primer::Dark::successFg}, |
|
|
|
|
|
|
|
{u"TransferList.Uploading"_qs, Color::Primer::Dark::accentFg}, |
|
|
|
|
|
|
|
{u"TransferList.StalledUploading"_qs, Color::Primer::Dark::accentEmphasis}, |
|
|
|
|
|
|
|
{u"TransferList.ForcedUploading"_qs, Color::Primer::Dark::accentFg}, |
|
|
|
|
|
|
|
{u"TransferList.QueuedDownloading"_qs, Color::Primer::Dark::scaleYellow6}, |
|
|
|
|
|
|
|
{u"TransferList.QueuedUploading"_qs, Color::Primer::Dark::scaleYellow6}, |
|
|
|
|
|
|
|
{u"TransferList.CheckingDownloading"_qs, Color::Primer::Dark::successFg}, |
|
|
|
|
|
|
|
{u"TransferList.CheckingUploading"_qs, Color::Primer::Dark::successFg}, |
|
|
|
|
|
|
|
{u"TransferList.CheckingResumeData"_qs, Color::Primer::Dark::successFg}, |
|
|
|
|
|
|
|
{u"TransferList.PausedDownloading"_qs, Color::Primer::Dark::fgMuted}, |
|
|
|
|
|
|
|
{u"TransferList.PausedUploading"_qs, Color::Primer::Dark::doneFg}, |
|
|
|
|
|
|
|
{u"TransferList.Moving"_qs, Color::Primer::Dark::successFg}, |
|
|
|
|
|
|
|
{u"TransferList.MissingFiles"_qs, Color::Primer::Dark::dangerFg}, |
|
|
|
|
|
|
|
{u"TransferList.Error"_qs, Color::Primer::Dark::dangerFg} |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const QByteArray configData = readFile(m_userPath / Path(CONFIG_FILE_NAME)); |
|
|
|
|
|
|
|
if (configData.isEmpty()) |
|
|
|
|
|
|
|
return; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const QJsonObject config = parseThemeConfig(configData); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
auto colorOverrides = colorsFromJSON(config.value(u"colors").toObject()); |
|
|
|
|
|
|
|
// Overriding Palette colors is not allowed in the default theme
|
|
|
|
|
|
|
|
Algorithm::removeIf(colorOverrides, [](const QString &colorId, [[maybe_unused]] const QColor &color) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
return colorId.startsWith(u"Palette."); |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
m_colors.insert(colorOverrides); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
auto darkModeColorOverrides = colorsFromJSON(config.value(u"colors.dark").toObject()); |
|
|
|
|
|
|
|
// Overriding Palette colors is not allowed in the default theme
|
|
|
|
|
|
|
|
Algorithm::removeIf(darkModeColorOverrides, [](const QString &colorId, [[maybe_unused]] const QColor &color) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
return colorId.startsWith(u"Palette."); |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
m_darkModeColors.insert(darkModeColorOverrides); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const Path m_defaultPath {u":"_qs}; |
|
|
|
|
|
|
|
const Path m_userPath = specialFolderLocation(SpecialFolder::Config) / Path(u"themes/default"_qs); |
|
|
|
|
|
|
|
QHash<QString, QColor> m_colors; |
|
|
|
|
|
|
|
QHash<QString, QColor> m_darkModeColors; |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
class FolderThemeSource final : public UIThemeSource |
|
|
|
class CustomThemeSource : public UIThemeSource |
|
|
|
{ |
|
|
|
{ |
|
|
|
public: |
|
|
|
public: |
|
|
|
explicit FolderThemeSource(const Path &folderPath) |
|
|
|
QColor getColor(const QString &colorId, const ColorMode colorMode) const override |
|
|
|
: m_folder {folderPath} |
|
|
|
{ |
|
|
|
, m_iconsDir {m_folder / THEME_ICONS_DIR} |
|
|
|
if (colorMode == ColorMode::Dark) |
|
|
|
{ |
|
|
|
{ |
|
|
|
|
|
|
|
if (const QColor color = m_darkModeColors.value(colorId) |
|
|
|
|
|
|
|
; color.isValid()) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
return color; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
QByteArray readStyleSheet() override |
|
|
|
if (const QColor color = m_colors.value(colorId) |
|
|
|
|
|
|
|
; color.isValid()) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
return color; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return defaultThemeSource()->getColor(colorId, colorMode); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Path getIconPath(const QString &iconId, const ColorMode colorMode) const override |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
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()) |
|
|
|
{ |
|
|
|
{ |
|
|
|
QByteArray styleSheetData = readFile(m_folder / Path(STYLESHEET_FILE_NAME)); |
|
|
|
return iconPath; |
|
|
|
return styleSheetData.replace(STYLESHEET_RESOURCES_DIR.toUtf8(), m_folder.data().toUtf8()); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
QByteArray readConfig() override |
|
|
|
if (const Path iconPath = findIcon(iconId, (themeRootPath() / iconsPath)) |
|
|
|
|
|
|
|
; !iconPath.isEmpty()) |
|
|
|
{ |
|
|
|
{ |
|
|
|
return readFile(m_folder / Path(CONFIG_FILE_NAME)); |
|
|
|
return iconPath; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return defaultThemeSource()->getIconPath(iconId, colorMode); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
Path iconPath(const QString &iconId) const override |
|
|
|
QByteArray readStyleSheet() override |
|
|
|
{ |
|
|
|
{ |
|
|
|
return findIcon(iconId, m_iconsDir); |
|
|
|
return readFile(themeRootPath() / Path(STYLESHEET_FILE_NAME)); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected: |
|
|
|
|
|
|
|
virtual Path themeRootPath() const = 0; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
DefaultThemeSource *defaultThemeSource() const |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
return m_defaultThemeSource.get(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private: |
|
|
|
private: |
|
|
|
const Path m_folder; |
|
|
|
void loadColors() |
|
|
|
const Path m_iconsDir; |
|
|
|
{ |
|
|
|
|
|
|
|
const QByteArray configData = readFile(themeRootPath() / Path(CONFIG_FILE_NAME)); |
|
|
|
|
|
|
|
if (configData.isEmpty()) |
|
|
|
|
|
|
|
return; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const QJsonObject config = parseThemeConfig(configData); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
m_colors.insert(colorsFromJSON(config.value(u"colors").toObject())); |
|
|
|
|
|
|
|
m_darkModeColors.insert(colorsFromJSON(config.value(u"colors.dark").toObject())); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
return Path(u":/uitheme"_qs); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
std::unique_ptr<UIThemeSource> createUIThemeSource(const Path &themePath) |
|
|
|
class FolderThemeSource : public CustomThemeSource |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
public: |
|
|
|
|
|
|
|
explicit FolderThemeSource(const Path &folderPath) |
|
|
|
|
|
|
|
: m_folder {folderPath} |
|
|
|
{ |
|
|
|
{ |
|
|
|
if (themePath.filename() == CONFIG_FILE_NAME) |
|
|
|
} |
|
|
|
return std::make_unique<FolderThemeSource>(themePath.parentPath()); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if ((themePath.hasExtension(u".qbtheme"_qs)) |
|
|
|
QByteArray readStyleSheet() override |
|
|
|
&& QResource::registerResource(themePath.data(), u"/uitheme"_qs)) |
|
|
|
|
|
|
|
{ |
|
|
|
{ |
|
|
|
return std::make_unique<QRCThemeSource>(); |
|
|
|
// 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()); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return nullptr; |
|
|
|
private: |
|
|
|
|
|
|
|
Path themeRootPath() const override |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
return m_folder; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const Path m_folder; |
|
|
|
|
|
|
|
}; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
UIThemeManager *UIThemeManager::m_instance = nullptr; |
|
|
|
UIThemeManager *UIThemeManager::m_instance = nullptr; |
|
|
@ -175,17 +416,28 @@ UIThemeManager::UIThemeManager() |
|
|
|
, m_useSystemIcons {Preferences::instance()->useSystemIcons()} |
|
|
|
, m_useSystemIcons {Preferences::instance()->useSystemIcons()} |
|
|
|
#endif |
|
|
|
#endif |
|
|
|
{ |
|
|
|
{ |
|
|
|
const Path themePath = m_useCustomTheme |
|
|
|
if (m_useCustomTheme) |
|
|
|
? Preferences::instance()->customUIThemePath() |
|
|
|
|
|
|
|
: specialFolderLocation(SpecialFolder::Config) / Path(u"themes/default/config.json"_qs); |
|
|
|
|
|
|
|
m_themeSource = createUIThemeSource(themePath); |
|
|
|
|
|
|
|
if (!m_themeSource) |
|
|
|
|
|
|
|
{ |
|
|
|
{ |
|
|
|
|
|
|
|
const Path themePath = Preferences::instance()->customUIThemePath(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (themePath.hasExtension(u".qbtheme"_qs)) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
if (QResource::registerResource(themePath.data(), u"/uitheme"_qs)) |
|
|
|
|
|
|
|
m_themeSource = std::make_unique<QRCThemeSource>(); |
|
|
|
|
|
|
|
else |
|
|
|
LogMsg(tr("Failed to load UI theme from file: \"%1\"").arg(themePath.toString()), Log::WARNING); |
|
|
|
LogMsg(tr("Failed to load UI theme from file: \"%1\"").arg(themePath.toString()), Log::WARNING); |
|
|
|
} |
|
|
|
} |
|
|
|
else |
|
|
|
else if (themePath.filename() == CONFIG_FILE_NAME) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
m_themeSource = std::make_unique<FolderThemeSource>(themePath.parentPath()); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!m_themeSource) |
|
|
|
|
|
|
|
m_themeSource = std::make_unique<DefaultThemeSource>(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (m_useCustomTheme) |
|
|
|
{ |
|
|
|
{ |
|
|
|
loadColorsFromJSONConfig(); |
|
|
|
|
|
|
|
applyPalette(); |
|
|
|
applyPalette(); |
|
|
|
applyStyleSheet(); |
|
|
|
applyStyleSheet(); |
|
|
|
} |
|
|
|
} |
|
|
@ -203,9 +455,11 @@ void UIThemeManager::applyStyleSheet() const |
|
|
|
|
|
|
|
|
|
|
|
QIcon UIThemeManager::getIcon(const QString &iconId, [[maybe_unused]] const QString &fallback) const |
|
|
|
QIcon UIThemeManager::getIcon(const QString &iconId, [[maybe_unused]] const QString &fallback) const |
|
|
|
{ |
|
|
|
{ |
|
|
|
// Cache to avoid rescaling svg icons
|
|
|
|
const auto colorMode = isDarkTheme() ? ColorMode::Dark : ColorMode::Light; |
|
|
|
const auto iter = m_iconCache.find(iconId); |
|
|
|
auto &icons = (colorMode == ColorMode::Dark) ? m_darkModeIcons : m_icons; |
|
|
|
if (iter != m_iconCache.end()) |
|
|
|
|
|
|
|
|
|
|
|
const auto iter = icons.find(iconId); |
|
|
|
|
|
|
|
if (iter != icons.end()) |
|
|
|
return *iter; |
|
|
|
return *iter; |
|
|
|
|
|
|
|
|
|
|
|
#if (defined(Q_OS_UNIX) && !defined(Q_OS_MACOS)) |
|
|
|
#if (defined(Q_OS_UNIX) && !defined(Q_OS_MACOS)) |
|
|
@ -214,27 +468,28 @@ QIcon UIThemeManager::getIcon(const QString &iconId, [[maybe_unused]] const QStr |
|
|
|
{ |
|
|
|
{ |
|
|
|
auto icon = QIcon::fromTheme(iconId); |
|
|
|
auto icon = QIcon::fromTheme(iconId); |
|
|
|
if (icon.name() != iconId) |
|
|
|
if (icon.name() != iconId) |
|
|
|
icon = QIcon::fromTheme(fallback, QIcon(getIconPathFromResources(iconId).data())); |
|
|
|
icon = QIcon::fromTheme(fallback, QIcon(m_themeSource->getIconPath(iconId, colorMode).data())); |
|
|
|
return icon; |
|
|
|
return icon; |
|
|
|
} |
|
|
|
} |
|
|
|
#endif |
|
|
|
#endif |
|
|
|
|
|
|
|
|
|
|
|
const QIcon icon {getIconPathFromResources(iconId).data()}; |
|
|
|
const QIcon icon {m_themeSource->getIconPath(iconId, colorMode).data()}; |
|
|
|
m_iconCache[iconId] = icon; |
|
|
|
icons[iconId] = icon; |
|
|
|
return icon; |
|
|
|
return icon; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
QIcon UIThemeManager::getFlagIcon(const QString &countryIsoCode) const |
|
|
|
QIcon UIThemeManager::getFlagIcon(const QString &countryIsoCode) const |
|
|
|
{ |
|
|
|
{ |
|
|
|
if (countryIsoCode.isEmpty()) return {}; |
|
|
|
if (countryIsoCode.isEmpty()) |
|
|
|
|
|
|
|
return {}; |
|
|
|
|
|
|
|
|
|
|
|
const QString key = countryIsoCode.toLower(); |
|
|
|
const QString key = countryIsoCode.toLower(); |
|
|
|
const auto iter = m_flagCache.find(key); |
|
|
|
const auto iter = m_flags.find(key); |
|
|
|
if (iter != m_flagCache.end()) |
|
|
|
if (iter != m_flags.end()) |
|
|
|
return *iter; |
|
|
|
return *iter; |
|
|
|
|
|
|
|
|
|
|
|
const QIcon icon {u":/icons/flags/" + key + u".svg"}; |
|
|
|
const QIcon icon {u":/icons/flags/" + key + u".svg"}; |
|
|
|
m_flagCache[key] = icon; |
|
|
|
m_flags[key] = icon; |
|
|
|
return icon; |
|
|
|
return icon; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
@ -257,9 +512,12 @@ QPixmap UIThemeManager::getScaledPixmap(const QString &iconId, const int height) |
|
|
|
return pixmap; |
|
|
|
return pixmap; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
QColor UIThemeManager::getColor(const QString &id, const QColor &defaultColor) const |
|
|
|
QColor UIThemeManager::getColor(const QString &id) const |
|
|
|
{ |
|
|
|
{ |
|
|
|
return m_colors.value(id, defaultColor); |
|
|
|
const QColor color = m_themeSource->getColor(id, (isDarkTheme() ? ColorMode::Dark : ColorMode::Light)); |
|
|
|
|
|
|
|
Q_ASSERT(color.isValid()); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return color; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
#ifndef Q_OS_MACOS |
|
|
|
#ifndef Q_OS_MACOS |
|
|
@ -292,50 +550,6 @@ QIcon UIThemeManager::getSystrayIcon() const |
|
|
|
} |
|
|
|
} |
|
|
|
#endif |
|
|
|
#endif |
|
|
|
|
|
|
|
|
|
|
|
Path UIThemeManager::getIconPathFromResources(const QString &iconId) const |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
if (m_themeSource) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
const Path customIcon = m_themeSource->iconPath(iconId); |
|
|
|
|
|
|
|
if (!customIcon.isEmpty()) |
|
|
|
|
|
|
|
return customIcon; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return findIcon(iconId, DEFAULT_ICONS_DIR); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
void UIThemeManager::loadColorsFromJSONConfig() |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
const QByteArray config = m_themeSource->readConfig(); |
|
|
|
|
|
|
|
if (config.isEmpty()) |
|
|
|
|
|
|
|
return; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
QJsonParseError jsonError; |
|
|
|
|
|
|
|
const QJsonDocument configJsonDoc = QJsonDocument::fromJson(config, &jsonError); |
|
|
|
|
|
|
|
if (jsonError.error != QJsonParseError::NoError) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
LogMsg(tr("\"%1\" has invalid format. Reason: %2").arg(CONFIG_FILE_NAME, jsonError.errorString()), Log::WARNING); |
|
|
|
|
|
|
|
return; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if (!configJsonDoc.isObject()) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
LogMsg(tr("\"%1\" has invalid format. Reason: %2").arg(CONFIG_FILE_NAME, tr("Root JSON value is not an object")), Log::WARNING); |
|
|
|
|
|
|
|
return; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const QJsonObject colors = configJsonDoc.object().value(u"colors").toObject(); |
|
|
|
|
|
|
|
for (auto color = colors.constBegin(); color != colors.constEnd(); ++color) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
const QColor providedColor(color.value().toString()); |
|
|
|
|
|
|
|
if (!providedColor.isValid()) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
LogMsg(tr("Invalid color for ID \"%1\" is provided by theme").arg(color.key()), Log::WARNING); |
|
|
|
|
|
|
|
continue; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
m_colors.insert(color.key(), providedColor); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
void UIThemeManager::applyPalette() const |
|
|
|
void UIThemeManager::applyPalette() const |
|
|
|
{ |
|
|
|
{ |
|
|
|
struct ColorDescriptor |
|
|
|
struct ColorDescriptor |
|
|
@ -377,9 +591,11 @@ void UIThemeManager::applyPalette() const |
|
|
|
QPalette palette = qApp->palette(); |
|
|
|
QPalette palette = qApp->palette(); |
|
|
|
for (const ColorDescriptor &colorDescriptor : paletteColorDescriptors) |
|
|
|
for (const ColorDescriptor &colorDescriptor : paletteColorDescriptors) |
|
|
|
{ |
|
|
|
{ |
|
|
|
const QColor defaultColor = palette.color(colorDescriptor.colorGroup, colorDescriptor.colorRole); |
|
|
|
// For backward compatibility, the palette color overrides are read from the section of the "light mode" colors
|
|
|
|
const QColor newColor = getColor(colorDescriptor.id, defaultColor); |
|
|
|
const QColor newColor = m_themeSource->getColor(colorDescriptor.id, ColorMode::Light); |
|
|
|
|
|
|
|
if (newColor.isValid()) |
|
|
|
palette.setColor(colorDescriptor.colorGroup, colorDescriptor.colorRole, newColor); |
|
|
|
palette.setColor(colorDescriptor.colorGroup, colorDescriptor.colorRole, newColor); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
qApp->setPalette(palette); |
|
|
|
qApp->setPalette(palette); |
|
|
|
} |
|
|
|
} |
|
|
|