shift-shell/components/shellsettingsplugin/mobileshellsettings.cpp
Marco Allegretti 08264e5759 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.
2026-05-31 14:06:05 +02:00

1000 lines
39 KiB
C++

/*
* SPDX-FileCopyrightText: 2022 Devin Lin <devin@kde.org>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "mobileshellsettings.h"
#include <KIO/CommandLauncherJob>
#include <KNotificationJobUiDelegate>
#include <KPluginFactory>
#include <KRuntimePlatform>
#include <QDBusConnection>
#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)
if (group.name() == GENERAL_CONFIG_GROUP) {
Q_EMIT vibrationsEnabledChanged();
Q_EMIT vibrationDurationChanged();
Q_EMIT animationsEnabledChanged();
Q_EMIT dateInStatusBarChanged();
Q_EMIT statusBarScaleFactorChanged();
Q_EMIT showBatteryPercentageChanged();
Q_EMIT navigationPanelEnabledChanged();
Q_EMIT gesturePanelEnabledChanged();
Q_EMIT alwaysShowKeyboardToggleOnNavigationPanelChanged();
Q_EMIT keyboardButtonEnabledChanged();
Q_EMIT taskSwitcherPreviewsEnabledChanged();
Q_EMIT actionDrawerTopLeftModeChanged();
Q_EMIT actionDrawerTopRightModeChanged();
Q_EMIT convergenceModeEnabledChanged();
Q_EMIT autoHidePanelsEnabledChanged();
Q_EMIT gamingModeEnabledChanged();
Q_EMIT gamingDismissHintEnabledChanged();
Q_EMIT dynamicTilingEnabledChanged();
Q_EMIT dynamicTilingWindowRequestChanged();
Q_EMIT dynamicTilingWindowStateChanged();
Q_EMIT dynamicTilingLayoutRequestChanged();
Q_EMIT dynamicTilingLayoutStateChanged();
Q_EMIT snapLayoutsEnabledChanged();
Q_EMIT allowLogoutChanged();
Q_EMIT wallpaperThemeEnabledChanged();
}
if (group.name() == LOCKSCREEN_CONFIG_GROUP) {
Q_EMIT lockscreenLeftButtonActionChanged();
Q_EMIT lockscreenRightButtonActionChanged();
}
if (group.name() == QUICKSETTINGS_CONFIG_GROUP) {
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
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
return group.readEntry("vibrationsEnabled", true);
}
void MobileShellSettings::setVibrationsEnabled(bool vibrationsEnabled)
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
group.writeEntry("vibrationsEnabled", vibrationsEnabled, KConfigGroup::Notify);
m_config->sync();
}
int MobileShellSettings::vibrationDuration() const
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
return group.readEntry("vibrationDuration", 10);
}
void MobileShellSettings::setVibrationDuration(int vibrationDuration)
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
group.writeEntry("vibrationDuration", vibrationDuration, KConfigGroup::Notify);
m_config->sync();
}
bool MobileShellSettings::animationsEnabled() const
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
return group.readEntry("animationsEnabled", true);
}
void MobileShellSettings::setAnimationsEnabled(bool animationsEnabled)
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
group.writeEntry("animationsEnabled", animationsEnabled, KConfigGroup::Notify);
m_config->sync();
}
bool MobileShellSettings::dateInStatusBar() const
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
return group.readEntry("dateInStatusBar", false);
}
void MobileShellSettings::setDateInStatusBar(bool dateInStatusBar)
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
group.writeEntry("dateInStatusBar", dateInStatusBar, KConfigGroup::Notify);
m_config->sync();
}
float MobileShellSettings::statusBarScaleFactor() const
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
return group.readEntry("statusBarScaleFactor", 1.0);
}
void MobileShellSettings::setStatusBarScaleFactor(float statusBarScaleFactor)
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
group.writeEntry("statusBarScaleFactor", statusBarScaleFactor, KConfigGroup::Notify);
m_config->sync();
}
bool MobileShellSettings::showBatteryPercentage() const
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
return group.readEntry("showBatteryPercentage", true);
}
void MobileShellSettings::setShowBatteryPercentage(bool showBatteryPercentage)
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
group.writeEntry("showBatteryPercentage", showBatteryPercentage, KConfigGroup::Notify);
m_config->sync();
}
bool MobileShellSettings::navigationPanelEnabled() const
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
return group.readEntry("navigationPanelEnabled", true);
}
void MobileShellSettings::setNavigationPanelEnabled(bool navigationPanelEnabled)
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
group.writeEntry("navigationPanelEnabled", navigationPanelEnabled, KConfigGroup::Notify);
m_config->sync();
updateNavigationBarsInPlasma();
}
bool MobileShellSettings::gesturePanelEnabled() const
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
return group.readEntry("gesturePanelEnabled", true);
}
void MobileShellSettings::setGesturePanelEnabled(bool gesturePanelEnabled)
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
group.writeEntry("gesturePanelEnabled", gesturePanelEnabled, KConfigGroup::Notify);
m_config->sync();
updateNavigationBarsInPlasma();
}
bool MobileShellSettings::alwaysShowKeyboardToggleOnNavigationPanel() const
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
return group.readEntry("alwaysShowKeyboardToggleOnNavigationPanel", false);
}
void MobileShellSettings::setAlwaysShowKeyboardToggleOnNavigationPanel(bool alwaysShowKeyboardToggleOnNavigationPanel)
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
group.writeEntry("alwaysShowKeyboardToggleOnNavigationPanel", alwaysShowKeyboardToggleOnNavigationPanel, KConfigGroup::Notify);
m_config->sync();
}
MobileShellSettings::ActionDrawerMode MobileShellSettings::actionDrawerTopLeftMode() const
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
return (ActionDrawerMode)group.readEntry("actionDrawerTopLeftMode", (int)ActionDrawerMode::Pinned);
}
void MobileShellSettings::setActionDrawerTopLeftMode(ActionDrawerMode actionDrawerMode)
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
group.writeEntry("actionDrawerTopLeftMode", (int)actionDrawerMode, KConfigGroup::Notify);
m_config->sync();
}
int MobileShellSettings::quickSettingsColumns() const
{
auto group = KConfigGroup{m_config, QUICKSETTINGS_CONFIG_GROUP};
return group.readEntry("quickSettingsColumns", 3);
}
void MobileShellSettings::setQuickSettingsColumns(int columns)
{
auto group = KConfigGroup{m_config, QUICKSETTINGS_CONFIG_GROUP};
group.writeEntry("quickSettingsColumns", columns, KConfigGroup::Notify);
m_config->sync();
}
MobileShellSettings::ActionDrawerMode MobileShellSettings::actionDrawerTopRightMode() const
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
return (ActionDrawerMode)group.readEntry("actionDrawerTopRightMode", (int)ActionDrawerMode::Expanded);
}
void MobileShellSettings::setActionDrawerTopRightMode(ActionDrawerMode actionDrawerMode)
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
group.writeEntry("actionDrawerTopRightMode", (int)actionDrawerMode, KConfigGroup::Notify);
m_config->sync();
}
bool MobileShellSettings::convergenceModeEnabled() const
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
return group.readEntry("convergenceModeEnabled", false);
}
void MobileShellSettings::setConvergenceModeEnabled(bool enabled)
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
group.writeEntry("convergenceModeEnabled", enabled, KConfigGroup::Notify);
m_config->sync();
// update environment settings
auto *job = new KIO::CommandLauncherJob(QStringLiteral("plasma-mobile-envmanager --apply-settings"), {});
job->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoErrorHandlingEnabled));
job->setDesktopName(QStringLiteral("org.kde.plasma-mobile-envmanager"));
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};
return group.readEntry("autoHidePanelsEnabled", false);
}
void MobileShellSettings::setAutoHidePanelsEnabled(bool enabled)
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
group.writeEntry("autoHidePanelsEnabled", enabled, KConfigGroup::Notify);
m_config->sync();
}
bool MobileShellSettings::gamingModeEnabled() const
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
return group.readEntry("gamingModeEnabled", false);
}
void MobileShellSettings::setGamingModeEnabled(bool enabled)
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
group.writeEntry("gamingModeEnabled", enabled, KConfigGroup::Notify);
m_config->sync();
}
bool MobileShellSettings::gamingDismissHintEnabled() const
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
return group.readEntry("gamingDismissHintEnabled", true);
}
void MobileShellSettings::setGamingDismissHintEnabled(bool enabled)
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
group.writeEntry("gamingDismissHintEnabled", enabled, KConfigGroup::Notify);
m_config->sync();
}
bool MobileShellSettings::dynamicTilingEnabled() const
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
return group.readEntry("dynamicTilingEnabled", true);
}
void MobileShellSettings::setDynamicTilingEnabled(bool enabled)
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
group.writeEntry("dynamicTilingEnabled", enabled, KConfigGroup::Notify);
m_config->sync();
}
QString MobileShellSettings::dynamicTilingWindowRequestAction() const
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
return group.readEntry("dynamicTilingWindowRequestAction", QString{});
}
QString MobileShellSettings::dynamicTilingWindowRequestId() const
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
return group.readEntry("dynamicTilingWindowRequestId", QString{});
}
int MobileShellSettings::dynamicTilingWindowRequestSerial() const
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
return group.readEntry("dynamicTilingWindowRequestSerial", 0);
}
void MobileShellSettings::requestDynamicTilingWindowAction(const QString &windowId, const QString &action)
{
if (windowId.isEmpty() || action.isEmpty()) {
return;
}
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
const int serial = group.readEntry("dynamicTilingWindowRequestSerial", 0) + 1;
group.writeEntry("dynamicTilingWindowRequestId", windowId, KConfigGroup::Notify);
group.writeEntry("dynamicTilingWindowRequestAction", action, KConfigGroup::Notify);
group.writeEntry("dynamicTilingWindowRequestSerial", serial, KConfigGroup::Notify);
m_config->sync();
Q_EMIT dynamicTilingWindowRequestChanged();
}
QStringList MobileShellSettings::dynamicTilingMaximizedWindowIds() const
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
return group.readEntry("dynamicTilingMaximizedWindowIds", QStringList{});
}
int MobileShellSettings::dynamicTilingWindowStateSerial() const
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
return group.readEntry("dynamicTilingWindowStateSerial", 0);
}
bool MobileShellSettings::isDynamicTilingWindowMaximized(const QString &windowId) const
{
if (windowId.isEmpty()) {
return false;
}
return dynamicTilingMaximizedWindowIds().contains(windowId);
}
void MobileShellSettings::reportDynamicTilingWindowState(const QStringList &maximizedWindowIds)
{
QStringList normalizedIds;
for (const QString &windowId : maximizedWindowIds) {
if (!windowId.isEmpty() && !normalizedIds.contains(windowId)) {
normalizedIds.push_back(windowId);
}
}
normalizedIds.sort(Qt::CaseSensitive);
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
if (group.readEntry("dynamicTilingMaximizedWindowIds", QStringList{}) == normalizedIds) {
return;
}
const int serial = group.readEntry("dynamicTilingWindowStateSerial", 0) + 1;
group.writeEntry("dynamicTilingMaximizedWindowIds", normalizedIds, KConfigGroup::Notify);
group.writeEntry("dynamicTilingWindowStateSerial", serial, KConfigGroup::Notify);
m_config->sync();
Q_EMIT dynamicTilingWindowStateChanged();
}
QString MobileShellSettings::dynamicTilingLayoutRequestMode() const
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
return group.readEntry("dynamicTilingLayoutRequestMode", QString{});
}
int MobileShellSettings::dynamicTilingLayoutRequestSerial() const
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
return group.readEntry("dynamicTilingLayoutRequestSerial", 0);
}
void MobileShellSettings::requestDynamicTilingLayoutMode(const QString &mode)
{
if (mode.isEmpty()) {
return;
}
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
const int serial = group.readEntry("dynamicTilingLayoutRequestSerial", 0) + 1;
group.writeEntry("dynamicTilingLayoutRequestMode", mode, KConfigGroup::Notify);
group.writeEntry("dynamicTilingLayoutRequestSerial", serial, KConfigGroup::Notify);
m_config->sync();
Q_EMIT dynamicTilingLayoutRequestChanged();
}
QString MobileShellSettings::dynamicTilingLayoutMode() const
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
return group.readEntry("dynamicTilingLayoutMode", QString{});
}
int MobileShellSettings::dynamicTilingLayoutWindowCount() const
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
return group.readEntry("dynamicTilingLayoutWindowCount", 0);
}
int MobileShellSettings::dynamicTilingLayoutStateSerial() const
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
return group.readEntry("dynamicTilingLayoutStateSerial", 0);
}
void MobileShellSettings::reportDynamicTilingLayoutState(const QString &mode, int windowCount)
{
const int normalizedWindowCount = windowCount < 0 ? 0 : windowCount;
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
if (group.readEntry("dynamicTilingLayoutMode", QString{}) == mode && group.readEntry("dynamicTilingLayoutWindowCount", 0) == normalizedWindowCount) {
return;
}
const int serial = group.readEntry("dynamicTilingLayoutStateSerial", 0) + 1;
group.writeEntry("dynamicTilingLayoutMode", mode, KConfigGroup::Notify);
group.writeEntry("dynamicTilingLayoutWindowCount", normalizedWindowCount, KConfigGroup::Notify);
group.writeEntry("dynamicTilingLayoutStateSerial", serial, KConfigGroup::Notify);
m_config->sync();
Q_EMIT dynamicTilingLayoutStateChanged();
}
bool MobileShellSettings::snapLayoutsEnabled() const
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
return group.readEntry("snapLayoutsEnabled", true);
}
void MobileShellSettings::setSnapLayoutsEnabled(bool enabled)
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
group.writeEntry("snapLayoutsEnabled", enabled, KConfigGroup::Notify);
m_config->sync();
}
void MobileShellSettings::updateNavigationBarsInPlasma()
{
// Do not update panels when not in Plasma Mobile
bool isMobilePlatform = KRuntimePlatform::runtimePlatform().contains("phone");
if (!isMobilePlatform) {
return;
}
auto message = QDBusMessage::createMethodCall(QLatin1String("org.kde.plasmashell"),
QLatin1String("/PlasmaShell"),
QLatin1String("org.kde.PlasmaShell"),
QLatin1String("evaluateScript"));
if (navigationPanelEnabled() || gesturePanelEnabled()) {
QString createNavigationPanelScript = R"(
let allPanels = panels();
let foundPanel = false;
for (var i = 0; i < allPanels.length; i++) {
if (allPanels[i].type === "org.kde.plasma.mobile.taskpanel") {
foundPanel = true;
}
}
if (!foundPanel) {
loadTemplate("org.kde.plasma.mobile.defaultNavigationPanel");
}
)";
message << createNavigationPanelScript;
} else {
QString deleteNavigationPanelScript = R"(
let allPanels = panels();
for (var i = 0; i < allPanels.length; i++) {
if (allPanels[i].type === "org.kde.plasma.mobile.taskpanel") {
allPanels[i].remove();
}
}
)";
message << deleteNavigationPanelScript;
}
// TODO check for error response
QDBusConnection::sessionBus().asyncCall(message);
}
bool MobileShellSettings::allowLogout() const
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
return group.readEntry("allowLogout", true);
}
MobileShellSettings::LockscreenButtonAction MobileShellSettings::lockscreenLeftButtonAction() const
{
auto group = KConfigGroup{m_config, LOCKSCREEN_CONFIG_GROUP};
return (LockscreenButtonAction)group.readEntry("lockscreenLeftButtonAction", (int)LockscreenButtonAction::None);
}
void MobileShellSettings::setLockscreenLeftButtonAction(const LockscreenButtonAction action)
{
auto group = KConfigGroup{m_config, LOCKSCREEN_CONFIG_GROUP};
group.writeEntry("lockscreenLeftButtonAction", (int)action, KConfigGroup::Notify);
m_config->sync();
}
MobileShellSettings::LockscreenButtonAction MobileShellSettings::lockscreenRightButtonAction() const
{
auto group = KConfigGroup{m_config, LOCKSCREEN_CONFIG_GROUP};
return (LockscreenButtonAction)group.readEntry("lockscreenRightButtonAction", (int)LockscreenButtonAction::None);
}
void MobileShellSettings::setLockscreenRightButtonAction(const LockscreenButtonAction action)
{
auto group = KConfigGroup{m_config, LOCKSCREEN_CONFIG_GROUP};
group.writeEntry("lockscreenRightButtonAction", (int)action, KConfigGroup::Notify);
m_config->sync();
}