File indexing completed on 2024-05-19 05:42:06

0001 // ct_lvtgclr_colormanagement.cpp                                    -*-C++-*-
0002 
0003 /*
0004 // Copyright 2023 Codethink Ltd <codethink@codethink.co.uk>
0005 // SPDX-License-Identifier: Apache-2.0
0006 //
0007 // Licensed under the Apache License, Version 2.0 (the "License");
0008 // you may not use this file except in compliance with the License.
0009 // You may obtain a copy of the License at
0010 //
0011 //     http://www.apache.org/licenses/LICENSE-2.0
0012 //
0013 // Unless required by applicable law or agreed to in writing, software
0014 // distributed under the License is distributed on an "AS IS" BASIS,
0015 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
0016 // See the License for the specific language governing permissions and
0017 // limitations under the License.
0018 */
0019 
0020 #include <ct_lvtclr_colormanagement.h>
0021 
0022 #include <QCryptographicHash>
0023 
0024 #include <QDebug>
0025 #include <preferences.h>
0026 
0027 #include <unordered_map>
0028 #include <vector>
0029 
0030 // clazy:excludeall=qcolor-from-literal
0031 
0032 namespace Codethink::lvtclr {
0033 
0034 struct ColorManagement::Private {
0035     QColor baseColor;
0036     std::unordered_map<std::string, QColor> colorMap;
0037     std::unordered_map<std::string, QColor> userDefinedColorMap;
0038 
0039     std::unordered_map<std::string, Qt::BrushStyle> fillMap;
0040 
0041     /* 'v used the data here: http://mkweb.bcgsc.ca/colorblind/ and did
0042      * a few iterations of software that
0043      * can simulate color blindness over kwin.
0044      * - https://www.qt.io/blog/2017/09/15/testing-applications-color-blindness
0045      */
0046     const std::vector<QColor> colorBlindPalette = {
0047         "#E8384F", // Red
0048         "#FD817D", // orange
0049         "#FDAE33", // yellow-orange
0050         "#EECC16", // yellow
0051         "#A4C61A", // yellow-green
0052         "#62BB35", // "green"
0053         "#37A862", // "blue-green"
0054         "#8D9F9B", // "cool-gray"
0055         "#208EA3", // "aqua"
0056         "#4178BC", // "blue"
0057         "#7A71F6", // "indigo"
0058         "#AA71FF", // "purple"
0059         "#E37CFF", // "magenta"
0060         "#EA4E90", // "hot-pink"
0061         "#FCA7E4" // "pink"
0062     };
0063 
0064     const std::vector<Qt::BrushStyle> brushPatterns = {Qt::Dense1Pattern,
0065                                                        Qt::Dense2Pattern,
0066                                                        Qt::Dense3Pattern,
0067                                                        Qt::Dense4Pattern,
0068                                                        Qt::Dense5Pattern,
0069                                                        Qt::Dense6Pattern,
0070                                                        Qt::Dense7Pattern,
0071                                                        Qt::HorPattern,
0072                                                        Qt::VerPattern,
0073                                                        Qt::CrossPattern,
0074                                                        Qt::BDiagPattern,
0075                                                        Qt::FDiagPattern,
0076                                                        Qt::DiagCrossPattern};
0077 };
0078 
0079 ColorManagement::ColorManagement(bool doConnectSignals): d(std::make_unique<ColorManagement::Private>())
0080 {
0081     // Start the ColorManagement with a white base color.
0082     d->baseColor = QColor(255, 255, 255);
0083 
0084     if (doConnectSignals) {
0085         connect(Preferences::self(), &Preferences::useColorBlindFillChanged, this, &ColorManagement::colorBlindChanged);
0086         connect(Preferences::self(), &Preferences::colorBlindModeChanged, this, &ColorManagement::colorBlindChanged);
0087     }
0088 }
0089 
0090 ColorManagement::~ColorManagement() = default;
0091 
0092 void ColorManagement::setBaseColor(const QColor& color)
0093 {
0094     d->baseColor = color;
0095     resetCaches();
0096 }
0097 
0098 void ColorManagement::colorBlindChanged()
0099 {
0100     resetCaches();
0101     Q_EMIT requestNewColor();
0102 }
0103 
0104 Qt::BrushStyle ColorManagement::fillPattern(const std::string& id)
0105 {
0106     if (!isUsingColorBlindFill()) {
0107         return Qt::BrushStyle::SolidPattern;
0108     }
0109 
0110     auto it = d->fillMap.find(id);
0111     if (it != d->fillMap.end()) {
0112         return it->second;
0113     }
0114 
0115     static size_t currentPattern = 0;
0116     Qt::BrushStyle retValue = d->brushPatterns[currentPattern];
0117     currentPattern = (currentPattern + 1) % d->brushPatterns.size();
0118     d->fillMap[id] = retValue;
0119 
0120     return retValue;
0121 }
0122 
0123 QColor ColorManagement::getColorFor(const std::string& id)
0124 {
0125     // User defined colors are always preferred
0126     if (d->userDefinedColorMap.find(id) != d->userDefinedColorMap.end()) {
0127         return d->userDefinedColorMap[id];
0128     }
0129 
0130     // If used doesn't have any preferences, generate
0131     if (d->colorMap.find(id) == d->colorMap.end()) {
0132         // Could not find cached color. Calculate a new one.
0133         if (isColorBlindModeActive()) {
0134             d->colorMap[id] = generateColorblindColor();
0135         } else {
0136             d->colorMap[id] = generateUniqueColor(id);
0137         }
0138     }
0139     return d->colorMap[id];
0140 }
0141 
0142 void ColorManagement::setColorFor(const std::string& id, const QColor& color)
0143 {
0144     d->userDefinedColorMap[id] = color;
0145 }
0146 
0147 QColor ColorManagement::generateUniqueColor(const std::string& id) const
0148 {
0149     // given the fact that a single byte will change completely
0150     // the results of a md5 hash, we can use that to use the first
0151     // three letters as unsigned chars (so, range 0 - 255) to get the
0152     // color value for red, green and blue. This removes the randomness
0153     // of the colors on the view.
0154     auto md5Algorithm = QCryptographicHash(QCryptographicHash::Algorithm::Md5);
0155     md5Algorithm.addData(QString::fromStdString(id + "salt").toLocal8Bit());
0156     auto md5 = md5Algorithm.result().toStdString();
0157 
0158     int red = (unsigned char) md5[0];
0159     int green = (unsigned char) md5[1];
0160     int blue = (unsigned char) md5[2];
0161 
0162     red = (red + d->baseColor.red()) / 2;
0163     green = (green + d->baseColor.green()) / 2;
0164     blue = (blue + d->baseColor.blue()) / 2;
0165 
0166     return {red, green, blue};
0167 }
0168 
0169 QColor ColorManagement::generateColorblindColor() const
0170 {
0171     int idx = 0;
0172     bool found = false;
0173     bool brigthen = false;
0174     bool darken = false;
0175 
0176     QColor currColor;
0177     do {
0178         currColor = d->colorBlindPalette[idx];
0179         if (brigthen) {
0180             currColor = currColor.lighter();
0181         } else if (darken) {
0182             currColor = currColor.darker();
0183         }
0184 
0185         found = std::find_if(std::begin(d->colorMap),
0186                              std::end(d->colorMap),
0187                              [currColor](const std::pair<std::string, QColor>& color) {
0188                                  return color.second == currColor;
0189                              })
0190             != std::end(d->colorMap);
0191 
0192         idx += 1;
0193         if (idx == (int) d->colorBlindPalette.size()) {
0194             idx = 0;
0195             // too many colors. return the currently tested color. we tested
0196             // more than 45 colors here, it's unlikely that the amount of classes will
0197             // trigger this.
0198             if (brigthen && darken) {
0199                 break;
0200             }
0201             if (!darken) {
0202                 darken = true;
0203                 continue;
0204             }
0205             if (!brigthen) {
0206                 brigthen = true;
0207                 continue;
0208             }
0209         }
0210     } while (found);
0211 
0212     return currColor;
0213 }
0214 
0215 void ColorManagement::resetCaches()
0216 {
0217     d->colorMap.clear();
0218     d->fillMap.clear();
0219 }
0220 
0221 bool ColorManagement::isColorBlindModeActive() const
0222 {
0223     return Preferences::colorBlindMode();
0224 }
0225 
0226 bool Codethink::lvtclr::ColorManagement::isUsingColorBlindFill() const
0227 {
0228     return Preferences::useColorBlindFill();
0229 }
0230 
0231 } // namespace Codethink::lvtclr