From 50e382f554aee2bd12d8cd457b7e17b917162e6d Mon Sep 17 00:00:00 2001 From: redsky17 Date: Fri, 18 Jan 2019 17:17:25 +0000 Subject: [PATCH] Modified the code that generates user's colors so that it will work regardless of the theme choices the user makes. The code now incorporates the contrast between the background color and the color generated by the user_name when picking colors. It currently has two 'big' issues: 1. Colors are not cached. I am planning on adding a QHash for this a little later. This should improve performance by not calculating the color for the same users over and over and over again. 2. Theme changes do not trigger the colors to get refreshed. Currently, you will have to switch to a different room and back to get the colors to refresh. --- resources/styles/nheko-dark.qss | 4 ++ resources/styles/nheko.qss | 4 ++ resources/styles/system.qss | 4 ++ src/Utils.cpp | 112 ++++++++++++++++++++++++++++++-- src/Utils.h | 23 ++++++- src/timeline/TimelineItem.cpp | 8 ++- src/timeline/TimelineItem.h | 7 ++ 7 files changed, 152 insertions(+), 10 deletions(-) diff --git a/resources/styles/nheko-dark.qss b/resources/styles/nheko-dark.qss index 86056bb2..e81fa0b8 100644 --- a/resources/styles/nheko-dark.qss +++ b/resources/styles/nheko-dark.qss @@ -3,6 +3,10 @@ QLabel { color: #caccd1; } +TimelineItem { + qproperty-backgroundColor: #202228; +} + #chatPage, #chatPage > * { background-color: #202228; diff --git a/resources/styles/nheko.qss b/resources/styles/nheko.qss index ca5a8f0d..468ae0f1 100644 --- a/resources/styles/nheko.qss +++ b/resources/styles/nheko.qss @@ -3,6 +3,10 @@ QLabel { color: #333; } +TimelineItem { + qproperty-backgroundColor: white; +} + #chatPage, #chatPage > * { background-color: white; diff --git a/resources/styles/system.qss b/resources/styles/system.qss index 45263e96..6663ee6b 100644 --- a/resources/styles/system.qss +++ b/resources/styles/system.qss @@ -3,6 +3,10 @@ TypingDisplay { qproperty-backgroundColor: palette(window); } +TimelineItem { + qproperty-backgroundColor: palette(window); +} + TimelineView, TimelineView > * { border: none; diff --git a/src/Utils.cpp b/src/Utils.cpp index 6229d42a..6a5c3491 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -383,20 +383,120 @@ utils::linkColor() } QString -utils::generateHexColor(const QString &input) +utils::generateHexColor(const int hash) +{ + QString colour("#"); + for (int i = 0; i < 3; i++) { + int value = (hash >> (i * 8)) & 0xFF; + colour.append(("00" + QString::number(value, 16)).right(2)); + } + // nhlog::ui()->debug("Hex Generated {} -> {}", QString::number(hash).toStdString(), + // colour.toStdString()); + return colour.toUpper(); +} + +int +utils::hashQString(const QString &input) { auto hash = 0; for (int i = 0; i < input.length(); i++) { hash = input.at(i).digitValue() + ((hash << 5) - hash); } + hash *= 13; - QString colour("#"); - for (int i = 0; i < 3; i++) { - int value = (hash >> (i * 8)) & 0xFF; - colour.append(("00" + QString::number(value, 16)).right(2)); + + return hash; +} + +QString +utils::generateContrastingHexColor(const QString &input, const QString &background) +{ + nhlog::ui()->debug("Background hex {}", background.toStdString()); + const QColor backgroundCol(background); + const qreal backgroundLum = luminance(background); + + // Create a color for the input + auto hash = hashQString(input); + auto colorHex = generateHexColor(hash); + + // converting to a QColor makes the luminance calc easier. + QColor inputColor = QColor(colorHex); + + // attempt to score both the luminance and the contrast. + // contrast should have a higher precedence, but luminance + // helps dictate how exciting the colors are. + auto colorLum = luminance(inputColor); + auto contrast = computeContrast(colorLum, backgroundLum); + + // If the contrast or luminance don't meet our criteria, + // try again and again until they do. After 10 tries, + // the best-scoring color will be chosen. + int att = 0; + while ((contrast < 5 || (colorLum < 0.05 || colorLum > 0.95)) && ++att < 10) { + hash = hashQString(input) + ((hash << 2) * 13); + auto newHex = generateHexColor(hash); + inputColor.setNamedColor(newHex); + auto tmpLum = luminance(inputColor); + auto tmpContrast = computeContrast(tmpLum, backgroundLum); + + // Prioritize contrast over luminance + // If both values are better, it's a no brainer. + if (tmpContrast > contrast && (tmpLum > 0.05 && tmpLum < 0.95)) { + contrast = tmpContrast; + colorHex = newHex; + colorLum = tmpLum; + } + // Otherwise, if we still can get a more + // vibrant color and have met our contrast + // threshold, pick the more vibrant color, + // even if contrast will drop somewhat. + // choosing 50% luminance as ideal. + else if ((qAbs(tmpLum - 0.50) < qAbs(colorLum - 0.50)) && tmpContrast >= 5) { + contrast = tmpContrast; + colorHex = newHex; + colorLum = tmpLum; + } + // Otherwise, just take the better contrast. + else if (tmpContrast > contrast) { + contrast = tmpContrast; + colorHex = newHex; + colorLum = tmpLum; + } } - return colour; + + nhlog::ui()->debug("Hex Generated for {}: [hex: {}, contrast: {}, luminance: {}]", + input.toStdString(), + colorHex.toStdString(), + QString::number(contrast).toStdString(), + QString::number(colorLum).toStdString()); + return colorHex; +} + +qreal +utils::computeContrast(const qreal &one, const qreal &two) +{ + auto ratio = (one + 0.05) / (two + 0.05); + + if (two > one) { + ratio = 1 / ratio; + } + + return ratio; +} + +qreal +utils::luminance(const QColor &col) +{ + int colRgb[3] = {col.red(), col.green(), col.blue()}; + qreal lumRgb[3]; + + for (int i = 0; i < 3; i++) { + qreal v = colRgb[i] / 255.0; + v <= 0.03928 ? lumRgb[i] = v / 12.92 : lumRgb[i] = qPow((v + 0.055) / 1.055, 2.4); + } + + return lumRgb[0] * 0.2126 + lumRgb[1] * 0.7152 + lumRgb[2] * 0.0722; } void diff --git a/src/Utils.h b/src/Utils.h index 3ce2d758..8b3392da 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -14,6 +14,8 @@ #include #include +#include + class QComboBox; namespace utils { @@ -227,9 +229,26 @@ markdownToHtml(const QString &text); QString linkColor(); -//! Given an input string, create a color string +//! Given an input integer, create a color string in #RRGGBB format QString -generateHexColor(const QString &string); +generateHexColor(const int hash); + +//! Returns the hash code of the input QString +int +hashQString(const QString &input); + +//! Generate a color (matching #RRGGBB) that has an acceptable contrast to background that is based +//! on the input string. +QString +generateContrastingHexColor(const QString &input, const QString &background); + +//! Given two luminance values, compute the contrast ratio between them. +qreal +computeContrast(const qreal &one, const qreal &two); + +//! Compute the luminance of a single color. Based on https://stackoverflow.com/a/9733420 +qreal +luminance(const QColor &col); //! Center a widget in relation to another widget. void diff --git a/src/timeline/TimelineItem.cpp b/src/timeline/TimelineItem.cpp index 3df78ff6..bd3d73bc 100644 --- a/src/timeline/TimelineItem.cpp +++ b/src/timeline/TimelineItem.cpp @@ -622,8 +622,6 @@ TimelineItem::generateUserName(const QString &user_id, const QString &displaynam sender = displayname.split(":")[0].split("@")[1]; } - auto userColor = utils::generateHexColor(user_id); - QFont usernameFont; usernameFont.setPointSizeF(usernameFont.pointSizeF() * 1.1); usernameFont.setWeight(QFont::Medium); @@ -639,6 +637,12 @@ TimelineItem::generateUserName(const QString &user_id, const QString &displaynam userName_->setAlignment(Qt::AlignLeft | Qt::AlignTop); userName_->setFixedWidth(QFontMetrics(userName_->font()).width(userName_->text())); + // TimelineItem isn't displayed. This forces the QSS to get + // loaded. + qApp->style()->polish(this); + // generate user's unique color. + auto backCol = backgroundColor().name(); + auto userColor = utils::generateContrastingHexColor(user_id, backCol); userName_->setStyleSheet("QLabel { color : " + userColor + "; }"); auto filter = new UserProfileFilter(user_id, userName_); diff --git a/src/timeline/TimelineItem.h b/src/timeline/TimelineItem.h index 8159e370..c7f320d5 100644 --- a/src/timeline/TimelineItem.h +++ b/src/timeline/TimelineItem.h @@ -132,6 +132,8 @@ private: class TimelineItem : public QWidget { Q_OBJECT + Q_PROPERTY(QColor backgroundColor READ backgroundColor WRITE setBackgroundColor) + public: TimelineItem(const mtx::events::RoomEvent &e, bool with_sender, @@ -202,6 +204,9 @@ public: const QString &room_id, QWidget *parent); + void setBackgroundColor(const QColor &color) { backgroundColor_ = color; } + QColor backgroundColor() const { return backgroundColor_; } + void setUserAvatar(const QImage &pixmap); DescInfo descriptionMessage() const { return descriptionMsg_; } QString eventId() const { return event_id_; } @@ -282,6 +287,8 @@ private: QLabel *timestamp_; QLabel *userName_; TextLabel *body_; + + QColor backgroundColor_; }; template