theme.cpp 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  1. #include "theme.h"
  2. #include <QDir>
  3. #include <QRegularExpression>
  4. #include <QSettings>
  5. #include <QFileInfo>
  6. #include <QJsonDocument>
  7. #include "exception.h"
  8. #include <utils/fileutils.h>
  9. #include <utils/pathutils.h>
  10. #include <utils/utils.h>
  11. #include <utils/widgetutils.h>
  12. using namespace vnotex;
  13. Theme::Theme(const QString &p_themeFolderPath,
  14. const Metadata &p_metadata,
  15. const Palette &p_palette)
  16. : m_themeFolderPath(p_themeFolderPath),
  17. m_metadata(p_metadata),
  18. m_palette(p_palette)
  19. {
  20. }
  21. bool Theme::isValidThemeFolder(const QString &p_folder)
  22. {
  23. QDir dir(p_folder);
  24. if (!dir.exists()) {
  25. qWarning() << "theme folder does not exist" << p_folder;
  26. return false;
  27. }
  28. // The Palette file must exist.
  29. auto file = getFileName(File::Palette);
  30. if (!dir.exists(file)) {
  31. qWarning() << "Not a valid theme folder" << p_folder;
  32. return false;
  33. }
  34. return true;
  35. }
  36. QString Theme::getDisplayName(const QString &p_folder, const QString &p_locale)
  37. {
  38. auto obj = readPaletteFile(p_folder);
  39. const auto metaObj = obj[QStringLiteral("metadata")].toObject();
  40. QString prefix("display_name");
  41. if (!p_locale.isEmpty()) {
  42. // Check full locale.
  43. auto fullLocale = QString("%1_%2").arg(prefix, p_locale);
  44. if (metaObj.contains(fullLocale)) {
  45. return metaObj.value(fullLocale).toString();
  46. }
  47. auto shortLocale = QString("%1_%2").arg(prefix, p_locale.split('_')[0]);
  48. if (metaObj.contains(shortLocale)) {
  49. return metaObj.value(shortLocale).toString();
  50. }
  51. }
  52. if (metaObj.contains(prefix)) {
  53. return metaObj.value(prefix).toString();
  54. }
  55. return PathUtils::dirName(p_folder);
  56. }
  57. Theme *Theme::fromFolder(const QString &p_folder)
  58. {
  59. Q_ASSERT(!p_folder.isEmpty());
  60. auto obj = readPaletteFile(p_folder);
  61. auto metadata = readMetadata(obj);
  62. auto paletteObj = translatePalette(obj);
  63. return new Theme(p_folder,
  64. metadata,
  65. paletteObj);
  66. }
  67. Theme::Metadata Theme::readMetadata(const Palette &p_obj)
  68. {
  69. Metadata data;
  70. const auto metaObj = p_obj[QStringLiteral("metadata")].toObject();
  71. data.m_revision = metaObj[QStringLiteral("revision")].toInt();
  72. data.m_editorHighlightTheme = metaObj[QStringLiteral("editor-highlight-theme")].toString();
  73. data.m_markdownEditorHighlightTheme = metaObj[QStringLiteral("markdown-editor-highlight-theme")].toString();
  74. return data;
  75. }
  76. Theme::Palette Theme::translatePalette(const QJsonObject &p_obj)
  77. {
  78. const QString paletteSection("palette");
  79. const QString baseSection("base");
  80. const QString widgetsSection("widgets");
  81. // @p_palette may contain referenced definitons: derived=@base#sub#sub2.
  82. Palette palette;
  83. palette[paletteSection] = p_obj[paletteSection];
  84. palette[baseSection] = p_obj[baseSection];
  85. palette[widgetsSection] = p_obj[widgetsSection];
  86. // Skip paletteSection since it will not contain any reference.
  87. translatePaletteObject(palette, palette, baseSection);
  88. translatePaletteObject(palette, palette, widgetsSection);
  89. return palette;
  90. }
  91. void Theme::translatePaletteObject(const Palette &p_palette,
  92. QJsonObject &p_obj,
  93. const QString &p_key)
  94. {
  95. int lastUnresolvedRefs = 0;
  96. while (true)
  97. {
  98. auto ret = translatePaletteObjectOnce(p_palette, p_obj, p_key);
  99. if (!ret.first) {
  100. break;
  101. }
  102. if (ret.second > 0 && ret.second == lastUnresolvedRefs) {
  103. qWarning() << "found cyclic references in palette definitions" << p_obj[p_key];
  104. break;
  105. }
  106. lastUnresolvedRefs = ret.second;
  107. }
  108. }
  109. QPair<bool, int> Theme::translatePaletteObjectOnce(const Palette &p_palette,
  110. QJsonObject &p_obj,
  111. const QString &p_key)
  112. {
  113. bool changed = false;
  114. int unresolvedRefs = 0;
  115. // May contain referenced definitions: derived=@base#sub#sub2.
  116. QRegularExpression refRe("\\A@(\\w+(?:#\\w+)*)\\z");
  117. const int baseCapturedIdx = 1;
  118. auto obj = p_obj[p_key].toObject();
  119. for (auto it = obj.begin(); it != obj.end(); ++it) {
  120. auto val = it.value();
  121. if (val.isString()) {
  122. // Check if it references to another key.
  123. auto match = refRe.match(val.toString());
  124. if (match.hasMatch()) {
  125. auto refVal = findValueByKeyPath(p_palette, match.captured(baseCapturedIdx));
  126. if (refVal.isUndefined()) {
  127. ++unresolvedRefs;
  128. qWarning() << "failed to find palette key" << match.captured(baseCapturedIdx);
  129. break;
  130. } else if (val.toString() == refVal.toString()) {
  131. ++unresolvedRefs;
  132. qWarning() << "found cyclic references in palette definitions" << it.key() << val.toString();
  133. break;
  134. }
  135. Q_ASSERT_X(refVal.isString(), "translatePaletteObjectOnce", val.toString().toStdString().c_str());
  136. it.value() = refVal.toString();
  137. if (isRef(refVal.toString())) {
  138. // It is another ref again.
  139. ++unresolvedRefs;
  140. }
  141. changed = true;
  142. }
  143. } else if (val.isObject()) {
  144. auto ret = translatePaletteObjectOnce(p_palette, obj, it.key());
  145. changed = changed || ret.first;
  146. unresolvedRefs += ret.second;
  147. } else {
  148. Q_ASSERT(false);
  149. }
  150. }
  151. if (changed) {
  152. p_obj[p_key] = obj;
  153. }
  154. return qMakePair(changed, unresolvedRefs);
  155. }
  156. QString Theme::fetchQtStyleSheet() const
  157. {
  158. const auto qtStyleFile = getFile(File::QtStyleSheet);
  159. if (qtStyleFile.isEmpty()) {
  160. return "";
  161. }
  162. auto style = FileUtils::readTextFile(qtStyleFile);
  163. translateStyleByPalette(m_palette, style);
  164. translateUrlToAbsolute(m_themeFolderPath, style);
  165. translateFontFamilyList(style);
  166. translateScaledSize(WidgetUtils::calculateScaleFactor(), style);
  167. return style;
  168. }
  169. void Theme::translateStyleByPalette(const Palette &p_palette, QString &p_style)
  170. {
  171. QRegularExpression refRe("(\\s|:)@(\\w+(?:#\\w+)*)");
  172. const int prefixCapturedIdx = 1;
  173. const int refCapturedIdx = 2;
  174. int pos = 0;
  175. QRegularExpressionMatch match;
  176. while (pos < p_style.size()) {
  177. int idx = p_style.indexOf(refRe, pos, &match);
  178. if (idx == -1) {
  179. break;
  180. }
  181. auto name = match.captured(refCapturedIdx);
  182. auto val = findValueByKeyPath(p_palette, name).toString();
  183. if (val.isEmpty() || isRef(val)) {
  184. qWarning() << "failed to translate style" << name << val;
  185. pos = idx + match.capturedLength();
  186. } else {
  187. pos = idx + match.capturedLength() + val.size() - (name.size() + 1);
  188. p_style.replace(idx + match.captured(prefixCapturedIdx).size(),
  189. name.size() + 1,
  190. val);
  191. }
  192. }
  193. }
  194. void Theme::translateUrlToAbsolute(const QString &p_basePath, QString &p_style)
  195. {
  196. QRegularExpression urlRe("(\\s|:)url\\(([^\\(\\)]+)\\)");
  197. const int prefixCapturedIdx = 1;
  198. const int urlCapturedIdx = 2;
  199. QDir dir(p_basePath);
  200. const int literalSize = QString("url(").size();
  201. int pos = 0;
  202. QRegularExpressionMatch match;
  203. while (pos < p_style.size()) {
  204. int idx = p_style.indexOf(urlRe, pos, &match);
  205. if (idx == -1) {
  206. break;
  207. }
  208. auto url = match.captured(urlCapturedIdx);
  209. if (QFileInfo(url).isRelative()) {
  210. auto absoluteUrl = dir.filePath(url);
  211. pos = idx + match.capturedLength() + absoluteUrl.size() - url.size();
  212. p_style.replace(idx + match.captured(prefixCapturedIdx).size() + literalSize,
  213. url.size(),
  214. absoluteUrl);
  215. } else {
  216. pos = idx + match.capturedLength();
  217. }
  218. }
  219. }
  220. void Theme::translateFontFamilyList(QString &p_style)
  221. {
  222. QRegularExpression fontRe("(\\s|^)font-family:([^;]+);");
  223. const int prefixCapturedIdx = 1;
  224. const int fontCapturedIdx = 2;
  225. int pos = 0;
  226. QRegularExpressionMatch match;
  227. while (pos < p_style.size()) {
  228. int idx = p_style.indexOf(fontRe, pos, &match);
  229. if (idx == -1) {
  230. break;
  231. }
  232. auto familyList = match.captured(fontCapturedIdx).trimmed();
  233. familyList.remove('"');
  234. auto family = Utils::pickAvailableFontFamily(familyList.split(','));
  235. if (family.isEmpty()) {
  236. // Could not find available font. Remove it.
  237. auto newStr = match.captured(prefixCapturedIdx);
  238. p_style.replace(idx, match.capturedLength(), newStr);
  239. pos = idx + newStr.size();
  240. } else if (family != familyList) {
  241. if (family.contains(' ')) {
  242. family = "\"" + family + "\"";
  243. }
  244. auto newStr = QString("%1font-family: %2;").arg(match.captured(prefixCapturedIdx), family);
  245. p_style.replace(idx, match.capturedLength(), newStr);
  246. pos = idx + newStr.size();
  247. } else {
  248. pos = idx + match.capturedLength();
  249. }
  250. }
  251. }
  252. void Theme::translateScaledSize(qreal p_factor, QString &p_style)
  253. {
  254. QRegularExpression scaleRe("(\\s|:)\\$([+-]?)(\\d+)(?=\\D)");
  255. const int prefixCapturedIdx = 1;
  256. const int signCapturedIdx = 2;
  257. const int numCapturedIdx = 3;
  258. int pos = 0;
  259. QRegularExpressionMatch match;
  260. while (pos < p_style.size()) {
  261. int idx = p_style.indexOf(scaleRe, pos, &match);
  262. if (idx == -1) {
  263. break;
  264. }
  265. auto numStr = match.captured(numCapturedIdx);
  266. bool ok = false;
  267. int val = numStr.toInt(&ok);
  268. if (!ok) {
  269. pos = idx + match.capturedLength();
  270. continue;
  271. }
  272. val = val * p_factor + 0.5;
  273. auto newStr = QString("%1%2%3").arg(match.captured(prefixCapturedIdx),
  274. match.captured(signCapturedIdx),
  275. QString::number(val));
  276. p_style.replace(idx, match.capturedLength(), newStr);
  277. pos = idx + newStr.size();
  278. }
  279. }
  280. QString Theme::paletteColor(const QString &p_name) const
  281. {
  282. auto val = findValueByKeyPath(m_palette, p_name).toString();
  283. if (!val.isEmpty() && !isRef(val)) {
  284. return val;
  285. }
  286. qWarning() << "undefined or invalid palette color" << p_name;
  287. return QString("#ff0000");
  288. }
  289. QJsonObject Theme::readJsonFile(const QString &p_filePath)
  290. {
  291. auto bytes = FileUtils::readFile(p_filePath);
  292. return QJsonDocument::fromJson(bytes).object();
  293. }
  294. QJsonObject Theme::readPaletteFile(const QString &p_folder)
  295. {
  296. auto obj = readJsonFile(QDir(p_folder).filePath(getFileName(File::Palette)));
  297. return obj;
  298. }
  299. QJsonValue Theme::findValueByKeyPath(const Palette &p_palette, const QString &p_keyPath)
  300. {
  301. auto keys = p_keyPath.split('#');
  302. Q_ASSERT(!keys.isEmpty());
  303. if (keys.size() == 1) {
  304. return p_palette[keys.first()];
  305. }
  306. auto obj = p_palette;
  307. for (int i = 0; i < keys.size() - 1; ++i) {
  308. obj = obj[keys[i]].toObject();
  309. }
  310. return obj[keys.last()];
  311. }
  312. bool Theme::isRef(const QString &p_str)
  313. {
  314. return p_str.startsWith('@');
  315. }
  316. QString Theme::getFile(File p_fileType) const
  317. {
  318. QDir dir(m_themeFolderPath);
  319. if (dir.exists(getFileName(p_fileType))) {
  320. return dir.filePath(getFileName(p_fileType));
  321. } else if (p_fileType == File::MarkdownEditorStyle) {
  322. // Fallback to text editor style.
  323. if (dir.exists(getFileName(File::TextEditorStyle))) {
  324. return dir.filePath(getFileName(File::TextEditorStyle));
  325. }
  326. }
  327. return "";
  328. }
  329. QString Theme::getFileName(File p_fileType)
  330. {
  331. switch (p_fileType) {
  332. case File::Palette:
  333. return QStringLiteral("palette.json");
  334. case File::QtStyleSheet:
  335. return QStringLiteral("interface.qss");
  336. case File::WebStyleSheet:
  337. return QStringLiteral("web.css");
  338. case File::HighlightStyleSheet:
  339. return QStringLiteral("highlight.css");
  340. case File::TextEditorStyle:
  341. return QStringLiteral("text-editor.theme");
  342. case File::MarkdownEditorStyle:
  343. return QStringLiteral("markdown-text-editor.theme");
  344. case File::EditorHighlightStyle:
  345. return QStringLiteral("editor-highlight.theme");
  346. case File::MarkdownEditorHighlightStyle:
  347. return QStringLiteral("markdown-editor-highlight.theme");
  348. case File::Cover:
  349. return QStringLiteral("cover.png");
  350. default:
  351. Q_ASSERT(false);
  352. return "";
  353. }
  354. }
  355. QString Theme::getEditorHighlightTheme() const
  356. {
  357. auto file = getFile(File::EditorHighlightStyle);
  358. if (file.isEmpty()) {
  359. return m_metadata.m_editorHighlightTheme;
  360. } else {
  361. return file;
  362. }
  363. }
  364. QString Theme::getMarkdownEditorHighlightTheme() const
  365. {
  366. auto file = getFile(File::MarkdownEditorHighlightStyle);
  367. if (!file.isEmpty()) {
  368. return file;
  369. }
  370. if (!m_metadata.m_markdownEditorHighlightTheme.isEmpty()) {
  371. return m_metadata.m_markdownEditorHighlightTheme;
  372. }
  373. return getEditorHighlightTheme();
  374. }
  375. QString Theme::name() const
  376. {
  377. return PathUtils::dirName(m_themeFolderPath);
  378. }
  379. QPixmap Theme::getCover(const QString &p_folder)
  380. {
  381. QDir dir(p_folder);
  382. if (dir.exists(getFileName(File::Cover))) {
  383. const auto coverFile = dir.filePath(getFileName(File::Cover));
  384. return QPixmap(coverFile);
  385. }
  386. return QPixmap();
  387. }