Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/app/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ add_library(logitune-app-lib STATIC
services/DeviceCommandHandler.cpp
services/ButtonActionDispatcher.cpp
services/ProfileOrchestrator.cpp
services/SystemColorScheme.cpp
dialogs/GitHubIssueBuilder.cpp
dialogs/CrashReportDialog.cpp
TrayManager.cpp
Expand All @@ -23,7 +24,7 @@ target_include_directories(logitune-app-lib PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/services
)

target_link_libraries(logitune-app-lib PUBLIC logitune-core Qt6::Quick Qt6::Widgets)
target_link_libraries(logitune-app-lib PUBLIC logitune-core Qt6::Quick Qt6::Widgets Qt6::DBus)
target_compile_definitions(logitune-app-lib PUBLIC
PROJECT_VERSION="${PROJECT_VERSION}"
PROJECT_VERSION_FULL="${LOGITUNE_DISPLAY_VERSION}"
Expand Down
45 changes: 22 additions & 23 deletions src/app/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,14 @@
#include <QLockFile>
#include <QStandardPaths>
#include <QSystemTrayIcon>
#include <QDBusConnection>
#include <QDBusMessage>
#include <QDBusVariant>

#include "AppRoot.h"
#include "models/EditorModel.h"
#include "logging/LogManager.h"
#include "logging/CrashHandler.h"
#include "dialogs/CrashReportDialog.h"
#include "TrayManager.h"
#include "services/SystemColorScheme.h"

int main(int argc, char *argv[])
{
Expand Down Expand Up @@ -145,22 +143,12 @@ int main(int argc, char *argv[])
qCInfo(lcApp) << "settings file:" << settings.fileName()
<< "contains theme/dark:" << settings.contains("theme/dark")
<< "value:" << settings.value("theme/dark");
logitune::SystemColorScheme systemScheme;
if (settings.contains("theme/dark")) {
isDark = settings.value("theme/dark").toBool();
} else {
QDBusMessage msg = QDBusMessage::createMethodCall(
"org.freedesktop.portal.Desktop",
"/org/freedesktop/portal/desktop",
"org.freedesktop.portal.Settings",
"Read");
msg << QStringLiteral("org.freedesktop.appearance")
<< QStringLiteral("color-scheme");
QDBusMessage reply = QDBusConnection::sessionBus().call(msg, QDBus::Block, 250);
if (reply.type() == QDBusMessage::ReplyMessage && !reply.arguments().isEmpty()) {
QVariant value = reply.arguments().first().value<QDBusVariant>().variant();
if (value.canConvert<QDBusVariant>())
value = value.value<QDBusVariant>().variant();
isDark = (value.toUInt() == 1);
if (auto portalDark = systemScheme.readDark()) {
isDark = *portalDark;
} else {
QColor windowBg = app.palette().window().color();
double lum = windowBg.redF() * 0.299 + windowBg.greenF() * 0.587
Expand Down Expand Up @@ -234,18 +222,29 @@ int main(int argc, char *argv[])
}

// Set theme via the root QML object which has access to the Theme singleton
qCInfo(lcApp) << "Setting theme dark:" << isDark;
if (!engine.rootObjects().isEmpty()) {
auto applyDark = [&engine](bool dark) {
qCInfo(lcApp) << "Setting theme dark:" << dark;
if (engine.rootObjects().isEmpty())
return;
QObject *root = engine.rootObjects().first();
QQmlExpression expr(QQmlEngine::contextForObject(root), root,
isDark ? QStringLiteral("Theme.dark = true")
: QStringLiteral("Theme.dark = false"));
QVariant result = expr.evaluate();
dark ? QStringLiteral("Theme.dark = true")
: QStringLiteral("Theme.dark = false"));
expr.evaluate();
if (expr.hasError())
qCWarning(lcApp) << "Theme expression error:" << expr.error().toString();
else
qCInfo(lcApp) << "Theme.dark applied:" << isDark;
}
qCInfo(lcApp) << "Theme.dark applied:" << dark;
};
applyDark(isDark);

// Follow later desktop switches (darkman, GNOME's toggle) unless the user
// picked a theme in Settings, which saves theme/dark and takes priority.
QObject::connect(&systemScheme, &logitune::SystemColorScheme::darkChanged,
&engine, [applyDark](bool dark) {
if (!QSettings().contains("theme/dark"))
applyDark(dark);
});

// System tray
logitune::TrayManager tray(controller.deviceModel());
Expand Down
61 changes: 61 additions & 0 deletions src/app/services/SystemColorScheme.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#include "SystemColorScheme.h"
#include <QDBusConnection>
#include <QDBusMessage>
#include <QDBusVariant>

namespace logitune {

namespace {
const QString kService = QStringLiteral("org.freedesktop.portal.Desktop");
const QString kPath = QStringLiteral("/org/freedesktop/portal/desktop");
const QString kInterface = QStringLiteral("org.freedesktop.portal.Settings");
const QString kNamespace = QStringLiteral("org.freedesktop.appearance");
const QString kKey = QStringLiteral("color-scheme");
}

SystemColorScheme::SystemColorScheme(QObject *parent)
: QObject(parent)
{
QDBusConnection::sessionBus().connect(kService, kPath, kInterface,
QStringLiteral("SettingChanged"), this,
SLOT(onSettingChanged(QString,QString,QDBusVariant)));
}

std::optional<bool> SystemColorScheme::readDark() const
{
QDBusMessage msg = QDBusMessage::createMethodCall(kService, kPath, kInterface,
QStringLiteral("Read"));
msg << kNamespace << kKey;
QDBusMessage reply = QDBusConnection::sessionBus().call(msg, QDBus::Block, 250);
if (reply.type() != QDBusMessage::ReplyMessage || reply.arguments().isEmpty())
return std::nullopt;
m_lastDark = darkFromPortalValue(reply.arguments().first());
return m_lastDark;
}

std::optional<bool> SystemColorScheme::darkFromPortalValue(const QVariant &value)
{
QVariant v = value;
while (v.userType() == qMetaTypeId<QDBusVariant>())
v = v.value<QDBusVariant>().variant();
bool ok = false;
const uint scheme = v.toUInt(&ok);
if (!ok)
return std::nullopt;
return scheme == 1;
}

void SystemColorScheme::onSettingChanged(const QString &ns, const QString &key,
const QDBusVariant &value)
{
if (ns != kNamespace || key != kKey)
return;
// GNOME sends each switch twice, so repeats are dropped.
auto dark = darkFromPortalValue(value.variant());
if (!dark || dark == m_lastDark)
return;
m_lastDark = dark;
emit darkChanged(*dark);
}

} // namespace logitune
39 changes: 39 additions & 0 deletions src/app/services/SystemColorScheme.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#pragma once
#include <QObject>
#include <QString>
#include <QVariant>
#include <optional>

class QDBusVariant;

namespace logitune {

/// Follows the desktop's light/dark preference through the XDG desktop
/// portal (org.freedesktop.appearance color-scheme). Reads the value once on
/// request and emits darkChanged whenever the desktop switches, so the window
/// can follow tools like darkman or GNOME's own toggle without a restart.
class SystemColorScheme : public QObject {
Q_OBJECT
public:
explicit SystemColorScheme(QObject *parent = nullptr);

/// Current preference from the portal, or nullopt if the portal did not
/// answer within the timeout.
std::optional<bool> readDark() const;

/// Maps a portal color-scheme value to dark/light. The value may arrive
/// wrapped in one or two QDBusVariant layers. 1 means "prefer dark";
/// 0 (no preference) and 2 (prefer light) both mean light.
static std::optional<bool> darkFromPortalValue(const QVariant &value);

signals:
void darkChanged(bool dark);

public slots:
void onSettingChanged(const QString &ns, const QString &key, const QDBusVariant &value);

private:
mutable std::optional<bool> m_lastDark;
};

} // namespace logitune
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ add_executable(logitune-tests
test_desktop_factory.cpp
test_profile_apply_behavior.cpp
test_settings_model.cpp
test_system_color_scheme.cpp
test_json_device.cpp
test_device_fetcher.cpp
test_dpi_cycle_ring.cpp
Expand Down
71 changes: 71 additions & 0 deletions tests/test_system_color_scheme.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/// Tests for SystemColorScheme: portal value parsing and live change filtering.
#include <gtest/gtest.h>
#include <QDBusVariant>
#include <QSignalSpy>
#include "services/SystemColorScheme.h"
#include "helpers/TestFixtures.h"

namespace logitune::test {

TEST(SystemColorScheme, PreferDarkIsDark) {
EXPECT_EQ(SystemColorScheme::darkFromPortalValue(QVariant(1u)), true);
}

TEST(SystemColorScheme, NoPreferenceAndPreferLightAreLight) {
EXPECT_EQ(SystemColorScheme::darkFromPortalValue(QVariant(0u)), false);
EXPECT_EQ(SystemColorScheme::darkFromPortalValue(QVariant(2u)), false);
}

TEST(SystemColorScheme, UnwrapsNestedDBusVariants) {
// Read() replies wrap the value twice; SettingChanged wraps it once.
QVariant once = QVariant::fromValue(QDBusVariant(QVariant(1u)));
QVariant twice = QVariant::fromValue(QDBusVariant(once));
EXPECT_EQ(SystemColorScheme::darkFromPortalValue(once), true);
EXPECT_EQ(SystemColorScheme::darkFromPortalValue(twice), true);
}

TEST(SystemColorScheme, NonNumericValueIsIgnored) {
EXPECT_FALSE(SystemColorScheme::darkFromPortalValue(QVariant(QStringLiteral("dark"))).has_value());
}

TEST(SystemColorScheme, EmitsOnColorSchemeChange) {
ensureApp();
SystemColorScheme scheme;
QSignalSpy spy(&scheme, &SystemColorScheme::darkChanged);

scheme.onSettingChanged(QStringLiteral("org.freedesktop.appearance"),
QStringLiteral("color-scheme"), QDBusVariant(QVariant(1u)));
scheme.onSettingChanged(QStringLiteral("org.freedesktop.appearance"),
QStringLiteral("color-scheme"), QDBusVariant(QVariant(0u)));

ASSERT_EQ(spy.count(), 2);
EXPECT_TRUE(spy.at(0).at(0).toBool());
EXPECT_FALSE(spy.at(1).at(0).toBool());
}

TEST(SystemColorScheme, RepeatedValueEmitsOnce) {
ensureApp();
SystemColorScheme scheme;
QSignalSpy spy(&scheme, &SystemColorScheme::darkChanged);

for (int i = 0; i < 2; ++i)
scheme.onSettingChanged(QStringLiteral("org.freedesktop.appearance"),
QStringLiteral("color-scheme"), QDBusVariant(QVariant(1u)));

EXPECT_EQ(spy.count(), 1);
}

TEST(SystemColorScheme, IgnoresOtherSettings) {
ensureApp();
SystemColorScheme scheme;
QSignalSpy spy(&scheme, &SystemColorScheme::darkChanged);

scheme.onSettingChanged(QStringLiteral("org.freedesktop.appearance"),
QStringLiteral("accent-color"), QDBusVariant(QVariant(1u)));
scheme.onSettingChanged(QStringLiteral("org.gnome.desktop.interface"),
QStringLiteral("color-scheme"), QDBusVariant(QVariant(1u)));

EXPECT_EQ(spy.count(), 0);
}

} // namespace logitune::test