From 72ac92ec682b96924bdec606bc4383430f3f7067 Mon Sep 17 00:00:00 2001 From: Vladimir Golovnev Date: Tue, 7 Feb 2023 22:07:15 +0300 Subject: [PATCH] Allow to use another icons in dark mode PR #18435. --- src/gui/log/logmodel.cpp | 21 +- src/gui/rss/articlelistwidget.cpp | 9 +- src/gui/transferlistmodel.cpp | 51 +--- src/gui/uithememanager.cpp | 444 ++++++++++++++++++++++-------- src/gui/uithememanager.h | 24 +- src/gui/utils.cpp | 9 - src/webui/webapplication.cpp | 2 +- 7 files changed, 358 insertions(+), 202 deletions(-) diff --git a/src/gui/log/logmodel.cpp b/src/gui/log/logmodel.cpp index 5c205b351..12bea6160 100644 --- a/src/gui/log/logmodel.cpp +++ b/src/gui/log/logmodel.cpp @@ -32,12 +32,9 @@ #include #include #include -#include #include "base/global.h" -#include "gui/color.h" #include "gui/uithememanager.h" -#include "gui/utils.h" namespace { @@ -45,38 +42,32 @@ namespace QColor getTimestampColor() { - return UIThemeManager::instance()->getColor(u"Log.TimeStamp"_qs - , (Utils::Gui::isDarkTheme() ? Color::Primer::Dark::fgSubtle : Color::Primer::Light::fgSubtle)); + return UIThemeManager::instance()->getColor(u"Log.TimeStamp"_qs); } QColor getLogNormalColor() { - return UIThemeManager::instance()->getColor(u"Log.Normal"_qs - , QApplication::palette().color(QPalette::Active, QPalette::WindowText)); + return UIThemeManager::instance()->getColor(u"Log.Normal"_qs); } QColor getLogInfoColor() { - return UIThemeManager::instance()->getColor(u"Log.Info"_qs - , (Utils::Gui::isDarkTheme() ? Color::Primer::Dark::accentFg : Color::Primer::Light::accentFg)); + return UIThemeManager::instance()->getColor(u"Log.Info"_qs); } QColor getLogWarningColor() { - return UIThemeManager::instance()->getColor(u"Log.Warning"_qs - , (Utils::Gui::isDarkTheme() ? Color::Primer::Dark::severeFg : Color::Primer::Light::severeFg)); + return UIThemeManager::instance()->getColor(u"Log.Warning"_qs); } QColor getLogCriticalColor() { - return UIThemeManager::instance()->getColor(u"Log.Critical"_qs - , (Utils::Gui::isDarkTheme() ? Color::Primer::Dark::dangerFg : Color::Primer::Light::dangerFg)); + return UIThemeManager::instance()->getColor(u"Log.Critical"_qs); } QColor getPeerBannedColor() { - return UIThemeManager::instance()->getColor(u"Log.BannedPeer"_qs - , (Utils::Gui::isDarkTheme() ? Color::Primer::Dark::dangerFg : Color::Primer::Light::dangerFg)); + return UIThemeManager::instance()->getColor(u"Log.BannedPeer"_qs); } } diff --git a/src/gui/rss/articlelistwidget.cpp b/src/gui/rss/articlelistwidget.cpp index 3f3f3b0aa..5b4cac365 100644 --- a/src/gui/rss/articlelistwidget.cpp +++ b/src/gui/rss/articlelistwidget.cpp @@ -102,8 +102,7 @@ void ArticleListWidget::handleArticleRead(RSS::Article *rssArticle) auto item = mapRSSArticle(rssArticle); if (!item) return; - const QColor defaultColor {palette().color(QPalette::Inactive, QPalette::WindowText)}; - const QBrush foregroundBrush {UIThemeManager::instance()->getColor(u"RSS.ReadArticle"_qs, defaultColor)}; + const QBrush foregroundBrush {UIThemeManager::instance()->getColor(u"RSS.ReadArticle"_qs)}; item->setData(Qt::ForegroundRole, foregroundBrush); item->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"loading"_qs, u"sphere"_qs)); @@ -130,15 +129,13 @@ QListWidgetItem *ArticleListWidget::createItem(RSS::Article *article) const item->setData(Qt::UserRole, QVariant::fromValue(article)); if (article->isRead()) { - const QColor defaultColor {palette().color(QPalette::Inactive, QPalette::WindowText)}; - const QBrush foregroundBrush {UIThemeManager::instance()->getColor(u"RSS.ReadArticle"_qs, defaultColor)}; + const QBrush foregroundBrush {UIThemeManager::instance()->getColor(u"RSS.ReadArticle"_qs)}; item->setData(Qt::ForegroundRole, foregroundBrush); item->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"loading"_qs, u"sphere"_qs)); } else { - const QColor defaultColor {palette().color(QPalette::Active, QPalette::Link)}; - const QBrush foregroundBrush {UIThemeManager::instance()->getColor(u"RSS.UnreadArticle"_qs, defaultColor)}; + const QBrush foregroundBrush {UIThemeManager::instance()->getColor(u"RSS.UnreadArticle"_qs)}; item->setData(Qt::ForegroundRole, foregroundBrush); item->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"loading"_qs, u"sphere"_qs)); } diff --git a/src/gui/transferlistmodel.cpp b/src/gui/transferlistmodel.cpp index 71edfab78..dbfb11e82 100644 --- a/src/gui/transferlistmodel.cpp +++ b/src/gui/transferlistmodel.cpp @@ -43,54 +43,10 @@ #include "base/utils/fs.h" #include "base/utils/misc.h" #include "base/utils/string.h" -#include "color.h" #include "uithememanager.h" -#include "utils.h" namespace { - QColor getDefaultColorByState(const BitTorrent::TorrentState state) - { - const bool isDarkTheme = Utils::Gui::isDarkTheme(); - - switch (state) - { - case BitTorrent::TorrentState::Downloading: - case BitTorrent::TorrentState::ForcedDownloading: - case BitTorrent::TorrentState::DownloadingMetadata: - case BitTorrent::TorrentState::ForcedDownloadingMetadata: - return (isDarkTheme ? Color::Primer::Dark::successFg : Color::Primer::Light::successFg); - case BitTorrent::TorrentState::StalledDownloading: - return (isDarkTheme ? Color::Primer::Dark::successEmphasis : Color::Primer::Light::successEmphasis); - case BitTorrent::TorrentState::StalledUploading: - return (isDarkTheme ? Color::Primer::Dark::accentEmphasis : Color::Primer::Light::accentEmphasis); - case BitTorrent::TorrentState::Uploading: - case BitTorrent::TorrentState::ForcedUploading: - return (isDarkTheme ? Color::Primer::Dark::accentFg : Color::Primer::Light::accentFg); - case BitTorrent::TorrentState::PausedDownloading: - return (isDarkTheme ? Color::Primer::Dark::fgMuted : Color::Primer::Light::fgMuted); - case BitTorrent::TorrentState::PausedUploading: - return (isDarkTheme ? Color::Primer::Dark::doneFg : Color::Primer::Light::doneFg); - case BitTorrent::TorrentState::QueuedDownloading: - case BitTorrent::TorrentState::QueuedUploading: - return (isDarkTheme ? Color::Primer::Dark::scaleYellow6 : Color::Primer::Light::scaleYellow6); - case BitTorrent::TorrentState::CheckingDownloading: - case BitTorrent::TorrentState::CheckingUploading: - case BitTorrent::TorrentState::CheckingResumeData: - case BitTorrent::TorrentState::Moving: - return (isDarkTheme ? Color::Primer::Dark::successFg : Color::Primer::Light::successFg); - case BitTorrent::TorrentState::Error: - case BitTorrent::TorrentState::MissingFiles: - case BitTorrent::TorrentState::Unknown: - return (isDarkTheme ? Color::Primer::Dark::dangerFg : Color::Primer::Light::dangerFg); - default: - Q_ASSERT(false); - break; - } - - return {}; - } - QHash torrentStateColorsFromUITheme() { struct TorrentStateColorDescriptor @@ -124,9 +80,8 @@ namespace QHash colors; for (const TorrentStateColorDescriptor &colorDescriptor : colorDescriptors) { - const QColor themeColor = UIThemeManager::instance()->getColor(colorDescriptor.id, QColor()); - if (themeColor.isValid()) - colors.insert(colorDescriptor.state, themeColor); + const QColor themeColor = UIThemeManager::instance()->getColor(colorDescriptor.id); + colors.insert(colorDescriptor.state, themeColor); } return colors; } @@ -548,7 +503,7 @@ QVariant TransferListModel::data(const QModelIndex &index, const int role) const switch (role) { case Qt::ForegroundRole: - return m_stateThemeColors.value(torrent->state(), getDefaultColorByState(torrent->state())); + return m_stateThemeColors.value(torrent->state()); case Qt::DisplayRole: return displayValue(torrent, index.column()); case UnderlyingDataRole: diff --git a/src/gui/uithememanager.cpp b/src/gui/uithememanager.cpp index 5f41df9c0..77a237811 100644 --- a/src/gui/uithememanager.cpp +++ b/src/gui/uithememanager.cpp @@ -1,5 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2023 Vladimir Golovnev * Copyright (C) 2019, 2021 Prince Gupta * * This program is free software; you can redistribute it and/or @@ -38,37 +39,25 @@ #include #include +#include "base/algorithm.h" #include "base/global.h" #include "base/logger.h" #include "base/path.h" #include "base/preferences.h" #include "base/profile.h" #include "base/utils/fs.h" +#include "color.h" namespace { - const Path DEFAULT_ICONS_DIR {u":icons"_qs}; const QString CONFIG_FILE_NAME = u"config.json"_qs; const QString STYLESHEET_FILE_NAME = u"stylesheet.qss"_qs; - // 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 STYLESHEET_RESOURCES_DIR = u":/uitheme"_qs; - - const Path THEME_ICONS_DIR {u"icons"_qs}; - - Path findIcon(const QString &iconId, const Path &dir) + bool isDarkTheme() { - 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 {}; + const QPalette palette = qApp->palette(); + const QColor &color = palette.color(QPalette::Active, QPalette::Base); + return (color.lightness() < 127); } QByteArray readFile(const Path &filePath) @@ -86,73 +75,325 @@ namespace return {}; } - class QRCThemeSource final : public UIThemeSource + 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 colorsFromJSON(const QJsonObject &jsonObj) + { + QHash 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); + } + + 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 {}; + } + + class DefaultThemeSource final : public UIThemeSource { public: + DefaultThemeSource() + { + loadColors(); + } + QByteArray readStyleSheet() override { - return readFile(m_qrcThemeDir / Path(STYLESHEET_FILE_NAME)); + return {}; } - QByteArray readConfig() override + QColor getColor(const QString &colorId, const ColorMode colorMode) const override { - return readFile(m_qrcThemeDir / Path(CONFIG_FILE_NAME)); + if (colorMode == ColorMode::Dark) + { + if (const QColor color = m_darkModeColors.value(colorId) + ; color.isValid()) + { + return color; + } + } + + return m_colors.value(colorId); } - Path iconPath(const QString &iconId) const override + Path getIconPath(const QString &iconId, const ColorMode colorMode) const override { - return findIcon(iconId, m_qrcIconsDir); + const Path iconsPath {u"icons"_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; + } + } + + if (const Path iconPath = findIcon(iconId, (m_userPath / iconsPath)) + ; !iconPath.isEmpty()) + { + return iconPath; + } + + return findIcon(iconId, (m_defaultPath / iconsPath)); } private: - const Path m_qrcThemeDir {u":/uitheme"_qs}; - const Path m_qrcIconsDir = m_qrcThemeDir / THEME_ICONS_DIR; + void loadColors() + { + 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 m_colors; + QHash m_darkModeColors; }; - class FolderThemeSource final : public UIThemeSource + class CustomThemeSource : public UIThemeSource { public: - explicit FolderThemeSource(const Path &folderPath) - : m_folder {folderPath} - , m_iconsDir {m_folder / THEME_ICONS_DIR} + QColor getColor(const QString &colorId, const ColorMode colorMode) const override { + 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); } - QByteArray readStyleSheet() override + Path getIconPath(const QString &iconId, const ColorMode colorMode) const override { - QByteArray styleSheetData = readFile(m_folder / Path(STYLESHEET_FILE_NAME)); - return styleSheetData.replace(STYLESHEET_RESOURCES_DIR.toUtf8(), m_folder.data().toUtf8()); + 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 readConfig() override + QByteArray readStyleSheet() override { - return readFile(m_folder / Path(CONFIG_FILE_NAME)); + return readFile(themeRootPath() / Path(STYLESHEET_FILE_NAME)); } - Path iconPath(const QString &iconId) const override + protected: + virtual Path themeRootPath() const = 0; + + DefaultThemeSource *defaultThemeSource() const { - return findIcon(iconId, m_iconsDir); + return m_defaultThemeSource.get(); } private: - const Path m_folder; - const Path m_iconsDir; + void 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(u"colors").toObject())); + m_darkModeColors.insert(colorsFromJSON(config.value(u"colors.dark").toObject())); + } + + const std::unique_ptr m_defaultThemeSource = std::make_unique(); + QHash m_colors; + QHash m_darkModeColors; }; + class QRCThemeSource final : public CustomThemeSource + { + private: + Path themeRootPath() const override + { + return Path(u":/uitheme"_qs); + } + }; - std::unique_ptr createUIThemeSource(const Path &themePath) + class FolderThemeSource : public CustomThemeSource { - if (themePath.filename() == CONFIG_FILE_NAME) - return std::make_unique(themePath.parentPath()); + public: + explicit FolderThemeSource(const Path &folderPath) + : m_folder {folderPath} + { + } - if ((themePath.hasExtension(u".qbtheme"_qs)) - && QResource::registerResource(themePath.data(), u"/uitheme"_qs)) + QByteArray readStyleSheet() override { - return std::make_unique(); + // 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; @@ -175,17 +416,28 @@ UIThemeManager::UIThemeManager() , m_useSystemIcons {Preferences::instance()->useSystemIcons()} #endif { - const Path themePath = m_useCustomTheme - ? Preferences::instance()->customUIThemePath() - : specialFolderLocation(SpecialFolder::Config) / Path(u"themes/default/config.json"_qs); - m_themeSource = createUIThemeSource(themePath); - if (!m_themeSource) + if (m_useCustomTheme) { - LogMsg(tr("Failed to load UI theme from file: \"%1\"").arg(themePath.toString()), Log::WARNING); + 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(); + else + LogMsg(tr("Failed to load UI theme from file: \"%1\"").arg(themePath.toString()), Log::WARNING); + } + else if (themePath.filename() == CONFIG_FILE_NAME) + { + m_themeSource = std::make_unique(themePath.parentPath()); + } } - else + + if (!m_themeSource) + m_themeSource = std::make_unique(); + + if (m_useCustomTheme) { - loadColorsFromJSONConfig(); applyPalette(); applyStyleSheet(); } @@ -203,9 +455,11 @@ void UIThemeManager::applyStyleSheet() const QIcon UIThemeManager::getIcon(const QString &iconId, [[maybe_unused]] const QString &fallback) const { - // Cache to avoid rescaling svg icons - const auto iter = m_iconCache.find(iconId); - if (iter != m_iconCache.end()) + const auto colorMode = isDarkTheme() ? ColorMode::Dark : ColorMode::Light; + auto &icons = (colorMode == ColorMode::Dark) ? m_darkModeIcons : m_icons; + + const auto iter = icons.find(iconId); + if (iter != icons.end()) return *iter; #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); 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; } #endif - const QIcon icon {getIconPathFromResources(iconId).data()}; - m_iconCache[iconId] = icon; + const QIcon icon {m_themeSource->getIconPath(iconId, colorMode).data()}; + icons[iconId] = icon; return icon; } QIcon UIThemeManager::getFlagIcon(const QString &countryIsoCode) const { - if (countryIsoCode.isEmpty()) return {}; + if (countryIsoCode.isEmpty()) + return {}; const QString key = countryIsoCode.toLower(); - const auto iter = m_flagCache.find(key); - if (iter != m_flagCache.end()) + const auto iter = m_flags.find(key); + if (iter != m_flags.end()) return *iter; const QIcon icon {u":/icons/flags/" + key + u".svg"}; - m_flagCache[key] = icon; + m_flags[key] = icon; return icon; } @@ -257,9 +512,12 @@ QPixmap UIThemeManager::getScaledPixmap(const QString &iconId, const int height) 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 @@ -292,50 +550,6 @@ QIcon UIThemeManager::getSystrayIcon() const } #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 { struct ColorDescriptor @@ -377,9 +591,11 @@ void UIThemeManager::applyPalette() const QPalette palette = qApp->palette(); for (const ColorDescriptor &colorDescriptor : paletteColorDescriptors) { - const QColor defaultColor = palette.color(colorDescriptor.colorGroup, colorDescriptor.colorRole); - const QColor newColor = getColor(colorDescriptor.id, defaultColor); - palette.setColor(colorDescriptor.colorGroup, colorDescriptor.colorRole, newColor); + // For backward compatibility, the palette color overrides are read from the section of the "light mode" colors + const QColor newColor = m_themeSource->getColor(colorDescriptor.id, ColorMode::Light); + if (newColor.isValid()) + palette.setColor(colorDescriptor.colorGroup, colorDescriptor.colorRole, newColor); } + qApp->setPalette(palette); } diff --git a/src/gui/uithememanager.h b/src/gui/uithememanager.h index 8262ce418..d84d7a55f 100644 --- a/src/gui/uithememanager.h +++ b/src/gui/uithememanager.h @@ -1,5 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2023 Vladimir Golovnev * Copyright (C) 2019 Prince Gupta * * This program is free software; you can redistribute it and/or @@ -39,17 +40,23 @@ #include "base/pathfwd.h" +enum class ColorMode +{ + Light, + Dark +}; + class 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; - virtual QByteArray readConfig() = 0; - virtual Path iconPath(const QString &iconId) const = 0; }; -class UIThemeManager : public QObject +class UIThemeManager final : public QObject { Q_OBJECT Q_DISABLE_COPY_MOVE(UIThemeManager) @@ -63,7 +70,7 @@ public: QIcon getFlagIcon(const QString &countryIsoCode) const; QPixmap getScaledPixmap(const QString &iconId, int height) const; - QColor getColor(const QString &id, const QColor &defaultColor) const; + QColor getColor(const QString &id) const; #ifndef Q_OS_MACOS QIcon getSystrayIcon() const; @@ -71,8 +78,7 @@ public: private: UIThemeManager(); // singleton class - Path getIconPathFromResources(const QString &iconId) const; - void loadColorsFromJSONConfig(); + void applyPalette() const; void applyStyleSheet() const; @@ -82,7 +88,7 @@ private: const bool m_useSystemIcons; #endif std::unique_ptr m_themeSource; - QHash m_colors; - mutable QHash m_iconCache; - mutable QHash m_flagCache; + mutable QHash m_icons; + mutable QHash m_darkModeIcons; + mutable QHash m_flags; }; diff --git a/src/gui/utils.cpp b/src/gui/utils.cpp index b57e8a1e9..12613ba61 100644 --- a/src/gui/utils.cpp +++ b/src/gui/utils.cpp @@ -35,10 +35,8 @@ #endif #include -#include #include #include -#include #include #include #include @@ -57,13 +55,6 @@ #include "base/utils/fs.h" #include "base/utils/version.h" -bool Utils::Gui::isDarkTheme() -{ - const QPalette palette = qApp->palette(); - const QColor &color = palette.color(QPalette::Active, QPalette::Base); - return (color.lightness() < 127); -} - QPixmap Utils::Gui::scaledPixmap(const QIcon &icon, const QWidget *widget, const int height) { Q_UNUSED(widget); // TODO: remove it diff --git a/src/webui/webapplication.cpp b/src/webui/webapplication.cpp index f03475e26..e96a469bb 100644 --- a/src/webui/webapplication.cpp +++ b/src/webui/webapplication.cpp @@ -554,7 +554,7 @@ Http::Response WebApplication::processRequest(const Http::Request &request, cons // block suspicious requests if ((m_isCSRFProtectionEnabled && isCrossSiteRequest(m_request)) || (m_isHostHeaderValidationEnabled && !validateHostHeader(m_domainList))) - { + { throw UnauthorizedHTTPError(); }