1 /*
2 This source file is part of Konsole, a terminal emulator.
3
4 SPDX-FileCopyrightText: 2007-2008 Robert Knight <robertknight@gmail.com>
5
6 SPDX-License-Identifier: GPL-2.0-or-later
7 */
8
9 // Own
10 #include "ColorScheme.h"
11 #include "RandomizationRange.h"
12 #include "hsluv.h"
13
14 // Qt
15 #include <QPainter>
16
17 // KDE
18 #include <KConfig>
19 #include <KConfigGroup>
20 #include <KLocalizedString>
21
22 // STL
23 #include <random>
24
25 // Konsole
26 #include "colorschemedebug.h"
27
28 namespace
29 {
30 const int FGCOLOR_INDEX = 0;
31 const int BGCOLOR_INDEX = 1;
32
33 const char RandomHueRangeKey[] = "RandomHueRange";
34 const char RandomSaturationRangeKey[] = "RandomSaturationRange";
35 const char RandomLightnessRangeKey[] = "RandomLightnessRange";
36 const char EnableColorRandomizationKey[] = "ColorRandomization";
37
38 const double MaxHue = 360.0;
39 const double MaxSaturation = 100.0;
40 const double MaxLightness = 100.0;
41 }
42
43 using namespace Konsole;
44
45 // The following are almost IBM standard color codes, with some slight
46 // gamma correction for the dim colors to compensate for bright X screens.
47 // It contains the 8 ansiterm/xterm colors in 2 intensities.
48 const QColor ColorScheme::defaultTable[TABLE_COLORS] = {
49 QColor(0x00, 0x00, 0x00), // Dfore
50 QColor(0xFF, 0xFF, 0xFF), // Dback
51 QColor(0x00, 0x00, 0x00), // Black
52 QColor(0xB2, 0x18, 0x18), // Red
53 QColor(0x18, 0xB2, 0x18), // Green
54 QColor(0xB2, 0x68, 0x18), // Yellow
55 QColor(0x18, 0x18, 0xB2), // Blue
56 QColor(0xB2, 0x18, 0xB2), // Magenta
57 QColor(0x18, 0xB2, 0xB2), // Cyan
58 QColor(0xB2, 0xB2, 0xB2), // White
59 // intensive versions
60 QColor(0x00, 0x00, 0x00),
61 QColor(0xFF, 0xFF, 0xFF),
62 QColor(0x68, 0x68, 0x68),
63 QColor(0xFF, 0x54, 0x54),
64 QColor(0x54, 0xFF, 0x54),
65 QColor(0xFF, 0xFF, 0x54),
66 QColor(0x54, 0x54, 0xFF),
67 QColor(0xFF, 0x54, 0xFF),
68 QColor(0x54, 0xFF, 0xFF),
69 QColor(0xFF, 0xFF, 0xFF),
70 // Here are faint intensities, which may not be good.
71 // faint versions
72 QColor(0x00, 0x00, 0x00),
73 QColor(0xFF, 0xFF, 0xFF),
74 QColor(0x00, 0x00, 0x00),
75 QColor(0x65, 0x00, 0x00),
76 QColor(0x00, 0x65, 0x00),
77 QColor(0x65, 0x5E, 0x00),
78 QColor(0x00, 0x00, 0x65),
79 QColor(0x65, 0x00, 0x65),
80 QColor(0x00, 0x65, 0x65),
81 QColor(0x65, 0x65, 0x65),
82 };
83
84 const char *const ColorScheme::colorNames[TABLE_COLORS] = {
85 "Foreground",
86 "Background",
87 "Color0",
88 "Color1",
89 "Color2",
90 "Color3",
91 "Color4",
92 "Color5",
93 "Color6",
94 "Color7",
95 "ForegroundIntense",
96 "BackgroundIntense",
97 "Color0Intense",
98 "Color1Intense",
99 "Color2Intense",
100 "Color3Intense",
101 "Color4Intense",
102 "Color5Intense",
103 "Color6Intense",
104 "Color7Intense",
105 "ForegroundFaint",
106 "BackgroundFaint",
107 "Color0Faint",
108 "Color1Faint",
109 "Color2Faint",
110 "Color3Faint",
111 "Color4Faint",
112 "Color5Faint",
113 "Color6Faint",
114 "Color7Faint",
115 };
116 const char *const ColorScheme::translatedColorNames[TABLE_COLORS] = {
117 I18N_NOOP2("@item:intable palette", "Foreground"),
118 I18N_NOOP2("@item:intable palette", "Background"),
119 I18N_NOOP2("@item:intable palette", "Color 1"),
120 I18N_NOOP2("@item:intable palette", "Color 2"),
121 I18N_NOOP2("@item:intable palette", "Color 3"),
122 I18N_NOOP2("@item:intable palette", "Color 4"),
123 I18N_NOOP2("@item:intable palette", "Color 5"),
124 I18N_NOOP2("@item:intable palette", "Color 6"),
125 I18N_NOOP2("@item:intable palette", "Color 7"),
126 I18N_NOOP2("@item:intable palette", "Color 8"),
127 I18N_NOOP2("@item:intable palette", "Foreground (Intense)"),
128 I18N_NOOP2("@item:intable palette", "Background (Intense)"),
129 I18N_NOOP2("@item:intable palette", "Color 1 (Intense)"),
130 I18N_NOOP2("@item:intable palette", "Color 2 (Intense)"),
131 I18N_NOOP2("@item:intable palette", "Color 3 (Intense)"),
132 I18N_NOOP2("@item:intable palette", "Color 4 (Intense)"),
133 I18N_NOOP2("@item:intable palette", "Color 5 (Intense)"),
134 I18N_NOOP2("@item:intable palette", "Color 6 (Intense)"),
135 I18N_NOOP2("@item:intable palette", "Color 7 (Intense)"),
136 I18N_NOOP2("@item:intable palette", "Color 8 (Intense)"),
137 I18N_NOOP2("@item:intable palette", "Foreground (Faint)"),
138 I18N_NOOP2("@item:intable palette", "Background (Faint)"),
139 I18N_NOOP2("@item:intable palette", "Color 1 (Faint)"),
140 I18N_NOOP2("@item:intable palette", "Color 2 (Faint)"),
141 I18N_NOOP2("@item:intable palette", "Color 3 (Faint)"),
142 I18N_NOOP2("@item:intable palette", "Color 4 (Faint)"),
143 I18N_NOOP2("@item:intable palette", "Color 5 (Faint)"),
144 I18N_NOOP2("@item:intable palette", "Color 6 (Faint)"),
145 I18N_NOOP2("@item:intable palette", "Color 7 (Faint)"),
146 I18N_NOOP2("@item:intable palette", "Color 8 (Faint)"),
147 };
148
colorNameForIndex(int index)149 QString ColorScheme::colorNameForIndex(int index)
150 {
151 Q_ASSERT(index >= 0 && index < TABLE_COLORS);
152
153 return QString(QLatin1String(colorNames[index]));
154 }
155
translatedColorNameForIndex(int index)156 QString ColorScheme::translatedColorNameForIndex(int index)
157 {
158 Q_ASSERT(index >= 0 && index < TABLE_COLORS);
159
160 return i18nc("@item:intable palette", translatedColorNames[index]);
161 }
162
ColorScheme()163 ColorScheme::ColorScheme()
164 : _description(QString())
165 , _name(QString())
166 , _table(nullptr)
167 , _randomTable(nullptr)
168 , _opacity(1.0)
169 , _blur(false)
170 , _colorRandomization(false)
171 , _wallpaper(nullptr)
172 {
173 setWallpaper(QString());
174 }
175
ColorScheme(const ColorScheme & other)176 ColorScheme::ColorScheme(const ColorScheme &other)
177 : _description(QString())
178 , _name(QString())
179 , _table(nullptr)
180 , _randomTable(nullptr)
181 , _opacity(other._opacity)
182 , _blur(other._blur)
183 , _colorRandomization(other._colorRandomization)
184 , _wallpaper(other._wallpaper)
185 {
186 setName(other.name());
187 setDescription(other.description());
188
189 if (other._table != nullptr) {
190 for (int i = 0; i < TABLE_COLORS; i++) {
191 setColorTableEntry(i, other._table[i]);
192 }
193 }
194
195 if (other._randomTable != nullptr) {
196 for (int i = 0; i < TABLE_COLORS; i++) {
197 const RandomizationRange &range = other._randomTable[i];
198 setRandomizationRange(i, range.hue, range.saturation, range.lightness);
199 }
200 }
201 }
202
~ColorScheme()203 ColorScheme::~ColorScheme()
204 {
205 delete[] _table;
206 delete[] _randomTable;
207 }
208
setDescription(const QString & description)209 void ColorScheme::setDescription(const QString &description)
210 {
211 _description = description;
212 }
213
description() const214 QString ColorScheme::description() const
215 {
216 return _description;
217 }
218
setName(const QString & name)219 void ColorScheme::setName(const QString &name)
220 {
221 _name = name;
222 }
223
name() const224 QString ColorScheme::name() const
225 {
226 return _name;
227 }
228
setColorTableEntry(int index,const QColor & entry)229 void ColorScheme::setColorTableEntry(int index, const QColor &entry)
230 {
231 Q_ASSERT(index >= 0 && index < TABLE_COLORS);
232
233 if (_table == nullptr) {
234 _table = new QColor[TABLE_COLORS];
235
236 std::copy_n(defaultTable, TABLE_COLORS, _table);
237 }
238
239 if (entry.isValid()) {
240 _table[index] = entry;
241 } else {
242 _table[index] = defaultTable[index];
243 qCDebug(ColorSchemeDebug) << "ColorScheme" << name() << "has an invalid color index" << index << ", using default table color";
244 }
245 }
246
colorEntry(int index,uint randomSeed) const247 QColor ColorScheme::colorEntry(int index, uint randomSeed) const
248 {
249 Q_ASSERT(index >= 0 && index < TABLE_COLORS);
250
251 QColor entry = colorTable()[index];
252
253 if (!_colorRandomization || randomSeed == 0 || _randomTable == nullptr || _randomTable[index].isNull()) {
254 return entry;
255 }
256
257 double baseHue;
258 double baseSaturation;
259 double baseLightness;
260 rgb2hsluv(entry.redF(), entry.greenF(), entry.blueF(), &baseHue, &baseSaturation, &baseLightness);
261
262 const RandomizationRange &range = _randomTable[index];
263
264 // 32-bit Mersenne Twister
265 // Can't use default_random_engine, because in GCC this maps to
266 // minstd_rand0 which always gives us 0 on the first number.
267 std::mt19937 randomEngine(randomSeed);
268
269 // Use hues located around base color's hue.
270 // H=0 [|= =] H=128 [ =|= ] H=360 [= =|]
271 const double minHue = baseHue - range.hue / 2.0;
272 const double maxHue = baseHue + range.hue / 2.0;
273 std::uniform_real_distribution<> hueDistribution(minHue, maxHue);
274 // Hue value is an angle, it wraps after 360°. Adding MAX_HUE
275 // guarantees that the sum is not negative.
276 const double hue = fmod(MaxHue + hueDistribution(randomEngine), MaxHue);
277
278 // Saturation is always decreased. With more saturation more
279 // information about hue is preserved in RGB color space
280 // (consider red with S=100 and "red" with S=0 which is gray).
281 // Additionally, I think it can be easier to imagine more
282 // toned color than more vivid one.
283 // S=0 [|== ] S=50 [ ==| ] S=100 [ ==|]
284 const double minSaturation = qMax(baseSaturation - range.saturation, 0.0);
285 const double maxSaturation = qMax(range.saturation, baseSaturation);
286 // Use rising linear distribution as colors with lower
287 // saturation are less distinguishable.
288 double saturation;
289 if (qFuzzyCompare(minSaturation, maxSaturation)) {
290 saturation = baseSaturation;
291 } else {
292 std::piecewise_linear_distribution<> saturationDistribution({minSaturation, maxSaturation}, [](double v) {
293 return v;
294 });
295 saturation = saturationDistribution(randomEngine);
296 }
297
298 // Lightness range has base value at its center. The base
299 // value is clamped to prevent the range from shrinking.
300 // L=0 [=|= ] L=50 [ =|= ] L=100 [ =|=]
301 baseLightness = qBound(range.lightness / 2.0, baseLightness, MaxLightness - range.lightness);
302 const double minLightness = qMax(baseLightness - range.lightness / 2.0, 0.0);
303 const double maxLightness = qMin(baseLightness + range.lightness / 2.0, MaxLightness);
304 // Use triangular distribution with peak at L=50.0.
305 // Dark and very light colors are less distinguishable.
306 double lightness;
307 if (qFuzzyCompare(minLightness, maxLightness)) {
308 lightness = baseLightness;
309 } else {
310 static const auto lightnessWeightsFunc = [](double v) {
311 return 50.0 - qAbs(v - 50.0);
312 };
313 std::piecewise_linear_distribution<> lightnessDistribution;
314 if (minLightness < 50.0 && 50.0 < maxLightness) {
315 lightnessDistribution = std::piecewise_linear_distribution<>({minLightness, 50.0, maxLightness}, lightnessWeightsFunc);
316 } else {
317 lightnessDistribution = std::piecewise_linear_distribution<>({minLightness, maxLightness}, lightnessWeightsFunc);
318 }
319 lightness = lightnessDistribution(randomEngine);
320 }
321
322 double red;
323 double green;
324 double blue;
325 hsluv2rgb(hue, saturation, lightness, &red, &green, &blue);
326
327 return {qRound(red * 255), qRound(green * 255), qRound(blue * 255)};
328 }
329
getColorTable(QColor * table,uint randomSeed) const330 void ColorScheme::getColorTable(QColor *table, uint randomSeed) const
331 {
332 for (int i = 0; i < TABLE_COLORS; i++) {
333 table[i] = colorEntry(i, randomSeed);
334 }
335 }
336
isColorRandomizationEnabled() const337 bool ColorScheme::isColorRandomizationEnabled() const
338 {
339 return (_colorRandomization && _randomTable != nullptr);
340 }
341
setColorRandomization(bool randomize)342 void ColorScheme::setColorRandomization(bool randomize)
343 {
344 _colorRandomization = randomize;
345 if (randomize) {
346 bool hasAnyRandomizationEntries = false;
347 if (_randomTable != nullptr) {
348 for (int i = 0; !hasAnyRandomizationEntries && i < TABLE_COLORS; i++) {
349 hasAnyRandomizationEntries = !_randomTable[i].isNull();
350 }
351 }
352 // Set default randomization settings
353 if (!hasAnyRandomizationEntries) {
354 static const int ColorIndexesForRandomization[] = {
355 ColorFgIndex,
356 ColorBgIndex,
357 ColorFgIntenseIndex,
358 ColorBgIntenseIndex,
359 ColorFgFaintIndex,
360 ColorBgFaintIndex,
361 };
362 for (int index : ColorIndexesForRandomization) {
363 setRandomizationRange(index, MaxHue, MaxSaturation, 0.0);
364 }
365 }
366 }
367 }
368
setRandomizationRange(int index,double hue,double saturation,double lightness)369 void ColorScheme::setRandomizationRange(int index, double hue, double saturation, double lightness)
370 {
371 Q_ASSERT(hue <= MaxHue);
372 Q_ASSERT(index >= 0 && index < TABLE_COLORS);
373
374 if (_randomTable == nullptr) {
375 _randomTable = new RandomizationRange[TABLE_COLORS];
376 }
377
378 _randomTable[index].hue = hue;
379 _randomTable[index].saturation = saturation;
380 _randomTable[index].lightness = lightness;
381 }
382
colorTable() const383 const QColor *ColorScheme::colorTable() const
384 {
385 if (_table != nullptr) {
386 return _table;
387 }
388 return defaultTable;
389 }
390
foregroundColor() const391 QColor ColorScheme::foregroundColor() const
392 {
393 return colorTable()[FGCOLOR_INDEX];
394 }
395
backgroundColor() const396 QColor ColorScheme::backgroundColor() const
397 {
398 return colorTable()[BGCOLOR_INDEX];
399 }
400
hasDarkBackground() const401 bool ColorScheme::hasDarkBackground() const
402 {
403 double h;
404 double s;
405 double l;
406 const double r = backgroundColor().redF();
407 const double g = backgroundColor().greenF();
408 const double b = backgroundColor().blueF();
409 rgb2hsluv(r, g, b, &h, &s, &l);
410 return l < 50;
411 }
412
setOpacity(qreal opacity)413 void ColorScheme::setOpacity(qreal opacity)
414 {
415 if (opacity < 0.0 || opacity > 1.0) {
416 qCDebug(ColorSchemeDebug) << "ColorScheme" << name() << "has an invalid opacity" << opacity << "using 1";
417 opacity = 1.0;
418 }
419 _opacity = opacity;
420 }
421
opacity() const422 qreal ColorScheme::opacity() const
423 {
424 return _opacity;
425 }
426
setBlur(bool blur)427 void ColorScheme::setBlur(bool blur)
428 {
429 _blur = blur;
430 }
431
blur() const432 bool ColorScheme::blur() const
433 {
434 return _blur;
435 }
436
read(const KConfig & config)437 void ColorScheme::read(const KConfig &config)
438 {
439 KConfigGroup configGroup = config.group("General");
440
441 const QString schemeDescription = configGroup.readEntry("Description", i18nc("@item", "Un-named Color Scheme"));
442
443 _description = i18n(schemeDescription.toUtf8().constData());
444 setOpacity(configGroup.readEntry("Opacity", 1.0));
445 _blur = configGroup.readEntry("Blur", false);
446 setWallpaper(configGroup.readEntry("Wallpaper", QString()));
447 _colorRandomization = configGroup.readEntry(EnableColorRandomizationKey, false);
448
449 for (int i = 0; i < TABLE_COLORS; i++) {
450 readColorEntry(config, i);
451 }
452 }
453
readColorEntry(const KConfig & config,int index)454 void ColorScheme::readColorEntry(const KConfig &config, int index)
455 {
456 KConfigGroup configGroup = config.group(colorNameForIndex(index));
457
458 if (!configGroup.hasKey("Color") && _table != nullptr) {
459 setColorTableEntry(index, _table[index % BASE_COLORS]);
460 return;
461 }
462
463 QColor entry;
464
465 entry = configGroup.readEntry("Color", QColor());
466 setColorTableEntry(index, entry);
467
468 const auto readAndCheckConfigEntry = [&](const char *key, double min, double max) -> double {
469 const double value = configGroup.readEntry(key, min);
470 if (min > value || value > max) {
471 qCDebug(ColorSchemeDebug) << QStringLiteral(
472 "Color scheme \"%1\": color index 2 has an invalid value: %3 = %4. "
473 "Allowed value range: %5 - %6. Using %7.")
474 .arg(name())
475 .arg(index)
476 .arg(QLatin1String(key))
477 .arg(value, 0, 'g', 1)
478 .arg(min, 0, 'g', 1)
479 .arg(max, 0, 'g', 1)
480 .arg(min, 0, 'g', 1);
481 return min;
482 }
483 return value;
484 };
485
486 double hue = readAndCheckConfigEntry(RandomHueRangeKey, 0.0, MaxHue);
487 double saturation = readAndCheckConfigEntry(RandomSaturationRangeKey, 0.0, MaxSaturation);
488 double lightness = readAndCheckConfigEntry(RandomLightnessRangeKey, 0.0, MaxLightness);
489
490 if (!qFuzzyIsNull(hue) || !qFuzzyIsNull(saturation) || !qFuzzyIsNull(lightness)) {
491 setRandomizationRange(index, hue, saturation, lightness);
492 }
493 }
494
write(KConfig & config) const495 void ColorScheme::write(KConfig &config) const
496 {
497 KConfigGroup configGroup = config.group("General");
498
499 configGroup.writeEntry("Description", _description);
500 configGroup.writeEntry("Opacity", _opacity);
501 configGroup.writeEntry("Blur", _blur);
502 configGroup.writeEntry("Wallpaper", _wallpaper->path());
503 configGroup.writeEntry(EnableColorRandomizationKey, _colorRandomization);
504
505 for (int i = 0; i < TABLE_COLORS; i++) {
506 writeColorEntry(config, i);
507 }
508 }
509
writeColorEntry(KConfig & config,int index) const510 void ColorScheme::writeColorEntry(KConfig &config, int index) const
511 {
512 KConfigGroup configGroup = config.group(colorNameForIndex(index));
513
514 const QColor &entry = colorTable()[index];
515
516 configGroup.writeEntry("Color", entry);
517
518 // Remove unused keys
519 static const char *obsoleteKeys[] = {
520 "Transparent",
521 "Transparency",
522 "Bold",
523 // Uncomment when people stop using Konsole from 2019:
524 // "MaxRandomHue",
525 // "MaxRandomValue",
526 // "MaxRandomSaturation"
527 };
528 for (const auto key : obsoleteKeys) {
529 if (configGroup.hasKey(key)) {
530 configGroup.deleteEntry(key);
531 }
532 }
533
534 RandomizationRange random = _randomTable != nullptr ? _randomTable[index] : RandomizationRange();
535
536 const auto checkAndMaybeSaveValue = [&](const char *key, double value) {
537 const bool valueIsNull = qFuzzyCompare(value, 0.0);
538 const bool keyExists = configGroup.hasKey(key);
539 const bool keyExistsAndHasDifferentValue = !qFuzzyCompare(configGroup.readEntry(key, value), value);
540 if ((!valueIsNull && !keyExists) || keyExistsAndHasDifferentValue) {
541 configGroup.writeEntry(key, value);
542 }
543 };
544
545 checkAndMaybeSaveValue(RandomHueRangeKey, random.hue);
546 checkAndMaybeSaveValue(RandomSaturationRangeKey, random.saturation);
547 checkAndMaybeSaveValue(RandomLightnessRangeKey, random.lightness);
548 }
549
setWallpaper(const QString & path)550 void ColorScheme::setWallpaper(const QString &path)
551 {
552 _wallpaper = new ColorSchemeWallpaper(path);
553 }
554
wallpaper() const555 ColorSchemeWallpaper::Ptr ColorScheme::wallpaper() const
556 {
557 return _wallpaper;
558 }
559