mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 09:33:25 -08:00
Merge db4e07e55d into 64c7809332
This commit is contained in:
commit
246fd26b1d
35 changed files with 339 additions and 53 deletions
|
|
@ -30,7 +30,7 @@ FilterBuilder::FilterBuilder(QWidget *parent) : QWidget(parent)
|
||||||
typeCombo->addItem(CardFilter::typeName(static_cast<CardFilter::Type>(i)), QVariant(i));
|
typeCombo->addItem(CardFilter::typeName(static_cast<CardFilter::Type>(i)), QVariant(i));
|
||||||
}
|
}
|
||||||
|
|
||||||
QPushButton *ok = new QPushButton(loadThemedIcon(QStringLiteral("icons/increment")), QString());
|
QPushButton *ok = new QPushButton(loadThemedIcon(QStringLiteral("icons/increment"), AppColor::Positive), QString());
|
||||||
ok->setObjectName("ok");
|
ok->setObjectName("ok");
|
||||||
ok->setMaximumSize(20, 20);
|
ok->setMaximumSize(20, 20);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,8 +68,8 @@ PlayerListWidget::PlayerListWidget(TabSupervisor *_tabSupervisor,
|
||||||
QWidget *parent)
|
QWidget *parent)
|
||||||
: QTreeWidget(parent), tabSupervisor(_tabSupervisor), client(_client), game(_game), gameStarted(false)
|
: QTreeWidget(parent), tabSupervisor(_tabSupervisor), client(_client), game(_game), gameStarted(false)
|
||||||
{
|
{
|
||||||
readyIcon = loadThemedIcon(QStringLiteral("icons/ready_start"));
|
readyIcon = loadThemedIcon(QStringLiteral("icons/ready_start"), AppColor::Positive);
|
||||||
notReadyIcon = loadThemedIcon(QStringLiteral("icons/not_ready_start"));
|
notReadyIcon = loadThemedIcon(QStringLiteral("icons/not_ready_start"), AppColor::Negative);
|
||||||
concededIcon = loadThemedIcon(QStringLiteral("icons/conceded"));
|
concededIcon = loadThemedIcon(QStringLiteral("icons/conceded"));
|
||||||
playerIcon = loadThemedIcon(QStringLiteral("icons/player"));
|
playerIcon = loadThemedIcon(QStringLiteral("icons/player"));
|
||||||
judgeIcon = loadThemedIcon(QStringLiteral("icons/scales"));
|
judgeIcon = loadThemedIcon(QStringLiteral("icons/scales"));
|
||||||
|
|
|
||||||
|
|
@ -167,6 +167,10 @@ PaletteConfig fromAccent(const QColor &accent, int intensity, const QString &sch
|
||||||
// the lightened, desaturated companion used for button-gradient highlights.
|
// the lightened, desaturated companion used for button-gradient highlights.
|
||||||
cfg.appColors[AppColor::AccentStrong] = hl;
|
cfg.appColors[AppColor::AccentStrong] = hl;
|
||||||
cfg.appColors[AppColor::AccentSoft] = hsl(accent.lightness() + 60, qRound(accent.hslSaturation() * 70 / 100.0));
|
cfg.appColors[AppColor::AccentSoft] = hsl(accent.lightness() + 60, qRound(accent.hslSaturation() * 70 / 100.0));
|
||||||
|
// Semantic roles stay independent of the accent: a generated palette must
|
||||||
|
// still paint add/ready green and remove/not-ready red.
|
||||||
|
cfg.appColors[AppColor::Positive] = defaultAppColor(AppColor::Positive, dark);
|
||||||
|
cfg.appColors[AppColor::Negative] = defaultAppColor(AppColor::Negative, dark);
|
||||||
|
|
||||||
// BrightText
|
// BrightText
|
||||||
QColor bright;
|
QColor bright;
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QColor>
|
#include <QColor>
|
||||||
|
#include <QCoreApplication>
|
||||||
#include <QEvent>
|
#include <QEvent>
|
||||||
#include <QFont>
|
#include <QFont>
|
||||||
#include <QFrame>
|
#include <QFrame>
|
||||||
|
|
@ -62,6 +63,10 @@ static const QMap<QPalette::ColorRole, const char *> ROLE_DESCRIPTIONS = {
|
||||||
static const QMap<AppColor::Role, const char *> APP_ROLE_DESCRIPTIONS = {
|
static const QMap<AppColor::Role, const char *> APP_ROLE_DESCRIPTIONS = {
|
||||||
{AppColor::AccentStrong, QT_TR_NOOP("Vivid primary accent (e.g. home-tab button gradient start)")},
|
{AppColor::AccentStrong, QT_TR_NOOP("Vivid primary accent (e.g. home-tab button gradient start)")},
|
||||||
{AppColor::AccentSoft, QT_TR_NOOP("Lightened, desaturated accent (e.g. home-tab button gradient end)")},
|
{AppColor::AccentSoft, QT_TR_NOOP("Lightened, desaturated accent (e.g. home-tab button gradient end)")},
|
||||||
|
{AppColor::Positive,
|
||||||
|
QT_TRANSLATE_NOOP("PaletteGridWidget", "Positive state and additive actions (ready, add, increment)")},
|
||||||
|
{AppColor::Negative,
|
||||||
|
QT_TRANSLATE_NOOP("PaletteGridWidget", "Negative state and removal actions (not ready, remove, decrement)")},
|
||||||
};
|
};
|
||||||
|
|
||||||
PaletteGridWidget::PaletteGridWidget(QWidget *parent) : QWidget(parent)
|
PaletteGridWidget::PaletteGridWidget(QWidget *parent) : QWidget(parent)
|
||||||
|
|
@ -179,7 +184,7 @@ void PaletteGridWidget::buildGrid(QWidget *host)
|
||||||
}
|
}
|
||||||
|
|
||||||
auto *label = new QLabel(QString(appEnum.valueToKey(role)), host);
|
auto *label = new QLabel(QString(appEnum.valueToKey(role)), host);
|
||||||
label->setToolTip(APP_ROLE_DESCRIPTIONS.value(role, {}));
|
label->setToolTip(QCoreApplication::translate("PaletteGridWidget", APP_ROLE_DESCRIPTIONS.value(role, {})));
|
||||||
label->setContentsMargins(4, 2, 8, 2);
|
label->setContentsMargins(4, 2, 8, 2);
|
||||||
grid->addWidget(label, row, 0);
|
grid->addWidget(label, row, 0);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
#include <QList>
|
#include <QList>
|
||||||
#include <QMetaEnum>
|
#include <QMetaEnum>
|
||||||
#include <QPair>
|
#include <QPair>
|
||||||
|
#include <QStringLiteral>
|
||||||
#include <QTextStream>
|
#include <QTextStream>
|
||||||
#include <qnamespace.h>
|
#include <qnamespace.h>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
@ -451,3 +452,19 @@ FontConfig FontConfig::fromThemeDir(const QString &themeDirPath)
|
||||||
|
|
||||||
return fromFile(QDir(themeDirPath).absoluteFilePath(fileName()));
|
return fromFile(QDir(themeDirPath).absoluteFilePath(fileName()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QColor defaultAppColor(AppColor::Role role, bool dark)
|
||||||
|
{
|
||||||
|
switch (role) {
|
||||||
|
case AppColor::AccentStrong:
|
||||||
|
case AppColor::AccentSoft:
|
||||||
|
// Derived from the palette highlight by ThemeManager::appColor().
|
||||||
|
return {};
|
||||||
|
case AppColor::Positive:
|
||||||
|
return QColor(dark ? QStringLiteral("#6dd58c") : QStringLiteral("#1b7f3b"));
|
||||||
|
case AppColor::Negative:
|
||||||
|
return QColor(dark ? QStringLiteral("#ff8a80") : QStringLiteral("#b3261e"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ enum Role
|
||||||
{
|
{
|
||||||
AccentStrong,
|
AccentStrong,
|
||||||
AccentSoft,
|
AccentSoft,
|
||||||
|
Positive,
|
||||||
|
Negative,
|
||||||
};
|
};
|
||||||
Q_ENUM_NS(Role)
|
Q_ENUM_NS(Role)
|
||||||
} // namespace AppColor
|
} // namespace AppColor
|
||||||
|
|
@ -41,6 +43,16 @@ enum Role
|
||||||
Q_ENUM_NS(Role)
|
Q_ENUM_NS(Role)
|
||||||
} // namespace AppFont
|
} // namespace AppFont
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Fallback for an application color role when the active theme does not
|
||||||
|
* define one.
|
||||||
|
*
|
||||||
|
* Accent roles return an invalid QColor because ThemeManager derives them from
|
||||||
|
* the palette highlight. Semantic roles return an opaque, scheme-appropriate
|
||||||
|
* default so a theme written before the role existed still renders it sensibly.
|
||||||
|
*/
|
||||||
|
QColor defaultAppColor(AppColor::Role role, bool dark);
|
||||||
|
|
||||||
struct ThemeConfig
|
struct ThemeConfig
|
||||||
{
|
{
|
||||||
QString colorScheme;
|
QString colorScheme;
|
||||||
|
|
|
||||||
|
|
@ -23,19 +23,74 @@
|
||||||
#include <QStyle>
|
#include <QStyle>
|
||||||
#include <QSvgRenderer>
|
#include <QSvgRenderer>
|
||||||
#include <QtGlobal>
|
#include <QtGlobal>
|
||||||
|
#include <cmath>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @brief Palette color that replaces `currentColor`, following the icon mode.
|
* @brief WCAG relative luminance, used to test a tint against a background.
|
||||||
*/
|
*/
|
||||||
QByteArray sentinelColor(QIcon::Mode mode, QPalette::ColorRole role)
|
double relativeLuminance(const QColor &color)
|
||||||
{
|
{
|
||||||
|
const auto linear = [](double channel) {
|
||||||
|
channel /= 255.0;
|
||||||
|
return channel <= 0.03928 ? channel / 12.92 : std::pow((channel + 0.055) / 1.055, 2.4);
|
||||||
|
};
|
||||||
|
return 0.2126 * linear(color.red()) + 0.7152 * linear(color.green()) + 0.0722 * linear(color.blue());
|
||||||
|
}
|
||||||
|
|
||||||
|
double contrastRatio(const QColor &first, const QColor &second)
|
||||||
|
{
|
||||||
|
const double a = relativeLuminance(first);
|
||||||
|
const double b = relativeLuminance(second);
|
||||||
|
return (qMax(a, b) + 0.05) / (qMin(a, b) + 0.05);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
IconTint::IconTint(QPalette::ColorRole role) : paletteRole(role)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
IconTint::IconTint(AppColor::Role role) : source(Source::AppColor), appRole(role)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
QColor IconTint::resolve(QIcon::Mode mode) const
|
||||||
|
{
|
||||||
|
if (source == Source::AppColor) {
|
||||||
|
// A disabled control must not keep signalling its semantic color.
|
||||||
|
if (mode == QIcon::Disabled) {
|
||||||
|
return qApp->palette().color(QPalette::Disabled, QPalette::WindowText);
|
||||||
|
}
|
||||||
|
const QColor color = themeManager->appColor(appRole);
|
||||||
|
if (mode != QIcon::Selected) {
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
// Selected rows paint their icon on the highlight colour. Keep the
|
||||||
|
// semantic tint when it stays legible there; otherwise the glyph would
|
||||||
|
// vanish into the selection.
|
||||||
|
constexpr double MIN_SELECTED_CONTRAST = 3.0;
|
||||||
|
const QColor highlight = qApp->palette().color(QPalette::Active, QPalette::Highlight);
|
||||||
|
return contrastRatio(color, highlight) >= MIN_SELECTED_CONTRAST
|
||||||
|
? color
|
||||||
|
: qApp->palette().color(QPalette::Active, QPalette::HighlightedText);
|
||||||
|
}
|
||||||
|
|
||||||
const QPalette::ColorGroup group = mode == QIcon::Disabled ? QPalette::Disabled : QPalette::Active;
|
const QPalette::ColorGroup group = mode == QIcon::Disabled ? QPalette::Disabled : QPalette::Active;
|
||||||
const QPalette::ColorRole effective =
|
const QPalette::ColorRole effective =
|
||||||
mode == QIcon::Selected && role == QPalette::WindowText ? QPalette::HighlightedText : role;
|
mode == QIcon::Selected && paletteRole == QPalette::WindowText ? QPalette::HighlightedText : paletteRole;
|
||||||
return qApp->palette().color(group, effective).name(QColor::HexRgb).toLatin1();
|
return qApp->palette().color(group, effective);
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @brief Color that replaces `currentColor`, following the icon mode.
|
||||||
|
*/
|
||||||
|
QByteArray sentinelColor(QIcon::Mode mode, const IconTint &tint)
|
||||||
|
{
|
||||||
|
return tint.resolve(mode).name(QColor::HexRgb).toLatin1();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -106,7 +161,7 @@ QString cacheKey(const QString &path, const QByteArray &variant, const QSize &si
|
||||||
class ThemeSvgIconEngine final : public QObject, public QIconEngine
|
class ThemeSvgIconEngine final : public QObject, public QIconEngine
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
ThemeSvgIconEngine(QString _prefix, QPalette::ColorRole _role) : prefix(std::move(_prefix)), role(_role)
|
ThemeSvgIconEngine(QString _prefix, IconTint _tint) : prefix(std::move(_prefix)), tint(_tint)
|
||||||
{
|
{
|
||||||
if (themeManager) {
|
if (themeManager) {
|
||||||
// A theme switch changes the resolved asset (scheme-qualified
|
// A theme switch changes the resolved asset (scheme-qualified
|
||||||
|
|
@ -120,7 +175,7 @@ public:
|
||||||
|
|
||||||
QIconEngine *clone() const override
|
QIconEngine *clone() const override
|
||||||
{
|
{
|
||||||
return new ThemeSvgIconEngine(prefix, role);
|
return new ThemeSvgIconEngine(prefix, tint);
|
||||||
}
|
}
|
||||||
|
|
||||||
void paint(QPainter *painter, const QRect &rect, QIcon::Mode mode, QIcon::State) override
|
void paint(QPainter *painter, const QRect &rect, QIcon::Mode mode, QIcon::State) override
|
||||||
|
|
@ -238,7 +293,7 @@ private:
|
||||||
// with its own colors varies by mode (Disabled is generated). A color
|
// with its own colors varies by mode (Disabled is generated). A color
|
||||||
// variant is never a bare digit, so the two key spaces cannot collide.
|
// variant is never a bare digit, so the two key spaces cannot collide.
|
||||||
const QByteArray variant =
|
const QByteArray variant =
|
||||||
resolvedHasSentinel ? sentinelColor(mode, role) : QByteArray::number(static_cast<int>(mode));
|
resolvedHasSentinel ? sentinelColor(mode, tint) : QByteArray::number(static_cast<int>(mode));
|
||||||
const QString key = cacheKey(resolvedPath, variant, pixelSize, devicePixelRatio);
|
const QString key = cacheKey(resolvedPath, variant, pixelSize, devicePixelRatio);
|
||||||
const auto cached = cache.constFind(key);
|
const auto cached = cache.constFind(key);
|
||||||
if (cached != cache.constEnd()) {
|
if (cached != cache.constEnd()) {
|
||||||
|
|
@ -266,7 +321,7 @@ private:
|
||||||
}
|
}
|
||||||
|
|
||||||
QString prefix;
|
QString prefix;
|
||||||
QPalette::ColorRole role;
|
IconTint tint;
|
||||||
mutable QHash<QString, QPixmap> cache;
|
mutable QHash<QString, QPixmap> cache;
|
||||||
mutable QString resolvedPath;
|
mutable QString resolvedPath;
|
||||||
mutable QString resolvedThemePath;
|
mutable QString resolvedThemePath;
|
||||||
|
|
@ -279,12 +334,12 @@ private:
|
||||||
};
|
};
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
QIcon loadThemedIcon(QStringView prefix, QPalette::ColorRole role)
|
QIcon loadThemedIcon(QStringView prefix, IconTint tint)
|
||||||
{
|
{
|
||||||
return QIcon(new ThemeSvgIconEngine(prefix.toString(), role));
|
return QIcon(new ThemeSvgIconEngine(prefix.toString(), tint));
|
||||||
}
|
}
|
||||||
|
|
||||||
QPixmap loadSentinelSvgPixmap(const QString &resolvedPath, QPalette::ColorRole role, const QSize &size)
|
QPixmap loadSentinelSvgPixmap(const QString &resolvedPath, IconTint tint, const QSize &size)
|
||||||
{
|
{
|
||||||
if (!resolvedPath.endsWith(QStringLiteral(".svg"), Qt::CaseInsensitive)) {
|
if (!resolvedPath.endsWith(QStringLiteral(".svg"), Qt::CaseInsensitive)) {
|
||||||
return {};
|
return {};
|
||||||
|
|
@ -295,6 +350,6 @@ QPixmap loadSentinelSvgPixmap(const QString &resolvedPath, QPalette::ColorRole r
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const QByteArray color = qApp->palette().color(role).name(QColor::HexRgb).toLatin1();
|
const QByteArray color = tint.resolve(QIcon::Normal).name(QColor::HexRgb).toLatin1();
|
||||||
return renderSvg(resolveSvgColorSentinel(svg, color), size, 1.0);
|
return renderSvg(resolveSvgColorSentinel(svg, color), size, 1.0);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,9 @@
|
||||||
#ifndef THEME_ICON_H
|
#ifndef THEME_ICON_H
|
||||||
#define THEME_ICON_H
|
#define THEME_ICON_H
|
||||||
|
|
||||||
|
#include "theme_config.h"
|
||||||
|
|
||||||
|
#include <QColor>
|
||||||
#include <QIcon>
|
#include <QIcon>
|
||||||
#include <QPalette>
|
#include <QPalette>
|
||||||
#include <QPixmap>
|
#include <QPixmap>
|
||||||
|
|
@ -13,6 +16,41 @@
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QStringView>
|
#include <QStringView>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Selects the color that replaces the `currentColor` sentinel.
|
||||||
|
*
|
||||||
|
* A palette role follows Qt's structural palette. An application color carries
|
||||||
|
* meaning that QPalette cannot express, such as positive and negative states.
|
||||||
|
* Callers pick the role by intent; the theme owns the concrete value.
|
||||||
|
*/
|
||||||
|
class IconTint
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
IconTint() = default;
|
||||||
|
IconTint(QPalette::ColorRole role); // implicit: existing palette-role callers
|
||||||
|
IconTint(AppColor::Role role); // implicit: semantic callers
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Resolves the concrete color for an icon mode.
|
||||||
|
*
|
||||||
|
* Disabled application colors resolve to the disabled foreground so an
|
||||||
|
* unavailable control does not keep signalling its semantic color. Selected
|
||||||
|
* palette colors keep the existing WindowText -> HighlightedText swap.
|
||||||
|
*/
|
||||||
|
QColor resolve(QIcon::Mode mode) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
enum class Source
|
||||||
|
{
|
||||||
|
Palette,
|
||||||
|
AppColor,
|
||||||
|
};
|
||||||
|
|
||||||
|
Source source = Source::Palette;
|
||||||
|
QPalette::ColorRole paletteRole = QPalette::WindowText;
|
||||||
|
AppColor::Role appRole = AppColor::AccentStrong;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Builds a palette-aware icon from a `theme:` asset.
|
* @brief Builds a palette-aware icon from a `theme:` asset.
|
||||||
*
|
*
|
||||||
|
|
@ -24,11 +62,11 @@
|
||||||
*
|
*
|
||||||
* @param prefix Asset prefix relative to the `theme:` search path, with no file
|
* @param prefix Asset prefix relative to the `theme:` search path, with no file
|
||||||
* extension (for example `icons/search`), matching themePixmap().
|
* extension (for example `icons/search`), matching themePixmap().
|
||||||
* @param role Palette role whose color replaces the sentinel. Selected glyphs
|
* @param tint Color that replaces the sentinel: a palette role or a semantic
|
||||||
* use HighlightedText unless a role other than WindowText is given.
|
* application color. Defaults to WindowText.
|
||||||
* @return An icon that follows the application palette.
|
* @return An icon that follows the application palette.
|
||||||
*/
|
*/
|
||||||
QIcon loadThemedIcon(QStringView prefix, QPalette::ColorRole role = QPalette::WindowText);
|
QIcon loadThemedIcon(QStringView prefix, IconTint tint = {});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Renders a resolved theme SVG when it carries the `currentColor` sentinel.
|
* @brief Renders a resolved theme SVG when it carries the `currentColor` sentinel.
|
||||||
|
|
@ -38,13 +76,12 @@ QIcon loadThemedIcon(QStringView prefix, QPalette::ColorRole role = QPalette::Wi
|
||||||
*
|
*
|
||||||
* @param resolvedPath Asset path relative to the `theme:` search path, as
|
* @param resolvedPath Asset path relative to the `theme:` search path, as
|
||||||
* returned by ThemeManager::assetPath (includes extension).
|
* returned by ThemeManager::assetPath (includes extension).
|
||||||
* @param role Palette role whose color replaces the sentinel.
|
* @param tint Color that replaces the sentinel: a palette role or a semantic
|
||||||
|
* application color. Defaults to WindowText.
|
||||||
* @param size Pixel size to render at; an empty size uses the SVG's intrinsic size.
|
* @param size Pixel size to render at; an empty size uses the SVG's intrinsic size.
|
||||||
* @return The recolored pixmap, or a null pixmap when the asset is not a
|
* @return The recolored pixmap, or a null pixmap when the asset is not a
|
||||||
* sentinel-carrying SVG.
|
* sentinel-carrying SVG.
|
||||||
*/
|
*/
|
||||||
QPixmap loadSentinelSvgPixmap(const QString &resolvedPath,
|
QPixmap loadSentinelSvgPixmap(const QString &resolvedPath, IconTint tint = {}, const QSize &size = {});
|
||||||
QPalette::ColorRole role = QPalette::WindowText,
|
|
||||||
const QSize &size = {});
|
|
||||||
|
|
||||||
#endif // THEME_ICON_H
|
#endif // THEME_ICON_H
|
||||||
|
|
|
||||||
|
|
@ -639,14 +639,23 @@ QColor ThemeManager::appColor(AppColor::Role role) const
|
||||||
// unconditionally keeps every Qt version rendering identically.
|
// unconditionally keeps every Qt version rendering identically.
|
||||||
const QColor accent = qApp->palette().color(QPalette::Active, QPalette::Highlight);
|
const QColor accent = qApp->palette().color(QPalette::Active, QPalette::Highlight);
|
||||||
|
|
||||||
if (role == AppColor::AccentSoft) {
|
switch (role) {
|
||||||
constexpr int SOFT_SATURATION_PERCENT = 70;
|
case AppColor::AccentStrong:
|
||||||
constexpr int SOFT_LIGHTNESS_OFFSET = 60;
|
return accent;
|
||||||
|
case AppColor::AccentSoft: {
|
||||||
|
constexpr int SOFT_SATURATION_PERCENT = 70;
|
||||||
|
constexpr int SOFT_LIGHTNESS_OFFSET = 60;
|
||||||
|
|
||||||
// Light end of the gradient: same hue, softened and lightened
|
// Light end of the gradient: same hue, softened and lightened
|
||||||
return QColor::fromHsl(qMax(0, accent.hslHue()),
|
return QColor::fromHsl(qMax(0, accent.hslHue()),
|
||||||
qBound(0, qRound(accent.hslSaturation() * SOFT_SATURATION_PERCENT / 100.0), 255),
|
qBound(0, qRound(accent.hslSaturation() * SOFT_SATURATION_PERCENT / 100.0), 255),
|
||||||
qBound(0, accent.lightness() + SOFT_LIGHTNESS_OFFSET, 255));
|
qBound(0, accent.lightness() + SOFT_LIGHTNESS_OFFSET, 255));
|
||||||
|
}
|
||||||
|
case AppColor::Positive:
|
||||||
|
case AppColor::Negative:
|
||||||
|
// Semantic colors are independent of the accent: a missing role must
|
||||||
|
// not silently paint "remove" in the theme's highlight color.
|
||||||
|
return defaultAppColor(role, isDarkModeActive());
|
||||||
}
|
}
|
||||||
|
|
||||||
return accent;
|
return accent;
|
||||||
|
|
|
||||||
|
|
@ -147,7 +147,8 @@ public:
|
||||||
void previewFonts(const FontConfig &cfg);
|
void previewFonts(const FontConfig &cfg);
|
||||||
|
|
||||||
// Resolves an application color role: the theme's stored [AppColors] value
|
// Resolves an application color role: the theme's stored [AppColors] value
|
||||||
// when present, otherwise a palette-accent-derived fallback.
|
// when present, otherwise a palette-accent-derived fallback for accents and
|
||||||
|
// a scheme-appropriate semantic default for positive / negative.
|
||||||
QColor appColor(AppColor::Role role) const;
|
QColor appColor(AppColor::Role role) const;
|
||||||
/** @brief Resolves an application font role: the theme's stored [Fonts]
|
/** @brief Resolves an application font role: the theme's stored [Fonts]
|
||||||
* value when present, otherwise the application font. Roles that
|
* value when present, otherwise the application font. Roles that
|
||||||
|
|
|
||||||
|
|
@ -83,12 +83,14 @@ DeckEditorDatabaseDisplayWidget::DeckEditorDatabaseDisplayWidget(QWidget *parent
|
||||||
&DeckEditorDatabaseDisplayWidget::onRelatedCardClicked);
|
&DeckEditorDatabaseDisplayWidget::onRelatedCardClicked);
|
||||||
|
|
||||||
aAddCard = new QAction(QString(), this);
|
aAddCard = new QAction(QString(), this);
|
||||||
aAddCard->setIcon(loadThemedIcon(QStringLiteral("icons/arrow_right")));
|
aAddCard->setIcon(loadThemedIcon(QStringLiteral("icons/arrow_right"), AppColor::Positive));
|
||||||
connect(aAddCard, &QAction::triggered, this, &DeckEditorDatabaseDisplayWidget::actAddCardToMainDeck);
|
connect(aAddCard, &QAction::triggered, this, &DeckEditorDatabaseDisplayWidget::actAddCardToMainDeck);
|
||||||
auto *tbAddCard = new QToolButton(this);
|
auto *tbAddCard = new QToolButton(this);
|
||||||
tbAddCard->setDefaultAction(aAddCard);
|
tbAddCard->setDefaultAction(aAddCard);
|
||||||
|
|
||||||
aAddCardToSideboard = new QAction(QString(), this);
|
aAddCardToSideboard = new QAction(QString(), this);
|
||||||
|
// Sideboard is a destination, not a positive state: its distinct glyph
|
||||||
|
// keeps the target legible without competing with the green main-deck add.
|
||||||
aAddCardToSideboard->setIcon(loadThemedIcon(QStringLiteral("icons/arrow_right_sideboard")));
|
aAddCardToSideboard->setIcon(loadThemedIcon(QStringLiteral("icons/arrow_right_sideboard")));
|
||||||
connect(aAddCardToSideboard, &QAction::triggered, this, &DeckEditorDatabaseDisplayWidget::actAddCardToSideboard);
|
connect(aAddCardToSideboard, &QAction::triggered, this, &DeckEditorDatabaseDisplayWidget::actAddCardToSideboard);
|
||||||
auto *tbAddCardToSideboard = new QToolButton(this);
|
auto *tbAddCardToSideboard = new QToolButton(this);
|
||||||
|
|
|
||||||
|
|
@ -248,19 +248,19 @@ void DeckEditorDeckDockWidget::createDeckDock()
|
||||||
&DeckEditorDeckDockWidget::applyActiveGroupCriteria);
|
&DeckEditorDeckDockWidget::applyActiveGroupCriteria);
|
||||||
|
|
||||||
aIncrement = new QAction(QString(), this);
|
aIncrement = new QAction(QString(), this);
|
||||||
aIncrement->setIcon(loadThemedIcon(QStringLiteral("icons/increment")));
|
aIncrement->setIcon(loadThemedIcon(QStringLiteral("icons/increment"), AppColor::Positive));
|
||||||
connect(aIncrement, &QAction::triggered, this, &DeckEditorDeckDockWidget::actIncrementSelection);
|
connect(aIncrement, &QAction::triggered, this, &DeckEditorDeckDockWidget::actIncrementSelection);
|
||||||
auto *tbIncrement = new QToolButton(this);
|
auto *tbIncrement = new QToolButton(this);
|
||||||
tbIncrement->setDefaultAction(aIncrement);
|
tbIncrement->setDefaultAction(aIncrement);
|
||||||
|
|
||||||
aDecrement = new QAction(QString(), this);
|
aDecrement = new QAction(QString(), this);
|
||||||
aDecrement->setIcon(loadThemedIcon(QStringLiteral("icons/decrement")));
|
aDecrement->setIcon(loadThemedIcon(QStringLiteral("icons/decrement"), AppColor::Negative));
|
||||||
connect(aDecrement, &QAction::triggered, this, &DeckEditorDeckDockWidget::actDecrementSelection);
|
connect(aDecrement, &QAction::triggered, this, &DeckEditorDeckDockWidget::actDecrementSelection);
|
||||||
auto *tbDecrement = new QToolButton(this);
|
auto *tbDecrement = new QToolButton(this);
|
||||||
tbDecrement->setDefaultAction(aDecrement);
|
tbDecrement->setDefaultAction(aDecrement);
|
||||||
|
|
||||||
aRemoveCard = new QAction(QString(), this);
|
aRemoveCard = new QAction(QString(), this);
|
||||||
aRemoveCard->setIcon(loadThemedIcon(QStringLiteral("icons/remove_row")));
|
aRemoveCard->setIcon(loadThemedIcon(QStringLiteral("icons/remove_row"), AppColor::Negative));
|
||||||
connect(aRemoveCard, &QAction::triggered, this, &DeckEditorDeckDockWidget::actRemoveCard);
|
connect(aRemoveCard, &QAction::triggered, this, &DeckEditorDeckDockWidget::actRemoveCard);
|
||||||
auto *tbRemoveCard = new QToolButton(this);
|
auto *tbRemoveCard = new QToolButton(this);
|
||||||
tbRemoveCard->setDefaultAction(aRemoveCard);
|
tbRemoveCard->setDefaultAction(aRemoveCard);
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ void DeckEditorFilterDockWidget::createFiltersDock()
|
||||||
connect(filterBuilder, &FilterBuilder::add, filterModel, &FilterTreeModel::addFilter);
|
connect(filterBuilder, &FilterBuilder::add, filterModel, &FilterTreeModel::addFilter);
|
||||||
|
|
||||||
aClearFilterOne = new QAction(QString(), this);
|
aClearFilterOne = new QAction(QString(), this);
|
||||||
aClearFilterOne->setIcon(loadThemedIcon(QStringLiteral("icons/decrement")));
|
aClearFilterOne->setIcon(loadThemedIcon(QStringLiteral("icons/decrement"), AppColor::Negative));
|
||||||
connect(aClearFilterOne, &QAction::triggered, this, &DeckEditorFilterDockWidget::actClearFilterOne);
|
connect(aClearFilterOne, &QAction::triggered, this, &DeckEditorFilterDockWidget::actClearFilterOne);
|
||||||
|
|
||||||
aClearFilterAll = new QAction(QString(), this);
|
aClearFilterAll = new QAction(QString(), this);
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ DlgConnect::DlgConnect(QWidget *parent) : QDialog(parent)
|
||||||
previousHosts = new QComboBox(this);
|
previousHosts = new QComboBox(this);
|
||||||
|
|
||||||
btnDeleteServer = new QPushButton(this);
|
btnDeleteServer = new QPushButton(this);
|
||||||
btnDeleteServer->setIcon(loadThemedIcon(QStringLiteral("icons/remove_row")));
|
btnDeleteServer->setIcon(loadThemedIcon(QStringLiteral("icons/remove_row"), AppColor::Negative));
|
||||||
btnDeleteServer->setToolTip(tr("Delete the currently selected saved server"));
|
btnDeleteServer->setToolTip(tr("Delete the currently selected saved server"));
|
||||||
btnDeleteServer->setFixedWidth(30);
|
btnDeleteServer->setFixedWidth(30);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -108,10 +108,10 @@ DlgEditTokens::DlgEditTokens(QWidget *parent) : QDialog(parent), currentCard(nul
|
||||||
&DlgEditTokens::tokenSelectionChanged);
|
&DlgEditTokens::tokenSelectionChanged);
|
||||||
|
|
||||||
QAction *aAddToken = new QAction(tr("Add token"), this);
|
QAction *aAddToken = new QAction(tr("Add token"), this);
|
||||||
aAddToken->setIcon(loadThemedIcon(QStringLiteral("icons/increment")));
|
aAddToken->setIcon(loadThemedIcon(QStringLiteral("icons/increment"), AppColor::Positive));
|
||||||
connect(aAddToken, &QAction::triggered, this, &DlgEditTokens::actAddToken);
|
connect(aAddToken, &QAction::triggered, this, &DlgEditTokens::actAddToken);
|
||||||
QAction *aRemoveToken = new QAction(tr("Remove token"), this);
|
QAction *aRemoveToken = new QAction(tr("Remove token"), this);
|
||||||
aRemoveToken->setIcon(loadThemedIcon(QStringLiteral("icons/decrement")));
|
aRemoveToken->setIcon(loadThemedIcon(QStringLiteral("icons/decrement"), AppColor::Negative));
|
||||||
connect(aRemoveToken, &QAction::triggered, this, &DlgEditTokens::actRemoveToken);
|
connect(aRemoveToken, &QAction::triggered, this, &DlgEditTokens::actRemoveToken);
|
||||||
|
|
||||||
auto *databaseToolBar = new QToolBar;
|
auto *databaseToolBar = new QToolBar;
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ DlgRegister::DlgRegister(QWidget *parent) : QDialog(parent)
|
||||||
previousHosts = new QComboBox(this);
|
previousHosts = new QComboBox(this);
|
||||||
|
|
||||||
btnDeleteServer = new QPushButton(this);
|
btnDeleteServer = new QPushButton(this);
|
||||||
btnDeleteServer->setIcon(loadThemedIcon(QStringLiteral("icons/remove_row")));
|
btnDeleteServer->setIcon(loadThemedIcon(QStringLiteral("icons/remove_row"), AppColor::Negative));
|
||||||
btnDeleteServer->setToolTip(tr("Delete the currently selected saved server"));
|
btnDeleteServer->setToolTip(tr("Delete the currently selected saved server"));
|
||||||
btnDeleteServer->setFixedWidth(30);
|
btnDeleteServer->setFixedWidth(30);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ DeckEditorSettingsPage::DeckEditorSettingsPage()
|
||||||
}
|
}
|
||||||
|
|
||||||
aAdd = new QAction(this);
|
aAdd = new QAction(this);
|
||||||
aAdd->setIcon(loadThemedIcon(QStringLiteral("icons/increment")));
|
aAdd->setIcon(loadThemedIcon(QStringLiteral("icons/increment"), AppColor::Positive));
|
||||||
connect(aAdd, &QAction::triggered, this, &DeckEditorSettingsPage::actAddURL);
|
connect(aAdd, &QAction::triggered, this, &DeckEditorSettingsPage::actAddURL);
|
||||||
|
|
||||||
aEdit = new QAction(this);
|
aEdit = new QAction(this);
|
||||||
|
|
@ -88,7 +88,7 @@ DeckEditorSettingsPage::DeckEditorSettingsPage()
|
||||||
connect(aEdit, &QAction::triggered, this, &DeckEditorSettingsPage::actEditURL);
|
connect(aEdit, &QAction::triggered, this, &DeckEditorSettingsPage::actEditURL);
|
||||||
|
|
||||||
aRemove = new QAction(this);
|
aRemove = new QAction(this);
|
||||||
aRemove->setIcon(loadThemedIcon(QStringLiteral("icons/decrement")));
|
aRemove->setIcon(loadThemedIcon(QStringLiteral("icons/decrement"), AppColor::Negative));
|
||||||
connect(aRemove, &QAction::triggered, this, &DeckEditorSettingsPage::actRemoveURL);
|
connect(aRemove, &QAction::triggered, this, &DeckEditorSettingsPage::actRemoveURL);
|
||||||
|
|
||||||
aRateLimit = new QAction(this);
|
aRateLimit = new QAction(this);
|
||||||
|
|
|
||||||
|
|
@ -146,7 +146,7 @@ MessagesSettingsPage::MessagesSettingsPage()
|
||||||
}
|
}
|
||||||
|
|
||||||
aAdd = new QAction(this);
|
aAdd = new QAction(this);
|
||||||
aAdd->setIcon(loadThemedIcon(QStringLiteral("icons/increment")));
|
aAdd->setIcon(loadThemedIcon(QStringLiteral("icons/increment"), AppColor::Positive));
|
||||||
connect(aAdd, &QAction::triggered, this, &MessagesSettingsPage::actAdd);
|
connect(aAdd, &QAction::triggered, this, &MessagesSettingsPage::actAdd);
|
||||||
|
|
||||||
aEdit = new QAction(this);
|
aEdit = new QAction(this);
|
||||||
|
|
@ -154,7 +154,7 @@ MessagesSettingsPage::MessagesSettingsPage()
|
||||||
connect(aEdit, &QAction::triggered, this, &MessagesSettingsPage::actEdit);
|
connect(aEdit, &QAction::triggered, this, &MessagesSettingsPage::actEdit);
|
||||||
|
|
||||||
aRemove = new QAction(this);
|
aRemove = new QAction(this);
|
||||||
aRemove->setIcon(loadThemedIcon(QStringLiteral("icons/decrement")));
|
aRemove->setIcon(loadThemedIcon(QStringLiteral("icons/decrement"), AppColor::Negative));
|
||||||
connect(aRemove, &QAction::triggered, this, &MessagesSettingsPage::actRemove);
|
connect(aRemove, &QAction::triggered, this, &MessagesSettingsPage::actRemove);
|
||||||
|
|
||||||
auto *messageToolBar = new QToolBar;
|
auto *messageToolBar = new QToolBar;
|
||||||
|
|
|
||||||
|
|
@ -199,7 +199,7 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor,
|
||||||
aNewLocalFolder->setIcon(qApp->style()->standardIcon(QStyle::SP_FileDialogNewFolder));
|
aNewLocalFolder->setIcon(qApp->style()->standardIcon(QStyle::SP_FileDialogNewFolder));
|
||||||
connect(aNewLocalFolder, &QAction::triggered, this, &TabDeckStorage::actNewLocalFolder);
|
connect(aNewLocalFolder, &QAction::triggered, this, &TabDeckStorage::actNewLocalFolder);
|
||||||
aDeleteLocalDeck = new QAction(this);
|
aDeleteLocalDeck = new QAction(this);
|
||||||
aDeleteLocalDeck->setIcon(loadThemedIcon(QStringLiteral("icons/remove_row")));
|
aDeleteLocalDeck->setIcon(loadThemedIcon(QStringLiteral("icons/remove_row"), AppColor::Negative));
|
||||||
connect(aDeleteLocalDeck, &QAction::triggered, this, &TabDeckStorage::actDeleteLocalDeck);
|
connect(aDeleteLocalDeck, &QAction::triggered, this, &TabDeckStorage::actDeleteLocalDeck);
|
||||||
|
|
||||||
aOpenDecksFolder = new QAction(this);
|
aOpenDecksFolder = new QAction(this);
|
||||||
|
|
@ -217,7 +217,7 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor,
|
||||||
aNewFolder->setIcon(qApp->style()->standardIcon(QStyle::SP_FileDialogNewFolder));
|
aNewFolder->setIcon(qApp->style()->standardIcon(QStyle::SP_FileDialogNewFolder));
|
||||||
connect(aNewFolder, &QAction::triggered, this, &TabDeckStorage::actNewFolder);
|
connect(aNewFolder, &QAction::triggered, this, &TabDeckStorage::actNewFolder);
|
||||||
aDeleteRemoteDeck = new QAction(this);
|
aDeleteRemoteDeck = new QAction(this);
|
||||||
aDeleteRemoteDeck->setIcon(loadThemedIcon(QStringLiteral("icons/remove_row")));
|
aDeleteRemoteDeck->setIcon(loadThemedIcon(QStringLiteral("icons/remove_row"), AppColor::Negative));
|
||||||
connect(aDeleteRemoteDeck, &QAction::triggered, this, &TabDeckStorage::actDeleteRemoteDeck);
|
connect(aDeleteRemoteDeck, &QAction::triggered, this, &TabDeckStorage::actDeleteRemoteDeck);
|
||||||
|
|
||||||
aShareDecks = new QAction(this);
|
aShareDecks = new QAction(this);
|
||||||
|
|
|
||||||
|
|
@ -155,7 +155,7 @@ QGroupBox *TabReplays::createLeftLayout()
|
||||||
aNewLocalFolder->setIcon(qApp->style()->standardIcon(QStyle::SP_FileDialogNewFolder));
|
aNewLocalFolder->setIcon(qApp->style()->standardIcon(QStyle::SP_FileDialogNewFolder));
|
||||||
connect(aNewLocalFolder, &QAction::triggered, this, &TabReplays::actNewLocalFolder);
|
connect(aNewLocalFolder, &QAction::triggered, this, &TabReplays::actNewLocalFolder);
|
||||||
aDeleteLocalReplay = new QAction(this);
|
aDeleteLocalReplay = new QAction(this);
|
||||||
aDeleteLocalReplay->setIcon(loadThemedIcon(QStringLiteral("icons/remove_row")));
|
aDeleteLocalReplay->setIcon(loadThemedIcon(QStringLiteral("icons/remove_row"), AppColor::Negative));
|
||||||
connect(aDeleteLocalReplay, &QAction::triggered, this, &TabReplays::actDeleteLocalReplay);
|
connect(aDeleteLocalReplay, &QAction::triggered, this, &TabReplays::actDeleteLocalReplay);
|
||||||
|
|
||||||
aOpenReplaysFolder = new QAction(this);
|
aOpenReplaysFolder = new QAction(this);
|
||||||
|
|
@ -217,7 +217,7 @@ QGroupBox *TabReplays::createRightLayout()
|
||||||
aKeep->setIcon(loadThemedIcon(QStringLiteral("icons/lock")));
|
aKeep->setIcon(loadThemedIcon(QStringLiteral("icons/lock")));
|
||||||
connect(aKeep, &QAction::triggered, this, &TabReplays::actKeepRemoteReplay);
|
connect(aKeep, &QAction::triggered, this, &TabReplays::actKeepRemoteReplay);
|
||||||
aDeleteRemoteReplay = new QAction(this);
|
aDeleteRemoteReplay = new QAction(this);
|
||||||
aDeleteRemoteReplay->setIcon(loadThemedIcon(QStringLiteral("icons/remove_row")));
|
aDeleteRemoteReplay->setIcon(loadThemedIcon(QStringLiteral("icons/remove_row"), AppColor::Negative));
|
||||||
connect(aDeleteRemoteReplay, &QAction::triggered, this, &TabReplays::actDeleteRemoteReplay);
|
connect(aDeleteRemoteReplay, &QAction::triggered, this, &TabReplays::actDeleteRemoteReplay);
|
||||||
aGetReplayCode = new QAction(this);
|
aGetReplayCode = new QAction(this);
|
||||||
aGetReplayCode->setIcon(loadThemedIcon(QStringLiteral("icons/share")));
|
aGetReplayCode->setIcon(loadThemedIcon(QStringLiteral("icons/share")));
|
||||||
|
|
|
||||||
|
|
@ -156,7 +156,7 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent,
|
||||||
|
|
||||||
clearFilterWidget = new QToolButton();
|
clearFilterWidget = new QToolButton();
|
||||||
clearFilterWidget->setFixedSize(32, 32);
|
clearFilterWidget->setFixedSize(32, 32);
|
||||||
clearFilterWidget->setIcon(loadThemedIcon(QStringLiteral("icons/delete")));
|
clearFilterWidget->setIcon(loadThemedIcon(QStringLiteral("icons/delete"), AppColor::Negative));
|
||||||
connect(clearFilterWidget, &QToolButton::clicked, this, [this] {
|
connect(clearFilterWidget, &QToolButton::clicked, this, [this] {
|
||||||
filterModel->blockSignals(true);
|
filterModel->blockSignals(true);
|
||||||
filterModel->filterTree()->blockSignals(true);
|
filterModel->filterTree()->blockSignals(true);
|
||||||
|
|
|
||||||
|
|
@ -71,4 +71,6 @@ Accent = #4a82e6
|
||||||
[AppColors]
|
[AppColors]
|
||||||
AccentStrong = #3a6fd6
|
AccentStrong = #3a6fd6
|
||||||
AccentSoft = #8fb8f2
|
AccentSoft = #8fb8f2
|
||||||
|
Positive = #6dd58c
|
||||||
|
Negative = #ff8a80
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -71,4 +71,6 @@ Accent = #2f6fd6
|
||||||
[AppColors]
|
[AppColors]
|
||||||
AccentStrong = #3a6fd6
|
AccentStrong = #3a6fd6
|
||||||
AccentSoft = #8fb8f2
|
AccentSoft = #8fb8f2
|
||||||
|
Positive = #1b7f3b
|
||||||
|
Negative = #b3261e
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -71,3 +71,5 @@ Accent = #ff1e1e1e
|
||||||
[AppColors]
|
[AppColors]
|
||||||
AccentStrong = #ff139740
|
AccentStrong = #ff139740
|
||||||
AccentSoft = #ffc9fd62
|
AccentSoft = #ffc9fd62
|
||||||
|
Positive = #6dd58c
|
||||||
|
Negative = #ff8a80
|
||||||
|
|
|
||||||
|
|
@ -71,3 +71,5 @@ Accent = #fff0f0f0
|
||||||
[AppColors]
|
[AppColors]
|
||||||
AccentStrong = #ff139740
|
AccentStrong = #ff139740
|
||||||
AccentSoft = #ffc9fd62
|
AccentSoft = #ffc9fd62
|
||||||
|
Positive = #1b7f3b
|
||||||
|
Negative = #b3261e
|
||||||
|
|
|
||||||
|
|
@ -71,4 +71,6 @@ Accent = #c9995a
|
||||||
[AppColors]
|
[AppColors]
|
||||||
AccentStrong = #b7823b
|
AccentStrong = #b7823b
|
||||||
AccentSoft = #e4c58f
|
AccentSoft = #e4c58f
|
||||||
|
Positive = #6dd58c
|
||||||
|
Negative = #ff8a80
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -71,4 +71,6 @@ Accent = #a5712f
|
||||||
[AppColors]
|
[AppColors]
|
||||||
AccentStrong = #b7823b
|
AccentStrong = #b7823b
|
||||||
AccentSoft = #e4c58f
|
AccentSoft = #e4c58f
|
||||||
|
Positive = #1b7f3b
|
||||||
|
Negative = #b3261e
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -71,4 +71,6 @@ Accent = #6a4df0
|
||||||
[AppColors]
|
[AppColors]
|
||||||
AccentStrong = #5b3ee0
|
AccentStrong = #5b3ee0
|
||||||
AccentSoft = #a28bf7
|
AccentSoft = #a28bf7
|
||||||
|
Positive = #6dd58c
|
||||||
|
Negative = #ff8a80
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -71,4 +71,6 @@ Accent = #5a3ee2
|
||||||
[AppColors]
|
[AppColors]
|
||||||
AccentStrong = #5b3ee0
|
AccentStrong = #5b3ee0
|
||||||
AccentSoft = #a28bf7
|
AccentSoft = #a28bf7
|
||||||
|
Positive = #1b7f3b
|
||||||
|
Negative = #b3261e
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
# The System theme derives all of its colors from the platform palette
|
# The System theme derives all of its colors from the platform palette
|
||||||
# (e.g. qt6ct's custom color scheme on Linux), so it must NOT ship palette
|
# (e.g. qt6ct's custom color scheme on Linux), so it must NOT ship palette
|
||||||
# overrides. Only the identity app colors are provided here, so buttons keep
|
# overrides. Only the identity and semantic app colors are provided here, so
|
||||||
# their classic greens while the actual palette stays native to the OS.
|
# buttons keep their classic greens while the actual palette stays native to
|
||||||
|
# the OS.
|
||||||
[AppColors]
|
[AppColors]
|
||||||
AccentStrong = #ff148c3c
|
AccentStrong = #ff148c3c
|
||||||
AccentSoft = #ff78c850
|
AccentSoft = #ff78c850
|
||||||
|
Positive = #ff6dd58c
|
||||||
|
Negative = #ffff8a80
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
# The System theme derives all of its colors from the platform palette
|
# The System theme derives all of its colors from the platform palette
|
||||||
# (e.g. qt6ct's custom color scheme on Linux), so it must NOT ship palette
|
# (e.g. qt6ct's custom color scheme on Linux), so it must NOT ship palette
|
||||||
# overrides. Only the identity app colors are provided here, so buttons keep
|
# overrides. Only the identity and semantic app colors are provided here, so
|
||||||
# their classic greens while the actual palette stays native to the OS.
|
# buttons keep their classic greens while the actual palette stays native to
|
||||||
|
# the OS.
|
||||||
[AppColors]
|
[AppColors]
|
||||||
AccentStrong = #ff148c3c
|
AccentStrong = #ff148c3c
|
||||||
AccentSoft = #ff78c850
|
AccentSoft = #ff78c850
|
||||||
|
Positive = #ff1b7f3b
|
||||||
|
Negative = #ffb3261e
|
||||||
|
|
|
||||||
|
|
@ -71,4 +71,6 @@ Accent = #8fa6c0
|
||||||
[AppColors]
|
[AppColors]
|
||||||
AccentStrong = #54687e
|
AccentStrong = #54687e
|
||||||
AccentSoft = #9db0c4
|
AccentSoft = #9db0c4
|
||||||
|
Positive = #6dd58c
|
||||||
|
Negative = #ff8a80
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -71,4 +71,6 @@ Accent = #54687e
|
||||||
[AppColors]
|
[AppColors]
|
||||||
AccentStrong = #54687e
|
AccentStrong = #54687e
|
||||||
AccentSoft = #9db0c4
|
AccentSoft = #9db0c4
|
||||||
|
Positive = #1b7f3b
|
||||||
|
Negative = #b3261e
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -187,3 +187,86 @@ TEST_F(ThemeIconRenderTest, LowercaseSentinelIsResolved)
|
||||||
ASSERT_FALSE(pixmap.isNull());
|
ASSERT_FALSE(pixmap.isNull());
|
||||||
EXPECT_GT(countOpaquePixels(pixmap, QColor(QStringLiteral("#123456"))), 0);
|
EXPECT_GT(countOpaquePixels(pixmap, QColor(QStringLiteral("#123456"))), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A semantic tint must paint its theme color, not the selection accent.
|
||||||
|
*
|
||||||
|
* A theme written before Positive/Negative existed has no stored value, so this
|
||||||
|
* also covers the fallback that used to return the highlight color.
|
||||||
|
*/
|
||||||
|
TEST_F(ThemeIconRenderTest, AppColorTintUsesSemanticColorNotTheAccent)
|
||||||
|
{
|
||||||
|
QPalette palette = qApp->palette();
|
||||||
|
const QColor accent(QStringLiteral("#ff00ff"));
|
||||||
|
palette.setColor(QPalette::Active, QPalette::Highlight, accent);
|
||||||
|
qApp->setPalette(palette);
|
||||||
|
|
||||||
|
const QColor positive = themeManager->appColor(AppColor::Positive);
|
||||||
|
const QColor negative = themeManager->appColor(AppColor::Negative);
|
||||||
|
ASSERT_TRUE(positive.isValid());
|
||||||
|
ASSERT_TRUE(negative.isValid());
|
||||||
|
EXPECT_NE(positive, accent);
|
||||||
|
EXPECT_NE(negative, accent);
|
||||||
|
|
||||||
|
const QPixmap positivePixmap = loadSentinelSvgPixmap(QStringLiteral("icons/search.svg"), AppColor::Positive);
|
||||||
|
ASSERT_FALSE(positivePixmap.isNull());
|
||||||
|
EXPECT_GT(countOpaquePixels(positivePixmap, positive), 0);
|
||||||
|
EXPECT_EQ(countOpaquePixels(positivePixmap, accent), 0);
|
||||||
|
|
||||||
|
const QPixmap negativePixmap = loadSentinelSvgPixmap(QStringLiteral("icons/search.svg"), AppColor::Negative);
|
||||||
|
ASSERT_FALSE(negativePixmap.isNull());
|
||||||
|
EXPECT_GT(countOpaquePixels(negativePixmap, negative), 0);
|
||||||
|
EXPECT_EQ(countOpaquePixels(negativePixmap, accent), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief A disabled control must not keep signalling its semantic color. */
|
||||||
|
TEST_F(ThemeIconRenderTest, DisabledAppColorUsesDisabledForeground)
|
||||||
|
{
|
||||||
|
QPalette palette = qApp->palette();
|
||||||
|
const QColor disabledText(QStringLiteral("#fedcba"));
|
||||||
|
palette.setColor(QPalette::Disabled, QPalette::WindowText, disabledText);
|
||||||
|
qApp->setPalette(palette);
|
||||||
|
|
||||||
|
const IconTint tint(AppColor::Negative);
|
||||||
|
EXPECT_EQ(tint.resolve(QIcon::Disabled), disabledText);
|
||||||
|
EXPECT_NE(tint.resolve(QIcon::Normal), disabledText);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief Palette tints keep the WindowText -> HighlightedText selected swap. */
|
||||||
|
TEST_F(ThemeIconRenderTest, SelectedPaletteTintUsesHighlightedText)
|
||||||
|
{
|
||||||
|
QPalette palette = qApp->palette();
|
||||||
|
const QColor activeText(QStringLiteral("#111111"));
|
||||||
|
const QColor selectedText(QStringLiteral("#222222"));
|
||||||
|
palette.setColor(QPalette::Active, QPalette::WindowText, activeText);
|
||||||
|
palette.setColor(QPalette::Active, QPalette::HighlightedText, selectedText);
|
||||||
|
qApp->setPalette(palette);
|
||||||
|
|
||||||
|
const IconTint tint(QPalette::WindowText);
|
||||||
|
EXPECT_EQ(tint.resolve(QIcon::Normal), activeText);
|
||||||
|
EXPECT_EQ(tint.resolve(QIcon::Selected), selectedText);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief A selected row falls back to HighlightedText when the tint vanishes. */
|
||||||
|
TEST_F(ThemeIconRenderTest, SelectedAppColorFallsBackWhenItMatchesTheHighlight)
|
||||||
|
{
|
||||||
|
const QColor semantic = themeManager->appColor(AppColor::Positive);
|
||||||
|
const QColor selectedText(QStringLiteral("#fedcba"));
|
||||||
|
QPalette palette = qApp->palette();
|
||||||
|
palette.setColor(QPalette::Active, QPalette::Highlight, semantic);
|
||||||
|
palette.setColor(QPalette::Active, QPalette::HighlightedText, selectedText);
|
||||||
|
qApp->setPalette(palette);
|
||||||
|
|
||||||
|
EXPECT_EQ(IconTint(AppColor::Positive).resolve(QIcon::Selected), selectedText);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief A selected row keeps a semantic tint that still contrasts enough. */
|
||||||
|
TEST_F(ThemeIconRenderTest, SelectedAppColorKeepsAContrastingTint)
|
||||||
|
{
|
||||||
|
QPalette palette = qApp->palette();
|
||||||
|
palette.setColor(QPalette::Active, QPalette::Highlight, QColor(Qt::black));
|
||||||
|
palette.setColor(QPalette::Active, QPalette::HighlightedText, QColor(Qt::white));
|
||||||
|
qApp->setPalette(palette);
|
||||||
|
|
||||||
|
EXPECT_EQ(IconTint(AppColor::Positive).resolve(QIcon::Selected), themeManager->appColor(AppColor::Positive));
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,41 @@ TEST_F(ThemeMigrationTest, IsIdempotent)
|
||||||
EXPECT_EQ(readThemeFile(themesPath, CURRENT, "style.css").toStdString(), std::string("css"));
|
EXPECT_EQ(readThemeFile(themesPath, CURRENT, "style.css").toStdString(), std::string("css"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(ThemeAppColorTest, SemanticDefaultsFollowTheScheme)
|
||||||
|
{
|
||||||
|
EXPECT_EQ(defaultAppColor(AppColor::Positive, false), QColor(QStringLiteral("#1b7f3b")));
|
||||||
|
EXPECT_EQ(defaultAppColor(AppColor::Positive, true), QColor(QStringLiteral("#6dd58c")));
|
||||||
|
EXPECT_EQ(defaultAppColor(AppColor::Negative, false), QColor(QStringLiteral("#b3261e")));
|
||||||
|
EXPECT_EQ(defaultAppColor(AppColor::Negative, true), QColor(QStringLiteral("#ff8a80")));
|
||||||
|
// Accent roles are derived from the palette, so they have no static default.
|
||||||
|
EXPECT_FALSE(defaultAppColor(AppColor::AccentStrong, false).isValid());
|
||||||
|
EXPECT_FALSE(defaultAppColor(AppColor::AccentSoft, true).isValid());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ThemeAppColorTest, PaletteConfigRoundTripsSemanticRoles)
|
||||||
|
{
|
||||||
|
QTemporaryDir dir;
|
||||||
|
ASSERT_TRUE(dir.isValid());
|
||||||
|
|
||||||
|
PaletteConfig config;
|
||||||
|
const QColor positive(QStringLiteral("#1b7f3b"));
|
||||||
|
const QColor negative(QStringLiteral("#b3261e"));
|
||||||
|
config.appColors[AppColor::Positive] = positive;
|
||||||
|
config.appColors[AppColor::Negative] = negative;
|
||||||
|
|
||||||
|
const QString path = QDir(dir.path()).absoluteFilePath(QStringLiteral("palette-light.toml"));
|
||||||
|
QFile file(path);
|
||||||
|
ASSERT_TRUE(file.open(QIODevice::WriteOnly | QIODevice::Text));
|
||||||
|
file.write(config.toToml().toUtf8());
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
const PaletteConfig parsed = PaletteConfig::fromFile(path);
|
||||||
|
ASSERT_TRUE(parsed.appColors.contains(AppColor::Positive));
|
||||||
|
ASSERT_TRUE(parsed.appColors.contains(AppColor::Negative));
|
||||||
|
EXPECT_EQ(parsed.appColors.value(AppColor::Positive), positive);
|
||||||
|
EXPECT_EQ(parsed.appColors.value(AppColor::Negative), negative);
|
||||||
|
}
|
||||||
|
|
||||||
// Every test brings its own main(): GTest is not guaranteed to be found on the build machines, and
|
// Every test brings its own main(): GTest is not guaranteed to be found on the build machines, and
|
||||||
// the fallback then links the bare gtest library without gtest_main.
|
// the fallback then links the bare gtest library without gtest_main.
|
||||||
int main(int argc, char **argv)
|
int main(int argc, char **argv)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue