File indexing completed on 2024-05-05 04:38:47

0001 /*
0002     SPDX-FileCopyrightText: 2015 Kevin Funk <kfunk@kde.org>
0003 
0004     SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005 */
0006 
0007 #include "widgetcolorizer.h"
0008 
0009 #include <KColorUtils>
0010 #include <KSharedConfig>
0011 #include <KConfigGroup>
0012 
0013 #include <QColor>
0014 #include <QPainter>
0015 #include <QPalette>
0016 #include <QTreeView>
0017 #include <QTextDocument>
0018 #include <QApplication>
0019 #include <QTextCharFormat>
0020 #include <QTextFrame>
0021 
0022 #include <optional>
0023 
0024 using namespace KDevelop;
0025 
0026 QColor WidgetColorizer::blendForeground(QColor color, float ratio,
0027                                         const QColor& foreground, const QColor& background)
0028 {
0029     if (KColorUtils::luma(foreground) > KColorUtils::luma(background)) {
0030         // for dark color schemes, produce a fitting color first
0031         color = KColorUtils::tint(foreground, color, 0.5);
0032     }
0033     // adapt contrast
0034     return KColorUtils::mix(foreground, color, ratio);
0035 }
0036 
0037 QColor WidgetColorizer::blendBackground(const QColor& color, float ratio,
0038                                         const QColor& /*foreground*/, const QColor& background)
0039 {
0040     // adapt contrast
0041     return KColorUtils::mix(background, color, ratio);
0042 }
0043 
0044 void WidgetColorizer::drawBranches(const QTreeView* treeView, QPainter* painter,
0045                                    const QRect& rect, const QModelIndex& /*index*/,
0046                                    const QColor& baseColor)
0047 {
0048     QRect newRect(rect);
0049     newRect.setWidth(treeView->indentation());
0050     painter->fillRect(newRect, baseColor);
0051 }
0052 
0053 QColor WidgetColorizer::colorForId(uint id, const QPalette& activePalette, bool forBackground)
0054 {
0055     const int high = 255;
0056     const int low = 100;
0057     auto color = QColor(qAbs(id % (high - low)),
0058                         qAbs((id / 50) % (high - low)),
0059                         qAbs((id / (50 * 50)) % (high - low)));
0060     const auto& foreground = activePalette.windowText().color();
0061     const auto& background = activePalette.window().color();
0062     if (forBackground) {
0063         return blendBackground(color, .5, foreground, background);
0064     } else {
0065         return blendForeground(color, .5, foreground, background);
0066     }
0067 }
0068 
0069 bool WidgetColorizer::colorizeByProject()
0070 {
0071     return KSharedConfig::openConfig()->group("UiSettings").readEntry("ColorizeByProject", true);
0072 }
0073 
0074 namespace
0075 {
0076 struct FormatRange {
0077     int start = 0;
0078     int end = 0;
0079     QTextCharFormat format;
0080 };
0081 
0082 QColor invertColor(const QColor& color)
0083 {
0084     auto hue = color.hsvHue();
0085     if (hue == -1) {
0086         // achromatic color
0087         hue = 0;
0088     }
0089     return QColor::fromHsv(hue, color.hsvSaturation(), 255 - color.value());
0090 }
0091 
0092 std::optional<QColor> foregroundColor(const QTextFormat& format)
0093 {
0094     if (!format.hasProperty(QTextFormat::ForegroundBrush))
0095         return std::nullopt;
0096     return format.foreground().color();
0097 }
0098 
0099 std::optional<QColor> backgroundColor(const QTextFormat& format)
0100 {
0101     if (!format.hasProperty(QTextFormat::BackgroundBrush))
0102         return std::nullopt;
0103     return format.background().color();
0104 }
0105 
0106 /**
0107  * Inverting is used for white colors, because it is assumed white in light color scheme
0108  * should be black in dark color scheme. White inverted will give you black, but blending
0109  * would give you a grey color depending on the ratio. Above 0.5 (middle) will ensure all
0110  * whites are inverted in blacks (below 0.5) and exactly 0.5 can stay the same.
0111  * The 0.08 saturation covered all the white tones found in the color schemes tested.
0112  */
0113 bool canInvertBrightColor(const QColor& color)
0114 {
0115     // this check here determines if the color can be considered close to white
0116     return color.valueF() > 0.5 && color.hsvSaturationF() < 0.08;
0117 }
0118 
0119 /**
0120  * Blending is used for non white (colorful?) colors to increase contrast (get a brighter color).
0121  * Inverting is not possible for non white/black colors and would just create a different color
0122  * not guaranteed to be brighter.
0123  */
0124 bool canBlendForegroundColor(const QColor& color)
0125 {
0126     // a foreground color with other hsv values will give bad contrast against a dark background
0127     return color.valueF() < 0.7;
0128 }
0129 
0130 bool isBrightBackgroundColor(const QColor& color)
0131 {
0132     // NOTE that foreground contrast and background contrast work differently
0133     return color.valueF() > 0.3;
0134 }
0135 
0136 bool canInvertDarkColor(const QColor& color)
0137 {
0138     return !isBrightBackgroundColor(color);
0139 }
0140 
0141 void collectRanges(QTextFrame* frame, const QColor& fgcolor, const QColor& bgcolor, bool bgSet,
0142                    std::vector<FormatRange>& ranges)
0143 {
0144     for (auto it = frame->begin(); it != frame->end(); ++it) {
0145         if (auto frame = it.currentFrame()) {
0146             auto fmt = it.currentFrame()->frameFormat();
0147             if (auto background = backgroundColor(fmt)) {
0148                 collectRanges(frame, fgcolor, *background, true, ranges);
0149             } else {
0150                 collectRanges(frame, fgcolor, bgcolor, bgSet, ranges);
0151             }
0152         }
0153 
0154         const auto block = it.currentBlock();
0155         if (!block.isValid()) {
0156             continue;
0157         }
0158 
0159         for (auto jt = block.begin(); jt != block.end(); ++jt) {
0160             auto fragment = jt.fragment();
0161             auto text = QStringView(fragment.text()).trimmed();
0162             if (!text.isEmpty()) {
0163                 auto fmt = fragment.charFormat();
0164                 auto foreground = foregroundColor(fmt);
0165                 auto background = backgroundColor(fmt);
0166 
0167                 if (!bgSet && !background) {
0168                     if (!foreground || foreground == Qt::black) {
0169                         fmt.setForeground(fgcolor);
0170                     } else if (canInvertDarkColor(*foreground)) {
0171                         fmt.setForeground(invertColor(*foreground));
0172                     } else if (canBlendForegroundColor(*foreground)) {
0173                         fmt.setForeground(WidgetColorizer::blendForeground(*foreground, 1.0, fgcolor, bgcolor));
0174                     }
0175                 } else {
0176                     auto bg = background.value_or(bgcolor);
0177                     auto fg = foreground.value_or(fgcolor);
0178                     if (background && canInvertBrightColor(bg)) {
0179                         bg = invertColor(bg);
0180                         fmt.setBackground(bg);
0181                         if (canInvertDarkColor(fg)) {
0182                             fmt.setForeground(invertColor(fg));
0183                         } else if (canBlendForegroundColor(fg)) {
0184                             fmt.setForeground(WidgetColorizer::blendForeground(fg, 1.0, fgcolor, bg));
0185                         }
0186                     } else if (isBrightBackgroundColor(bg) && canInvertBrightColor(fg)) {
0187                         fg = invertColor(fg);
0188                         fmt.setForeground(fg);
0189                     }
0190                 }
0191                 ranges.push_back({fragment.position(), fragment.position() + fragment.length(), fmt});
0192             }
0193         }
0194     }
0195 };
0196 }
0197 
0198 // see also: https://invent.kde.org/kdevelop/kdevelop/-/merge_requests/370#note_487717
0199 void WidgetColorizer::convertDocumentToDarkTheme(QTextDocument* doc)
0200 {
0201     const auto palette = QApplication::palette();
0202     const auto bgcolor = palette.color(QPalette::Base);
0203     const auto fgcolor = palette.color(QPalette::Text);
0204 
0205     if (fgcolor.value() < bgcolor.value())
0206         return;
0207 
0208     std::vector<FormatRange> ranges;
0209     collectRanges(doc->rootFrame(), fgcolor, bgcolor, false, ranges);
0210 
0211     auto cur = QTextCursor(doc);
0212     cur.beginEditBlock();
0213     for (const auto& [start, end, format] : ranges) {
0214         cur.setPosition(start);
0215         cur.setPosition(end, QTextCursor::KeepAnchor);
0216         cur.setCharFormat(format);
0217     }
0218     cur.endEditBlock();
0219 }