mirror of
https://gitee.com/vnotex/vnote.git
synced 2025-07-05 22:09:52 +08:00
448 lines
14 KiB
C++
448 lines
14 KiB
C++
#include "theme.h"
|
|
|
|
#include <QDir>
|
|
#include <QRegularExpression>
|
|
#include <QSettings>
|
|
#include <QFileInfo>
|
|
#include <QJsonDocument>
|
|
|
|
#include "exception.h"
|
|
#include <utils/fileutils.h>
|
|
#include <utils/pathutils.h>
|
|
#include <utils/utils.h>
|
|
#include <utils/widgetutils.h>
|
|
|
|
using namespace vnotex;
|
|
|
|
Theme::Theme(const QString &p_themeFolderPath,
|
|
const Metadata &p_metadata,
|
|
const Palette &p_palette)
|
|
: m_themeFolderPath(p_themeFolderPath),
|
|
m_metadata(p_metadata),
|
|
m_palette(p_palette)
|
|
{
|
|
}
|
|
|
|
bool Theme::isValidThemeFolder(const QString &p_folder)
|
|
{
|
|
QDir dir(p_folder);
|
|
if (!dir.exists()) {
|
|
qWarning() << "theme folder does not exist" << p_folder;
|
|
return false;
|
|
}
|
|
|
|
// The Palette file must exist.
|
|
auto file = getFileName(File::Palette);
|
|
if (!dir.exists(file)) {
|
|
qWarning() << "Not a valid theme folder" << p_folder;
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
QString Theme::getDisplayName(const QString &p_folder, const QString &p_locale)
|
|
{
|
|
auto obj = readPaletteFile(p_folder);
|
|
const auto metaObj = obj[QStringLiteral("metadata")].toObject();
|
|
QString prefix("display_name");
|
|
|
|
if (!p_locale.isEmpty()) {
|
|
// Check full locale.
|
|
auto fullLocale = QString("%1_%2").arg(prefix, p_locale);
|
|
if (metaObj.contains(fullLocale)) {
|
|
return metaObj.value(fullLocale).toString();
|
|
}
|
|
|
|
auto shortLocale = QString("%1_%2").arg(prefix, p_locale.split('_')[0]);
|
|
if (metaObj.contains(shortLocale)) {
|
|
return metaObj.value(shortLocale).toString();
|
|
}
|
|
}
|
|
|
|
if (metaObj.contains(prefix)) {
|
|
return metaObj.value(prefix).toString();
|
|
}
|
|
return PathUtils::dirName(p_folder);
|
|
}
|
|
|
|
Theme *Theme::fromFolder(const QString &p_folder)
|
|
{
|
|
Q_ASSERT(!p_folder.isEmpty());
|
|
auto obj = readPaletteFile(p_folder);
|
|
auto metadata = readMetadata(obj);
|
|
auto paletteObj = translatePalette(obj);
|
|
return new Theme(p_folder,
|
|
metadata,
|
|
paletteObj);
|
|
}
|
|
|
|
Theme::Metadata Theme::readMetadata(const Palette &p_obj)
|
|
{
|
|
Metadata data;
|
|
|
|
const auto metaObj = p_obj[QStringLiteral("metadata")].toObject();
|
|
|
|
data.m_revision = metaObj[QStringLiteral("revision")].toInt();
|
|
data.m_editorHighlightTheme = metaObj[QStringLiteral("editor-highlight-theme")].toString();
|
|
data.m_markdownEditorHighlightTheme = metaObj[QStringLiteral("markdown-editor-highlight-theme")].toString();
|
|
|
|
return data;
|
|
}
|
|
|
|
Theme::Palette Theme::translatePalette(const QJsonObject &p_obj)
|
|
{
|
|
const QString paletteSection("palette");
|
|
const QString baseSection("base");
|
|
const QString widgetsSection("widgets");
|
|
|
|
// @p_palette may contain referenced definitons: derived=@base#sub#sub2.
|
|
Palette palette;
|
|
|
|
palette[paletteSection] = p_obj[paletteSection];
|
|
palette[baseSection] = p_obj[baseSection];
|
|
palette[widgetsSection] = p_obj[widgetsSection];
|
|
|
|
// Skip paletteSection since it will not contain any reference.
|
|
|
|
translatePaletteObject(palette, palette, baseSection);
|
|
|
|
translatePaletteObject(palette, palette, widgetsSection);
|
|
|
|
return palette;
|
|
}
|
|
|
|
void Theme::translatePaletteObject(const Palette &p_palette,
|
|
QJsonObject &p_obj,
|
|
const QString &p_key)
|
|
{
|
|
int lastUnresolvedRefs = 0;
|
|
while (true)
|
|
{
|
|
auto ret = translatePaletteObjectOnce(p_palette, p_obj, p_key);
|
|
if (!ret.first) {
|
|
break;
|
|
}
|
|
|
|
if (ret.second > 0 && ret.second == lastUnresolvedRefs) {
|
|
qWarning() << "found cyclic references in palette definitions" << p_obj[p_key];
|
|
break;
|
|
}
|
|
lastUnresolvedRefs = ret.second;
|
|
}
|
|
}
|
|
|
|
QPair<bool, int> Theme::translatePaletteObjectOnce(const Palette &p_palette,
|
|
QJsonObject &p_obj,
|
|
const QString &p_key)
|
|
{
|
|
bool changed = false;
|
|
int unresolvedRefs = 0;
|
|
|
|
// May contain referenced definitions: derived=@base#sub#sub2.
|
|
QRegularExpression refRe("\\A@(\\w+(?:#\\w+)*)\\z");
|
|
const int baseCapturedIdx = 1;
|
|
|
|
auto obj = p_obj[p_key].toObject();
|
|
for (auto it = obj.begin(); it != obj.end(); ++it) {
|
|
auto val = it.value();
|
|
if (val.isString()) {
|
|
// Check if it references to another key.
|
|
auto match = refRe.match(val.toString());
|
|
if (match.hasMatch()) {
|
|
auto refVal = findValueByKeyPath(p_palette, match.captured(baseCapturedIdx));
|
|
if (refVal.isUndefined()) {
|
|
++unresolvedRefs;
|
|
qWarning() << "failed to find palette key" << match.captured(baseCapturedIdx);
|
|
break;
|
|
} else if (val.toString() == refVal.toString()) {
|
|
++unresolvedRefs;
|
|
qWarning() << "found cyclic references in palette definitions" << it.key() << val.toString();
|
|
break;
|
|
}
|
|
|
|
Q_ASSERT_X(refVal.isString(), "translatePaletteObjectOnce", val.toString().toStdString().c_str());
|
|
it.value() = refVal.toString();
|
|
if (isRef(refVal.toString())) {
|
|
// It is another ref again.
|
|
++unresolvedRefs;
|
|
}
|
|
changed = true;
|
|
}
|
|
} else if (val.isObject()) {
|
|
auto ret = translatePaletteObjectOnce(p_palette, obj, it.key());
|
|
changed = changed || ret.first;
|
|
unresolvedRefs += ret.second;
|
|
} else {
|
|
Q_ASSERT(false);
|
|
}
|
|
}
|
|
|
|
if (changed) {
|
|
p_obj[p_key] = obj;
|
|
}
|
|
return qMakePair(changed, unresolvedRefs);
|
|
}
|
|
|
|
QString Theme::fetchQtStyleSheet() const
|
|
{
|
|
const auto qtStyleFile = getFile(File::QtStyleSheet);
|
|
if (qtStyleFile.isEmpty()) {
|
|
return "";
|
|
}
|
|
auto style = FileUtils::readTextFile(qtStyleFile);
|
|
translateStyleByPalette(m_palette, style);
|
|
translateUrlToAbsolute(m_themeFolderPath, style);
|
|
translateFontFamilyList(style);
|
|
translateScaledSize(WidgetUtils::calculateScaleFactor(), style);
|
|
return style;
|
|
}
|
|
|
|
void Theme::translateStyleByPalette(const Palette &p_palette, QString &p_style)
|
|
{
|
|
QRegularExpression refRe("(\\s|:)@(\\w+(?:#\\w+)*)");
|
|
const int prefixCapturedIdx = 1;
|
|
const int refCapturedIdx = 2;
|
|
|
|
int pos = 0;
|
|
QRegularExpressionMatch match;
|
|
while (pos < p_style.size()) {
|
|
int idx = p_style.indexOf(refRe, pos, &match);
|
|
if (idx == -1) {
|
|
break;
|
|
}
|
|
|
|
auto name = match.captured(refCapturedIdx);
|
|
auto val = findValueByKeyPath(p_palette, name).toString();
|
|
if (val.isEmpty() || isRef(val)) {
|
|
qWarning() << "failed to translate style" << name << val;
|
|
pos = idx + match.capturedLength();
|
|
} else {
|
|
pos = idx + match.capturedLength() + val.size() - (name.size() + 1);
|
|
p_style.replace(idx + match.captured(prefixCapturedIdx).size(),
|
|
name.size() + 1,
|
|
val);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Theme::translateUrlToAbsolute(const QString &p_basePath, QString &p_style)
|
|
{
|
|
QRegularExpression urlRe("(\\s|:)url\\(([^\\(\\)]+)\\)");
|
|
const int prefixCapturedIdx = 1;
|
|
const int urlCapturedIdx = 2;
|
|
|
|
QDir dir(p_basePath);
|
|
const int literalSize = QString("url(").size();
|
|
int pos = 0;
|
|
QRegularExpressionMatch match;
|
|
while (pos < p_style.size()) {
|
|
int idx = p_style.indexOf(urlRe, pos, &match);
|
|
if (idx == -1) {
|
|
break;
|
|
}
|
|
|
|
auto url = match.captured(urlCapturedIdx);
|
|
if (QFileInfo(url).isRelative()) {
|
|
auto absoluteUrl = dir.filePath(url);
|
|
pos = idx + match.capturedLength() + absoluteUrl.size() - url.size();
|
|
p_style.replace(idx + match.captured(prefixCapturedIdx).size() + literalSize,
|
|
url.size(),
|
|
absoluteUrl);
|
|
} else {
|
|
pos = idx + match.capturedLength();
|
|
}
|
|
}
|
|
}
|
|
|
|
void Theme::translateFontFamilyList(QString &p_style)
|
|
{
|
|
QRegularExpression fontRe("(\\s|^)font-family:([^;]+);");
|
|
const int prefixCapturedIdx = 1;
|
|
const int fontCapturedIdx = 2;
|
|
|
|
int pos = 0;
|
|
QRegularExpressionMatch match;
|
|
while (pos < p_style.size()) {
|
|
int idx = p_style.indexOf(fontRe, pos, &match);
|
|
if (idx == -1) {
|
|
break;
|
|
}
|
|
|
|
auto familyList = match.captured(fontCapturedIdx).trimmed();
|
|
familyList.remove('"');
|
|
auto family = Utils::pickAvailableFontFamily(familyList.split(','));
|
|
if (family.isEmpty()) {
|
|
// Could not find available font. Remove it.
|
|
auto newStr = match.captured(prefixCapturedIdx);
|
|
p_style.replace(idx, match.capturedLength(), newStr);
|
|
pos = idx + newStr.size();
|
|
} else if (family != familyList) {
|
|
if (family.contains(' ')) {
|
|
family = "\"" + family + "\"";
|
|
}
|
|
|
|
auto newStr = QString("%1font-family: %2;").arg(match.captured(prefixCapturedIdx), family);
|
|
p_style.replace(idx, match.capturedLength(), newStr);
|
|
pos = idx + newStr.size();
|
|
} else {
|
|
pos = idx + match.capturedLength();
|
|
}
|
|
}
|
|
}
|
|
|
|
void Theme::translateScaledSize(qreal p_factor, QString &p_style)
|
|
{
|
|
QRegularExpression scaleRe("(\\s|:)\\$([+-]?)(\\d+)(?=\\D)");
|
|
const int prefixCapturedIdx = 1;
|
|
const int signCapturedIdx = 2;
|
|
const int numCapturedIdx = 3;
|
|
|
|
int pos = 0;
|
|
QRegularExpressionMatch match;
|
|
while (pos < p_style.size()) {
|
|
int idx = p_style.indexOf(scaleRe, pos, &match);
|
|
if (idx == -1) {
|
|
break;
|
|
}
|
|
|
|
auto numStr = match.captured(numCapturedIdx);
|
|
bool ok = false;
|
|
int val = numStr.toInt(&ok);
|
|
if (!ok) {
|
|
pos = idx + match.capturedLength();
|
|
continue;
|
|
}
|
|
|
|
val = val * p_factor + 0.5;
|
|
auto newStr = QString("%1%2%3").arg(match.captured(prefixCapturedIdx),
|
|
match.captured(signCapturedIdx),
|
|
QString::number(val));
|
|
p_style.replace(idx, match.capturedLength(), newStr);
|
|
pos = idx + newStr.size();
|
|
}
|
|
}
|
|
|
|
QString Theme::paletteColor(const QString &p_name) const
|
|
{
|
|
auto val = findValueByKeyPath(m_palette, p_name).toString();
|
|
if (!val.isEmpty() && !isRef(val)) {
|
|
return val;
|
|
}
|
|
qWarning() << "undefined or invalid palette color" << p_name;
|
|
return QString("#ff0000");
|
|
}
|
|
|
|
QJsonObject Theme::readJsonFile(const QString &p_filePath)
|
|
{
|
|
auto bytes = FileUtils::readFile(p_filePath);
|
|
return QJsonDocument::fromJson(bytes).object();
|
|
}
|
|
|
|
QJsonObject Theme::readPaletteFile(const QString &p_folder)
|
|
{
|
|
auto obj = readJsonFile(QDir(p_folder).filePath(getFileName(File::Palette)));
|
|
return obj;
|
|
}
|
|
|
|
QJsonValue Theme::findValueByKeyPath(const Palette &p_palette, const QString &p_keyPath)
|
|
{
|
|
auto keys = p_keyPath.split('#');
|
|
Q_ASSERT(!keys.isEmpty());
|
|
if (keys.size() == 1) {
|
|
return p_palette[keys.first()];
|
|
}
|
|
|
|
auto obj = p_palette;
|
|
for (int i = 0; i < keys.size() - 1; ++i) {
|
|
obj = obj[keys[i]].toObject();
|
|
}
|
|
|
|
return obj[keys.last()];
|
|
}
|
|
|
|
bool Theme::isRef(const QString &p_str)
|
|
{
|
|
return p_str.startsWith('@');
|
|
}
|
|
|
|
QString Theme::getFile(File p_fileType) const
|
|
{
|
|
QDir dir(m_themeFolderPath);
|
|
if (dir.exists(getFileName(p_fileType))) {
|
|
return dir.filePath(getFileName(p_fileType));
|
|
} else if (p_fileType == File::MarkdownEditorStyle) {
|
|
// Fallback to text editor style.
|
|
if (dir.exists(getFileName(File::TextEditorStyle))) {
|
|
return dir.filePath(getFileName(File::TextEditorStyle));
|
|
}
|
|
}
|
|
return "";
|
|
}
|
|
|
|
QString Theme::getFileName(File p_fileType)
|
|
{
|
|
switch (p_fileType) {
|
|
case File::Palette:
|
|
return QStringLiteral("palette.json");
|
|
case File::QtStyleSheet:
|
|
return QStringLiteral("interface.qss");
|
|
case File::WebStyleSheet:
|
|
return QStringLiteral("web.css");
|
|
case File::HighlightStyleSheet:
|
|
return QStringLiteral("highlight.css");
|
|
case File::TextEditorStyle:
|
|
return QStringLiteral("text-editor.theme");
|
|
case File::MarkdownEditorStyle:
|
|
return QStringLiteral("markdown-text-editor.theme");
|
|
case File::EditorHighlightStyle:
|
|
return QStringLiteral("editor-highlight.theme");
|
|
case File::MarkdownEditorHighlightStyle:
|
|
return QStringLiteral("markdown-editor-highlight.theme");
|
|
case File::Cover:
|
|
return QStringLiteral("cover.png");
|
|
default:
|
|
Q_ASSERT(false);
|
|
return "";
|
|
}
|
|
}
|
|
|
|
QString Theme::getEditorHighlightTheme() const
|
|
{
|
|
auto file = getFile(File::EditorHighlightStyle);
|
|
if (file.isEmpty()) {
|
|
return m_metadata.m_editorHighlightTheme;
|
|
} else {
|
|
return file;
|
|
}
|
|
}
|
|
|
|
QString Theme::getMarkdownEditorHighlightTheme() const
|
|
{
|
|
auto file = getFile(File::MarkdownEditorHighlightStyle);
|
|
if (!file.isEmpty()) {
|
|
return file;
|
|
}
|
|
|
|
if (!m_metadata.m_markdownEditorHighlightTheme.isEmpty()) {
|
|
return m_metadata.m_markdownEditorHighlightTheme;
|
|
}
|
|
|
|
return getEditorHighlightTheme();
|
|
}
|
|
|
|
QString Theme::name() const
|
|
{
|
|
return PathUtils::dirName(m_themeFolderPath);
|
|
}
|
|
|
|
QPixmap Theme::getCover(const QString &p_folder)
|
|
{
|
|
QDir dir(p_folder);
|
|
if (dir.exists(getFileName(File::Cover))) {
|
|
const auto coverFile = dir.filePath(getFileName(File::Cover));
|
|
return QPixmap(coverFile);
|
|
}
|
|
return QPixmap();
|
|
}
|