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