RedPanda-CPP/RedPandaIDE/thememanager.cpp

405 lines
16 KiB
C++
Raw Normal View History

2021-12-26 23:18:28 +08:00
/*
* Copyright (C) 2020-2022 Roy Qu (royqh1979@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 3 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, see <https://www.gnu.org/licenses/>.
*/
2021-10-15 13:49:28 +08:00
#include "thememanager.h"
#include <QApplication>
#include <QDirIterator>
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QMetaEnum>
#include <QMetaObject>
#include <QStyle>
#include "utils.h"
2022-01-28 16:13:20 +08:00
#include "settings.h"
#include "systemconsts.h"
2021-10-15 13:49:28 +08:00
#ifdef ENABLE_LUA_ADDON
#include "addon/executor.h"
#include "addon/runtime.h"
#endif
ThemeManager::ThemeManager(QObject *parent) : QObject(parent)
2021-10-15 13:49:28 +08:00
{
}
PAppTheme ThemeManager::theme(const QString &themeName)
{
QString customThemeDir = pSettings->dirs().config(Settings::Dirs::DataType::Theme);
QString builtInThemeDir = pSettings->dirs().data(Settings::Dirs::DataType::Theme);
PAppTheme appTheme = nullptr;
// custom overrides built-in
2024-04-17 12:48:47 +08:00
if (tryLoadThemeFromDir(customThemeDir, AppTheme::ThemeCategory::Custom, themeName, appTheme))
return appTheme;
2024-04-17 12:48:47 +08:00
if (tryLoadThemeFromDir(builtInThemeDir, AppTheme::ThemeCategory::BuiltIn, themeName, appTheme))
return appTheme;
return AppTheme::fallbackTheme();
}
QList<PAppTheme> ThemeManager::getThemes()
2022-01-28 16:13:20 +08:00
{
QString customThemeDir = pSettings->dirs().config(Settings::Dirs::DataType::Theme);
QString builtInThemeDir = pSettings->dirs().data(Settings::Dirs::DataType::Theme);
std::set<PAppTheme, ThemeCompare> themes;
2022-01-28 16:13:20 +08:00
// custom overrides built-in
2024-04-17 12:48:47 +08:00
loadThemesFromDir(customThemeDir, AppTheme::ThemeCategory::Custom, themes);
loadThemesFromDir(builtInThemeDir, AppTheme::ThemeCategory::BuiltIn, themes);
QList<PAppTheme> result(themes.begin(), themes.end());
return result;
2022-01-28 16:13:20 +08:00
}
bool ThemeManager::ThemeCompare::operator()(const PAppTheme &lhs, const PAppTheme &rhs) const
2022-01-28 16:13:20 +08:00
{
return QFileInfo(lhs->filename()).baseName() < QFileInfo(rhs->filename()).baseName();
2022-01-28 16:13:20 +08:00
}
2024-04-17 12:48:47 +08:00
bool ThemeManager::tryLoadThemeFromDir(const QString &dir, AppTheme::ThemeCategory category, const QString &themeName, PAppTheme &theme)
{
for (const auto &[extension, type] : searchTypes) {
QString filename = QString("%2.%3").arg(themeName, extension);
QString fullPath = QString("%1/%2").arg(dir, filename);
if (QFile::exists(fullPath)) {
try {
2024-04-17 12:48:47 +08:00
theme = std::make_shared<AppTheme>(fullPath, type, category);
return true;
} catch(FileError e) {
//just skip it
}
#ifdef ENABLE_LUA_ADDON
catch(AddOn::LuaError e) {
qDebug() << e.reason();
}
#endif
}
}
return false;
}
2024-04-17 12:48:47 +08:00
void ThemeManager::loadThemesFromDir(const QString &dir, AppTheme::ThemeCategory category, std::set<PAppTheme, ThemeCompare> &themes)
{
for (const auto &[extension, type] : searchTypes) {
QDirIterator it(dir);
while (it.hasNext()) {
it.next();
QFileInfo fileInfo = it.fileInfo();
if (fileInfo.suffix().compare(extension, PATH_SENSITIVITY) == 0) {
try {
2024-04-17 12:48:47 +08:00
PAppTheme appTheme = std::make_shared<AppTheme>(fileInfo.absoluteFilePath(), type, category);
themes.insert(appTheme);
} catch(FileError e) {
//just skip it
}
#ifdef ENABLE_LUA_ADDON
catch(AddOn::LuaError e) {
qDebug() << e.reason();
}
#endif
}
}
}
}
QColor AppTheme::color(ColorRole role) const
{
return mColors.value(role,QColor());
}
QPalette AppTheme::palette() const
{
QPalette pal = initialPalette();
const static struct {
ColorRole themeColor;
QPalette::ColorRole paletteColorRole;
QPalette::ColorGroup paletteColorGroup;
bool setColorRoleAsBrush;
} mapping[] = {
{ColorRole::PaletteWindow, QPalette::Window, QPalette::All, false},
{ColorRole::PaletteWindowDisabled, QPalette::Window, QPalette::Disabled, false},
{ColorRole::PaletteWindowText, QPalette::WindowText, QPalette::All, true},
{ColorRole::PaletteWindowTextDisabled, QPalette::WindowText, QPalette::Disabled, true},
{ColorRole::PaletteBase, QPalette::Base, QPalette::All, false},
{ColorRole::PaletteBaseDisabled, QPalette::Base, QPalette::Disabled, false},
{ColorRole::PaletteAlternateBase, QPalette::AlternateBase, QPalette::All, false},
{ColorRole::PaletteAlternateBaseDisabled, QPalette::AlternateBase, QPalette::Disabled, false},
{ColorRole::PaletteToolTipBase, QPalette::ToolTipBase, QPalette::All, true},
{ColorRole::PaletteToolTipBaseDisabled, QPalette::ToolTipBase, QPalette::Disabled, true},
{ColorRole::PaletteToolTipText, QPalette::ToolTipText, QPalette::All, false},
{ColorRole::PaletteToolTipTextDisabled, QPalette::ToolTipText, QPalette::Disabled, false},
{ColorRole::PaletteText, QPalette::Text, QPalette::All, true},
{ColorRole::PaletteTextDisabled, QPalette::Text, QPalette::Disabled, true},
{ColorRole::PaletteButton, QPalette::Button, QPalette::All, false},
{ColorRole::PaletteButtonDisabled, QPalette::Button, QPalette::Disabled, false},
{ColorRole::PaletteButtonText, QPalette::ButtonText, QPalette::All, true},
{ColorRole::PaletteButtonTextDisabled, QPalette::ButtonText, QPalette::Disabled, true},
{ColorRole::PaletteBrightText, QPalette::BrightText, QPalette::All, false},
{ColorRole::PaletteBrightTextDisabled, QPalette::BrightText, QPalette::Disabled, false},
{ColorRole::PaletteHighlight, QPalette::Highlight, QPalette::All, true},
{ColorRole::PaletteHighlightDisabled, QPalette::Highlight, QPalette::Disabled, true},
{ColorRole::PaletteHighlightedText, QPalette::HighlightedText, QPalette::All, true},
{ColorRole::PaletteHighlightedTextDisabled, QPalette::HighlightedText, QPalette::Disabled, true},
{ColorRole::PaletteLink, QPalette::Link, QPalette::All, false},
{ColorRole::PaletteLinkDisabled, QPalette::Link, QPalette::Disabled, false},
{ColorRole::PaletteLinkVisited, QPalette::LinkVisited, QPalette::All, false},
{ColorRole::PaletteLinkVisitedDisabled, QPalette::LinkVisited, QPalette::Disabled, false},
{ColorRole::PaletteLight, QPalette::Light, QPalette::All, false},
{ColorRole::PaletteLightDisabled, QPalette::Light, QPalette::Disabled, false},
{ColorRole::PaletteMidlight, QPalette::Midlight, QPalette::All, false},
{ColorRole::PaletteMidlightDisabled, QPalette::Midlight, QPalette::Disabled, false},
{ColorRole::PaletteDark, QPalette::Dark, QPalette::All, false},
{ColorRole::PaletteDarkDisabled, QPalette::Dark, QPalette::Disabled, false},
{ColorRole::PaletteMid, QPalette::Mid, QPalette::All, false},
{ColorRole::PaletteMidDisabled, QPalette::Mid, QPalette::Disabled, false},
{ColorRole::PaletteShadow, QPalette::Shadow, QPalette::All, false},
{ColorRole::PaletteShadowDisabled, QPalette::Shadow, QPalette::Disabled, false}
};
for (auto entry: mapping) {
const QColor themeColor = color(entry.themeColor);
// Use original color if color is not defined in theme.
if (themeColor.isValid()) {
// if (entry.setColorRoleAsBrush)
// // TODO: Find out why sometimes setBrush is used
// pal.setBrush(entry.paletteColorGroup, entry.paletteColorRole, themeColor);
// else
// pal.setColor(entry.paletteColorGroup, entry.paletteColorRole, themeColor);
pal.setBrush(entry.paletteColorGroup, entry.paletteColorRole, themeColor);
pal.setColor(entry.paletteColorGroup, entry.paletteColorRole, themeColor);
}
}
return pal;
}
2024-04-17 12:48:47 +08:00
AppTheme::AppTheme(const QString &filename, ThemeType type, ThemeCategory category, QObject *parent):QObject(parent)
{
2024-01-18 17:15:37 +08:00
mFilename = filename;
2024-04-17 12:48:47 +08:00
mType = type;
mCategory = category;
QFile file(filename);
2022-01-28 16:13:20 +08:00
if (!file.exists()) {
throw FileError(tr("Theme file '%1' doesn't exist!")
.arg(filename));
}
if (file.open(QFile::ReadOnly)) {
2024-04-01 19:33:28 +08:00
QByteArray content = file.readAll().trimmed();
if (content.isEmpty())
return;
QJsonObject obj;
switch (type) {
2024-04-17 12:48:47 +08:00
case ThemeType::HardCoded:
__builtin_unreachable();
case ThemeType::JSON: {
QJsonParseError error;
QJsonDocument doc(QJsonDocument::fromJson(content, &error));
if (error.error != QJsonParseError::NoError) {
throw FileError(tr("Error in json file '%1':%2 : %3")
.arg(filename)
.arg(error.offset)
.arg(error.errorString()));
}
obj = doc.object();
// In Lua-based theme, the "style" key has replaced "isDark" and "useQtFusionStyle" keys.
// The following part handles old "isDark" and "useQtFusionStyle" keys.
if (!obj.contains("style")) {
bool useQtFusionStyle = obj["useQtFusionStyle"].toBool(true);
if (useQtFusionStyle) {
bool isDark = obj["isDark"].toBool(false);
obj["style"] = isDark ? "RedPandaDarkFusion" : "RedPandaLightFusion";
} else {
obj["style"] = AppTheme::initialStyle();
}
}
// In Lua-based theme, the script handles name localization.
// The following part handles old "name_xx_XX" keys.
QString localeName = obj["name_"+pSettings->environment().language()].toString();
if (!localeName.isEmpty())
obj["name"] = localeName;
break;
}
#ifdef ENABLE_LUA_ADDON
case ThemeType::Lua: {
obj = AddOn::ThemeExecutor{}(content, filename);
break;
}
#endif
}
QFileInfo fileInfo(filename);
mName = fileInfo.baseName();
mDisplayName = obj["name"].toString();
2024-04-17 12:48:47 +08:00
if (mDisplayName.isEmpty())
mDisplayName = mName;
mStyle = obj["style"].toString();
mDefaultColorScheme = obj["default scheme"].toString();
mDefaultIconSet = obj["default iconset"].toString();
QJsonObject colors = obj["palette"].toObject();
const QMetaObject &m = *metaObject();
QMetaEnum e = m.enumerator(m.indexOfEnumerator("ColorRole"));
for (int i = 0, total = e.keyCount(); i < total; ++i) {
const QString key = QLatin1String(e.key(i));
if (colors.contains(key)) {
QString val=colors[key].toString();
mColors.insert(i, QColor(val));
}
}
} else {
2022-01-28 16:13:20 +08:00
throw FileError(tr("Can't open the theme file '%1' for read.")
.arg(filename));
}
}
bool AppTheme::isSystemInDarkMode() {
// https://www.qt.io/blog/dark-mode-on-windows-11-with-qt-6.5
// compare the window color with the text color to determine whether the palette is dark or light
return initialPalette().color(QPalette::WindowText).lightness() > initialPalette().color(QPalette::Window).lightness();
}
// If you copy QPalette, default values stay at default, even if that default is different
// within the context of different widgets. Create deep copy.
static QPalette copyPalette(const QPalette &p)
{
QPalette res;
for (int group = 0; group < QPalette::NColorGroups; ++group) {
for (int role = 0; role < QPalette::NColorRoles; ++role) {
res.setBrush(QPalette::ColorGroup(group),
QPalette::ColorRole(role),
p.brush(QPalette::ColorGroup(group), QPalette::ColorRole(role)));
res.setColor(QPalette::ColorGroup(group),
QPalette::ColorRole(role),
p.color(QPalette::ColorGroup(group), QPalette::ColorRole(role)));
}
}
return res;
}
QPalette AppTheme::initialPalette()
{
static QPalette palette = copyPalette(QApplication::palette());
return palette;
}
2024-04-23 14:03:48 +08:00
AppTheme::ThemeCategory AppTheme::category() const
{
return mCategory;
}
QString AppTheme::initialStyle()
{
static QString style = QApplication::style()->objectName();
return style;
}
2024-01-18 17:15:37 +08:00
const QString &AppTheme::filename() const
{
return mFilename;
}
2024-04-17 12:48:47 +08:00
const QString AppTheme::categoryIcon() const
{
switch (mCategory) {
case ThemeCategory::FailSafe:
return QString{"\U0000274C"}; //❌
2024-04-17 12:48:47 +08:00
case ThemeCategory::BuiltIn:
return QString{"\U0001F4E6"}; //📦
2024-04-17 12:48:47 +08:00
case ThemeCategory::Custom:
return QString{"\U0001F4C4"}; //📄
2024-04-17 12:48:47 +08:00
case ThemeCategory::Shared:
return QString{"\U0001F310"}; //🌐
2024-04-17 12:48:47 +08:00
default:
return "";
}
}
2024-04-23 14:03:48 +08:00
bool AppTheme::copyTo(const QString &targetFolder)
{
QFileInfo fileInfo{mFilename};
QFile originFile{fileInfo.absoluteFilePath()};
QFile targetFile{QDir(targetFolder).absoluteFilePath(fileInfo.fileName())};
if (!originFile.open(QFile::ReadOnly))
return false;
if (!targetFile.open(QFile::WriteOnly))
return false;
QByteArray contents = originFile.readAll();
if (targetFile.write(contents)!=contents.length())
return false;
targetFile.close();
originFile.close();
return true;
2024-04-23 14:03:48 +08:00
}
const QString &AppTheme::defaultIconSet() const
{
return mDefaultIconSet;
}
void AppTheme::setDefaultIconSet(const QString &newDefaultIconSet)
{
mDefaultIconSet = newDefaultIconSet;
}
const QString &AppTheme::name() const
{
return mName;
}
const QString &AppTheme::displayName() const
{
return mDisplayName;
}
const QString &AppTheme::defaultColorScheme() const
{
return mDefaultColorScheme;
}
void AppTheme::setDefaultColorScheme(const QString &newDefaultColorScheme)
{
mDefaultColorScheme = newDefaultColorScheme;
}
const QString &AppTheme::style() const
{
return mStyle;
}
2024-04-13 09:06:13 +08:00
AppTheme::AppTheme() :
mName("__failsafe__theme__"),
mDisplayName("Fusion"),
2024-04-13 09:06:13 +08:00
mStyle("fusion"),
mDefaultColorScheme("Adaptive"),
2024-04-17 12:48:47 +08:00
mDefaultIconSet("newlook"),
mType(ThemeType::HardCoded),
mCategory(ThemeCategory::FailSafe)
2024-04-13 09:06:13 +08:00
{
}
PAppTheme AppTheme::fallbackTheme()
{
static PAppTheme theme = PAppTheme(new AppTheme());
return theme;
}