1/*
2 SPDX-FileCopyrightText: 2007, 2008 Matthew Woehlke <mw_triad@users.sourceforge.net>
3 SPDX-FileCopyrightText: 2001-2003 Christoph Cullmann <cullmann@kde.org>
4 SPDX-FileCopyrightText: 2002, 2003 Anders Lund <anders.lund@lund.tdcadsl.dk>
5 SPDX-FileCopyrightText: 2012-2018 Dominik Haumann <dhaumann@kde.org>
6
7 SPDX-License-Identifier: LGPL-2.0-or-later
8*/
9
10// BEGIN Includes
11#include "katethemeconfig.h"
12
13#include "katecolortreewidget.h"
14#include "kateconfig.h"
15#include "katedocument.h"
16#include "kateglobal.h"
17#include "katehighlight.h"
18#include "katestyletreewidget.h"
19#include "katesyntaxmanager.h"
20#include "kateview.h"
21
22#include <KLocalizedString>
23#include <KMessageBox>
24#include <KMessageWidget>
25
26#include <QComboBox>
27#include <QFileDialog>
28#include <QGridLayout>
29#include <QInputDialog>
30#include <QJsonObject>
31#include <QLabel>
32#include <QMetaEnum>
33#include <QPushButton>
34#include <QShowEvent>
35#include <QTabWidget>
36
37// END
38
39/**
40 * Return the translated name of default style @p n.
41 */
42static inline QString defaultStyleName(KSyntaxHighlighting::Theme::TextStyle style)
43{
44 using namespace KTextEditor;
45 switch (style) {
46 case KSyntaxHighlighting::Theme::TextStyle::Normal:
47 return i18nc("@item:intable Text context", "Normal");
48 case KSyntaxHighlighting::Theme::TextStyle::Keyword:
49 return i18nc("@item:intable Text context", "Keyword");
50 case KSyntaxHighlighting::Theme::TextStyle::Function:
51 return i18nc("@item:intable Text context", "Function");
52 case KSyntaxHighlighting::Theme::TextStyle::Variable:
53 return i18nc("@item:intable Text context", "Variable");
54 case KSyntaxHighlighting::Theme::TextStyle::ControlFlow:
55 return i18nc("@item:intable Text context", "Control Flow");
56 case KSyntaxHighlighting::Theme::TextStyle::Operator:
57 return i18nc("@item:intable Text context", "Operator");
58 case KSyntaxHighlighting::Theme::TextStyle::BuiltIn:
59 return i18nc("@item:intable Text context", "Built-in");
60 case KSyntaxHighlighting::Theme::TextStyle::Extension:
61 return i18nc("@item:intable Text context", "Extension");
62 case KSyntaxHighlighting::Theme::TextStyle::Preprocessor:
63 return i18nc("@item:intable Text context", "Preprocessor");
64 case KSyntaxHighlighting::Theme::TextStyle::Attribute:
65 return i18nc("@item:intable Text context", "Attribute");
66
67 case KSyntaxHighlighting::Theme::TextStyle::Char:
68 return i18nc("@item:intable Text context", "Character");
69 case KSyntaxHighlighting::Theme::TextStyle::SpecialChar:
70 return i18nc("@item:intable Text context", "Special Character");
71 case KSyntaxHighlighting::Theme::TextStyle::String:
72 return i18nc("@item:intable Text context", "String");
73 case KSyntaxHighlighting::Theme::TextStyle::VerbatimString:
74 return i18nc("@item:intable Text context", "Verbatim String");
75 case KSyntaxHighlighting::Theme::TextStyle::SpecialString:
76 return i18nc("@item:intable Text context", "Special String");
77 case KSyntaxHighlighting::Theme::TextStyle::Import:
78 return i18nc("@item:intable Text context", "Imports, Modules, Includes");
79
80 case KSyntaxHighlighting::Theme::TextStyle::DataType:
81 return i18nc("@item:intable Text context", "Data Type");
82 case KSyntaxHighlighting::Theme::TextStyle::DecVal:
83 return i18nc("@item:intable Text context", "Decimal/Value");
84 case KSyntaxHighlighting::Theme::TextStyle::BaseN:
85 return i18nc("@item:intable Text context", "Base-N Integer");
86 case KSyntaxHighlighting::Theme::TextStyle::Float:
87 return i18nc("@item:intable Text context", "Floating Point");
88 case KSyntaxHighlighting::Theme::TextStyle::Constant:
89 return i18nc("@item:intable Text context", "Constant");
90
91 case KSyntaxHighlighting::Theme::TextStyle::Comment:
92 return i18nc("@item:intable Text context", "Comment");
93 case KSyntaxHighlighting::Theme::TextStyle::Documentation:
94 return i18nc("@item:intable Text context", "Documentation");
95 case KSyntaxHighlighting::Theme::TextStyle::Annotation:
96 return i18nc("@item:intable Text context", "Annotation");
97 case KSyntaxHighlighting::Theme::TextStyle::CommentVar:
98 return i18nc("@item:intable Text context", "Comment Variable");
99 case KSyntaxHighlighting::Theme::TextStyle::RegionMarker:
100 // this next one is for denoting the beginning/end of a user defined folding region
101 return i18nc("@item:intable Text context", "Region Marker");
102 case KSyntaxHighlighting::Theme::TextStyle::Information:
103 return i18nc("@item:intable Text context", "Information");
104 case KSyntaxHighlighting::Theme::TextStyle::Warning:
105 return i18nc("@item:intable Text context", "Warning");
106 case KSyntaxHighlighting::Theme::TextStyle::Alert:
107 return i18nc("@item:intable Text context", "Alert");
108
109 case KSyntaxHighlighting::Theme::TextStyle::Others:
110 return i18nc("@item:intable Text context", "Others");
111 case KSyntaxHighlighting::Theme::TextStyle::Error:
112 // this one is for marking invalid input
113 return i18nc("@item:intable Text context", "Error");
114 };
115 Q_UNREACHABLE();
116}
117
118/**
119 * Helper to get json object for given valid theme.
120 * @param theme theme to get json object for
121 * @return json object of theme
122 */
123static QJsonObject jsonForTheme(const KSyntaxHighlighting::Theme &theme)
124{
125 // load json content, shall work, as the theme as valid, but still abort on errors
126 QFile loadFile(theme.filePath());
127 if (!loadFile.open(flags: QIODevice::ReadOnly)) {
128 return QJsonObject();
129 }
130 const QByteArray jsonData = loadFile.readAll();
131 QJsonParseError parseError;
132 QJsonDocument jsonDoc = QJsonDocument::fromJson(json: jsonData, error: &parseError);
133 if (parseError.error != QJsonParseError::NoError) {
134 return QJsonObject();
135 }
136 return jsonDoc.object();
137}
138
139/**
140 * Helper to write json data to given file path.
141 * After the function returns, stuff is flushed to disk for sure.
142 * @param json json object with theme data
143 * @param themeFileName file name to write to
144 * @return did writing succeed?
145 */
146static bool writeJson(const QJsonObject &json, const QString &themeFileName)
147{
148 QFile saveFile(themeFileName);
149 if (!saveFile.open(flags: QIODevice::WriteOnly)) {
150 return false;
151 }
152 saveFile.write(data: QJsonDocument(json).toJson());
153 return true;
154}
155
156// BEGIN KateThemeConfigColorTab -- 'Colors' tab
157KateThemeConfigColorTab::KateThemeConfigColorTab()
158{
159 QGridLayout *l = new QGridLayout(this);
160
161 ui = new KateColorTreeWidget(this);
162 QPushButton *btnUseColorScheme = new QPushButton(i18n("Use Default Colors"), this);
163
164 l->addWidget(ui, row: 0, column: 0, rowSpan: 1, columnSpan: 2);
165 l->addWidget(btnUseColorScheme, row: 1, column: 1);
166
167 l->setColumnStretch(column: 0, stretch: 1);
168 l->setColumnStretch(column: 1, stretch: 0);
169
170 connect(sender: btnUseColorScheme, signal: &QPushButton::clicked, context: ui, slot: &KateColorTreeWidget::selectDefaults);
171 connect(sender: ui, signal: &KateColorTreeWidget::changed, context: this, slot: &KateThemeConfigColorTab::changed);
172}
173
174static QList<KateColorItem> colorItemList(const KSyntaxHighlighting::Theme &theme)
175{
176 QList<KateColorItem> items;
177
178 //
179 // editor background colors
180 //
181 KateColorItem ci(KSyntaxHighlighting::Theme::BackgroundColor);
182 ci.category = i18n("Editor Background Colors");
183
184 ci.name = i18n("Text Area");
185 ci.key = QStringLiteral("Color Background");
186 ci.whatsThis = i18n("<p>Sets the background color of the editing area.</p>");
187 ci.defaultColor = QColor::fromRgba(rgba: theme.editorColor(role: ci.role));
188 items.append(t: ci);
189
190 ci.role = KSyntaxHighlighting::Theme::TextSelection;
191 ci.name = i18n("Selected Text");
192 ci.key = QStringLiteral("Color Selection");
193 ci.whatsThis = i18n(
194 "<p>Sets the background color of the selection.</p><p>To set the text color for selected text, use the &quot;<b>Configure Highlighting</b>&quot; "
195 "dialog.</p>");
196 ci.defaultColor = QColor::fromRgba(rgba: theme.editorColor(role: ci.role));
197 items.append(t: ci);
198
199 ci.role = KSyntaxHighlighting::Theme::CurrentLine;
200 ci.name = i18n("Current Line");
201 ci.key = QStringLiteral("Color Highlighted Line");
202 ci.whatsThis = i18n("<p>Sets the background color of the currently active line, which means the line where your cursor is positioned.</p>");
203 ci.defaultColor = QColor::fromRgba(rgba: theme.editorColor(role: ci.role));
204 items.append(t: ci);
205
206 ci.role = KSyntaxHighlighting::Theme::SearchHighlight;
207 ci.name = i18n("Search Highlight");
208 ci.key = QStringLiteral("Color Search Highlight");
209 ci.whatsThis = i18n("<p>Sets the background color of search results.</p>");
210 ci.defaultColor = QColor::fromRgba(rgba: theme.editorColor(role: ci.role));
211 items.append(t: ci);
212
213 ci.role = KSyntaxHighlighting::Theme::ReplaceHighlight;
214 ci.name = i18n("Replace Highlight");
215 ci.key = QStringLiteral("Color Replace Highlight");
216 ci.whatsThis = i18n("<p>Sets the background color of replaced text.</p>");
217 ci.defaultColor = QColor::fromRgba(rgba: theme.editorColor(role: ci.role));
218 items.append(t: ci);
219
220 //
221 // icon border
222 //
223 ci.category = i18n("Icon Border");
224
225 ci.role = KSyntaxHighlighting::Theme::IconBorder;
226 ci.name = i18n("Background Area");
227 ci.key = QStringLiteral("Color Icon Bar");
228 ci.whatsThis = i18n("<p>Sets the background color of the icon border.</p>");
229 ci.defaultColor = QColor::fromRgba(rgba: theme.editorColor(role: ci.role));
230 items.append(t: ci);
231
232 ci.role = KSyntaxHighlighting::Theme::LineNumbers;
233 ci.name = i18n("Line Numbers");
234 ci.key = QStringLiteral("Color Line Number");
235 ci.whatsThis = i18n("<p>This color will be used to draw the line numbers (if enabled).</p>");
236 ci.defaultColor = QColor::fromRgba(rgba: theme.editorColor(role: ci.role));
237 items.append(t: ci);
238
239 ci.role = KSyntaxHighlighting::Theme::CurrentLineNumber;
240 ci.name = i18n("Current Line Number");
241 ci.key = QStringLiteral("Color Current Line Number");
242 ci.whatsThis = i18n("<p>This color will be used to draw the number of the current line (if enabled).</p>");
243 ci.defaultColor = QColor::fromRgba(rgba: theme.editorColor(role: ci.role));
244 items.append(t: ci);
245
246 ci.role = KSyntaxHighlighting::Theme::Separator;
247 ci.name = i18n("Separator");
248 ci.key = QStringLiteral("Color Separator");
249 ci.whatsThis = i18n("<p>This color will be used to draw the line between line numbers and the icon borders, if both are enabled.</p>");
250 ci.defaultColor = QColor::fromRgba(rgba: theme.editorColor(role: ci.role));
251 items.append(t: ci);
252
253 ci.role = KSyntaxHighlighting::Theme::WordWrapMarker;
254 ci.name = i18n("Word Wrap Marker");
255 ci.key = QStringLiteral("Color Word Wrap Marker");
256 ci.whatsThis = i18n(
257 "<p>Sets the color of Word Wrap-related markers:</p><dl><dt>Static Word Wrap</dt><dd>A vertical line which shows the column where text is going to be "
258 "wrapped</dd><dt>Dynamic Word Wrap</dt><dd>An arrow shown to the left of "
259 "visually-wrapped lines</dd></dl>");
260 ci.defaultColor = QColor::fromRgba(rgba: theme.editorColor(role: ci.role));
261 items.append(t: ci);
262
263 ci.role = KSyntaxHighlighting::Theme::CodeFolding;
264 ci.name = i18n("Code Folding");
265 ci.key = QStringLiteral("Color Code Folding");
266 ci.whatsThis = i18n("<p>Sets the color of the code folding bar.</p>");
267 ci.defaultColor = QColor::fromRgba(rgba: theme.editorColor(role: ci.role));
268 items.append(t: ci);
269
270 ci.role = KSyntaxHighlighting::Theme::ModifiedLines;
271 ci.name = i18n("Modified Lines");
272 ci.key = QStringLiteral("Color Modified Lines");
273 ci.whatsThis = i18n("<p>Sets the color of the line modification marker for modified lines.</p>");
274 ci.defaultColor = QColor::fromRgba(rgba: theme.editorColor(role: ci.role));
275 items.append(t: ci);
276
277 ci.role = KSyntaxHighlighting::Theme::SavedLines;
278 ci.name = i18n("Saved Lines");
279 ci.key = QStringLiteral("Color Saved Lines");
280 ci.whatsThis = i18n("<p>Sets the color of the line modification marker for saved lines.</p>");
281 ci.defaultColor = QColor::fromRgba(rgba: theme.editorColor(role: ci.role));
282 items.append(t: ci);
283
284 //
285 // text decorations
286 //
287 ci.category = i18n("Text Decorations");
288
289 ci.role = KSyntaxHighlighting::Theme::SpellChecking;
290 ci.name = i18n("Spelling Mistake Line");
291 ci.key = QStringLiteral("Color Spelling Mistake Line");
292 ci.whatsThis = i18n("<p>Sets the color of the line that is used to indicate spelling mistakes.</p>");
293 ci.defaultColor = QColor::fromRgba(rgba: theme.editorColor(role: ci.role));
294 items.append(t: ci);
295
296 ci.role = KSyntaxHighlighting::Theme::TabMarker;
297 ci.name = i18n("Tab and Space Markers");
298 ci.key = QStringLiteral("Color Tab Marker");
299 ci.whatsThis = i18n("<p>Sets the color of the tabulator marks.</p>");
300 ci.defaultColor = QColor::fromRgba(rgba: theme.editorColor(role: ci.role));
301 items.append(t: ci);
302
303 ci.role = KSyntaxHighlighting::Theme::IndentationLine;
304 ci.name = i18n("Indentation Line");
305 ci.key = QStringLiteral("Color Indentation Line");
306 ci.whatsThis = i18n("<p>Sets the color of the vertical indentation lines.</p>");
307 ci.defaultColor = QColor::fromRgba(rgba: theme.editorColor(role: ci.role));
308 items.append(t: ci);
309
310 ci.role = KSyntaxHighlighting::Theme::BracketMatching;
311 ci.name = i18n("Bracket Highlight");
312 ci.key = QStringLiteral("Color Highlighted Bracket");
313 ci.whatsThis = i18n(
314 "<p>Sets the bracket matching color. This means, if you place the cursor e.g. at a <b>(</b>, the matching <b>)</b> will be highlighted with this "
315 "color.</p>");
316 ci.defaultColor = QColor::fromRgba(rgba: theme.editorColor(role: ci.role));
317 items.append(t: ci);
318
319 //
320 // marker colors
321 //
322 ci.category = i18n("Marker Colors");
323
324 const QString markerNames[KSyntaxHighlighting::Theme::MarkError - KSyntaxHighlighting::Theme::MarkBookmark + 1] = {i18n("Bookmark"),
325 i18n("Active Breakpoint"),
326 i18n("Reached Breakpoint"),
327 i18n("Disabled Breakpoint"),
328 i18n("Execution"),
329 i18n("Warning"),
330 i18n("Error")};
331
332 ci.whatsThis = i18n("<p>Sets the background color of mark type.</p><p><b>Note</b>: The marker color is displayed lightly because of transparency.</p>");
333 for (int i = 0; i <= KSyntaxHighlighting::Theme::MarkError - KSyntaxHighlighting::Theme::MarkBookmark; ++i) {
334 ci.role = static_cast<KSyntaxHighlighting::Theme::EditorColorRole>(i + KSyntaxHighlighting::Theme::MarkBookmark);
335 ci.defaultColor = QColor::fromRgba(rgba: theme.editorColor(role: ci.role));
336 ci.name = markerNames[i];
337 ci.key = QLatin1String("Color MarkType ") + QString::number(i + 1);
338 items.append(t: ci);
339 }
340
341 //
342 // text templates
343 //
344 ci.category = i18n("Text Templates & Snippets");
345
346 ci.whatsThis = QString(); // TODO: add whatsThis for text templates
347
348 ci.role = KSyntaxHighlighting::Theme::TemplateBackground;
349 ci.name = i18n("Background");
350 ci.key = QStringLiteral("Color Template Background");
351 ci.defaultColor = QColor::fromRgba(rgba: theme.editorColor(role: ci.role));
352 items.append(t: ci);
353
354 ci.role = KSyntaxHighlighting::Theme::TemplatePlaceholder;
355 ci.name = i18n("Editable Placeholder");
356 ci.key = QStringLiteral("Color Template Editable Placeholder");
357 ci.defaultColor = QColor::fromRgba(rgba: theme.editorColor(role: ci.role));
358 items.append(t: ci);
359
360 ci.role = KSyntaxHighlighting::Theme::TemplateFocusedPlaceholder;
361 ci.name = i18n("Focused Editable Placeholder");
362 ci.key = QStringLiteral("Color Template Focused Editable Placeholder");
363 ci.defaultColor = QColor::fromRgba(rgba: theme.editorColor(role: ci.role));
364 items.append(t: ci);
365
366 ci.role = KSyntaxHighlighting::Theme::TemplateReadOnlyPlaceholder;
367 ci.name = i18n("Not Editable Placeholder");
368 ci.key = QStringLiteral("Color Template Not Editable Placeholder");
369 ci.defaultColor = QColor::fromRgba(rgba: theme.editorColor(role: ci.role));
370 items.append(t: ci);
371
372 //
373 // finally, add all elements
374 //
375 return items;
376}
377
378void KateThemeConfigColorTab::schemaChanged(const QString &newSchema)
379{
380 // ensure invalid or read-only stuff can't be changed
381 const auto theme = KateHlManager::self()->repository().theme(themeName: newSchema);
382 ui->setReadOnly(!theme.isValid() || theme.isReadOnly());
383
384 // save current schema
385 if (!m_currentSchema.isEmpty()) {
386 auto it = m_schemas.find(x: m_currentSchema);
387 if (it != m_schemas.end()) {
388 m_schemas.erase(x: m_currentSchema); // clear this color schema
389 }
390
391 // now add it again
392 m_schemas[m_currentSchema] = ui->colorItems();
393 }
394
395 if (newSchema == m_currentSchema) {
396 return;
397 }
398
399 // switch
400 m_currentSchema = newSchema;
401
402 // If we havent this schema, read in from config file
403 if (m_schemas.find(x: newSchema) == m_schemas.end()) {
404 QList<KateColorItem> items = colorItemList(theme);
405 for (auto &item : items) {
406 item.color = QColor::fromRgba(rgba: theme.editorColor(role: item.role));
407 }
408 m_schemas[newSchema] = std::move(items);
409 }
410
411 // first block signals otherwise setColor emits changed
412 const bool blocked = blockSignals(b: true);
413
414 ui->clear();
415 ui->addColorItems(colorItems: m_schemas[m_currentSchema]);
416
417 blockSignals(b: blocked);
418}
419
420/**
421 * @brief Converts @p c to its hex value, if @p c has alpha then the returned string
422 * will be of the format #AARRGGBB other wise #RRGGBB
423 */
424static QString hexName(const QColor &c)
425{
426 return c.alpha() == 0xFF ? c.name() : c.name(format: QColor::HexArgb);
427}
428
429void KateThemeConfigColorTab::apply()
430{
431 schemaChanged(newSchema: m_currentSchema);
432
433 // we use meta-data of enum for computing the json keys
434 static const auto idx = KSyntaxHighlighting::Theme::staticMetaObject.indexOfEnumerator(name: "EditorColorRole");
435 Q_ASSERT(idx >= 0);
436 const auto metaEnum = KSyntaxHighlighting::Theme::staticMetaObject.enumerator(index: idx);
437
438 // export all themes we cached data for
439 for (auto it = m_schemas.cbegin(); it != m_schemas.cend(); ++it) {
440 // get theme for key, skip invalid or read-only themes for writing
441 const auto theme = KateHlManager::self()->repository().theme(themeName: it->first);
442 if (!theme.isValid() || theme.isReadOnly()) {
443 continue;
444 }
445
446 // get current theme data from disk
447 QJsonObject newThemeObject = jsonForTheme(theme);
448
449 // patch the editor-colors part
450 QJsonObject colors;
451 const auto &colorItems = it->second;
452 for (const KateColorItem &item : colorItems) {
453 QColor c = item.useDefault ? item.defaultColor : item.color;
454 colors[QLatin1String(metaEnum.key(index: item.role))] = hexName(c);
455 }
456 newThemeObject[QLatin1String("editor-colors")] = colors;
457
458 // write json back to file
459 writeJson(json: newThemeObject, themeFileName: theme.filePath());
460 }
461
462 // all colors are written, so throw away all cached schemas
463 m_schemas.clear();
464}
465
466void KateThemeConfigColorTab::reload()
467{
468 // drop all cached data
469 m_schemas.clear();
470
471 // trigger re-creation of ui from theme
472 const auto backupName = m_currentSchema;
473 m_currentSchema.clear();
474 schemaChanged(newSchema: backupName);
475}
476
477QColor KateThemeConfigColorTab::backgroundColor() const
478{
479 return ui->findColor(QStringLiteral("Color Background"));
480}
481
482QColor KateThemeConfigColorTab::selectionColor() const
483{
484 return ui->findColor(QStringLiteral("Color Selection"));
485}
486// END KateThemeConfigColorTab
487
488// BEGIN FontColorConfig -- 'Normal Text Styles' tab
489KateThemeConfigDefaultStylesTab::KateThemeConfigDefaultStylesTab(KateThemeConfigColorTab *colorTab)
490{
491 m_colorTab = colorTab;
492
493 // size management
494 QGridLayout *grid = new QGridLayout(this);
495
496 m_defaultStyles = new KateStyleTreeWidget(this);
497 connect(sender: m_defaultStyles, signal: &KateStyleTreeWidget::changed, context: this, slot: &KateThemeConfigDefaultStylesTab::changed);
498 grid->addWidget(m_defaultStyles, row: 0, column: 0);
499
500 m_defaultStyles->setWhatsThis(
501 i18n("<p>This list displays the default styles for the current color theme and "
502 "offers the means to edit them. The style name reflects the current "
503 "style settings.</p>"
504 "<p>To edit the colors, click the colored squares, or select the color "
505 "to edit from the popup menu.</p><p>You can unset the Background and Selected "
506 "Background colors from the popup menu when appropriate.</p>"));
507}
508
509KateAttributeList *KateThemeConfigDefaultStylesTab::attributeList(const QString &schema)
510{
511 auto it = m_defaultStyleLists.find(x: schema);
512 if (it == m_defaultStyleLists.end()) {
513 // get list of all default styles
514 const auto numStyles = QMetaEnum::fromType<KSyntaxHighlighting::Theme::TextStyle>().keyCount();
515 KateAttributeList list;
516 list.reserve(asize: numStyles);
517 const KSyntaxHighlighting::Theme currentTheme = KateHlManager::self()->repository().theme(themeName: schema);
518 for (int z = 0; z < numStyles; z++) {
519 KTextEditor::Attribute::Ptr i(new KTextEditor::Attribute());
520 const auto style = static_cast<KSyntaxHighlighting::Theme::TextStyle>(z);
521
522 if (const auto col = currentTheme.textColor(style)) {
523 i->setForeground(QColor::fromRgba(rgba: col));
524 }
525
526 if (const auto col = currentTheme.selectedTextColor(style)) {
527 i->setSelectedForeground(QColor::fromRgba(rgba: col));
528 }
529
530 if (const auto col = currentTheme.backgroundColor(style)) {
531 i->setBackground(QColor::fromRgba(rgba: col));
532 } else {
533 i->clearBackground();
534 }
535
536 if (const auto col = currentTheme.selectedBackgroundColor(style)) {
537 i->setSelectedBackground(QColor::fromRgba(rgba: col));
538 } else {
539 i->clearProperty(propertyId: SelectedBackground);
540 }
541
542 i->setFontBold(currentTheme.isBold(style));
543 i->setFontItalic(currentTheme.isItalic(style));
544 i->setFontUnderline(currentTheme.isUnderline(style));
545 i->setFontStrikeOut(currentTheme.isStrikeThrough(style));
546 list.append(t: i);
547 }
548 it = m_defaultStyleLists.emplace(args: schema, args&: list).first;
549 }
550
551 return &(it->second);
552}
553
554void KateThemeConfigDefaultStylesTab::schemaChanged(const QString &schema)
555{
556 // ensure invalid or read-only stuff can't be changed
557 const auto theme = KateHlManager::self()->repository().theme(themeName: schema);
558 m_defaultStyles->setReadOnly(!theme.isValid() || theme.isReadOnly());
559
560 m_currentSchema = schema;
561
562 m_defaultStyles->clear();
563
564 KateAttributeList *l = attributeList(schema);
565 updateColorPalette(textColor: l->at(i: 0)->foreground().color());
566
567 // normal text and source code
568 QTreeWidgetItem *parent = new QTreeWidgetItem(m_defaultStyles, QStringList() << i18nc("@item:intable", "Normal Text & Source Code"));
569 parent->setFirstColumnSpanned(true);
570 for (int i = (int)KSyntaxHighlighting::Theme::TextStyle::Normal; i <= (int)KSyntaxHighlighting::Theme::TextStyle::Attribute; ++i) {
571 m_defaultStyles->addItem(parent, styleName: defaultStyleName(style: static_cast<KSyntaxHighlighting::Theme::TextStyle>(i)), defaultstyle: l->at(i));
572 }
573
574 // Number, Types & Constants
575 parent = new QTreeWidgetItem(m_defaultStyles, QStringList() << i18nc("@item:intable", "Numbers, Types & Constants"));
576 parent->setFirstColumnSpanned(true);
577 for (int i = (int)KSyntaxHighlighting::Theme::TextStyle::DataType; i <= (int)KSyntaxHighlighting::Theme::TextStyle::Constant; ++i) {
578 m_defaultStyles->addItem(parent, styleName: defaultStyleName(style: static_cast<KSyntaxHighlighting::Theme::TextStyle>(i)), defaultstyle: l->at(i));
579 }
580
581 // strings & characters
582 parent = new QTreeWidgetItem(m_defaultStyles, QStringList() << i18nc("@item:intable", "Strings & Characters"));
583 parent->setFirstColumnSpanned(true);
584 for (int i = (int)KSyntaxHighlighting::Theme::TextStyle::Char; i <= (int)KSyntaxHighlighting::Theme::TextStyle::Import; ++i) {
585 m_defaultStyles->addItem(parent, styleName: defaultStyleName(style: static_cast<KSyntaxHighlighting::Theme::TextStyle>(i)), defaultstyle: l->at(i));
586 }
587
588 // comments & documentation
589 parent = new QTreeWidgetItem(m_defaultStyles, QStringList() << i18nc("@item:intable", "Comments & Documentation"));
590 parent->setFirstColumnSpanned(true);
591 for (int i = (int)KSyntaxHighlighting::Theme::TextStyle::Comment; i <= (int)KSyntaxHighlighting::Theme::TextStyle::Alert; ++i) {
592 m_defaultStyles->addItem(parent, styleName: defaultStyleName(style: static_cast<KSyntaxHighlighting::Theme::TextStyle>(i)), defaultstyle: l->at(i));
593 }
594
595 // Misc
596 parent = new QTreeWidgetItem(m_defaultStyles, QStringList() << i18nc("@item:intable", "Miscellaneous"));
597 parent->setFirstColumnSpanned(true);
598 for (int i = (int)KSyntaxHighlighting::Theme::TextStyle::Others; i <= (int)KSyntaxHighlighting::Theme::TextStyle::Error; ++i) {
599 m_defaultStyles->addItem(parent, styleName: defaultStyleName(style: static_cast<KSyntaxHighlighting::Theme::TextStyle>(i)), defaultstyle: l->at(i));
600 }
601
602 m_defaultStyles->expandAll();
603}
604
605void KateThemeConfigDefaultStylesTab::updateColorPalette(const QColor &textColor)
606{
607 QPalette p(m_defaultStyles->palette());
608 p.setColor(acr: QPalette::Base, acolor: m_colorTab->backgroundColor());
609 p.setColor(acr: QPalette::Highlight, acolor: m_colorTab->selectionColor());
610 p.setColor(acr: QPalette::Text, acolor: textColor);
611 m_defaultStyles->setPalette(p);
612}
613
614void KateThemeConfigDefaultStylesTab::reload()
615{
616 m_defaultStyles->clear();
617 m_defaultStyleLists.clear();
618
619 schemaChanged(schema: m_currentSchema);
620}
621
622void KateThemeConfigDefaultStylesTab::apply()
623{
624 // get enum meta data for json keys
625 static const auto idx = KSyntaxHighlighting::Theme::staticMetaObject.indexOfEnumerator(name: "TextStyle");
626 Q_ASSERT(idx >= 0);
627 const auto metaEnum = KSyntaxHighlighting::Theme::staticMetaObject.enumerator(index: idx);
628
629 // export all configured styles of the cached themes
630 for (const auto &kv : m_defaultStyleLists) {
631 // get theme for key, skip invalid or read-only themes for writing
632 const auto theme = KateHlManager::self()->repository().theme(themeName: kv.first);
633 if (!theme.isValid() || theme.isReadOnly()) {
634 continue;
635 }
636
637 // get current theme data from disk
638 QJsonObject newThemeObject = jsonForTheme(theme);
639
640 // patch the text-styles part
641 QJsonObject styles;
642 const auto numStyles = QMetaEnum::fromType<KSyntaxHighlighting::Theme::TextStyle>().keyCount();
643 for (int z = 0; z < numStyles; z++) {
644 QJsonObject style;
645 KTextEditor::Attribute::Ptr p = kv.second.at(i: z);
646 if (p->hasProperty(propertyId: QTextFormat::ForegroundBrush)) {
647 style[QLatin1String("text-color")] = hexName(c: p->foreground().color());
648 }
649 if (p->hasProperty(propertyId: QTextFormat::BackgroundBrush)) {
650 style[QLatin1String("background-color")] = hexName(c: p->background().color());
651 }
652 if (p->hasProperty(propertyId: SelectedForeground)) {
653 style[QLatin1String("selected-text-color")] = hexName(c: p->selectedForeground().color());
654 }
655 if (p->hasProperty(propertyId: SelectedBackground)) {
656 style[QLatin1String("selected-background-color")] = hexName(c: p->selectedBackground().color());
657 }
658 if (p->hasProperty(propertyId: QTextFormat::FontWeight) && p->fontBold()) {
659 style[QLatin1String("bold")] = true;
660 }
661 if (p->hasProperty(propertyId: QTextFormat::FontItalic) && p->fontItalic()) {
662 style[QLatin1String("italic")] = true;
663 }
664 if (p->hasProperty(propertyId: QTextFormat::TextUnderlineStyle) && p->fontUnderline()) {
665 style[QLatin1String("underline")] = true;
666 }
667 if (p->hasProperty(propertyId: QTextFormat::FontStrikeOut) && p->fontStrikeOut()) {
668 style[QLatin1String("strike-through")] = true;
669 }
670 styles[QLatin1String(metaEnum.key(index: (z)))] = style;
671 }
672 newThemeObject[QLatin1String("text-styles")] = styles;
673
674 // write json back to file
675 writeJson(json: newThemeObject, themeFileName: theme.filePath());
676 }
677}
678
679void KateThemeConfigDefaultStylesTab::showEvent(QShowEvent *event)
680{
681 if (!event->spontaneous() && !m_currentSchema.isEmpty()) {
682 KateAttributeList *l = attributeList(schema: m_currentSchema);
683 Q_ASSERT(l != nullptr);
684 updateColorPalette(textColor: l->at(i: 0)->foreground().color());
685 }
686
687 QWidget::showEvent(event);
688}
689// END FontColorConfig
690
691// BEGIN KateThemeConfigHighlightTab -- 'Highlighting Text Styles' tab
692KateThemeConfigHighlightTab::KateThemeConfigHighlightTab(KateThemeConfigDefaultStylesTab *page, KateThemeConfigColorTab *colorTab)
693{
694 m_defaults = page;
695 m_colorTab = colorTab;
696
697 m_hl = 0;
698
699 QVBoxLayout *layout = new QVBoxLayout(this);
700
701 QHBoxLayout *headerLayout = new QHBoxLayout;
702 layout->addLayout(layout: headerLayout);
703
704 QLabel *lHl = new QLabel(i18n("H&ighlight:"), this);
705 headerLayout->addWidget(lHl);
706
707 hlCombo = new QComboBox(this);
708 hlCombo->setEditable(false);
709 headerLayout->addWidget(hlCombo);
710
711 lHl->setBuddy(hlCombo);
712 connect(sender: hlCombo, signal: &QComboBox::activated, context: this, slot: &KateThemeConfigHighlightTab::hlChanged);
713
714 headerLayout->addStretch();
715
716 const auto modeList = KateHlManager::self()->modeList();
717 for (const auto &hl : modeList) {
718 const auto section = hl.translatedSection();
719 if (!section.isEmpty()) {
720 hlCombo->addItem(atext: section + QLatin1Char('/') + hl.translatedName());
721 } else {
722 hlCombo->addItem(atext: hl.translatedName());
723 }
724 }
725 hlCombo->setCurrentIndex(0);
726
727 // styles listview
728 m_styles = new KateStyleTreeWidget(this, true);
729 connect(sender: m_styles, signal: &KateStyleTreeWidget::changed, context: this, slot: &KateThemeConfigHighlightTab::changed);
730 layout->addWidget(m_styles, stretch: 999);
731
732 // get current highlighting from the host application
733 int hl = 0;
734 KTextEditor::ViewPrivate *kv =
735 qobject_cast<KTextEditor::ViewPrivate *>(object: KTextEditor::EditorPrivate::self()->application()->activeMainWindow()->activeView());
736 if (kv) {
737 const QString hlName = kv->doc()->highlight()->name();
738 hl = KateHlManager::self()->nameFind(name: hlName);
739 Q_ASSERT(hl >= 0);
740 }
741
742 hlCombo->setCurrentIndex(hl);
743 hlChanged(z: hl);
744
745 m_styles->setWhatsThis(
746 i18n("<p>This list displays the contexts of the current syntax highlight mode and "
747 "offers the means to edit them. The context name reflects the current "
748 "style settings.</p><p>To edit using the keyboard, press "
749 "<strong>&lt;SPACE&gt;</strong> and choose a property from the popup menu.</p>"
750 "<p>To edit the colors, click the colored squares, or select the color "
751 "to edit from the popup menu.</p><p>You can unset the Background and Selected "
752 "Background colors from the context menu when appropriate.</p>"));
753}
754
755void KateThemeConfigHighlightTab::hlChanged(int z)
756{
757 m_hl = z;
758 schemaChanged(schema: m_schema);
759}
760
761/**
762 * Helper to get the "default attributes" for the given schema + highlighting.
763 * This means all stuff set without taking theme overrides for the highlighting into account.
764 */
765static KateAttributeList defaultsForHighlighting(const std::vector<KSyntaxHighlighting::Format> &formats, const KateAttributeList &defaultStyleAttributes)
766{
767 const KSyntaxHighlighting::Theme invalidTheme;
768 KateAttributeList defaults;
769 for (const auto &format : formats) {
770 // create a KTextEditor attribute matching the default style for this format
771 // use the default style attribute we got passed to have the one we currently have configured in the settings here
772 KTextEditor::Attribute::Ptr newAttribute(new KTextEditor::Attribute(*defaultStyleAttributes.at(i: format.textStyle())));
773
774 // check for override => if yes, set attribute as overridden, use invalid theme to avoid the usage of theme override!
775
776 if (format.hasTextColorOverride()) {
777 newAttribute->setForeground(format.textColor(theme: invalidTheme));
778 }
779 if (format.hasBackgroundColorOverride()) {
780 newAttribute->setBackground(format.backgroundColor(theme: invalidTheme));
781 }
782 if (format.hasSelectedTextColorOverride()) {
783 newAttribute->setSelectedForeground(format.selectedTextColor(theme: invalidTheme));
784 }
785 if (format.hasSelectedBackgroundColorOverride()) {
786 newAttribute->setSelectedBackground(format.selectedBackgroundColor(theme: invalidTheme));
787 }
788 if (format.hasBoldOverride()) {
789 newAttribute->setFontBold(format.isBold(theme: invalidTheme));
790 }
791 if (format.hasItalicOverride()) {
792 newAttribute->setFontItalic(format.isItalic(theme: invalidTheme));
793 }
794 if (format.hasUnderlineOverride()) {
795 newAttribute->setFontUnderline(format.isUnderline(theme: invalidTheme));
796 }
797 if (format.hasStrikeThroughOverride()) {
798 newAttribute->setFontStrikeOut(format.isStrikeThrough(theme: invalidTheme));
799 }
800
801 // not really relevant, set it as configured
802 newAttribute->setSkipSpellChecking(format.spellCheck());
803 defaults.append(t: newAttribute);
804 }
805 return defaults;
806}
807
808void KateThemeConfigHighlightTab::schemaChanged(const QString &schema)
809{
810 // ensure invalid or read-only stuff can't be changed
811 const auto theme = KateHlManager::self()->repository().theme(themeName: schema);
812
813 // NOTE: None (m_hl == 0) can't be changed with the current way
814 // TODO: removed it from the list?
815 const auto isNoneSchema = m_hl == 0;
816 m_styles->setReadOnly(!theme.isValid() || theme.isReadOnly() || isNoneSchema);
817
818 m_schema = schema;
819
820 m_styles->clear();
821
822 auto it = m_hlDict.find(key: m_schema);
823 if (it == m_hlDict.end()) {
824 it = m_hlDict.insert(key: schema, value: QHash<int, QList<KTextEditor::Attribute::Ptr>>());
825 }
826
827 // Set listview colors
828 KateAttributeList *l = m_defaults->attributeList(schema);
829 updateColorPalette(textColor: l->at(i: 0)->foreground().color());
830
831 // create unified stuff
832 auto attributes = KateHlManager::self()->getHl(n: m_hl)->attributesForDefinition(schema: m_schema);
833 auto formats = KateHlManager::self()->getHl(n: m_hl)->formats();
834 auto defaults = defaultsForHighlighting(formats, defaultStyleAttributes: *l);
835
836 for (int i = 0; i < attributes.size(); ++i) {
837 // All stylenames have their language mode prefixed, e.g. HTML:Comment
838 // split them and put them into nice substructures.
839 int c = attributes[i]->name().indexOf(c: QLatin1Char(':'));
840 if (c <= 0) {
841 continue;
842 }
843
844 QString highlighting = attributes[i]->name().left(n: c);
845 QString name = attributes[i]->name().mid(position: c + 1);
846 auto &uniqueAttribute = m_uniqueAttributes[m_schema][highlighting][name].first;
847 auto &uniqueAttributeDefault = m_uniqueAttributes[m_schema][highlighting][name].second;
848
849 if (uniqueAttribute.data()) {
850 attributes[i] = uniqueAttribute;
851 } else {
852 uniqueAttribute = attributes[i];
853 }
854
855 if (uniqueAttributeDefault.data()) {
856 defaults[i] = uniqueAttributeDefault;
857 } else {
858 uniqueAttributeDefault = defaults[i];
859 }
860 }
861
862 auto &subMap = it.value();
863 auto it1 = subMap.find(key: m_hl);
864 if (it1 == subMap.end()) {
865 it1 = subMap.insert(key: m_hl, value: attributes);
866 }
867
868 QHash<QString, QTreeWidgetItem *> prefixes;
869 const auto &attribs = it1.value();
870 auto vec_it = attribs.cbegin();
871 int i = 0;
872 while (vec_it != attribs.end()) {
873 const KTextEditor::Attribute::Ptr itemData = *vec_it;
874 Q_ASSERT(itemData);
875
876 // All stylenames have their language mode prefixed, e.g. HTML:Comment
877 // split them and put them into nice substructures.
878 int c = itemData->name().indexOf(c: QLatin1Char(':'));
879 if (c > 0) {
880 QString prefix = itemData->name().left(n: c);
881 QString name = itemData->name().mid(position: c + 1);
882
883 QTreeWidgetItem *parent = prefixes[prefix];
884 if (!parent) {
885 parent = new QTreeWidgetItem(m_styles, QStringList() << prefix);
886 m_styles->expandItem(item: parent);
887 prefixes.insert(key: prefix, value: parent);
888 }
889 m_styles->addItem(parent, styleName: name, defaultstyle: defaults.at(i), data: itemData);
890 } else {
891 m_styles->addItem(styleName: itemData->name(), defaultstyle: defaults.at(i), data: itemData);
892 }
893 ++vec_it;
894 ++i;
895 }
896
897 m_styles->resizeColumns();
898}
899
900void KateThemeConfigHighlightTab::updateColorPalette(const QColor &textColor)
901{
902 QPalette p(m_styles->palette());
903 p.setColor(acr: QPalette::Base, acolor: m_colorTab->backgroundColor());
904 p.setColor(acr: QPalette::Highlight, acolor: m_colorTab->selectionColor());
905 p.setColor(acr: QPalette::Text, acolor: textColor);
906 m_styles->setPalette(p);
907}
908
909void KateThemeConfigHighlightTab::reload()
910{
911 m_styles->clear();
912
913 m_hlDict.clear();
914 m_uniqueAttributes.clear();
915
916 hlChanged(z: hlCombo->currentIndex());
917}
918
919void KateThemeConfigHighlightTab::apply()
920{
921 // handle all cached themes data
922 for (const auto &themeIt : m_uniqueAttributes) {
923 // get theme for key, skip invalid or read-only themes for writing
924 const auto theme = KateHlManager::self()->repository().theme(themeName: themeIt.first);
925 if (!theme.isValid() || theme.isReadOnly()) {
926 continue;
927 }
928
929 // get current theme data from disk
930 QJsonObject newThemeObject = jsonForTheme(theme);
931
932 // look at all highlightings we have info stored, important: keep info we did load from file and not overwrite here!
933 QJsonObject overrides = newThemeObject[QLatin1String("custom-styles")].toObject();
934 for (const auto &highlightingIt : themeIt.second) {
935 // start with stuff we know from the loaded json
936 const QString definitionName = highlightingIt.first;
937 QJsonObject styles = overrides[definitionName].toObject();
938 for (const auto &attributeIt : highlightingIt.second) {
939 // we need to store even if we have nothing set as long as the value differs from the default, see bug 459093
940 QJsonObject style;
941 KTextEditor::Attribute::Ptr p = attributeIt.second.first;
942 KTextEditor::Attribute::Ptr pDefault = attributeIt.second.second;
943 if (p->foreground().color() != pDefault->foreground().color()) {
944 style[QLatin1String("text-color")] = hexName(c: p->foreground().color());
945 }
946 if (p->background().color() != pDefault->background().color()) {
947 style[QLatin1String("background-color")] = hexName(c: p->background().color());
948 }
949 if (p->selectedForeground().color() != pDefault->selectedForeground().color()) {
950 style[QLatin1String("selected-text-color")] = hexName(c: p->selectedForeground().color());
951 }
952 if (p->selectedBackground().color() != pDefault->selectedBackground().color()) {
953 style[QLatin1String("selected-background-color")] = hexName(c: p->selectedBackground().color());
954 }
955 if (p->fontBold() != pDefault->fontBold()) {
956 style[QLatin1String("bold")] = p->fontBold();
957 }
958 if (p->fontItalic() != pDefault->fontItalic()) {
959 style[QLatin1String("italic")] = p->fontItalic();
960 }
961 if (p->fontUnderline() != pDefault->fontUnderline()) {
962 style[QLatin1String("underline")] = p->fontUnderline();
963 }
964 if (p->fontStrikeOut() != pDefault->fontStrikeOut()) {
965 style[QLatin1String("strike-through")] = p->fontStrikeOut();
966 }
967
968 // either set the new stuff or erase the old entry we might have set from the loaded json
969 if (!style.isEmpty()) {
970 styles[attributeIt.first] = style;
971 } else {
972 styles.remove(key: attributeIt.first);
973 }
974 }
975
976 // either set the new stuff or erase the old entry we might have set from the loaded json
977 if (!styles.isEmpty()) {
978 overrides[definitionName] = styles;
979 } else {
980 overrides.remove(key: definitionName);
981 }
982 }
983
984 // we set even empty overrides, to ensure we overwrite stuff!
985 newThemeObject[QLatin1String("custom-styles")] = overrides;
986
987 // write json back to file
988 writeJson(json: newThemeObject, themeFileName: theme.filePath());
989 }
990}
991
992QList<int> KateThemeConfigHighlightTab::hlsForSchema(const QString &schema)
993{
994 auto it = m_hlDict.find(key: schema);
995 if (it != m_hlDict.end()) {
996 return it.value().keys();
997 }
998 return {};
999}
1000
1001void KateThemeConfigHighlightTab::showEvent(QShowEvent *event)
1002{
1003 if (!event->spontaneous()) {
1004 KateAttributeList *l = m_defaults->attributeList(schema: m_schema);
1005 Q_ASSERT(l != nullptr);
1006 updateColorPalette(textColor: l->at(i: 0)->foreground().color());
1007 }
1008
1009 QWidget::showEvent(event);
1010}
1011// END KateThemeConfigHighlightTab
1012
1013// BEGIN KateThemeConfigPage -- Main dialog page
1014KateThemeConfigPage::KateThemeConfigPage(QWidget *parent)
1015 : KateConfigPage(parent)
1016{
1017 QHBoxLayout *layout = new QHBoxLayout(this);
1018 layout->setContentsMargins({});
1019
1020 QTabWidget *tabWidget = new QTabWidget(this);
1021 tabWidget->setDocumentMode(true);
1022 layout->addWidget(tabWidget);
1023
1024 auto *themeEditor = new QWidget(this);
1025 auto *themeChooser = new QWidget(this);
1026 tabWidget->addTab(widget: themeChooser, i18n("Default Theme"));
1027 tabWidget->addTab(widget: themeEditor, i18n("Theme Editor"));
1028 layoutThemeChooserTab(tab: themeChooser);
1029 layoutThemeEditorTab(tab: themeEditor);
1030
1031 reload();
1032}
1033
1034void KateThemeConfigPage::layoutThemeChooserTab(QWidget *tab)
1035{
1036 QVBoxLayout *layout = new QVBoxLayout(tab);
1037 layout->setContentsMargins({});
1038
1039 auto *comboLayout = new QHBoxLayout;
1040
1041 auto lHl = new QLabel(i18n("Select theme:"), this);
1042 comboLayout->addWidget(lHl);
1043
1044 defaultSchemaCombo = new QComboBox(this);
1045 comboLayout->addWidget(defaultSchemaCombo);
1046 defaultSchemaCombo->setEditable(false);
1047 lHl->setBuddy(defaultSchemaCombo);
1048 connect(sender: defaultSchemaCombo, signal: &QComboBox::currentIndexChanged, context: this, slot: &KateThemeConfigPage::slotChanged);
1049 comboLayout->addStretch();
1050
1051 layout->addLayout(layout: comboLayout);
1052
1053 m_doc = new KTextEditor::DocumentPrivate;
1054 m_doc->setParent(this);
1055
1056 const auto code = R"sample(/**
1057* SPDX-FileCopyrightText: 2020 Christoph Cullmann <cullmann@kde.org>
1058* SPDX-License-Identifier: MIT
1059*/
1060
1061// BEGIN
1062#include <QString>
1063#include <string>
1064// END
1065
1066/**
1067* TODO: improve documentation
1068* @param magicArgument some magic argument
1069* @return magic return value
1070*/
1071int main(uint64_t magicArgument)
1072{
1073 if (magicArgument > 1) {
1074 const std::string string = "source file: \"" __FILE__ "\"";
1075 const QString qString(QStringLiteral("test"));
1076 return qrand();
1077 }
1078
1079 /* BUG: bogus integer constant inside next line */
1080 const double g = 1.1e12 * 0b01'01'01'01 - 43a + 0x11234 * 0234ULL - 'c' * 42;
1081 return g > 1.3f;
1082})sample";
1083
1084 m_doc->setText(QString::fromUtf8(utf8: code));
1085 m_doc->setHighlightingMode(QStringLiteral("C++"));
1086 m_themePreview = new KTextEditor::ViewPrivate(m_doc, this);
1087
1088 layout->addWidget(m_themePreview);
1089
1090 connect(sender: defaultSchemaCombo, signal: &QComboBox::currentIndexChanged, context: this, slot: [this](int idx) {
1091 const QString schema = defaultSchemaCombo->itemData(index: idx).toString();
1092 m_themePreview->rendererConfig()->setSchema(schema);
1093 if (schema.isEmpty()) {
1094 m_themePreview->rendererConfig()->setValue(key: KateRendererConfig::AutoColorThemeSelection, value: true);
1095 } else {
1096 m_themePreview->rendererConfig()->setValue(key: KateRendererConfig::AutoColorThemeSelection, value: false);
1097 }
1098 });
1099}
1100
1101void KateThemeConfigPage::layoutThemeEditorTab(QWidget *tab)
1102{
1103 QVBoxLayout *layout = new QVBoxLayout(tab);
1104 layout->setContentsMargins(left: 0, top: 0, right: 0, bottom: 0);
1105
1106 // header
1107 QHBoxLayout *headerLayout = new QHBoxLayout;
1108 layout->addLayout(layout: headerLayout);
1109
1110 QLabel *lHl = new QLabel(i18n("&Theme:"), this);
1111 headerLayout->addWidget(lHl);
1112
1113 schemaCombo = new QComboBox(this);
1114 schemaCombo->setEditable(false);
1115 lHl->setBuddy(schemaCombo);
1116 headerLayout->addWidget(schemaCombo);
1117 connect(sender: schemaCombo, signal: &QComboBox::currentIndexChanged, context: this, slot: &KateThemeConfigPage::comboBoxIndexChanged);
1118
1119 QPushButton *copyButton = new QPushButton(i18n("&Copy..."), this);
1120 headerLayout->addWidget(copyButton);
1121 connect(sender: copyButton, signal: &QPushButton::clicked, context: this, slot: &KateThemeConfigPage::copyTheme);
1122
1123 btndel = new QPushButton(i18n("&Delete"), this);
1124 headerLayout->addWidget(btndel);
1125 connect(sender: btndel, signal: &QPushButton::clicked, context: this, slot: &KateThemeConfigPage::deleteSchema);
1126
1127 QPushButton *btnexport = new QPushButton(i18n("Export..."), this);
1128 headerLayout->addWidget(btnexport);
1129 connect(sender: btnexport, signal: &QPushButton::clicked, context: this, slot: &KateThemeConfigPage::exportFullSchema);
1130
1131 QPushButton *btnimport = new QPushButton(i18n("Import..."), this);
1132 headerLayout->addWidget(btnimport);
1133 connect(sender: btnimport, signal: &QPushButton::clicked, context: this, slot: &KateThemeConfigPage::importFullSchema);
1134
1135 headerLayout->addStretch();
1136
1137 // label to inform about read-only state
1138 m_readOnlyThemeLabel = new KMessageWidget(i18n("Bundled read-only theme. To modify the theme, please copy it."), this);
1139 m_readOnlyThemeLabel->setCloseButtonVisible(false);
1140 m_readOnlyThemeLabel->setMessageType(KMessageWidget::Information);
1141 m_readOnlyThemeLabel->hide();
1142 layout->addWidget(m_readOnlyThemeLabel);
1143
1144 // tabs
1145 QTabWidget *tabWidget = new QTabWidget(this);
1146 layout->addWidget(tabWidget);
1147
1148 m_colorTab = new KateThemeConfigColorTab();
1149 tabWidget->addTab(widget: m_colorTab, i18n("Colors"));
1150 connect(sender: m_colorTab, signal: &KateThemeConfigColorTab::changed, context: this, slot: &KateThemeConfigPage::slotChanged);
1151
1152 m_defaultStylesTab = new KateThemeConfigDefaultStylesTab(m_colorTab);
1153 tabWidget->addTab(widget: m_defaultStylesTab, i18n("Default Text Styles"));
1154 connect(sender: m_defaultStylesTab, signal: &KateThemeConfigDefaultStylesTab::changed, context: this, slot: &KateThemeConfigPage::slotChanged);
1155
1156 m_highlightTab = new KateThemeConfigHighlightTab(m_defaultStylesTab, m_colorTab);
1157 tabWidget->addTab(widget: m_highlightTab, i18n("Highlighting Text Styles"));
1158 connect(sender: m_highlightTab, signal: &KateThemeConfigHighlightTab::changed, context: this, slot: &KateThemeConfigPage::slotChanged);
1159
1160 QHBoxLayout *footLayout = new QHBoxLayout;
1161 layout->addLayout(layout: footLayout);
1162}
1163
1164void KateThemeConfigPage::exportFullSchema()
1165{
1166 // get save destination
1167 const QString currentSchemaName = m_currentSchema;
1168 const QString destName = QFileDialog::getSaveFileName(parent: this,
1169 i18n("Exporting color theme: %1", currentSchemaName),
1170 dir: currentSchemaName + QLatin1String(".theme"),
1171 QStringLiteral("%1 (*.theme)").arg(i18n("Color theme")));
1172 if (destName.isEmpty()) {
1173 return;
1174 }
1175
1176 // get current theme
1177 const QString currentThemeName = schemaCombo->itemData(index: schemaCombo->currentIndex()).toString();
1178 const auto currentTheme = KateHlManager::self()->repository().theme(themeName: currentThemeName);
1179
1180 // ensure we overwrite
1181 if (QFile::exists(fileName: destName)) {
1182 QFile::remove(fileName: destName);
1183 }
1184
1185 // export is easy, just copy the file 1:1
1186 QFile::copy(fileName: currentTheme.filePath(), newName: destName);
1187}
1188
1189void KateThemeConfigPage::importFullSchema()
1190{
1191 const QString srcName =
1192 QFileDialog::getOpenFileName(parent: this, i18n("Importing Color Theme"), dir: QString(), QStringLiteral("%1 (*.theme)").arg(i18n("Color theme")));
1193 if (srcName.isEmpty()) {
1194 return;
1195 }
1196
1197 // location to write theme files to
1198 const QString themesPath = QStandardPaths::writableLocation(type: QStandardPaths::GenericDataLocation) + QStringLiteral("/org.kde.syntax-highlighting/themes");
1199
1200 // construct file name for imported theme
1201 const QString themesFullFileName = themesPath + QStringLiteral("/") + QFileInfo(srcName).fileName();
1202
1203 // if something might be overwritten, as the user
1204 if (QFile::exists(fileName: themesFullFileName)) {
1205 if (KMessageBox::warningContinueCancel(parent: this,
1206 i18n("Importing will overwrite the existing theme file \"%1\". This can not be undone.", themesFullFileName),
1207 i18n("Possible Data Loss"),
1208 buttonContinue: KGuiItem(i18n("Import Nevertheless")),
1209 buttonCancel: KStandardGuiItem::cancel())
1210 != KMessageBox::Continue) {
1211 return;
1212 }
1213 }
1214
1215 // copy theme file, we might need to create the local dir first
1216 QDir().mkpath(dirPath: themesPath);
1217
1218 // ensure we overwrite
1219 if (QFile::exists(fileName: themesFullFileName)) {
1220 QFile::remove(fileName: themesFullFileName);
1221 }
1222 QFile::copy(fileName: srcName, newName: themesFullFileName);
1223
1224 // reload themes DB & clear all attributes
1225 KateHlManager::self()->reload();
1226 for (int i = 0; i < KateHlManager::self()->modeList().size(); ++i) {
1227 KateHlManager::self()->getHl(n: i)->clearAttributeArrays();
1228 }
1229
1230 // KateThemeManager::update() sorts the schema alphabetically, hence the
1231 // schema indexes change. Thus, repopulate the schema list...
1232 refillCombos(schemaName: schemaCombo->itemData(index: schemaCombo->currentIndex()).toString(), defaultSchemaName: defaultSchemaCombo->itemData(index: defaultSchemaCombo->currentIndex()).toString());
1233}
1234
1235void KateThemeConfigPage::apply()
1236{
1237 // remember name + index
1238 const QString schemaName = schemaCombo->itemData(index: schemaCombo->currentIndex()).toString();
1239
1240 // first apply all tabs
1241 m_colorTab->apply();
1242 m_defaultStylesTab->apply();
1243 m_highlightTab->apply();
1244
1245 // reload themes DB & clear all attributes
1246 KateHlManager::self()->reload();
1247 for (int i = 0; i < KateHlManager::self()->modeList().size(); ++i) {
1248 KateHlManager::self()->getHl(n: i)->clearAttributeArrays();
1249 }
1250
1251 // than reload the whole stuff, special handle auto selection == empty theme name
1252 const auto defaultTheme = defaultSchemaCombo->itemData(index: defaultSchemaCombo->currentIndex()).toString();
1253 if (defaultTheme.isEmpty()) {
1254 KateRendererConfig::global()->setValue(key: KateRendererConfig::AutoColorThemeSelection, value: true);
1255 } else {
1256 KateRendererConfig::global()->setValue(key: KateRendererConfig::AutoColorThemeSelection, value: false);
1257 KateRendererConfig::global()->setSchema(defaultTheme);
1258 }
1259 KateRendererConfig::global()->reloadSchema();
1260
1261 // KateThemeManager::update() sorts the schema alphabetically, hence the
1262 // schema indexes change. Thus, repopulate the schema list...
1263 refillCombos(schemaName: schemaCombo->itemData(index: schemaCombo->currentIndex()).toString(), defaultSchemaName: defaultSchemaCombo->itemData(index: defaultSchemaCombo->currentIndex()).toString());
1264 schemaChanged(schema: schemaName);
1265
1266 // all tabs need to reload to discard all the cached data, as the index
1267 // mapping may have changed
1268 m_colorTab->reload();
1269 m_defaultStylesTab->reload();
1270 m_highlightTab->reload();
1271}
1272
1273void KateThemeConfigPage::reload()
1274{
1275 // reinitialize combo boxes
1276 refillCombos(schemaName: KateRendererConfig::global()->schema(), defaultSchemaName: KateRendererConfig::global()->schema());
1277
1278 // finally, activate the current schema again
1279 schemaChanged(schema: schemaCombo->itemData(index: schemaCombo->currentIndex()).toString());
1280
1281 // all tabs need to reload to discard all the cached data, as the index
1282 // mapping may have changed
1283 m_colorTab->reload();
1284 m_defaultStylesTab->reload();
1285 m_highlightTab->reload();
1286}
1287
1288void KateThemeConfigPage::refillCombos(const QString &schemaName, const QString &defaultSchemaName)
1289{
1290 schemaCombo->blockSignals(b: true);
1291 defaultSchemaCombo->blockSignals(b: true);
1292
1293 // reinitialize combo boxes
1294 schemaCombo->clear();
1295 defaultSchemaCombo->clear();
1296 defaultSchemaCombo->addItem(i18n("Follow System Color Scheme"), auserData: QString());
1297 defaultSchemaCombo->insertSeparator(index: 1);
1298 const auto themes = KateHlManager::self()->sortedThemes();
1299 for (const auto &theme : themes) {
1300 schemaCombo->addItem(atext: theme.translatedName(), auserData: theme.name());
1301 defaultSchemaCombo->addItem(atext: theme.translatedName(), auserData: theme.name());
1302 }
1303
1304 // set the correct indexes again, fallback to always existing default theme
1305 int schemaIndex = schemaCombo->findData(data: schemaName);
1306 if (schemaIndex == -1) {
1307 schemaIndex = schemaCombo->findData(
1308 data: KTextEditor::EditorPrivate::self()->hlManager()->repository().defaultTheme(t: KSyntaxHighlighting::Repository::LightTheme).name());
1309 }
1310
1311 // set the correct indexes again, fallback to auto-selection
1312 int defaultSchemaIndex = 0;
1313 if (!KateRendererConfig::global()->value(key: KateRendererConfig::AutoColorThemeSelection).toBool()) {
1314 defaultSchemaIndex = defaultSchemaCombo->findData(data: defaultSchemaName);
1315 if (defaultSchemaIndex == -1) {
1316 defaultSchemaIndex = 0;
1317 }
1318 }
1319
1320 Q_ASSERT(schemaIndex != -1);
1321 Q_ASSERT(defaultSchemaIndex != -1);
1322
1323 defaultSchemaCombo->setCurrentIndex(defaultSchemaIndex);
1324 schemaCombo->setCurrentIndex(schemaIndex);
1325
1326 schemaCombo->blockSignals(b: false);
1327 defaultSchemaCombo->blockSignals(b: false);
1328
1329 m_themePreview->rendererConfig()->setSchema(defaultSchemaName);
1330}
1331
1332void KateThemeConfigPage::reset()
1333{
1334 // reload themes DB & clear all attributes
1335 KateHlManager::self()->reload();
1336 for (int i = 0; i < KateHlManager::self()->modeList().size(); ++i) {
1337 KateHlManager::self()->getHl(n: i)->clearAttributeArrays();
1338 }
1339
1340 // reload the view
1341 reload();
1342}
1343
1344void KateThemeConfigPage::defaults()
1345{
1346 reset();
1347}
1348
1349void KateThemeConfigPage::deleteSchema()
1350{
1351 const int comboIndex = schemaCombo->currentIndex();
1352 const QString schemaNameToDelete = schemaCombo->itemData(index: comboIndex).toString();
1353
1354 // KSyntaxHighlighting themes can not be deleted, skip invalid themes, too
1355 const auto theme = KateHlManager::self()->repository().theme(themeName: schemaNameToDelete);
1356 if (!theme.isValid() || theme.isReadOnly()) {
1357 return;
1358 }
1359
1360 // ask the user again, this can't be undone
1361 if (KMessageBox::warningContinueCancel(parent: this,
1362 i18n("Do you really want to delete the theme \"%1\"? This can not be undone.", schemaNameToDelete),
1363 i18n("Possible Data Loss"),
1364 buttonContinue: KGuiItem(i18n("Delete Nevertheless")),
1365 buttonCancel: KStandardGuiItem::cancel())
1366 != KMessageBox::Continue) {
1367 return;
1368 }
1369
1370 // purge the theme file
1371 QFile::remove(fileName: theme.filePath());
1372
1373 // reset syntax manager repo to flush deleted theme
1374 KateHlManager::self()->reload();
1375
1376 // fallback to Default schema + auto
1377 schemaCombo->setCurrentIndex(schemaCombo->findData(
1378 data: QVariant(KTextEditor::EditorPrivate::self()->hlManager()->repository().defaultTheme(t: KSyntaxHighlighting::Repository::LightTheme).name())));
1379 if (defaultSchemaCombo->currentIndex() == defaultSchemaCombo->findData(data: schemaNameToDelete)) {
1380 defaultSchemaCombo->setCurrentIndex(0);
1381 }
1382
1383 // remove schema from combo box
1384 schemaCombo->removeItem(index: comboIndex);
1385 defaultSchemaCombo->removeItem(index: comboIndex);
1386
1387 // Reload the color tab, since it uses cached schemas
1388 m_colorTab->reload();
1389}
1390
1391bool KateThemeConfigPage::copyTheme()
1392{
1393 // get current theme data as template
1394 const QString currentThemeName = schemaCombo->itemData(index: schemaCombo->currentIndex()).toString();
1395 const auto currentTheme = KateHlManager::self()->repository().theme(themeName: currentThemeName);
1396
1397 // location to write theme files to
1398 const QString themesPath = QStandardPaths::writableLocation(type: QStandardPaths::GenericDataLocation) + QStringLiteral("/org.kde.syntax-highlighting/themes");
1399
1400 // get sane name
1401 QString schemaName;
1402 QString themeFileName;
1403 while (schemaName.isEmpty()) {
1404 QInputDialog newNameDialog(this);
1405 newNameDialog.setInputMode(QInputDialog::TextInput);
1406 newNameDialog.setWindowTitle(i18n("Copy theme"));
1407 newNameDialog.setLabelText(i18n("Name for copy of color theme \"%1\":", currentThemeName));
1408 newNameDialog.setTextValue(currentThemeName);
1409 if (newNameDialog.exec() == QDialog::Rejected) {
1410 return false;
1411 }
1412 schemaName = newNameDialog.textValue();
1413
1414 // try if schema already around => if yes, retry name input
1415 // we try for duplicated file names, too
1416 themeFileName = themesPath + QStringLiteral("/") + schemaName + QStringLiteral(".theme");
1417 if (KateHlManager::self()->repository().theme(themeName: schemaName).isValid() || QFile::exists(fileName: themeFileName)) {
1418 KMessageBox::information(parent: this,
1419 i18n("<p>The theme \"%1\" already exists.</p><p>Please choose a different theme name.</p>", schemaName),
1420 i18n("Copy Theme"));
1421 schemaName.clear();
1422 }
1423 }
1424
1425 // get json for current theme
1426 QJsonObject newThemeObject = jsonForTheme(theme: currentTheme);
1427 QJsonObject metaData;
1428 metaData[QLatin1String("revision")] = 1;
1429 metaData[QLatin1String("name")] = schemaName;
1430 newThemeObject[QLatin1String("metadata")] = metaData;
1431
1432 // write to new theme file, we might need to create the local dir first
1433 QDir().mkpath(dirPath: themesPath);
1434 if (!writeJson(json: newThemeObject, themeFileName)) {
1435 return false;
1436 }
1437
1438 // reset syntax manager repo to find new theme
1439 KateHlManager::self()->reload();
1440
1441 // append items to combo boxes
1442 schemaCombo->addItem(atext: schemaName, auserData: QVariant(schemaName));
1443 defaultSchemaCombo->addItem(atext: schemaName, auserData: QVariant(schemaName));
1444
1445 // finally, activate new schema (last item in the list)
1446 schemaCombo->setCurrentIndex(schemaCombo->count() - 1);
1447 return true;
1448}
1449
1450void KateThemeConfigPage::schemaChanged(const QString &schema)
1451{
1452 // we can't delete read-only themes, e.g. the stuff shipped inside Qt resources or system wide installed
1453 const auto theme = KateHlManager::self()->repository().theme(themeName: schema);
1454 btndel->setEnabled(!theme.isReadOnly());
1455 m_readOnlyThemeLabel->setVisible(theme.isReadOnly());
1456
1457 // propagate changed schema to all tabs
1458 m_colorTab->schemaChanged(newSchema: schema);
1459 m_defaultStylesTab->schemaChanged(schema);
1460 m_highlightTab->schemaChanged(schema);
1461
1462 // save current schema index
1463 m_currentSchema = schema;
1464}
1465
1466void KateThemeConfigPage::comboBoxIndexChanged(int currentIndex)
1467{
1468 schemaChanged(schema: schemaCombo->itemData(index: currentIndex).toString());
1469}
1470
1471QString KateThemeConfigPage::name() const
1472{
1473 return i18n("Color Themes");
1474}
1475
1476QString KateThemeConfigPage::fullName() const
1477{
1478 return i18n("Color Themes");
1479}
1480
1481QIcon KateThemeConfigPage::icon() const
1482{
1483 return QIcon::fromTheme(QStringLiteral("preferences-desktop-color"));
1484}
1485
1486// END KateThemeConfigPage
1487
1488#include "moc_katethemeconfig.cpp"
1489

source code of ktexteditor/src/syntax/katethemeconfig.cpp