mirror of
https://invent.kde.org/marcoa/shift-shell.git
synced 2026-06-11 00:47:22 +00:00
Add wallpaper-driven Shift dynamic theming
Expose theme and accent controls in MobileShellSettings and the\nAppearance KCM. Generate ShiftWallpaperDark/Light from Shift base\nschemes, apply them through KDE color tools, and debounce wallpaper\ncolor updates. Register a dedicated dynamic-theming regression test.
This commit is contained in:
parent
4af811c97c
commit
08264e5759
9 changed files with 938 additions and 17 deletions
|
|
@ -12,6 +12,7 @@ target_link_libraries(shellsettingsplugin PRIVATE
|
|||
Qt::DBus
|
||||
Qt::Gui
|
||||
Qt::Quick
|
||||
KF6::ConfigCore
|
||||
Plasma::KWaylandClient
|
||||
KF6::Service
|
||||
KF6::Package
|
||||
|
|
|
|||
|
|
@ -15,16 +15,163 @@
|
|||
#include <QDBusMessage>
|
||||
#include <QDBusPendingCall>
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QProcess>
|
||||
#include <QStandardPaths>
|
||||
#include <QtMath>
|
||||
|
||||
const QString CONFIG_FILE = QStringLiteral("plasmamobilerc");
|
||||
const QString GENERAL_CONFIG_GROUP = QStringLiteral("General");
|
||||
const QString KDE_GLOBALS_CONFIG_GROUP = QStringLiteral("General");
|
||||
const QString LOCKSCREEN_CONFIG_GROUP = QStringLiteral("Lockscreen");
|
||||
const QString QUICKSETTINGS_CONFIG_GROUP = QStringLiteral("QuickSettings");
|
||||
const QString WALLPAPER_THEME_MANUAL_DARK_KEY = QStringLiteral("wallpaperThemeManualDarkTheme");
|
||||
const QString SHIFT_DARK_COLOR_SCHEME = QStringLiteral("ShiftDark");
|
||||
const QString SHIFT_LIGHT_COLOR_SCHEME = QStringLiteral("ShiftLight");
|
||||
const QString SHIFT_WALLPAPER_DARK_COLOR_SCHEME = QStringLiteral("ShiftWallpaperDark");
|
||||
const QString SHIFT_WALLPAPER_LIGHT_COLOR_SCHEME = QStringLiteral("ShiftWallpaperLight");
|
||||
const QString SHIFT_DARK_PLASMA_THEME = QStringLiteral("shift-dark");
|
||||
const QString SHIFT_LIGHT_PLASMA_THEME = QStringLiteral("shift-light");
|
||||
constexpr qreal MIN_NORMAL_CONTRAST = 4.5;
|
||||
constexpr qreal MIN_INACTIVE_CONTRAST = 3.0;
|
||||
constexpr qreal MIN_LINK_CONTRAST = 3.5;
|
||||
|
||||
namespace
|
||||
{
|
||||
QString baseColorSchemeName(bool dark)
|
||||
{
|
||||
return dark ? SHIFT_DARK_COLOR_SCHEME : SHIFT_LIGHT_COLOR_SCHEME;
|
||||
}
|
||||
|
||||
QString wallpaperColorSchemeName(bool dark)
|
||||
{
|
||||
return dark ? SHIFT_WALLPAPER_DARK_COLOR_SCHEME : SHIFT_WALLPAPER_LIGHT_COLOR_SCHEME;
|
||||
}
|
||||
|
||||
QString wallpaperColorSchemeDisplayName(bool dark)
|
||||
{
|
||||
return dark ? QStringLiteral("SHIFT Wallpaper Dark") : QStringLiteral("SHIFT Wallpaper Light");
|
||||
}
|
||||
|
||||
QString rgbString(const QColor &color)
|
||||
{
|
||||
return QStringLiteral("%1,%2,%3").arg(color.red()).arg(color.green()).arg(color.blue());
|
||||
}
|
||||
|
||||
qreal relativeLuminance(const QColor &color)
|
||||
{
|
||||
return 0.2126 * color.redF() + 0.7152 * color.greenF() + 0.0722 * color.blueF();
|
||||
}
|
||||
|
||||
qreal linearizedChannel(qreal channel)
|
||||
{
|
||||
return channel <= 0.04045 ? channel / 12.92 : qPow((channel + 0.055) / 1.055, 2.4);
|
||||
}
|
||||
|
||||
qreal wcagLuminance(const QColor &color)
|
||||
{
|
||||
return 0.2126 * linearizedChannel(color.redF()) + 0.7152 * linearizedChannel(color.greenF()) + 0.0722 * linearizedChannel(color.blueF());
|
||||
}
|
||||
|
||||
qreal contrastRatio(const QColor &left, const QColor &right)
|
||||
{
|
||||
const qreal leftLum = wcagLuminance(left);
|
||||
const qreal rightLum = wcagLuminance(right);
|
||||
const qreal lighter = qMax(leftLum, rightLum);
|
||||
const qreal darker = qMin(leftLum, rightLum);
|
||||
return (lighter + 0.05) / (darker + 0.05);
|
||||
}
|
||||
|
||||
QColor contrastingTextColor(const QColor &color);
|
||||
QColor blendColors(const QColor &base, const QColor &overlay, qreal overlayAmount);
|
||||
|
||||
QColor ensureContrast(const QColor &foreground, const QColor &background, qreal minimumContrast)
|
||||
{
|
||||
if (contrastRatio(foreground, background) >= minimumContrast) {
|
||||
return foreground;
|
||||
}
|
||||
|
||||
const QColor highContrastTarget = contrastingTextColor(background);
|
||||
for (int i = 1; i <= 12; ++i) {
|
||||
const qreal amount = i / 12.0;
|
||||
const QColor candidate = blendColors(foreground, highContrastTarget, amount);
|
||||
if (contrastRatio(candidate, background) >= minimumContrast) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return highContrastTarget;
|
||||
}
|
||||
|
||||
QColor contrastingTextColor(const QColor &color)
|
||||
{
|
||||
const QColor white{Qt::white};
|
||||
const QColor black{Qt::black};
|
||||
return contrastRatio(color, white) >= contrastRatio(color, black) ? white : black;
|
||||
}
|
||||
|
||||
QColor blendColors(const QColor &base, const QColor &overlay, qreal overlayAmount)
|
||||
{
|
||||
const qreal clampedAmount = qBound(0.0, overlayAmount, 1.0);
|
||||
const qreal baseAmount = 1.0 - clampedAmount;
|
||||
return QColor::fromRgbF(base.redF() * baseAmount + overlay.redF() * clampedAmount,
|
||||
base.greenF() * baseAmount + overlay.greenF() * clampedAmount,
|
||||
base.blueF() * baseAmount + overlay.blueF() * clampedAmount,
|
||||
1.0);
|
||||
}
|
||||
|
||||
void writeAccentEntries(const KSharedConfig::Ptr &config, const QString &groupName, const QColor &accentColor)
|
||||
{
|
||||
auto group = KConfigGroup{config, groupName};
|
||||
group.writeEntry("DecorationFocus", rgbString(accentColor));
|
||||
group.writeEntry("DecorationHover", rgbString(accentColor));
|
||||
group.writeEntry("ForegroundActive", rgbString(accentColor));
|
||||
}
|
||||
|
||||
void tintBackgroundEntries(const KSharedConfig::Ptr &config, const QString &groupName, const QColor &accentColor, bool dark)
|
||||
{
|
||||
auto group = KConfigGroup{config, groupName};
|
||||
const QColor defaultBase = dark ? QColor(38, 41, 47) : QColor(244, 246, 251);
|
||||
const QColor baseNormal = group.readEntry("BackgroundNormal", defaultBase);
|
||||
const QColor baseAlternate = group.readEntry("BackgroundAlternate", baseNormal);
|
||||
const qreal normalBlend = dark ? 0.22 : 0.12;
|
||||
const qreal alternateBlend = dark ? 0.16 : 0.08;
|
||||
|
||||
group.writeEntry("BackgroundNormal", rgbString(blendColors(baseNormal, accentColor, normalBlend)));
|
||||
group.writeEntry("BackgroundAlternate", rgbString(blendColors(baseAlternate, accentColor, alternateBlend)));
|
||||
}
|
||||
|
||||
void tuneForegroundEntries(const KSharedConfig::Ptr &config, const QString &groupName, const QColor &accentColor, bool dark)
|
||||
{
|
||||
auto group = KConfigGroup{config, groupName};
|
||||
const QColor bgNormal = group.readEntry("BackgroundNormal", dark ? QColor(34, 37, 50) : QColor(252, 252, 252));
|
||||
const QColor baseNormal = contrastingTextColor(bgNormal);
|
||||
const QColor baseInactive = blendColors(baseNormal, bgNormal, dark ? 0.44 : 0.56);
|
||||
const QColor baseLink = dark ? blendColors(accentColor, QColor(Qt::white), 0.28) : blendColors(accentColor, QColor(Qt::black), 0.24);
|
||||
const QColor fgNormal = ensureContrast(baseNormal, bgNormal, MIN_NORMAL_CONTRAST);
|
||||
const QColor fgInactive = ensureContrast(baseInactive, bgNormal, MIN_INACTIVE_CONTRAST);
|
||||
const QColor fgLink = ensureContrast(baseLink, bgNormal, MIN_LINK_CONTRAST);
|
||||
const QColor fgActive = ensureContrast(accentColor, bgNormal, MIN_LINK_CONTRAST);
|
||||
|
||||
group.writeEntry("ForegroundNormal", rgbString(fgNormal));
|
||||
group.writeEntry("ForegroundInactive", rgbString(fgInactive));
|
||||
group.writeEntry("ForegroundActive", rgbString(fgActive));
|
||||
group.writeEntry("ForegroundLink", rgbString(fgLink));
|
||||
}
|
||||
}
|
||||
|
||||
MobileShellSettings::MobileShellSettings(QObject *parent)
|
||||
: QObject{parent}
|
||||
, m_config{KSharedConfig::openConfig(CONFIG_FILE)}
|
||||
, m_kdeGlobalsConfig{KSharedConfig::openConfig()}
|
||||
{
|
||||
m_wallpaperThemeTimer.setSingleShot(true);
|
||||
m_wallpaperThemeTimer.setInterval(450);
|
||||
connect(&m_wallpaperThemeTimer, &QTimer::timeout, this, [this]() -> void {
|
||||
setDarkThemeEnabled(m_pendingWallpaperThemeDark);
|
||||
});
|
||||
|
||||
m_configWatcher = KConfigWatcher::create(m_config);
|
||||
connect(m_configWatcher.data(), &KConfigWatcher::configChanged, this, [this](const KConfigGroup &group, const QByteArrayList &names) -> void {
|
||||
Q_UNUSED(names)
|
||||
|
|
@ -53,6 +200,7 @@ MobileShellSettings::MobileShellSettings(QObject *parent)
|
|||
Q_EMIT dynamicTilingLayoutStateChanged();
|
||||
Q_EMIT snapLayoutsEnabledChanged();
|
||||
Q_EMIT allowLogoutChanged();
|
||||
Q_EMIT wallpaperThemeEnabledChanged();
|
||||
}
|
||||
if (group.name() == LOCKSCREEN_CONFIG_GROUP) {
|
||||
Q_EMIT lockscreenLeftButtonActionChanged();
|
||||
|
|
@ -62,6 +210,24 @@ MobileShellSettings::MobileShellSettings(QObject *parent)
|
|||
Q_EMIT quickSettingsColumnsChanged();
|
||||
}
|
||||
});
|
||||
|
||||
m_kdeGlobalsConfigWatcher = KConfigWatcher::create(m_kdeGlobalsConfig);
|
||||
connect(m_kdeGlobalsConfigWatcher.data(), &KConfigWatcher::configChanged, this, [this](const KConfigGroup &group, const QByteArrayList &names) -> void {
|
||||
if (group.name() != KDE_GLOBALS_CONFIG_GROUP) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (names.contains(QByteArrayLiteral("ColorScheme"))) {
|
||||
Q_EMIT colorSchemeChanged();
|
||||
Q_EMIT darkThemeEnabledChanged();
|
||||
}
|
||||
if (names.contains(QByteArrayLiteral("AccentColor")) || names.contains(QByteArrayLiteral("LastUsedCustomAccentColor"))) {
|
||||
Q_EMIT accentColorChanged();
|
||||
}
|
||||
if (names.contains(QByteArrayLiteral("accentColorFromWallpaper"))) {
|
||||
Q_EMIT wallpaperAccentEnabledChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool MobileShellSettings::vibrationsEnabled() const
|
||||
|
|
@ -243,6 +409,312 @@ void MobileShellSettings::setConvergenceModeEnabled(bool enabled)
|
|||
job->start();
|
||||
}
|
||||
|
||||
QString MobileShellSettings::colorScheme() const
|
||||
{
|
||||
auto group = KConfigGroup{m_kdeGlobalsConfig, KDE_GLOBALS_CONFIG_GROUP};
|
||||
return group.readEntry("ColorScheme", SHIFT_DARK_COLOR_SCHEME);
|
||||
}
|
||||
|
||||
bool MobileShellSettings::darkThemeEnabled() const
|
||||
{
|
||||
return isDarkColorScheme(colorScheme());
|
||||
}
|
||||
|
||||
void MobileShellSettings::setDarkThemeEnabled(bool enabled)
|
||||
{
|
||||
const QString currentColorScheme = colorScheme();
|
||||
const QString nextColorScheme = effectiveColorSchemeName(enabled);
|
||||
const QString plasmaTheme = enabled ? SHIFT_DARK_PLASMA_THEME : SHIFT_LIGHT_PLASMA_THEME;
|
||||
QColor accentOverride;
|
||||
|
||||
if (wallpaperAccentEnabled() && m_lastWallpaperThemeColor.isValid() && m_lastWallpaperThemeColor.alpha() != 0) {
|
||||
accentOverride = m_lastWallpaperThemeColor;
|
||||
} else if (!wallpaperAccentEnabled()) {
|
||||
const QColor manualAccent = accentColor();
|
||||
if (manualAccent.isValid() && manualAccent.alpha() != 0) {
|
||||
accentOverride = manualAccent;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentColorScheme == nextColorScheme && nextColorScheme == baseColorSchemeName(enabled)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (accentOverride.isValid() && accentOverride.alpha() != 0) {
|
||||
applyColorScheme({QStringLiteral("--accent-color"), accentOverride.name(QColor::HexRgb), nextColorScheme});
|
||||
} else {
|
||||
QProcess::execute(QStringLiteral("plasma-apply-colorscheme"), {nextColorScheme});
|
||||
m_kdeGlobalsConfig->reparseConfiguration();
|
||||
}
|
||||
QProcess::execute(QStringLiteral("plasma-apply-desktoptheme"), {plasmaTheme});
|
||||
|
||||
Q_EMIT colorSchemeChanged();
|
||||
Q_EMIT darkThemeEnabledChanged();
|
||||
}
|
||||
|
||||
bool MobileShellSettings::wallpaperThemeEnabled() const
|
||||
{
|
||||
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
|
||||
return group.readEntry("wallpaperThemeEnabled", false);
|
||||
}
|
||||
|
||||
void MobileShellSettings::setWallpaperThemeEnabled(bool enabled)
|
||||
{
|
||||
if (wallpaperThemeEnabled() == enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
|
||||
if (enabled) {
|
||||
group.writeEntry(WALLPAPER_THEME_MANUAL_DARK_KEY, darkThemeEnabled(), KConfigGroup::Notify);
|
||||
}
|
||||
group.writeEntry("wallpaperThemeEnabled", enabled, KConfigGroup::Notify);
|
||||
m_config->sync();
|
||||
|
||||
if (!enabled) {
|
||||
m_wallpaperThemeTimer.stop();
|
||||
setDarkThemeEnabled(group.readEntry(WALLPAPER_THEME_MANUAL_DARK_KEY, darkThemeEnabled()));
|
||||
}
|
||||
|
||||
Q_EMIT wallpaperThemeEnabledChanged();
|
||||
}
|
||||
|
||||
void MobileShellSettings::applyWallpaperThemeColor(const QColor &color)
|
||||
{
|
||||
if (!wallpaperThemeEnabled() || !color.isValid() || color.alpha() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
QColor opaqueColor = color;
|
||||
opaqueColor.setAlpha(255);
|
||||
|
||||
const bool colorChanged = m_lastWallpaperThemeColor != opaqueColor;
|
||||
m_lastWallpaperThemeColor = opaqueColor;
|
||||
|
||||
if (colorChanged) {
|
||||
Q_EMIT wallpaperThemeColorChanged();
|
||||
}
|
||||
|
||||
const qreal luminance = relativeLuminance(opaqueColor);
|
||||
const bool shouldUseDarkTheme = luminance < 0.5;
|
||||
const bool sameThemeBucket = darkThemeEnabled() == shouldUseDarkTheme;
|
||||
|
||||
if (wallpaperAccentEnabled() && colorChanged && sameThemeBucket) {
|
||||
applyColorScheme({QStringLiteral("--accent-color"), opaqueColor.name(QColor::HexRgb), colorScheme()});
|
||||
}
|
||||
|
||||
if (!m_wallpaperThemeTimer.isActive() && darkThemeEnabled() == shouldUseDarkTheme && !colorChanged) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_pendingWallpaperThemeDark = shouldUseDarkTheme;
|
||||
m_wallpaperThemeTimer.start();
|
||||
}
|
||||
|
||||
QColor MobileShellSettings::wallpaperThemeColor() const
|
||||
{
|
||||
return m_lastWallpaperThemeColor;
|
||||
}
|
||||
|
||||
QColor MobileShellSettings::accentColor() const
|
||||
{
|
||||
auto group = KConfigGroup{m_kdeGlobalsConfig, KDE_GLOBALS_CONFIG_GROUP};
|
||||
return group.readEntry("AccentColor", QColor(Qt::transparent));
|
||||
}
|
||||
|
||||
void MobileShellSettings::setAccentColor(const QColor &color)
|
||||
{
|
||||
if (!color.isValid() || color.alpha() == 0) {
|
||||
resetAccentColor();
|
||||
return;
|
||||
}
|
||||
|
||||
QColor opaqueColor = color;
|
||||
opaqueColor.setAlpha(255);
|
||||
|
||||
auto group = KConfigGroup{m_kdeGlobalsConfig, KDE_GLOBALS_CONFIG_GROUP};
|
||||
group.writeEntry("accentColorFromWallpaper", false, KConfigGroup::Notify);
|
||||
group.writeEntry("LastUsedCustomAccentColor", opaqueColor, KConfigGroup::Notify);
|
||||
m_kdeGlobalsConfig->sync();
|
||||
|
||||
applyColorScheme({QStringLiteral("--accent-color"), opaqueColor.name(QColor::HexRgb), colorScheme()});
|
||||
|
||||
Q_EMIT wallpaperAccentEnabledChanged();
|
||||
Q_EMIT accentColorChanged();
|
||||
}
|
||||
|
||||
bool MobileShellSettings::wallpaperAccentEnabled() const
|
||||
{
|
||||
auto group = KConfigGroup{m_kdeGlobalsConfig, KDE_GLOBALS_CONFIG_GROUP};
|
||||
return group.readEntry("accentColorFromWallpaper", false);
|
||||
}
|
||||
|
||||
void MobileShellSettings::setWallpaperAccentEnabled(bool enabled)
|
||||
{
|
||||
if (wallpaperAccentEnabled() == enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto group = KConfigGroup{m_kdeGlobalsConfig, KDE_GLOBALS_CONFIG_GROUP};
|
||||
group.writeEntry("accentColorFromWallpaper", enabled, KConfigGroup::Notify);
|
||||
m_kdeGlobalsConfig->sync();
|
||||
|
||||
if (enabled && m_lastWallpaperThemeColor.isValid() && m_lastWallpaperThemeColor.alpha() != 0) {
|
||||
applyColorScheme({QStringLiteral("--accent-color"), m_lastWallpaperThemeColor.name(QColor::HexRgb), colorScheme()});
|
||||
Q_EMIT accentColorChanged();
|
||||
}
|
||||
|
||||
if (!enabled) {
|
||||
const QColor lastCustomColor = lastUsedCustomAccentColor();
|
||||
if (lastCustomColor.isValid() && lastCustomColor.alpha() != 0) {
|
||||
applyColorScheme({QStringLiteral("--accent-color"), lastCustomColor.name(QColor::HexRgb), colorScheme()});
|
||||
} else {
|
||||
resetAccentColor();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Q_EMIT wallpaperAccentEnabledChanged();
|
||||
}
|
||||
|
||||
void MobileShellSettings::resetAccentColor()
|
||||
{
|
||||
auto group = KConfigGroup{m_kdeGlobalsConfig, KDE_GLOBALS_CONFIG_GROUP};
|
||||
group.writeEntry("accentColorFromWallpaper", false, KConfigGroup::Notify);
|
||||
group.deleteEntry("AccentColor", KConfigGroup::Notify);
|
||||
m_kdeGlobalsConfig->sync();
|
||||
|
||||
applyColorScheme({colorScheme()});
|
||||
|
||||
Q_EMIT wallpaperAccentEnabledChanged();
|
||||
Q_EMIT accentColorChanged();
|
||||
}
|
||||
|
||||
QColor MobileShellSettings::lastUsedCustomAccentColor() const
|
||||
{
|
||||
auto group = KConfigGroup{m_kdeGlobalsConfig, KDE_GLOBALS_CONFIG_GROUP};
|
||||
return group.readEntry("LastUsedCustomAccentColor", QColor(Qt::transparent));
|
||||
}
|
||||
|
||||
void MobileShellSettings::applyColorScheme(const QStringList &arguments)
|
||||
{
|
||||
QProcess::execute(QStringLiteral("plasma-apply-colorscheme"), arguments);
|
||||
m_kdeGlobalsConfig->reparseConfiguration();
|
||||
}
|
||||
|
||||
QString MobileShellSettings::effectiveColorSchemeName(bool dark) const
|
||||
{
|
||||
if (!wallpaperThemeEnabled() || !m_lastWallpaperThemeColor.isValid() || m_lastWallpaperThemeColor.alpha() == 0) {
|
||||
return baseColorSchemeName(dark);
|
||||
}
|
||||
|
||||
if (!ensureWallpaperColorScheme(m_lastWallpaperThemeColor, dark)) {
|
||||
return baseColorSchemeName(dark);
|
||||
}
|
||||
|
||||
return wallpaperColorSchemeName(dark);
|
||||
}
|
||||
|
||||
bool MobileShellSettings::isDarkColorScheme(const QString &schemeName) const
|
||||
{
|
||||
return schemeName == SHIFT_DARK_COLOR_SCHEME || schemeName == SHIFT_WALLPAPER_DARK_COLOR_SCHEME;
|
||||
}
|
||||
|
||||
bool MobileShellSettings::ensureWallpaperColorScheme(const QColor &accentColor, bool dark) const
|
||||
{
|
||||
if (!accentColor.isValid() || accentColor.alpha() == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString sourcePath =
|
||||
QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("color-schemes/%1.colors").arg(baseColorSchemeName(dark)));
|
||||
if (sourcePath.isEmpty()) {
|
||||
qWarning() << "Unable to locate base Shift color scheme for wallpaper generation:" << baseColorSchemeName(dark);
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString outputDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/color-schemes");
|
||||
if (!QDir().mkpath(outputDir)) {
|
||||
qWarning() << "Unable to create dynamic color-scheme directory:" << outputDir;
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString outputPath = outputDir + QStringLiteral("/%1.colors").arg(wallpaperColorSchemeName(dark));
|
||||
QFile::remove(outputPath);
|
||||
if (!QFile::copy(sourcePath, outputPath)) {
|
||||
qWarning() << "Unable to create wallpaper-derived color scheme:" << outputPath;
|
||||
return false;
|
||||
}
|
||||
|
||||
auto generatedConfig = KSharedConfig::openConfig(outputPath, KConfig::SimpleConfig);
|
||||
auto generalGroup = KConfigGroup{generatedConfig, QStringLiteral("General")};
|
||||
generalGroup.writeEntry("ColorScheme", wallpaperColorSchemeName(dark));
|
||||
generalGroup.writeEntry("Name", wallpaperColorSchemeDisplayName(dark));
|
||||
|
||||
writeAccentEntries(generatedConfig, QStringLiteral("Colors:Button"), accentColor);
|
||||
writeAccentEntries(generatedConfig, QStringLiteral("Colors:Complementary"), accentColor);
|
||||
writeAccentEntries(generatedConfig, QStringLiteral("Colors:Header"), accentColor);
|
||||
writeAccentEntries(generatedConfig, QStringLiteral("Colors:Header][Inactive"), accentColor);
|
||||
writeAccentEntries(generatedConfig, QStringLiteral("Colors:Tooltip"), accentColor);
|
||||
writeAccentEntries(generatedConfig, QStringLiteral("Colors:View"), accentColor);
|
||||
writeAccentEntries(generatedConfig, QStringLiteral("Colors:Window"), accentColor);
|
||||
|
||||
tintBackgroundEntries(generatedConfig, QStringLiteral("Colors:Button"), accentColor, dark);
|
||||
tintBackgroundEntries(generatedConfig, QStringLiteral("Colors:View"), accentColor, dark);
|
||||
tintBackgroundEntries(generatedConfig, QStringLiteral("Colors:Window"), accentColor, dark);
|
||||
tintBackgroundEntries(generatedConfig, QStringLiteral("Colors:Header"), accentColor, dark);
|
||||
tintBackgroundEntries(generatedConfig, QStringLiteral("Colors:Header][Inactive"), accentColor, dark);
|
||||
tintBackgroundEntries(generatedConfig, QStringLiteral("Colors:Tooltip"), accentColor, dark);
|
||||
|
||||
tuneForegroundEntries(generatedConfig, QStringLiteral("Colors:Button"), accentColor, dark);
|
||||
tuneForegroundEntries(generatedConfig, QStringLiteral("Colors:View"), accentColor, dark);
|
||||
tuneForegroundEntries(generatedConfig, QStringLiteral("Colors:Window"), accentColor, dark);
|
||||
tuneForegroundEntries(generatedConfig, QStringLiteral("Colors:Header"), accentColor, dark);
|
||||
tuneForegroundEntries(generatedConfig, QStringLiteral("Colors:Header][Inactive"), accentColor, dark);
|
||||
tuneForegroundEntries(generatedConfig, QStringLiteral("Colors:Tooltip"), accentColor, dark);
|
||||
|
||||
auto complementaryGroup = KConfigGroup{generatedConfig, QStringLiteral("Colors:Complementary")};
|
||||
const QColor complementaryBase = complementaryGroup.readEntry("BackgroundNormal", dark ? QColor(24, 27, 38) : QColor(42, 46, 50));
|
||||
complementaryGroup.writeEntry("BackgroundNormal", rgbString(blendColors(complementaryBase, accentColor, dark ? 0.18 : 0.14)));
|
||||
complementaryGroup.writeEntry("BackgroundAlternate", rgbString(blendColors(complementaryBase, accentColor, dark ? 0.12 : 0.09)));
|
||||
tuneForegroundEntries(generatedConfig, QStringLiteral("Colors:Complementary"), accentColor, dark);
|
||||
|
||||
auto selectionGroup = KConfigGroup{generatedConfig, QStringLiteral("Colors:Selection")};
|
||||
const QColor selectionBackground = blendColors(accentColor, dark ? QColor(18, 20, 28) : QColor(255, 255, 255), dark ? 0.82 : 0.9);
|
||||
const QColor selectionAlternate = blendColors(selectionBackground, dark ? QColor(0, 0, 0) : QColor(255, 255, 255), dark ? 0.16 : 0.24);
|
||||
const QColor selectionForeground = ensureContrast(contrastingTextColor(selectionBackground), selectionBackground, MIN_NORMAL_CONTRAST);
|
||||
const QColor selectionInactive =
|
||||
ensureContrast(blendColors(selectionForeground, selectionBackground, dark ? 0.4 : 0.52), selectionBackground, MIN_INACTIVE_CONTRAST);
|
||||
const QColor selectionLink = ensureContrast(blendColors(accentColor, selectionForeground, dark ? 0.3 : 0.2), selectionBackground, MIN_LINK_CONTRAST);
|
||||
selectionGroup.writeEntry("BackgroundNormal", rgbString(selectionBackground));
|
||||
selectionGroup.writeEntry("BackgroundAlternate", rgbString(selectionAlternate));
|
||||
selectionGroup.writeEntry("DecorationFocus", rgbString(accentColor));
|
||||
selectionGroup.writeEntry("DecorationHover", rgbString(accentColor));
|
||||
selectionGroup.writeEntry("ForegroundActive", rgbString(selectionForeground));
|
||||
selectionGroup.writeEntry("ForegroundNormal", rgbString(selectionForeground));
|
||||
selectionGroup.writeEntry("ForegroundInactive", rgbString(selectionInactive));
|
||||
selectionGroup.writeEntry("ForegroundLink", rgbString(selectionLink));
|
||||
|
||||
auto wmGroup = KConfigGroup{generatedConfig, QStringLiteral("WM")};
|
||||
const QColor currentActiveBackground = wmGroup.readEntry("activeBackground", accentColor);
|
||||
const QColor tintedActiveBackground = blendColors(currentActiveBackground, accentColor, dark ? 0.42 : 0.26);
|
||||
const QColor currentInactiveBackground = wmGroup.readEntry("inactiveBackground", tintedActiveBackground);
|
||||
const QColor tintedInactiveBackground = blendColors(currentInactiveBackground, accentColor, dark ? 0.24 : 0.14);
|
||||
const QColor activeForeground = ensureContrast(contrastingTextColor(tintedActiveBackground), tintedActiveBackground, MIN_NORMAL_CONTRAST);
|
||||
const QColor inactiveForeground = ensureContrast(blendColors(contrastingTextColor(tintedInactiveBackground), tintedInactiveBackground, dark ? 0.36 : 0.52),
|
||||
tintedInactiveBackground,
|
||||
MIN_INACTIVE_CONTRAST);
|
||||
wmGroup.writeEntry("activeBackground", rgbString(tintedActiveBackground));
|
||||
wmGroup.writeEntry("activeBlend", rgbString(activeForeground));
|
||||
wmGroup.writeEntry("activeForeground", rgbString(activeForeground));
|
||||
wmGroup.writeEntry("inactiveBackground", rgbString(tintedInactiveBackground));
|
||||
wmGroup.writeEntry("inactiveBlend", rgbString(inactiveForeground));
|
||||
wmGroup.writeEntry("inactiveForeground", rgbString(inactiveForeground));
|
||||
|
||||
generatedConfig->sync();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MobileShellSettings::autoHidePanelsEnabled() const
|
||||
{
|
||||
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
|
||||
|
|
|
|||
|
|
@ -9,9 +9,11 @@
|
|||
#include <KConfigGroup>
|
||||
#include <KConfigWatcher>
|
||||
#include <KSharedConfig>
|
||||
#include <QColor>
|
||||
#include <QDBusConnection>
|
||||
#include <QObject>
|
||||
#include <QStringList>
|
||||
#include <QTimer>
|
||||
#include <qqmlregistration.h>
|
||||
|
||||
/**
|
||||
|
|
@ -50,6 +52,14 @@ class MobileShellSettings : public QObject
|
|||
// convergence mode
|
||||
Q_PROPERTY(bool convergenceModeEnabled READ convergenceModeEnabled WRITE setConvergenceModeEnabled NOTIFY convergenceModeEnabledChanged)
|
||||
|
||||
// theme
|
||||
Q_PROPERTY(QString colorScheme READ colorScheme NOTIFY colorSchemeChanged)
|
||||
Q_PROPERTY(bool darkThemeEnabled READ darkThemeEnabled WRITE setDarkThemeEnabled NOTIFY darkThemeEnabledChanged)
|
||||
Q_PROPERTY(bool wallpaperThemeEnabled READ wallpaperThemeEnabled WRITE setWallpaperThemeEnabled NOTIFY wallpaperThemeEnabledChanged)
|
||||
Q_PROPERTY(QColor wallpaperThemeColor READ wallpaperThemeColor NOTIFY wallpaperThemeColorChanged)
|
||||
Q_PROPERTY(QColor accentColor READ accentColor WRITE setAccentColor NOTIFY accentColorChanged)
|
||||
Q_PROPERTY(bool wallpaperAccentEnabled READ wallpaperAccentEnabled WRITE setWallpaperAccentEnabled NOTIFY wallpaperAccentEnabledChanged)
|
||||
|
||||
// Auto Hide Panels
|
||||
Q_PROPERTY(bool autoHidePanelsEnabled READ autoHidePanelsEnabled WRITE setAutoHidePanelsEnabled NOTIFY autoHidePanelsEnabledChanged)
|
||||
|
||||
|
|
@ -274,6 +284,19 @@ public:
|
|||
*/
|
||||
void setConvergenceModeEnabled(bool enabled);
|
||||
|
||||
QString colorScheme() const;
|
||||
bool darkThemeEnabled() const;
|
||||
void setDarkThemeEnabled(bool enabled);
|
||||
bool wallpaperThemeEnabled() const;
|
||||
void setWallpaperThemeEnabled(bool enabled);
|
||||
Q_INVOKABLE void applyWallpaperThemeColor(const QColor &color);
|
||||
QColor wallpaperThemeColor() const;
|
||||
QColor accentColor() const;
|
||||
void setAccentColor(const QColor &color);
|
||||
bool wallpaperAccentEnabled() const;
|
||||
void setWallpaperAccentEnabled(bool enabled);
|
||||
Q_INVOKABLE void resetAccentColor();
|
||||
|
||||
/**
|
||||
* Whether Auto Hide Panels is enabled.
|
||||
*/
|
||||
|
|
@ -368,6 +391,12 @@ Q_SIGNALS:
|
|||
void actionDrawerTopRightModeChanged();
|
||||
void quickSettingsColumnsChanged();
|
||||
void convergenceModeEnabledChanged();
|
||||
void colorSchemeChanged();
|
||||
void darkThemeEnabledChanged();
|
||||
void wallpaperThemeEnabledChanged();
|
||||
void wallpaperThemeColorChanged();
|
||||
void accentColorChanged();
|
||||
void wallpaperAccentEnabledChanged();
|
||||
void autoHidePanelsEnabledChanged();
|
||||
void gamingModeEnabledChanged();
|
||||
void gamingDismissHintEnabledChanged();
|
||||
|
|
@ -383,7 +412,17 @@ Q_SIGNALS:
|
|||
|
||||
private:
|
||||
void updateNavigationBarsInPlasma();
|
||||
QColor lastUsedCustomAccentColor() const;
|
||||
void applyColorScheme(const QStringList &arguments);
|
||||
QString effectiveColorSchemeName(bool dark) const;
|
||||
bool isDarkColorScheme(const QString &schemeName) const;
|
||||
bool ensureWallpaperColorScheme(const QColor &accentColor, bool dark) const;
|
||||
|
||||
KConfigWatcher::Ptr m_configWatcher;
|
||||
KConfigWatcher::Ptr m_kdeGlobalsConfigWatcher;
|
||||
KSharedConfig::Ptr m_config;
|
||||
KSharedConfig::Ptr m_kdeGlobalsConfig;
|
||||
QTimer m_wallpaperThemeTimer;
|
||||
bool m_pendingWallpaperThemeDark = false;
|
||||
QColor m_lastWallpaperThemeColor;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@
|
|||
"Description[zh_TW]": "設定系統殼層介面",
|
||||
"EnabledByDefault": true,
|
||||
"FormFactors": [
|
||||
"desktop",
|
||||
"handset",
|
||||
"tablet"
|
||||
],
|
||||
|
|
@ -130,7 +131,7 @@
|
|||
"Name[zh_TW]": "殼層",
|
||||
"Website": "https://invent.kde.org/marcoa/shift-shell"
|
||||
},
|
||||
"X-KDE-Keywords": "system,shell,panel",
|
||||
"X-KDE-Keywords": "system,shell,panel,appearance,theme,wallpaper,dark,light,accent,color scheme",
|
||||
"X-KDE-Keywords[ar]": "نظام,صدفة,لوحة",
|
||||
"X-KDE-Keywords[az]": "sistem,örtük,panel",
|
||||
"X-KDE-Keywords[ca@valencia]": "sistema,intèrpret d'ordres,quadro",
|
||||
|
|
|
|||
217
kcms/mobileshell/ui/AppearanceForm.qml
Normal file
217
kcms/mobileshell/ui/AppearanceForm.qml
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Marco Allegretti
|
||||
* SPDX-License-Identifier: EUPL-1.2
|
||||
*/
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Dialogs as Dialogs
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.formcard 1 as FormCard
|
||||
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
|
||||
|
||||
FormCard.FormCardPage {
|
||||
id: root
|
||||
|
||||
title: i18n("Appearance")
|
||||
|
||||
readonly property bool usingSchemeAccent: ShellSettings.Settings.accentColor.a === 0
|
||||
readonly property color previewAccentColor: usingSchemeAccent ? Kirigami.Theme.highlightColor : ShellSettings.Settings.accentColor
|
||||
readonly property bool wallpaperThemeActive: ShellSettings.Settings.wallpaperThemeEnabled && ShellSettings.Settings.colorScheme.indexOf("ShiftWallpaper") === 0
|
||||
|
||||
Dialogs.ColorDialog {
|
||||
id: accentColorDialog
|
||||
|
||||
title: i18n("Accent Color")
|
||||
selectedColor: root.previewAccentColor
|
||||
onAccepted: ShellSettings.Settings.accentColor = selectedColor
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
selectedColor = root.previewAccentColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FormCard.FormHeader {
|
||||
title: i18n("Theme")
|
||||
}
|
||||
|
||||
FormCard.FormCard {
|
||||
Layout.topMargin: Kirigami.Units.gridUnit
|
||||
|
||||
FormCard.FormSwitchDelegate {
|
||||
id: darkThemeSwitch
|
||||
|
||||
text: i18n("Dark Theme")
|
||||
description: ShellSettings.Settings.wallpaperThemeEnabled
|
||||
? i18n("Disabled while Theme from Wallpaper is controlling light and dark mode.")
|
||||
: i18n("Use the dark Shift theme manually.")
|
||||
enabled: !ShellSettings.Settings.wallpaperThemeEnabled
|
||||
checked: ShellSettings.Settings.darkThemeEnabled
|
||||
onCheckedChanged: {
|
||||
if (checked != ShellSettings.Settings.darkThemeEnabled) {
|
||||
ShellSettings.Settings.darkThemeEnabled = checked;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator { above: darkThemeSwitch; below: wallpaperThemeSwitch }
|
||||
|
||||
FormCard.FormSwitchDelegate {
|
||||
id: wallpaperThemeSwitch
|
||||
|
||||
text: i18n("Theme from Wallpaper")
|
||||
description: i18n("Automatically switch between light and dark Shift themes and generate a matching wallpaper-derived color scheme.")
|
||||
checked: ShellSettings.Settings.wallpaperThemeEnabled
|
||||
onCheckedChanged: {
|
||||
if (checked != ShellSettings.Settings.wallpaperThemeEnabled) {
|
||||
ShellSettings.Settings.wallpaperThemeEnabled = checked;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator { above: wallpaperThemeSwitch; below: wallpaperAccentSwitch }
|
||||
|
||||
FormCard.FormSwitchDelegate {
|
||||
id: wallpaperAccentSwitch
|
||||
|
||||
text: i18n("Accent from Wallpaper")
|
||||
description: i18n("Use the wallpaper's extracted accent color instead of a manually selected accent.")
|
||||
checked: ShellSettings.Settings.wallpaperAccentEnabled
|
||||
onCheckedChanged: {
|
||||
if (checked != ShellSettings.Settings.wallpaperAccentEnabled) {
|
||||
ShellSettings.Settings.wallpaperAccentEnabled = checked;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator { above: wallpaperAccentSwitch; below: accentColorDelegate }
|
||||
|
||||
FormCard.AbstractFormDelegate {
|
||||
id: accentColorDelegate
|
||||
|
||||
enabled: !ShellSettings.Settings.wallpaperAccentEnabled
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
text: i18n("Accent Color")
|
||||
color: accentColorDelegate.enabled ? Kirigami.Theme.textColor : Kirigami.Theme.disabledTextColor
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.smallMedium
|
||||
radius: Math.min(width, height) / 2
|
||||
color: root.previewAccentColor
|
||||
border.width: 1
|
||||
border.color: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.28)
|
||||
}
|
||||
|
||||
QQC2.ToolButton {
|
||||
text: i18nc("@action:button", "Choose accent color")
|
||||
icon.name: "color-picker"
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
enabled: accentColorDelegate.enabled
|
||||
onClicked: accentColorDialog.open()
|
||||
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
|
||||
QQC2.ToolButton {
|
||||
text: i18nc("@action:button", "Use scheme accent")
|
||||
icon.name: "edit-clear"
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
enabled: accentColorDelegate.enabled && !root.usingSchemeAccent
|
||||
onClicked: ShellSettings.Settings.resetAccentColor()
|
||||
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FormCard.FormSectionText {
|
||||
visible: ShellSettings.Settings.wallpaperThemeEnabled
|
||||
text: root.wallpaperThemeActive
|
||||
? i18n("Wallpaper theming is active. SHIFT is currently using %1.", ShellSettings.Settings.colorScheme)
|
||||
: i18n("Wallpaper theming is enabled, but SHIFT is still waiting for a usable wallpaper color.")
|
||||
}
|
||||
|
||||
FormCard.FormCard {
|
||||
visible: ShellSettings.Settings.wallpaperThemeEnabled
|
||||
|
||||
FormCard.AbstractFormDelegate {
|
||||
contentItem: RowLayout {
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
text: i18n("Active Color Scheme")
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
text: ShellSettings.Settings.colorScheme
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
||||
FormCard.AbstractFormDelegate {
|
||||
contentItem: RowLayout {
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
text: i18n("Wallpaper Source Color")
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
text: ShellSettings.Settings.wallpaperThemeColor.a === 0
|
||||
? i18n("Not available yet")
|
||||
: ShellSettings.Settings.wallpaperThemeColor.toString()
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.medium
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
|
||||
radius: Kirigami.Units.smallSpacing
|
||||
color: ShellSettings.Settings.wallpaperThemeColor.a === 0
|
||||
? "transparent"
|
||||
: ShellSettings.Settings.wallpaperThemeColor
|
||||
border.width: 1
|
||||
border.color: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.28)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -28,6 +28,19 @@ KCM.SimpleKCM {
|
|||
}
|
||||
|
||||
ColumnLayout {
|
||||
FormCard.FormHeader {
|
||||
title: i18n("Appearance")
|
||||
}
|
||||
|
||||
FormCard.FormCard {
|
||||
FormCard.FormButtonDelegate {
|
||||
id: appearanceButton
|
||||
icon.name: "preferences-desktop-theme-global"
|
||||
text: i18n("Appearance")
|
||||
onClicked: kcm.push("AppearanceForm.qml")
|
||||
}
|
||||
}
|
||||
|
||||
FormCard.FormHeader {
|
||||
title: i18n("General")
|
||||
}
|
||||
|
|
@ -314,7 +327,6 @@ KCM.SimpleKCM {
|
|||
|
||||
Component.onCompleted: {
|
||||
currentIndex = indexOfValue(ShellSettings.Settings.statusBarScaleFactor);
|
||||
dialog.parent = root;
|
||||
}
|
||||
onCurrentValueChanged: ShellSettings.Settings.statusBarScaleFactor = currentValue
|
||||
}
|
||||
|
|
@ -354,7 +366,6 @@ KCM.SimpleKCM {
|
|||
|
||||
Component.onCompleted: {
|
||||
currentIndex = indexOfValue(ShellSettings.Settings.actionDrawerTopLeftMode);
|
||||
dialog.parent = root;
|
||||
}
|
||||
onCurrentValueChanged: ShellSettings.Settings.actionDrawerTopLeftMode = currentValue
|
||||
}
|
||||
|
|
@ -376,7 +387,6 @@ KCM.SimpleKCM {
|
|||
|
||||
Component.onCompleted: {
|
||||
currentIndex = indexOfValue(ShellSettings.Settings.actionDrawerTopRightMode);
|
||||
dialog.parent = root
|
||||
}
|
||||
onCurrentValueChanged: ShellSettings.Settings.actionDrawerTopRightMode = currentValue
|
||||
}
|
||||
|
|
@ -407,7 +417,6 @@ KCM.SimpleKCM {
|
|||
|
||||
Component.onCompleted: {
|
||||
currentIndex = indexOfValue(ShellSettings.Settings.lockscreenLeftButtonAction);
|
||||
dialog.parent = root;
|
||||
}
|
||||
onCurrentValueChanged: ShellSettings.Settings.lockscreenLeftButtonAction = currentValue
|
||||
}
|
||||
|
|
@ -429,7 +438,6 @@ KCM.SimpleKCM {
|
|||
|
||||
Component.onCompleted: {
|
||||
currentIndex = indexOfValue(ShellSettings.Settings.lockscreenRightButtonAction);
|
||||
dialog.parent = root;
|
||||
}
|
||||
onCurrentValueChanged: ShellSettings.Settings.lockscreenRightButtonAction = currentValue
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import org.kde.kirigami as Kirigami
|
|||
|
||||
import org.kde.plasma.private.mobileshell as MobileShell
|
||||
import org.kde.plasma.private.mobileshell.state as MobileShellState
|
||||
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
|
@ -65,7 +66,7 @@ Rectangle {
|
|||
Loader {
|
||||
id: wallpaperColors
|
||||
|
||||
active: desktop.usedInAccentColor && root.containment && root.containment.wallpaper
|
||||
active: (desktop.usedInAccentColor || ShellSettings.Settings.wallpaperThemeEnabled) && root.containment && root.containment.wallpaper
|
||||
asynchronous: true
|
||||
|
||||
sourceComponent: Kirigami.ImageColors {
|
||||
|
|
@ -80,26 +81,46 @@ Rectangle {
|
|||
Kirigami.Theme.backgroundColor: backgroundColor
|
||||
Kirigami.Theme.textColor: textColor
|
||||
|
||||
readonly property color wallpaperColor: {
|
||||
if (!Qt.colorEqual(imageColors.colorFromPlugin, "transparent")) {
|
||||
return imageColors.colorFromPlugin;
|
||||
}
|
||||
if (!Qt.colorEqual(imageColors.highlight, "transparent")) {
|
||||
return imageColors.highlight;
|
||||
}
|
||||
if (!Qt.colorEqual(imageColors.dominant, "transparent")) {
|
||||
return imageColors.dominant;
|
||||
}
|
||||
return imageColors.fallbackHighlight;
|
||||
}
|
||||
|
||||
onBackgroundColorChanged: Qt.callLater(update)
|
||||
onTextColorChanged: Qt.callLater(update)
|
||||
onWallpaperColorChanged: Qt.callLater(applyWallpaperTheme)
|
||||
|
||||
function applyWallpaperTheme() {
|
||||
ShellSettings.Settings.applyWallpaperThemeColor(imageColors.wallpaperColor);
|
||||
}
|
||||
|
||||
property Connections settingsConnection: Connections {
|
||||
target: ShellSettings.Settings
|
||||
|
||||
function onWallpaperThemeEnabledChanged() {
|
||||
imageColors.applyWallpaperTheme();
|
||||
}
|
||||
}
|
||||
|
||||
property Binding colorBinding: Binding {
|
||||
target: desktop
|
||||
property: "accentColor"
|
||||
value: {
|
||||
if (!Qt.colorEqual(imageColors.colorFromPlugin, "transparent")) {
|
||||
return imageColors.colorFromPlugin;
|
||||
}
|
||||
if (imageColors.palette.length === 0) {
|
||||
return "transparent";
|
||||
}
|
||||
return imageColors.dominant;
|
||||
}
|
||||
value: imageColors.wallpaperColor
|
||||
when: desktop.usedInAccentColor // Without this, accentColor may still be updated after usedInAccentColor becomes false
|
||||
}
|
||||
|
||||
property Connections repaintConnection: Connections {
|
||||
target: root.containment.wallpaper
|
||||
ignoreUnknownSignals: true
|
||||
|
||||
function onRepaintNeeded(color) {
|
||||
imageColors.colorFromPlugin = color;
|
||||
|
||||
|
|
@ -110,7 +131,10 @@ Rectangle {
|
|||
}
|
||||
}
|
||||
|
||||
onLoaded: item.update()
|
||||
onLoaded: {
|
||||
item.update();
|
||||
item.applyWallpaperTheme();
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,11 @@ if(BASH_EXECUTABLE)
|
|||
COMMAND ${BASH_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/check-shift-theme-identity.sh
|
||||
)
|
||||
|
||||
add_test(
|
||||
NAME shift-dynamic-theming
|
||||
COMMAND ${BASH_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/check-shift-dynamic-theming.sh
|
||||
)
|
||||
|
||||
add_test(
|
||||
NAME shift-preview-qml-stubs
|
||||
COMMAND ${BASH_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/check-preview-qml-stubs.sh
|
||||
|
|
|
|||
154
tests/check-shift-dynamic-theming.sh
Normal file
154
tests/check-shift-dynamic-theming.sh
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
#!/usr/bin/env bash
|
||||
# SPDX-FileCopyrightText: 2026 Marco Allegretti
|
||||
# SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$repo_dir"
|
||||
|
||||
fail() {
|
||||
printf '%s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
require_line() {
|
||||
local file="$1"
|
||||
local pattern="$2"
|
||||
local message="$3"
|
||||
|
||||
grep -Eq -- "$pattern" "$file" || fail "$message"
|
||||
}
|
||||
|
||||
settings_header=components/shellsettingsplugin/mobileshellsettings.h
|
||||
settings_cpp=components/shellsettingsplugin/mobileshellsettings.cpp
|
||||
settings_cmake=components/shellsettingsplugin/CMakeLists.txt
|
||||
kcm_main=kcms/mobileshell/ui/main.qml
|
||||
kcm_appearance=kcms/mobileshell/ui/AppearanceForm.qml
|
||||
desktop_view=shell/contents/views/Desktop.qml
|
||||
kcm_metadata=kcms/mobileshell/kcm_mobileshell.json
|
||||
|
||||
require_line "$settings_header" 'Q_PROPERTY\(QString colorScheme READ colorScheme NOTIFY colorSchemeChanged\)' \
|
||||
"mobile shell settings must expose the active KDE color scheme"
|
||||
require_line "$settings_header" 'Q_PROPERTY\(bool darkThemeEnabled READ darkThemeEnabled WRITE setDarkThemeEnabled NOTIFY darkThemeEnabledChanged\)' \
|
||||
"mobile shell settings must expose Shift dark/light switching"
|
||||
require_line "$settings_header" 'Q_PROPERTY\(bool wallpaperThemeEnabled READ wallpaperThemeEnabled WRITE setWallpaperThemeEnabled NOTIFY wallpaperThemeEnabledChanged\)' \
|
||||
"mobile shell settings must expose wallpaper-driven theme mode"
|
||||
require_line "$settings_header" 'Q_PROPERTY\(QColor wallpaperThemeColor READ wallpaperThemeColor NOTIFY wallpaperThemeColorChanged\)' \
|
||||
"mobile shell settings must expose the extracted wallpaper theme color for UI inspection"
|
||||
require_line "$settings_header" 'Q_PROPERTY\(QColor accentColor READ accentColor WRITE setAccentColor NOTIFY accentColorChanged\)' \
|
||||
"mobile shell settings must expose manual accent color selection"
|
||||
require_line "$settings_header" 'Q_PROPERTY\(bool wallpaperAccentEnabled READ wallpaperAccentEnabled WRITE setWallpaperAccentEnabled NOTIFY wallpaperAccentEnabledChanged\)' \
|
||||
"mobile shell settings must expose wallpaper-derived accent colors"
|
||||
|
||||
require_line "$settings_cpp" 'KSharedConfig::openConfig\(\)' \
|
||||
"dynamic theming must use kdeglobals, not a parallel Shift-only config file"
|
||||
require_line "$settings_cpp" 'readEntry\("ColorScheme", SHIFT_DARK_COLOR_SCHEME\)' \
|
||||
"dark/light mode must be backed by kdeglobals ColorScheme"
|
||||
require_line "$settings_cpp" 'readEntry\("wallpaperThemeEnabled", false\)' \
|
||||
"wallpaper theme mode must be an explicit Shift setting"
|
||||
require_line "$settings_cpp" 'writeEntry\("wallpaperThemeEnabled", enabled, KConfigGroup::Notify\)' \
|
||||
"wallpaper theme mode must persist changes with config notifications"
|
||||
require_line "$settings_cpp" 'WALLPAPER_THEME_MANUAL_DARK_KEY' \
|
||||
"wallpaper theme mode must remember the previous manual dark/light choice"
|
||||
require_line "$settings_cpp" 'readEntry\(WALLPAPER_THEME_MANUAL_DARK_KEY, darkThemeEnabled\(\)\)' \
|
||||
"disabling wallpaper theme mode must restore the previous manual dark/light choice"
|
||||
require_line "$settings_cpp" 'm_wallpaperThemeTimer\.stop\(\)' \
|
||||
"disabling wallpaper theme mode must cancel pending automatic theme changes"
|
||||
require_line "$settings_cpp" 'applyWallpaperThemeColor' \
|
||||
"wallpaper theme mode must expose a shell hook for extracted wallpaper colors"
|
||||
require_line "$settings_cpp" 'Q_EMIT wallpaperThemeColorChanged\(\)' \
|
||||
"wallpaper theme mode must notify the UI when the extracted wallpaper theme color changes"
|
||||
require_line "$settings_cpp" 'm_wallpaperThemeTimer\.setInterval\(450\)' \
|
||||
"wallpaper theme mode must debounce wallpaper color changes"
|
||||
require_line "$settings_cpp" 'm_wallpaperThemeTimer\.start\(\)' \
|
||||
"wallpaper theme mode must apply extracted wallpaper colors through the debounce timer"
|
||||
require_line "$settings_cpp" '0\.2126 \* color\.redF\(\) \+ 0\.7152 \* color\.greenF\(\) \+ 0\.0722 \* color\.blueF\(\)' \
|
||||
"wallpaper theme mode must use relative luminance to choose light or dark Shift schemes"
|
||||
require_line "$settings_cpp" 'writeEntry\("accentColorFromWallpaper", enabled, KConfigGroup::Notify\)' \
|
||||
"wallpaper accent mode must persist KDE's accentColorFromWallpaper key"
|
||||
require_line "$settings_cpp" 'm_lastWallpaperThemeColor\.name\(QColor::HexRgb\)' \
|
||||
"wallpaper accent mode must apply the extracted wallpaper color directly as accent override"
|
||||
require_line "$settings_cpp" 'writeEntry\("LastUsedCustomAccentColor", opaqueColor, KConfigGroup::Notify\)' \
|
||||
"manual accent mode must preserve KDE's LastUsedCustomAccentColor key"
|
||||
require_line "$settings_cpp" 'deleteEntry\("AccentColor", KConfigGroup::Notify\)' \
|
||||
"resetting the accent must clear KDE's AccentColor key"
|
||||
require_line "$settings_cpp" 'plasma-apply-colorscheme' \
|
||||
"color-scheme changes must go through KDE's color applicator"
|
||||
require_line "$settings_cpp" '--accent-color' \
|
||||
"manual accent selection must use KDE's accent-color applicator path"
|
||||
require_line "$settings_cpp" 'plasma-apply-desktoptheme' \
|
||||
"dark/light switching must update the Shift Plasma desktop theme"
|
||||
require_line "$settings_cpp" 'ShiftDark' \
|
||||
"dynamic theming must keep ShiftDark as the dark scheme"
|
||||
require_line "$settings_cpp" 'ShiftLight' \
|
||||
"dynamic theming must keep ShiftLight as the light scheme"
|
||||
require_line "$settings_cpp" 'ShiftWallpaperDark' \
|
||||
"wallpaper theme mode must provide a generated dark Shift wallpaper scheme"
|
||||
require_line "$settings_cpp" 'ShiftWallpaperLight' \
|
||||
"wallpaper theme mode must provide a generated light Shift wallpaper scheme"
|
||||
require_line "$settings_cpp" 'QStandardPaths::locate\(QStandardPaths::GenericDataLocation,' \
|
||||
"wallpaper theme mode must derive generated schemes from the installed Shift base schemes"
|
||||
require_line "$settings_cpp" 'QStandardPaths::writableLocation\(QStandardPaths::GenericDataLocation\)' \
|
||||
"wallpaper theme mode must write generated schemes into the session data location"
|
||||
require_line "$settings_cpp" 'QFile::copy\(sourcePath, outputPath\)' \
|
||||
"wallpaper theme mode must generate a concrete .colors file before applying it"
|
||||
require_line "$settings_cpp" 'tintBackgroundEntries\(' \
|
||||
"wallpaper theme mode must generate tinted background roles in the derived color scheme"
|
||||
require_line "$settings_cpp" 'tuneForegroundEntries\(' \
|
||||
"wallpaper theme mode must generate contrast-aware foreground hierarchy entries"
|
||||
require_line "$settings_cpp" 'contrastRatio\(' \
|
||||
"wallpaper theme mode must use contrast-aware text color selection"
|
||||
require_line "$settings_cpp" 'ensureContrast\(' \
|
||||
"wallpaper theme mode must clamp generated foreground roles to a minimum contrast threshold"
|
||||
require_line "$settings_cpp" 'MIN_NORMAL_CONTRAST' \
|
||||
"wallpaper theme mode must define explicit minimum contrast targets for readability"
|
||||
require_line "$settings_cpp" 'writeEntry\("BackgroundNormal"' \
|
||||
"wallpaper theme mode must update BackgroundNormal in generated scheme groups"
|
||||
require_line "$settings_cpp" 'writeEntry\("inactiveBackground"' \
|
||||
"wallpaper theme mode must generate an inactive window background role"
|
||||
require_line "$settings_cpp" 'shift-dark' \
|
||||
"dynamic theming must keep shift-dark as the dark Plasma theme"
|
||||
require_line "$settings_cpp" 'shift-light' \
|
||||
"dynamic theming must keep shift-light as the light Plasma theme"
|
||||
require_line "$settings_cmake" 'KF6::ConfigCore' \
|
||||
"shell settings plugin must link explicitly to KConfigCore for kdeglobals access"
|
||||
require_line "$kcm_main" 'kcm\.push\("AppearanceForm\.qml"\)' \
|
||||
"mobile shell settings must expose the dynamic theming controls"
|
||||
if grep -Eq -- 'dialog\.parent[[:space:]]*=' "$kcm_main"; then
|
||||
fail "mobile shell KCM must not assign dialog.parent on FormComboBoxDelegate (invalid in current runtime)"
|
||||
fi
|
||||
require_line "$kcm_metadata" '"FormFactors"[[:space:]]*:[[:space:]]*\[' \
|
||||
"shell settings module metadata must declare form factors"
|
||||
require_line "$kcm_metadata" '"desktop"' \
|
||||
"shell settings module must be visible in desktop System Settings form factor"
|
||||
require_line "$kcm_metadata" '"X-KDE-Keywords"[[:space:]]*:[[:space:]]*"[^"]*appearance[^"]*theme[^"]*wallpaper[^"]*accent' \
|
||||
"shell settings module search keywords must include appearance/theme/wallpaper/accent terms"
|
||||
require_line "$kcm_appearance" 'ShellSettings\.Settings\.darkThemeEnabled' \
|
||||
"appearance settings must control Shift dark/light mode"
|
||||
require_line "$kcm_appearance" 'ShellSettings\.Settings\.wallpaperThemeEnabled' \
|
||||
"appearance settings must control wallpaper-driven theme mode"
|
||||
require_line "$kcm_appearance" 'ShellSettings\.Settings\.colorScheme' \
|
||||
"appearance settings must show the active generated wallpaper color scheme"
|
||||
require_line "$kcm_appearance" 'ShellSettings\.Settings\.wallpaperThemeColor' \
|
||||
"appearance settings must show the extracted wallpaper theme color"
|
||||
require_line "$kcm_appearance" 'ShellSettings\.Settings\.wallpaperAccentEnabled' \
|
||||
"appearance settings must control wallpaper-derived accent colors"
|
||||
require_line "$kcm_appearance" 'Dialogs\.ColorDialog' \
|
||||
"appearance settings must provide manual accent color selection"
|
||||
require_line "$kcm_appearance" 'ShellSettings\.Settings\.accentColor = selectedColor' \
|
||||
"appearance settings must apply the selected manual accent color"
|
||||
require_line "$kcm_appearance" 'ShellSettings\.Settings\.resetAccentColor\(\)' \
|
||||
"appearance settings must allow returning to the scheme accent"
|
||||
require_line "$desktop_view" 'usedInAccentColor' \
|
||||
"wallpaper accent mode depends on the Plasma desktop accent extraction hook"
|
||||
require_line "$desktop_view" 'ShellSettings\.Settings\.wallpaperThemeEnabled' \
|
||||
"wallpaper theme mode must reuse the Plasma desktop wallpaper color extraction hook"
|
||||
require_line "$desktop_view" 'ShellSettings\.Settings\.applyWallpaperThemeColor\(imageColors\.wallpaperColor\)' \
|
||||
"desktop wallpaper color extraction must feed Shift's wallpaper theme mode"
|
||||
require_line "$desktop_view" 'onWallpaperThemeEnabledChanged' \
|
||||
"desktop wallpaper color extraction must react when wallpaper theme mode is toggled"
|
||||
require_line tests/CMakeLists.txt 'NAME shift-dynamic-theming' \
|
||||
"dynamic theming regression test must be registered with CTest"
|
||||
|
||||
printf '%s\n' 'shift-dynamic-theming-ok'
|
||||
Loading…
Reference in a new issue