1/*
2 SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
3 SPDX-FileCopyrightText: 2016 Dominik Haumann <dhaumann@kde.org>
4 SPDX-FileCopyrightText: 2020 Jonathan Poelen <jonathan.poelen@gmail.com>
5
6 SPDX-License-Identifier: MIT
7*/
8
9#include "ksyntaxhighlighting_logging.h"
10#include "themedata_p.h"
11
12#include <QFile>
13#include <QFileInfo>
14#include <QJsonDocument>
15#include <QJsonObject>
16#include <QJsonValue>
17#include <QMetaEnum>
18
19using namespace KSyntaxHighlighting;
20
21ThemeData::ThemeData()
22{
23 memset(s: m_editorColors, c: 0, n: sizeof(m_editorColors));
24 m_textStyles.resize(new_size: QMetaEnum::fromType<Theme::TextStyle>().keyCount());
25}
26
27/**
28 * Convert QJsonValue @p val into a color, if possible. Valid colors are only
29 * in hex format: #aarrggbb. On error, returns 0x00000000.
30 */
31static inline QRgb readColor(const QJsonValue &val)
32{
33 const QRgb unsetColor = 0;
34 if (!val.isString()) {
35 return unsetColor;
36 }
37 const QString str = val.toString();
38 if (str.isEmpty() || str[0] != QLatin1Char('#')) {
39 return unsetColor;
40 }
41 const QColor color(str);
42 return color.isValid() ? color.rgba() : unsetColor;
43}
44
45static inline TextStyleData readThemeData(const QJsonObject &obj)
46{
47 TextStyleData td;
48
49 td.textColor = readColor(val: obj.value(key: QLatin1String("text-color")));
50 td.backgroundColor = readColor(val: obj.value(key: QLatin1String("background-color")));
51 td.selectedTextColor = readColor(val: obj.value(key: QLatin1String("selected-text-color")));
52 td.selectedBackgroundColor = readColor(val: obj.value(key: QLatin1String("selected-background-color")));
53
54 auto val = obj.value(key: QLatin1String("bold"));
55 if (val.isBool()) {
56 td.bold = val.toBool();
57 td.hasBold = true;
58 }
59 val = obj.value(key: QLatin1String("italic"));
60 if (val.isBool()) {
61 td.italic = val.toBool();
62 td.hasItalic = true;
63 }
64 val = obj.value(key: QLatin1String("underline"));
65 if (val.isBool()) {
66 td.underline = val.toBool();
67 td.hasUnderline = true;
68 }
69 val = obj.value(key: QLatin1String("strike-through"));
70 if (val.isBool()) {
71 td.strikeThrough = val.toBool();
72 td.hasStrikeThrough = true;
73 }
74
75 return td;
76}
77
78bool ThemeData::load(const QString &filePath)
79{
80 // flag first as done for the error cases
81 m_completelyLoaded = true;
82
83 QFile loadFile(filePath);
84 if (!loadFile.open(flags: QIODevice::ReadOnly)) {
85 return false;
86 }
87 const QByteArray jsonData = loadFile.readAll();
88 // look for metadata object
89 int metaDataStart = jsonData.indexOf(bv: "\"metadata\"");
90 int start = jsonData.indexOf(ch: '{', from: metaDataStart);
91 int end = jsonData.indexOf(bv: "}", from: metaDataStart);
92 if (start < 0 || end < 0) {
93 qCWarning(Log) << "Failed to parse theme file" << filePath << ":"
94 << "no metadata object found";
95 return false;
96 }
97
98 QJsonParseError parseError;
99 QJsonDocument jsonDoc = QJsonDocument::fromJson(json: jsonData.sliced(pos: start, n: (end + 1) - start), error: &parseError);
100 if (parseError.error != QJsonParseError::NoError) {
101 qCWarning(Log) << "Failed to parse theme file" << filePath << ":" << parseError.errorString();
102 return false;
103 }
104
105 m_filePath = filePath;
106
107 // we need more data later
108 m_completelyLoaded = false;
109
110 // read metadata
111 QJsonObject metadata = jsonDoc.object();
112 m_name = metadata.value(key: QLatin1String("name")).toString();
113 m_revision = metadata.value(key: QLatin1String("revision")).toInt();
114 return true;
115}
116
117void ThemeData::loadComplete()
118{
119 if (m_completelyLoaded) {
120 return;
121 }
122 m_completelyLoaded = true;
123
124 QFile loadFile(m_filePath);
125 if (!loadFile.open(flags: QIODevice::ReadOnly)) {
126 return;
127 }
128 const QByteArray jsonData = loadFile.readAll();
129
130 QJsonParseError parseError;
131 QJsonDocument jsonDoc = QJsonDocument::fromJson(json: jsonData, error: &parseError);
132 if (parseError.error != QJsonParseError::NoError) {
133 qCWarning(Log) << "Failed to parse theme file" << m_filePath << ":" << parseError.errorString();
134 return;
135 }
136
137 QJsonObject obj = jsonDoc.object();
138 // read text styles
139 const auto metaEnumStyle = QMetaEnum::fromType<Theme::TextStyle>();
140 const QJsonObject textStyles = obj.value(key: QLatin1String("text-styles")).toObject();
141 for (int i = 0; i < metaEnumStyle.keyCount(); ++i) {
142 Q_ASSERT(i == metaEnumStyle.value(i));
143 m_textStyles[i] = readThemeData(obj: textStyles.value(key: QLatin1String(metaEnumStyle.key(index: i))).toObject());
144 }
145
146 // read editor colors
147 const auto metaEnumColor = QMetaEnum::fromType<Theme::EditorColorRole>();
148 const QJsonObject editorColors = obj.value(key: QLatin1String("editor-colors")).toObject();
149 for (int i = 0; i < metaEnumColor.keyCount(); ++i) {
150 Q_ASSERT(i == metaEnumColor.value(i));
151 m_editorColors[i] = readColor(val: editorColors.value(key: QLatin1String(metaEnumColor.key(index: i))));
152 }
153
154 // if we have no new key around for Theme::BackgroundColor => use old variants to be compatible
155 if (!editorColors.contains(key: QLatin1String(metaEnumColor.key(index: Theme::BackgroundColor)))) {
156 m_editorColors[Theme::BackgroundColor] = readColor(val: editorColors.value(key: QLatin1String("background-color")));
157 m_editorColors[Theme::TextSelection] = readColor(val: editorColors.value(key: QLatin1String("selection")));
158 m_editorColors[Theme::CurrentLine] = readColor(val: editorColors.value(key: QLatin1String("current-line")));
159 m_editorColors[Theme::SearchHighlight] = readColor(val: editorColors.value(key: QLatin1String("search-highlight")));
160 m_editorColors[Theme::ReplaceHighlight] = readColor(val: editorColors.value(key: QLatin1String("replace-highlight")));
161 m_editorColors[Theme::BracketMatching] = readColor(val: editorColors.value(key: QLatin1String("bracket-matching")));
162 m_editorColors[Theme::TabMarker] = readColor(val: editorColors.value(key: QLatin1String("tab-marker")));
163 m_editorColors[Theme::SpellChecking] = readColor(val: editorColors.value(key: QLatin1String("spell-checking")));
164 m_editorColors[Theme::IndentationLine] = readColor(val: editorColors.value(key: QLatin1String("indentation-line")));
165 m_editorColors[Theme::IconBorder] = readColor(val: editorColors.value(key: QLatin1String("icon-border")));
166 m_editorColors[Theme::CodeFolding] = readColor(val: editorColors.value(key: QLatin1String("code-folding")));
167 m_editorColors[Theme::LineNumbers] = readColor(val: editorColors.value(key: QLatin1String("line-numbers")));
168 m_editorColors[Theme::CurrentLineNumber] = readColor(val: editorColors.value(key: QLatin1String("current-line-number")));
169 m_editorColors[Theme::WordWrapMarker] = readColor(val: editorColors.value(key: QLatin1String("word-wrap-marker")));
170 m_editorColors[Theme::ModifiedLines] = readColor(val: editorColors.value(key: QLatin1String("modified-lines")));
171 m_editorColors[Theme::SavedLines] = readColor(val: editorColors.value(key: QLatin1String("saved-lines")));
172 m_editorColors[Theme::Separator] = readColor(val: editorColors.value(key: QLatin1String("separator")));
173 m_editorColors[Theme::MarkBookmark] = readColor(val: editorColors.value(key: QLatin1String("mark-bookmark")));
174 m_editorColors[Theme::MarkBreakpointActive] = readColor(val: editorColors.value(key: QLatin1String("mark-breakpoint-active")));
175 m_editorColors[Theme::MarkBreakpointReached] = readColor(val: editorColors.value(key: QLatin1String("mark-breakpoint-reached")));
176 m_editorColors[Theme::MarkBreakpointDisabled] = readColor(val: editorColors.value(key: QLatin1String("mark-breakpoint-disabled")));
177 m_editorColors[Theme::MarkExecution] = readColor(val: editorColors.value(key: QLatin1String("mark-execution")));
178 m_editorColors[Theme::MarkWarning] = readColor(val: editorColors.value(key: QLatin1String("mark-warning")));
179 m_editorColors[Theme::MarkError] = readColor(val: editorColors.value(key: QLatin1String("mark-error")));
180 m_editorColors[Theme::TemplateBackground] = readColor(val: editorColors.value(key: QLatin1String("template-background")));
181 m_editorColors[Theme::TemplatePlaceholder] = readColor(val: editorColors.value(key: QLatin1String("template-placeholder")));
182 m_editorColors[Theme::TemplateFocusedPlaceholder] = readColor(val: editorColors.value(key: QLatin1String("template-focused-placeholder")));
183 m_editorColors[Theme::TemplateReadOnlyPlaceholder] = readColor(val: editorColors.value(key: QLatin1String("template-read-only-placeholder")));
184 }
185
186 // read per-definition style overrides
187 const auto customStyles = obj.value(key: QLatin1String("custom-styles")).toObject();
188 for (auto it = customStyles.begin(); it != customStyles.end(); ++it) {
189 const auto obj = it.value().toObject();
190 auto &overrideStyle = m_textStyleOverrides[it.key()];
191 for (auto it2 = obj.begin(); it2 != obj.end(); ++it2) {
192 overrideStyle.insert(key: it2.key(), value: readThemeData(obj: it2.value().toObject()));
193 }
194 }
195
196 return;
197}
198
199QString ThemeData::name() const
200{
201 return m_name;
202}
203
204int ThemeData::revision() const
205{
206 return m_revision;
207}
208
209bool ThemeData::isReadOnly() const
210{
211 return !QFileInfo(m_filePath).isWritable();
212}
213
214QString ThemeData::filePath() const
215{
216 return m_filePath;
217}
218
219TextStyleData ThemeData::textStyle(Theme::TextStyle style) const
220{
221 if (!m_completelyLoaded) {
222 const_cast<ThemeData *>(this)->loadComplete();
223 }
224 return m_textStyles[style];
225}
226
227QRgb ThemeData::textColor(Theme::TextStyle style) const
228{
229 return textStyle(style).textColor;
230}
231
232QRgb ThemeData::selectedTextColor(Theme::TextStyle style) const
233{
234 return textStyle(style).selectedTextColor;
235}
236
237QRgb ThemeData::backgroundColor(Theme::TextStyle style) const
238{
239 return textStyle(style).backgroundColor;
240}
241
242QRgb ThemeData::selectedBackgroundColor(Theme::TextStyle style) const
243{
244 return textStyle(style).selectedBackgroundColor;
245}
246
247bool ThemeData::isBold(Theme::TextStyle style) const
248{
249 return textStyle(style).bold;
250}
251
252bool ThemeData::isItalic(Theme::TextStyle style) const
253{
254 return textStyle(style).italic;
255}
256
257bool ThemeData::isUnderline(Theme::TextStyle style) const
258{
259 return textStyle(style).underline;
260}
261
262bool ThemeData::isStrikeThrough(Theme::TextStyle style) const
263{
264 return textStyle(style).strikeThrough;
265}
266
267QRgb ThemeData::editorColor(Theme::EditorColorRole role) const
268{
269 if (!m_completelyLoaded) {
270 const_cast<ThemeData *>(this)->loadComplete();
271 }
272 Q_ASSERT(static_cast<int>(role) >= 0 && static_cast<int>(role) <= static_cast<int>(Theme::TemplateReadOnlyPlaceholder));
273 return m_editorColors[role];
274}
275
276TextStyleData ThemeData::textStyleOverride(const QString &definitionName, const QString &attributeName) const
277{
278 if (!m_completelyLoaded) {
279 const_cast<ThemeData *>(this)->loadComplete();
280 }
281 auto it = m_textStyleOverrides.find(key: definitionName);
282 if (it != m_textStyleOverrides.end()) {
283 return it->value(key: attributeName);
284 }
285 return TextStyleData();
286}
287

source code of syntax-highlighting/src/lib/themedata.cpp