File indexing completed on 2024-05-19 05:28:15

0001 /*
0002     This source file is part of Konsole, a terminal emulator.
0003 
0004     SPDX-FileCopyrightText: 2007-2008 Robert Knight <robertknight@gmail.com>
0005 
0006     SPDX-License-Identifier: GPL-2.0-or-later
0007 
0008     This program is distributed in the hope that it will be useful,
0009     but WITHOUT ANY WARRANTY; without even the implied warranty of
0010     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0011     GNU General Public License for more details.
0012 
0013     You should have received a copy of the GNU General Public License
0014     along with this program; if not, write to the Free Software
0015     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
0016     02110-1301  USA.
0017 */
0018 
0019 // Own
0020 #include "ColorScheme.h"
0021 #include "tools.h"
0022 
0023 // Qt
0024 #include <QBrush>
0025 #include <QDir>
0026 #include <QFile>
0027 #include <QFileInfo>
0028 #include <QRandomGenerator>
0029 #include <QRegularExpression>
0030 #include <QSettings>
0031 #include <QtDebug>
0032 
0033 // KDE
0034 // #include <KColorScheme>
0035 // #include <KConfig>
0036 // #include <KLocale>
0037 // #include <KDebug>
0038 // #include <KConfigGroup>
0039 // #include <KStandardDirs>
0040 
0041 using namespace Konsole;
0042 
0043 constexpr std::array<ColorEntry, TABLE_COLORS> DEFAULT_TABLE =
0044     // The following are almost IBM standard color codes, with some slight
0045     // gamma correction for the dim colors to compensate for bright X screens.
0046     // It contains the 8 ansiterm/xterm colors in 2 intensities.
0047     {ColorEntry(QColor(0x00, 0x00, 0x00), false),
0048      ColorEntry(QColor(0xFF, 0xFF, 0xFF), true), // Dfore, Dback
0049      ColorEntry(QColor(0x00, 0x00, 0x00), false),
0050      ColorEntry(QColor(0xB2, 0x18, 0x18), false), // Black, Red
0051      ColorEntry(QColor(0x18, 0xB2, 0x18), false),
0052      ColorEntry(QColor(0xB2, 0x68, 0x18), false), // Green, Yellow
0053      ColorEntry(QColor(0x18, 0x18, 0xB2), false),
0054      ColorEntry(QColor(0xB2, 0x18, 0xB2), false), // Blue, Magenta
0055      ColorEntry(QColor(0x18, 0xB2, 0xB2), false),
0056      ColorEntry(QColor(0xB2, 0xB2, 0xB2), false), // Cyan, White
0057      // intensive
0058      ColorEntry(QColor(0x00, 0x00, 0x00), false),
0059      ColorEntry(QColor(0xFF, 0xFF, 0xFF), true),
0060      ColorEntry(QColor(0x68, 0x68, 0x68), false),
0061      ColorEntry(QColor(0xFF, 0x54, 0x54), false),
0062      ColorEntry(QColor(0x54, 0xFF, 0x54), false),
0063      ColorEntry(QColor(0xFF, 0xFF, 0x54), false),
0064      ColorEntry(QColor(0x54, 0x54, 0xFF), false),
0065      ColorEntry(QColor(0xFF, 0x54, 0xFF), false),
0066      ColorEntry(QColor(0x54, 0xFF, 0xFF), false),
0067      ColorEntry(QColor(0xFF, 0xFF, 0xFF), false)};
0068 
0069 std::array<ColorEntry, TABLE_COLORS> Konsole::defaultColorTable()
0070 {
0071     return DEFAULT_TABLE;
0072 }
0073 
0074 constexpr std::array<QStringView, TABLE_COLORS> COLOR_NAMES = {u"Foreground",
0075                                                                u"Background",
0076                                                                u"Color0",
0077                                                                u"Color1",
0078                                                                u"Color2",
0079                                                                u"Color3",
0080                                                                u"Color4",
0081                                                                u"Color5",
0082                                                                u"Color6",
0083                                                                u"Color7",
0084                                                                u"ForegroundIntense",
0085                                                                u"BackgroundIntense",
0086                                                                u"Color0Intense",
0087                                                                u"Color1Intense",
0088                                                                u"Color2Intense",
0089                                                                u"Color3Intense",
0090                                                                u"Color4Intense",
0091                                                                u"Color5Intense",
0092                                                                u"Color6Intense",
0093                                                                u"Color7Intense"};
0094 
0095 std::array<const char *, TABLE_COLORS> TRANSLATED_COLOR_NAMES = {QT_TR_NOOP("Foreground"),
0096                                                                  QT_TR_NOOP("Background"),
0097                                                                  QT_TR_NOOP("Color 1"),
0098                                                                  QT_TR_NOOP("Color 2"),
0099                                                                  QT_TR_NOOP("Color 3"),
0100                                                                  QT_TR_NOOP("Color 4"),
0101                                                                  QT_TR_NOOP("Color 5"),
0102                                                                  QT_TR_NOOP("Color 6"),
0103                                                                  QT_TR_NOOP("Color 7"),
0104                                                                  QT_TR_NOOP("Color 8"),
0105                                                                  QT_TR_NOOP("Foreground (Intense)"),
0106                                                                  QT_TR_NOOP("Background (Intense)"),
0107                                                                  QT_TR_NOOP("Color 1 (Intense)"),
0108                                                                  QT_TR_NOOP("Color 2 (Intense)"),
0109                                                                  QT_TR_NOOP("Color 3 (Intense)"),
0110                                                                  QT_TR_NOOP("Color 4 (Intense)"),
0111                                                                  QT_TR_NOOP("Color 5 (Intense)"),
0112                                                                  QT_TR_NOOP("Color 6 (Intense)"),
0113                                                                  QT_TR_NOOP("Color 7 (Intense)"),
0114                                                                  QT_TR_NOOP("Color 8 (Intense)")};
0115 
0116 ColorScheme::ColorScheme()
0117 {
0118     _table = {};
0119     _randomTable = {};
0120     _opacity = 1.0;
0121 }
0122 
0123 ColorScheme::ColorScheme(const ColorScheme &other)
0124     : _opacity(other._opacity)
0125 {
0126     setName(other.name());
0127     setDescription(other.description());
0128 
0129     _table = other._table;
0130     _randomTable = other._randomTable;
0131 }
0132 ColorScheme::~ColorScheme()
0133 {
0134 }
0135 
0136 void ColorScheme::setDescription(const QString &description)
0137 {
0138     _description = description;
0139 }
0140 QString ColorScheme::description() const
0141 {
0142     return _description;
0143 }
0144 
0145 void ColorScheme::setName(const QString &name)
0146 {
0147     _name = name;
0148 }
0149 QString ColorScheme::name() const
0150 {
0151     return _name;
0152 }
0153 
0154 void ColorScheme::setColorTableEntry(int index, const ColorEntry &entry)
0155 {
0156     Q_ASSERT(index >= 0 && index < TABLE_COLORS);
0157 
0158     if (!_table) {
0159         _table = std::vector<ColorEntry>(DEFAULT_TABLE.begin(), DEFAULT_TABLE.end());
0160     }
0161 
0162     // we always have a value set at this point, see above
0163     _table.value()[index] = entry;
0164 }
0165 ColorEntry ColorScheme::colorEntry(int index, uint randomSeed) const
0166 {
0167     Q_ASSERT(index >= 0 && index < TABLE_COLORS);
0168 
0169     ColorEntry entry = colorTable()[index];
0170 
0171     if (randomSeed != 0 && _randomTable.has_value() && !_randomTable->data()[index].isNull()) {
0172         const RandomizationRange &range = _randomTable->data()[index];
0173 
0174         int hueDifference = range.hue ? QRandomGenerator::system()->bounded(range.hue) - range.hue / 2 : 0;
0175         int saturationDifference = range.saturation ? (QRandomGenerator::system()->bounded(range.saturation)) - range.saturation / 2 : 0;
0176         int valueDifference = range.value ? (QRandomGenerator::system()->bounded(range.value)) - range.value / 2 : 0;
0177 
0178         QColor &color = entry.color;
0179 
0180         int newHue = qAbs((color.hue() + hueDifference) % MAX_HUE);
0181         int newValue = qMin(qAbs(color.value() + valueDifference), 255);
0182         int newSaturation = qMin(qAbs(color.saturation() + saturationDifference), 255);
0183 
0184         color.setHsv(newHue, newSaturation, newValue);
0185     }
0186 
0187     return entry;
0188 }
0189 
0190 std::array<ColorEntry, TABLE_COLORS> ColorScheme::getColorTable(uint randomSeed) const
0191 {
0192     std::array<ColorEntry, TABLE_COLORS> table;
0193     for (int i = 0; i < TABLE_COLORS; i++)
0194         table[i] = colorEntry(i, randomSeed);
0195 
0196     return table;
0197 }
0198 
0199 bool ColorScheme::randomizedBackgroundColor() const
0200 {
0201     return _randomTable.has_value() ? !_randomTable->data()[1].isNull() : false;
0202 }
0203 
0204 void ColorScheme::setRandomizedBackgroundColor(bool randomize)
0205 {
0206     // the hue of the background colour is allowed to be randomly
0207     // adjusted as much as possible.
0208     //
0209     // the value and saturation are left alone to maintain read-ability
0210     if (randomize) {
0211         setRandomizationRange(1 /* background color index */, MAX_HUE, 255, 0);
0212     } else {
0213         if (_randomTable)
0214             setRandomizationRange(1 /* background color index */, 0, 0, 0);
0215     }
0216 }
0217 
0218 void ColorScheme::setRandomizationRange(int index, quint16 hue, quint8 saturation, quint8 value)
0219 {
0220     Q_ASSERT(hue <= MAX_HUE);
0221     Q_ASSERT(index >= 0 && index < TABLE_COLORS);
0222 
0223     if (!_randomTable)
0224         _randomTable = std::vector<RandomizationRange>(TABLE_COLORS);
0225 
0226     _randomTable->data()[index].hue = hue;
0227     _randomTable->data()[index].value = value;
0228     _randomTable->data()[index].saturation = saturation;
0229 }
0230 
0231 const std::span<const ColorEntry> ColorScheme::colorTable() const
0232 {
0233     if (_table.has_value())
0234         return *_table;
0235     else
0236         return std::span(DEFAULT_TABLE);
0237 }
0238 
0239 QColor ColorScheme::foregroundColor() const
0240 {
0241     return colorTable()[0].color;
0242 }
0243 
0244 QColor ColorScheme::backgroundColor() const
0245 {
0246     return colorTable()[1].color;
0247 }
0248 
0249 bool ColorScheme::hasDarkBackground() const
0250 {
0251     // value can range from 0 - 255, with larger values indicating higher brightness.
0252     // so 127 is in the middle, anything less is deemed 'dark'
0253     return backgroundColor().value() < 127;
0254 }
0255 void ColorScheme::setOpacity(qreal opacity)
0256 {
0257     _opacity = opacity;
0258 }
0259 qreal ColorScheme::opacity() const
0260 {
0261     return _opacity;
0262 }
0263 
0264 void ColorScheme::read(const QString &fileName)
0265 {
0266     QSettings s(fileName, QSettings::IniFormat);
0267     s.beginGroup(QLatin1String("General"));
0268 
0269     _description = s.value(QLatin1String("Description"), QObject::tr("Un-named Color Scheme")).toString();
0270     _opacity = s.value(QLatin1String("Opacity"), qreal(1.0)).toDouble();
0271     s.endGroup();
0272 
0273     for (int i = 0; i < TABLE_COLORS; i++) {
0274         readColorEntry(&s, i);
0275     }
0276 }
0277 
0278 QString ColorScheme::colorNameForIndex(int index)
0279 {
0280     Q_ASSERT(index >= 0 && index < TABLE_COLORS);
0281 
0282     return COLOR_NAMES.at(index).toString();
0283 }
0284 QString ColorScheme::translatedColorNameForIndex(int index)
0285 {
0286     Q_ASSERT(index >= 0 && index < TABLE_COLORS);
0287 
0288     return QObject::tr(TRANSLATED_COLOR_NAMES.at(index));
0289 }
0290 
0291 void ColorScheme::readColorEntry(QSettings *s, int index)
0292 {
0293     QString colorName = colorNameForIndex(index);
0294 
0295     s->beginGroup(colorName);
0296 
0297     ColorEntry entry;
0298 
0299     QVariant colorValue = s->value(QLatin1String("Color"));
0300     QString colorStr;
0301     int r, g, b;
0302     bool ok = false;
0303     // XXX: Undocumented(?) QSettings behavior: values with commas are parsed
0304     // as QStringList and others QString
0305 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0306     if (colorValue.type() == QVariant::StringList)
0307 #else
0308     if (colorValue.metaType().id() == QMetaType::QStringList)
0309 #endif
0310     {
0311         QStringList rgbList = colorValue.toStringList();
0312         colorStr = rgbList.join(QLatin1Char(','));
0313         if (rgbList.size() == 3) {
0314             bool parse_ok;
0315 
0316             ok = true;
0317             r = rgbList[0].toInt(&parse_ok);
0318             ok = ok && parse_ok && (r >= 0 && r <= 0xff);
0319             g = rgbList[1].toInt(&parse_ok);
0320             ok = ok && parse_ok && (g >= 0 && g <= 0xff);
0321             b = rgbList[2].toInt(&parse_ok);
0322             ok = ok && parse_ok && (b >= 0 && b <= 0xff);
0323         }
0324     } else {
0325         colorStr = colorValue.toString();
0326         QRegularExpression hexColorPattern(QLatin1String("^#[0-9a-f]{6}$"), QRegularExpression::CaseInsensitiveOption);
0327         if (hexColorPattern.match(colorStr).hasMatch()) {
0328             // Parsing is always ok as already matched by the regexp
0329             r = QStringView(colorStr).mid(1, 2).toInt(nullptr, 16);
0330             g = QStringView(colorStr).mid(3, 2).toInt(nullptr, 16);
0331             b = QStringView(colorStr).mid(5, 2).toInt(nullptr, 16);
0332             ok = true;
0333         }
0334     }
0335     if (!ok) {
0336         qWarning().nospace() << "Invalid color value " << colorStr << " for " << colorName << ". Fallback to black.";
0337         r = g = b = 0;
0338     }
0339     entry.color = QColor(r, g, b);
0340 
0341     entry.transparent = s->value(QLatin1String("Transparent"), false).toBool();
0342 
0343     // Deprecated key from KDE 4.0 which set 'Bold' to true to force
0344     // a color to be bold or false to use the current format
0345     //
0346     // TODO - Add a new tri-state key which allows for bold, normal or
0347     // current format
0348     if (s->contains(QLatin1String("Bold")))
0349         entry.fontWeight = s->value(QLatin1String("Bold"), false).toBool() ? ColorEntry::Bold : ColorEntry::UseCurrentFormat;
0350 
0351     quint16 hue = s->value(QLatin1String("MaxRandomHue"), 0).toInt();
0352     quint8 value = s->value(QLatin1String("MaxRandomValue"), 0).toInt();
0353     quint8 saturation = s->value(QLatin1String("MaxRandomSaturation"), 0).toInt();
0354 
0355     setColorTableEntry(index, entry);
0356 
0357     if (hue != 0 || value != 0 || saturation != 0)
0358         setRandomizationRange(index, hue, saturation, value);
0359 
0360     s->endGroup();
0361 }
0362 
0363 //
0364 // Work In Progress - A color scheme for use on KDE setups for users
0365 // with visual disabilities which means that they may have trouble
0366 // reading text with the supplied color schemes.
0367 //
0368 // This color scheme uses only the 'safe' colors defined by the
0369 // KColorScheme class.
0370 //
0371 // A complication this introduces is that each color provided by
0372 // KColorScheme is defined as a 'background' or 'foreground' color.
0373 // Only foreground colors are allowed to be used to render text and
0374 // only background colors are allowed to be used for backgrounds.
0375 //
0376 // The ColorEntry and TerminalDisplay classes do not currently
0377 // support this restriction.
0378 //
0379 // Requirements:
0380 //  - A color scheme which uses only colors from the KColorScheme class
0381 //  - Ability to restrict which colors the TerminalDisplay widget
0382 //    uses as foreground and background color
0383 //  - Make use of KGlobalSettings::allowDefaultBackgroundImages() as
0384 //    a hint to determine whether this accessible color scheme should
0385 //    be used by default.
0386 //
0387 //
0388 // -- Robert Knight <robertknight@gmail.com> 21/07/2007
0389 //
0390 AccessibleColorScheme::AccessibleColorScheme()
0391     : ColorScheme()
0392 {
0393 #if 0
0394 // It's not finished in konsole and it breaks Qt4 compilation as well
0395     // basic attributes
0396     setName("accessible");
0397     setDescription(QObject::tr("Accessible Color Scheme"));
0398 
0399     // setup colors
0400     const int ColorRoleCount = 8;
0401 
0402     const KColorScheme colorScheme(QPalette::Active);
0403 
0404     QBrush colors[ColorRoleCount] =
0405     {
0406         colorScheme.foreground( colorScheme.NormalText ),
0407         colorScheme.background( colorScheme.NormalBackground ),
0408 
0409         colorScheme.foreground( colorScheme.InactiveText ),
0410         colorScheme.foreground( colorScheme.ActiveText ),
0411         colorScheme.foreground( colorScheme.LinkText ),
0412         colorScheme.foreground( colorScheme.VisitedText ),
0413         colorScheme.foreground( colorScheme.NegativeText ),
0414         colorScheme.foreground( colorScheme.NeutralText )
0415     };
0416 
0417     for ( int i = 0 ; i < TABLE_COLORS ; i++ )
0418     {
0419         ColorEntry entry;
0420         entry.color = colors[ i % ColorRoleCount ].color();
0421 
0422         setColorTableEntry( i , entry );
0423     }
0424 #endif
0425 }
0426 
0427 ColorSchemeManager::ColorSchemeManager()
0428     : _haveLoadedAll(false)
0429 {
0430 }
0431 
0432 ColorSchemeManager::~ColorSchemeManager() = default;
0433 
0434 void ColorSchemeManager::loadAllColorSchemes()
0435 {
0436     // qDebug() << "loadAllColorSchemes";
0437     int failed = 0;
0438 
0439     QList<QString> nativeColorSchemes = listColorSchemes();
0440     QListIterator<QString> nativeIter(nativeColorSchemes);
0441     while (nativeIter.hasNext()) {
0442         if (!loadColorScheme(nativeIter.next()))
0443             failed++;
0444     }
0445 
0446     /*if ( failed > 0 )
0447         qDebug() << "failed to load " << failed << " color schemes.";*/
0448 
0449     _haveLoadedAll = true;
0450 }
0451 QList<ColorScheme *> ColorSchemeManager::allColorSchemes()
0452 {
0453     if (!_haveLoadedAll) {
0454         loadAllColorSchemes();
0455     }
0456 
0457     QList<ColorScheme *> schemes;
0458     for (const auto &[k, scheme] : _colorSchemes) {
0459         schemes.push_back(scheme.get());
0460     }
0461     return schemes;
0462 }
0463 
0464 #if 0
0465 void ColorSchemeManager::addColorScheme(ColorScheme* scheme)
0466 {
0467     _colorSchemes.insert(scheme->name(),scheme);
0468 
0469     // save changes to disk
0470     QString path = KGlobal::dirs()->saveLocation("data","konsole/") + scheme->name() + ".colorscheme";
0471     KConfig config(path , KConfig::NoGlobals);
0472 
0473     scheme->write(config);
0474 }
0475 #endif
0476 
0477 bool ColorSchemeManager::loadCustomColorScheme(const QString &path)
0478 {
0479     if (path.endsWith(QLatin1String(".colorscheme")))
0480         return loadColorScheme(path);
0481 
0482     return false;
0483 }
0484 
0485 void ColorSchemeManager::addCustomColorSchemeDir(const QString &custom_dir)
0486 {
0487     add_custom_color_scheme_dir(custom_dir);
0488 }
0489 
0490 bool ColorSchemeManager::loadColorScheme(const QString &filePath)
0491 {
0492     if (!filePath.endsWith(QLatin1String(".colorscheme")) || !QFile::exists(filePath))
0493         return false;
0494 
0495     QFileInfo info(filePath);
0496 
0497     const QString &schemeName = info.baseName();
0498 
0499     auto scheme = std::make_unique<ColorScheme>();
0500     scheme->setName(schemeName);
0501     scheme->read(filePath);
0502 
0503     if (scheme->name().isEmpty()) {
0504         // qDebug() << "Color scheme in" << filePath << "does not have a valid name and was not loaded.";
0505         scheme.reset();
0506         return false;
0507     }
0508 
0509     if (!_colorSchemes.contains(schemeName)) {
0510         _colorSchemes.insert_or_assign(schemeName, std::move(scheme));
0511     } else {
0512         /*qDebug() << "color scheme with name" << schemeName << "has already been" <<
0513             "found, ignoring.";*/
0514 
0515         scheme.reset();
0516     }
0517 
0518     return true;
0519 }
0520 QList<QString> ColorSchemeManager::listColorSchemes()
0521 {
0522     QList<QString> ret;
0523     for (const QString &scheme_dir : colorSchemesDirs()) {
0524         const QString dname(scheme_dir);
0525         QDir dir(dname);
0526         QStringList filters;
0527         filters << QLatin1String("*.colorscheme");
0528         dir.setNameFilters(filters);
0529         const QStringList list = dir.entryList(filters);
0530         for (const QString &i : list)
0531             ret << dname + QLatin1Char('/') + i;
0532     }
0533     return ret;
0534     //    return KGlobal::dirs()->findAllResources("data",
0535     //                                             "konsole/*.colorscheme",
0536     //                                             KStandardDirs::NoDuplicates);
0537 }
0538 const ColorScheme ColorSchemeManager::_defaultColorScheme;
0539 const ColorScheme *ColorSchemeManager::defaultColorScheme() const
0540 {
0541     return &_defaultColorScheme;
0542 }
0543 bool ColorSchemeManager::deleteColorScheme(const QString &name)
0544 {
0545     Q_ASSERT(_colorSchemes.contains(name));
0546 
0547     // lookup the path and delete
0548     QString path = findColorSchemePath(name);
0549     if (QFile::remove(path)) {
0550         _colorSchemes.erase(name);
0551         return true;
0552     } else {
0553         // qDebug() << "Failed to remove color scheme -" << path;
0554         return false;
0555     }
0556 }
0557 QString ColorSchemeManager::findColorSchemePath(const QString &name) const
0558 {
0559     //    QString path = KStandardDirs::locate("data","konsole/"+name+".colorscheme");
0560     const QStringList dirs = colorSchemesDirs();
0561     if (dirs.isEmpty())
0562         return QString();
0563 
0564     const QString dir = dirs.first();
0565     QString path(dir + QLatin1Char('/') + name + QLatin1String(".colorscheme"));
0566     if (!path.isEmpty())
0567         return path;
0568 
0569     // path = KStandardDirs::locate("data","konsole/"+name+".schema");
0570     path = dir + QLatin1Char('/') + name + QLatin1String(".schema");
0571 
0572     return path;
0573 }
0574 const ColorScheme *ColorSchemeManager::findColorScheme(const QString &name)
0575 {
0576     if (name.isEmpty())
0577         return defaultColorScheme();
0578 
0579     if (_colorSchemes.contains(name))
0580         return _colorSchemes[name].get();
0581     else {
0582         // look for this color scheme
0583         QString path = findColorSchemePath(name);
0584         if (!path.isEmpty() && loadColorScheme(path)) {
0585             return findColorScheme(name);
0586         }
0587 
0588         // qDebug() << "Could not find color scheme - " << name;
0589 
0590         return nullptr;
0591     }
0592 }
0593 Q_GLOBAL_STATIC(ColorSchemeManager, theColorSchemeManager)
0594 ColorSchemeManager *ColorSchemeManager::instance()
0595 {
0596     return theColorSchemeManager;
0597 }