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 }