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