feat: use QQuickAttachedPropertyPropagator

Instead of manually trying to find ancestor ConfigScope, set attached
prop on ancestor and propagate via attached props
This commit is contained in:
2 * r + 2 * t 2026-04-13 03:06:13 +10:00
parent 8883156dd9
commit 8c8d196e20
14 changed files with 185 additions and 235 deletions

View file

@ -8,16 +8,10 @@ PanelWindow {
id: root id: root
required property string name required property string name
readonly property alias configScope: scope
default property alias contentData: scope.data
WlrLayershell.namespace: `caelestia-${name}` WlrLayershell.namespace: `caelestia-${name}`
color: "transparent" color: "transparent"
ConfigScope { contentItem.Config.screen: screen.name
id: scope contentItem.Tokens.screen: screen.name
anchors.fill: parent
screen: root.screen.name
}
} }

View file

@ -30,9 +30,9 @@ StyledWindow {
} }
return monitor?.activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false; return monitor?.activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false;
} }
property real borderThickness: hasFullscreen ? 0 : configScope.Config.border.thickness property real borderThickness: hasFullscreen ? 0 : contentItem.Config.border.thickness
readonly property real borderLayoutThickness: hasFullscreen ? 0 : configScope.Config.border.thickness readonly property real borderLayoutThickness: hasFullscreen ? 0 : contentItem.Config.border.thickness
property real borderRounding: hasFullscreen ? 0 : configScope.Config.border.rounding property real borderRounding: hasFullscreen ? 0 : contentItem.Config.border.rounding
property real shadowOpacity: hasFullscreen ? 0 : 0.7 property real shadowOpacity: hasFullscreen ? 0 : 0.7
readonly property int dragMaskPadding: { readonly property int dragMaskPadding: {
@ -44,8 +44,8 @@ StyledWindow {
const thresholds = []; const thresholds = [];
for (const panel of ["dashboard", "launcher", "session", "sidebar"]) for (const panel of ["dashboard", "launcher", "session", "sidebar"])
if (configScope.Config[panel].enabled) if (contentItem.Config[panel].enabled)
thresholds.push(configScope.Config[panel].dragThreshold); thresholds.push(contentItem.Config[panel].dragThreshold);
return Math.max(...thresholds); return Math.max(...thresholds);
} }

View file

@ -12,8 +12,8 @@ Region {
required property Panels panels required property Panels panels
required property var win required property var win
readonly property real borderThickness: win.configScope.Config.border.thickness readonly property real borderThickness: win.contentItem.Config.border.thickness
readonly property real clampedThickness: win.configScope.Config.border.clampedThickness readonly property real clampedThickness: win.contentItem.Config.border.clampedThickness
x: bar.clampedWidth + win.dragMaskPadding x: bar.clampedWidth + win.dragMaskPadding
y: clampedThickness + win.dragMaskPadding y: clampedThickness + win.dragMaskPadding

View file

@ -15,6 +15,9 @@ WlSessionLockSurface {
readonly property alias unlocking: unlockAnim.running readonly property alias unlocking: unlockAnim.running
contentItem.Config.screen: screen.name
contentItem.Tokens.screen: screen.name
color: "transparent" color: "transparent"
Connections { Connections {
@ -159,79 +162,72 @@ WlSessionLockSurface {
} }
} }
ConfigScope { ScreencopyView {
id: configScope id: background
anchors.fill: parent anchors.fill: parent
screen: root.screen captureSource: root.screen
opacity: 0
ScreencopyView { layer.enabled: true
id: background layer.effect: MultiEffect {
autoPaddingEnabled: false
blurEnabled: true
blur: 1
blurMax: 64
blurMultiplier: 1
}
}
Item {
id: lockContent
readonly property int size: lockIcon.implicitHeight + Tokens.padding.large * 4
readonly property int radius: size / 4 * Tokens.rounding.scale
anchors.centerIn: parent
implicitWidth: size
implicitHeight: size
rotation: 180
scale: 0
StyledRect {
id: lockBg
anchors.fill: parent anchors.fill: parent
captureSource: root.screen color: Colours.palette.m3surface
opacity: 0 radius: parent.radius
opacity: Colours.transparency.enabled ? Colours.transparency.base : 1
layer.enabled: true layer.enabled: true
layer.effect: MultiEffect { layer.effect: MultiEffect {
autoPaddingEnabled: false shadowEnabled: true
blurEnabled: true blurMax: 15
blur: 1 shadowColor: Qt.alpha(Colours.palette.m3shadow, 0.7)
blurMax: 64
blurMultiplier: 1
} }
} }
Item { MaterialIcon {
id: lockContent id: lockIcon
readonly property int size: lockIcon.implicitHeight + Tokens.padding.large * 4
readonly property int radius: size / 4 * Tokens.rounding.scale
anchors.centerIn: parent anchors.centerIn: parent
implicitWidth: size text: "lock"
implicitHeight: size font.pointSize: Tokens.font.size.extraLarge * 4
font.bold: true
rotation: 180 rotation: 180
}
Content {
id: content
anchors.centerIn: parent
width: (root.screen?.height ?? 0) * Tokens.sizes.lock.heightMult * Tokens.sizes.lock.ratio - Tokens.padding.large * 2
height: (root.screen?.height ?? 0) * Tokens.sizes.lock.heightMult - Tokens.padding.large * 2
lock: root
opacity: 0
scale: 0 scale: 0
StyledRect {
id: lockBg
anchors.fill: parent
color: Colours.palette.m3surface
radius: parent.radius
opacity: Colours.transparency.enabled ? Colours.transparency.base : 1
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
blurMax: 15
shadowColor: Qt.alpha(Colours.palette.m3shadow, 0.7)
}
}
MaterialIcon {
id: lockIcon
anchors.centerIn: parent
text: "lock"
font.pointSize: Tokens.font.size.extraLarge * 4
font.bold: true
rotation: 180
}
Content {
id: content
anchors.centerIn: parent
width: (root.screen?.height ?? 0) * Tokens.sizes.lock.heightMult * Tokens.sizes.lock.ratio - Tokens.padding.large * 2
height: (root.screen?.height ?? 0) * Tokens.sizes.lock.heightMult - Tokens.padding.large * 2
lock: root
opacity: 0
scale: 0
}
} }
} }
} }

View file

@ -1,4 +1,4 @@
find_package(Qt6 REQUIRED COMPONENTS ShaderTools Core Qml Gui Quick Concurrent Sql Network DBus) find_package(Qt6 REQUIRED COMPONENTS ShaderTools Core Qml Gui Quick QuickControls2 Concurrent Sql Network DBus)
find_package(PkgConfig REQUIRED) find_package(PkgConfig REQUIRED)
pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED) pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED)
pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED) pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED)

View file

@ -5,7 +5,6 @@ qml_module(caelestia-config
configattached.cpp configattached.cpp
configobject.cpp configobject.cpp
rootconfig.cpp rootconfig.cpp
configscope.cpp
appearanceconfig.cpp appearanceconfig.cpp
tokens.cpp tokens.cpp
tokensattached.cpp tokensattached.cpp
@ -29,4 +28,5 @@ qml_module(caelestia-config
winfoconfig.hpp winfoconfig.hpp
LIBRARIES LIBRARIES
Qt::Quick Qt::Quick
Qt::QuickControls2
) )

View file

@ -8,11 +8,11 @@ namespace caelestia::config {
class AnimCurves; class AnimCurves;
class AnimDurations; class AnimDurations;
class ConfigScope;
class AnimTokens : public QObject { class AnimTokens : public QObject {
Q_OBJECT Q_OBJECT
Q_MOC_INCLUDE("appearanceconfig.hpp") Q_MOC_INCLUDE("tokens.hpp") // AnimCurves
Q_MOC_INCLUDE("appearanceconfig.hpp") // AnimDurations
QML_ANONYMOUS QML_ANONYMOUS
Q_PROPERTY(QEasingCurve emphasized READ emphasized NOTIFY curvesChanged) Q_PROPERTY(QEasingCurve emphasized READ emphasized NOTIFY curvesChanged)

View file

@ -1,33 +1,56 @@
#include "configattached.hpp" #include "configattached.hpp"
#include "config.hpp" #include "config.hpp"
#include "configscope.hpp"
#include "monitorconfigmanager.hpp" #include "monitorconfigmanager.hpp"
namespace caelestia::config { namespace caelestia::config {
Config::Config(ConfigScope* scope, QObject* parent) Config::Config(QObject* parent)
: QObject(parent) : QQuickAttachedPropertyPropagator(parent) {
, m_scope(scope) { initialize();
connectScope();
} }
ConfigScope* Config::scope() const { QString Config::screen() const {
return m_scope; return m_screen;
} }
void Config::connectScope() { void Config::inheritScreen(const QString& screen) {
if (!m_scope) if (screen == m_screen)
return; return;
connect(m_scope, &ConfigScope::configChanged, this, &Config::sourceChanged);
connect(m_scope, &ConfigScope::tokensChanged, this, &Config::sourceChanged); m_screen = screen;
if (m_screen.isEmpty())
m_config = nullptr;
else
m_config = MonitorConfigManager::instance()->configForScreen(m_screen);
propagateScreen();
emit sourceChanged();
}
void Config::propagateScreen() {
const auto children = attachedChildren();
for (auto* const child : children) {
auto* const config = qobject_cast<Config*>(child);
if (config)
config->inheritScreen(m_screen);
}
}
void Config::attachedParentChange(
QQuickAttachedPropertyPropagator* newParent, QQuickAttachedPropertyPropagator* oldParent) {
Q_UNUSED(oldParent);
auto* const config = qobject_cast<Config*>(newParent);
if (config)
inheritScreen(config->screen());
} }
#define CONFIG_ATTACHED_GETTER(Type, name) \ #define CONFIG_ATTACHED_GETTER(Type, name) \
const Type* Config::name() const { \ const Type* Config::name() const { \
if (m_scope && m_scope->config()) \ if (m_config) \
return m_scope->config()->name(); \ return m_config->name(); \
if (parent()) \ if (parent()) \
qCWarning(lcConfig, "Config.%s accessed without a ConfigScope ancestor on %s", #name, \ qCWarning(lcConfig, "Config.%s accessed without a screen set on %s", #name, \
parent()->metaObject()->className()); \ parent()->metaObject()->className()); \
return GlobalConfig::instance()->name(); \ return GlobalConfig::instance()->name(); \
} }
@ -57,7 +80,7 @@ GlobalConfig* Config::forScreen(const QString& screen) {
} }
Config* Config::qmlAttachedProperties(QObject* object) { Config* Config::qmlAttachedProperties(QObject* object) {
return new Config(ConfigScope::find(object), object); return new Config(object);
} }
} // namespace caelestia::config } // namespace caelestia::config

View file

@ -1,17 +1,18 @@
#pragma once #pragma once
#include "config.hpp" #include "config.hpp"
#include "configscope.hpp"
#include <qquickattachedpropertypropagator.h>
namespace caelestia::config { namespace caelestia::config {
class Config : public QObject { class Config : public QQuickAttachedPropertyPropagator {
Q_OBJECT Q_OBJECT
QML_ELEMENT QML_ELEMENT
QML_UNCREATABLE("") QML_UNCREATABLE("")
QML_ATTACHED(Config) QML_ATTACHED(Config)
Q_PROPERTY(caelestia::config::ConfigScope* scope READ scope NOTIFY sourceChanged) Q_PROPERTY(QString screen READ screen WRITE inheritScreen NOTIFY sourceChanged)
Q_PROPERTY(const caelestia::config::AppearanceConfig* appearance READ appearance NOTIFY sourceChanged) Q_PROPERTY(const caelestia::config::AppearanceConfig* appearance READ appearance NOTIFY sourceChanged)
Q_PROPERTY(const caelestia::config::GeneralConfig* general READ general NOTIFY sourceChanged) Q_PROPERTY(const caelestia::config::GeneralConfig* general READ general NOTIFY sourceChanged)
Q_PROPERTY(const caelestia::config::BackgroundConfig* background READ background NOTIFY sourceChanged) Q_PROPERTY(const caelestia::config::BackgroundConfig* background READ background NOTIFY sourceChanged)
@ -31,9 +32,10 @@ class Config : public QObject {
Q_PROPERTY(const caelestia::config::UserPaths* paths READ paths NOTIFY sourceChanged) Q_PROPERTY(const caelestia::config::UserPaths* paths READ paths NOTIFY sourceChanged)
public: public:
explicit Config(ConfigScope* scope, QObject* parent = nullptr); explicit Config(QObject* parent = nullptr);
[[nodiscard]] ConfigScope* scope() const; [[nodiscard]] QString screen() const;
void inheritScreen(const QString& screen);
[[nodiscard]] const AppearanceConfig* appearance() const; [[nodiscard]] const AppearanceConfig* appearance() const;
[[nodiscard]] const GeneralConfig* general() const; [[nodiscard]] const GeneralConfig* general() const;
@ -60,10 +62,15 @@ public:
signals: signals:
void sourceChanged(); void sourceChanged();
private: protected:
void connectScope(); void attachedParentChange(
QQuickAttachedPropertyPropagator* newParent, QQuickAttachedPropertyPropagator* oldParent) override;
ConfigScope* m_scope; private:
void propagateScreen();
QString m_screen;
GlobalConfig* m_config = nullptr;
}; };
} // namespace caelestia::config } // namespace caelestia::config

View file

@ -1,64 +0,0 @@
#include "configscope.hpp"
#include "monitorconfigmanager.hpp"
namespace caelestia::config {
ConfigScope::ConfigScope(QQuickItem* parent)
: QQuickItem(parent) {}
QString ConfigScope::screen() const {
return m_screen;
}
GlobalConfig* ConfigScope::config() const {
return m_config;
}
TokenConfig* ConfigScope::tokens() const {
return m_tokens;
}
void ConfigScope::setScreen(const QString& screen) {
if (m_screen == screen)
return;
m_screen = screen;
emit screenChanged();
resolveConfig();
}
void ConfigScope::resolveConfig() {
GlobalConfig* newConfig = nullptr;
TokenConfig* newTokens = nullptr;
if (!m_screen.isEmpty()) {
if (auto* mgr = MonitorConfigManager::instance()) {
newConfig = mgr->configForScreen(m_screen);
newTokens = mgr->tokensForScreen(m_screen);
}
}
if (m_config != newConfig) {
m_config = newConfig;
emit configChanged();
}
if (m_tokens != newTokens) {
m_tokens = newTokens;
emit tokensChanged();
}
}
ConfigScope* ConfigScope::find(QObject* object) {
auto* item = qobject_cast<QQuickItem*>(object);
while (item) {
if (auto* scope = qobject_cast<ConfigScope*>(item))
return scope;
item = item->parentItem();
}
return nullptr;
}
} // namespace caelestia::config

View file

@ -1,45 +0,0 @@
#pragma once
#include <qqmlintegration.h>
#include <qquickitem.h>
namespace caelestia::config {
class GlobalConfig;
class TokenConfig;
class ConfigScope : public QQuickItem {
Q_OBJECT
Q_MOC_INCLUDE("config.hpp")
Q_MOC_INCLUDE("tokens.hpp")
QML_ELEMENT
Q_PROPERTY(QString screen READ screen WRITE setScreen NOTIFY screenChanged)
Q_PROPERTY(caelestia::config::GlobalConfig* config READ config NOTIFY configChanged)
Q_PROPERTY(caelestia::config::TokenConfig* tokens READ tokens NOTIFY tokensChanged)
public:
explicit ConfigScope(QQuickItem* parent = nullptr);
[[nodiscard]] QString screen() const;
void setScreen(const QString& screen);
[[nodiscard]] GlobalConfig* config() const;
[[nodiscard]] TokenConfig* tokens() const;
static ConfigScope* find(QObject* object);
signals:
void screenChanged();
void configChanged();
void tokensChanged();
private:
void resolveConfig();
QString m_screen;
GlobalConfig* m_config = nullptr;
TokenConfig* m_tokens = nullptr;
};
} // namespace caelestia::config

View file

@ -9,8 +9,6 @@
namespace caelestia::config { namespace caelestia::config {
class ConfigScope;
class AnimCurves : public ConfigObject { class AnimCurves : public ConfigObject {
Q_OBJECT Q_OBJECT
QML_ANONYMOUS QML_ANONYMOUS

View file

@ -1,7 +1,6 @@
#include "tokensattached.hpp" #include "tokensattached.hpp"
#include "anim.hpp" #include "anim.hpp"
#include "config.hpp" #include "config.hpp"
#include "configscope.hpp"
#include "monitorconfigmanager.hpp" #include "monitorconfigmanager.hpp"
#include "tokens.hpp" #include "tokens.hpp"
@ -9,29 +8,60 @@ namespace caelestia::config {
namespace { namespace {
const AppearanceConfig* resolveAppearance(ConfigScope* scope, const char* prop, QObject* parent) { const AppearanceConfig* resolveAppearance(GlobalConfig* config, const char* prop, QObject* parent) {
if (scope && scope->config()) if (config)
return scope->config()->appearance(); return config->appearance();
if (parent) if (parent)
qCWarning(lcConfig, "Tokens.%s accessed without a ConfigScope ancestor on %s", prop, qCWarning(lcConfig, "Tokens.%s accessed without a screen set on %s", prop, parent->metaObject()->className());
parent->metaObject()->className());
return GlobalConfig::instance()->appearance(); return GlobalConfig::instance()->appearance();
} }
} // namespace } // namespace
Tokens::Tokens(ConfigScope* scope, QObject* parent) Tokens::Tokens(QObject* parent)
: QObject(parent) : QQuickAttachedPropertyPropagator(parent)
, m_scope(scope)
, m_anim(new AnimTokens(this)) { , m_anim(new AnimTokens(this)) {
connectScope();
bindAnim(); bindAnim();
initialize();
} }
void Tokens::connectScope() { QString Tokens::screen() const {
if (!m_scope) return m_screen;
}
void Tokens::inheritScreen(const QString& screen) {
if (screen == m_screen)
return; return;
connect(m_scope, &ConfigScope::configChanged, this, &Tokens::sourceChanged);
m_screen = screen;
if (m_screen.isEmpty()) {
m_config = nullptr;
m_tokens = nullptr;
} else {
m_config = MonitorConfigManager::instance()->configForScreen(m_screen);
m_tokens = MonitorConfigManager::instance()->tokensForScreen(m_screen);
}
propagateScreen();
emit sourceChanged();
}
void Tokens::propagateScreen() {
const auto children = attachedChildren();
for (auto* const child : children) {
auto* const tokens = qobject_cast<Tokens*>(child);
if (tokens)
tokens->inheritScreen(m_screen);
}
}
void Tokens::attachedParentChange(
QQuickAttachedPropertyPropagator* newParent, QQuickAttachedPropertyPropagator* oldParent) {
Q_UNUSED(oldParent);
auto* const tokens = qobject_cast<Tokens*>(newParent);
if (tokens)
inheritScreen(tokens->screen());
} }
void Tokens::bindAnim() { void Tokens::bindAnim() {
@ -41,7 +71,7 @@ void Tokens::bindAnim() {
#define TOKENS_ATTACHED_GETTER(Type, name) \ #define TOKENS_ATTACHED_GETTER(Type, name) \
const Type* Tokens::name() const { \ const Type* Tokens::name() const { \
auto* a = resolveAppearance(m_scope, #name, parent()); \ auto* a = resolveAppearance(m_config, #name, parent()); \
return a ? a->name() : nullptr; \ return a ? a->name() : nullptr; \
} }
@ -57,11 +87,10 @@ const AppearanceTransparency* Tokens::transparency() const {
} }
const SizeTokens* Tokens::sizes() const { const SizeTokens* Tokens::sizes() const {
if (m_scope && m_scope->tokens()) if (m_tokens)
return m_scope->tokens()->sizes(); return m_tokens->sizes();
if (parent()) if (parent())
qCWarning(lcConfig, "Tokens.sizes accessed without a ConfigScope ancestor on %s", qCWarning(lcConfig, "Tokens.sizes accessed without a screen set on %s", parent()->metaObject()->className());
parent()->metaObject()->className());
return TokenConfig::instance()->sizes(); return TokenConfig::instance()->sizes();
} }
@ -74,7 +103,7 @@ TokenConfig* Tokens::forScreen(const QString& screen) {
} }
Tokens* Tokens::qmlAttachedProperties(QObject* object) { Tokens* Tokens::qmlAttachedProperties(QObject* object) {
return new Tokens(ConfigScope::find(object), object); return new Tokens(object);
} }
} // namespace caelestia::config } // namespace caelestia::config

View file

@ -2,17 +2,20 @@
#include "anim.hpp" #include "anim.hpp"
#include "appearanceconfig.hpp" #include "appearanceconfig.hpp"
#include "configscope.hpp" #include "config.hpp"
#include "tokens.hpp" #include "tokens.hpp"
#include <qquickattachedpropertypropagator.h>
namespace caelestia::config { namespace caelestia::config {
class Tokens : public QObject { class Tokens : public QQuickAttachedPropertyPropagator {
Q_OBJECT Q_OBJECT
QML_ELEMENT QML_ELEMENT
QML_UNCREATABLE("") QML_UNCREATABLE("")
QML_ATTACHED(Tokens) QML_ATTACHED(Tokens)
Q_PROPERTY(QString screen READ screen WRITE inheritScreen NOTIFY sourceChanged)
Q_PROPERTY(const caelestia::config::AppearanceRounding* rounding READ rounding NOTIFY sourceChanged) Q_PROPERTY(const caelestia::config::AppearanceRounding* rounding READ rounding NOTIFY sourceChanged)
Q_PROPERTY(const caelestia::config::AppearanceSpacing* spacing READ spacing NOTIFY sourceChanged) Q_PROPERTY(const caelestia::config::AppearanceSpacing* spacing READ spacing NOTIFY sourceChanged)
Q_PROPERTY(const caelestia::config::AppearancePadding* padding READ padding NOTIFY sourceChanged) Q_PROPERTY(const caelestia::config::AppearancePadding* padding READ padding NOTIFY sourceChanged)
@ -22,7 +25,10 @@ class Tokens : public QObject {
Q_PROPERTY(const caelestia::config::AnimTokens* anim READ anim NOTIFY sourceChanged) Q_PROPERTY(const caelestia::config::AnimTokens* anim READ anim NOTIFY sourceChanged)
public: public:
explicit Tokens(ConfigScope* scope, QObject* parent = nullptr); explicit Tokens(QObject* parent = nullptr);
[[nodiscard]] QString screen() const;
void inheritScreen(const QString& screen);
[[nodiscard]] const AppearanceRounding* rounding() const; [[nodiscard]] const AppearanceRounding* rounding() const;
[[nodiscard]] const AppearanceSpacing* spacing() const; [[nodiscard]] const AppearanceSpacing* spacing() const;
@ -40,11 +46,17 @@ public:
signals: signals:
void sourceChanged(); void sourceChanged();
protected:
void attachedParentChange(
QQuickAttachedPropertyPropagator* newParent, QQuickAttachedPropertyPropagator* oldParent) override;
private: private:
void connectScope(); void propagateScreen();
void bindAnim(); void bindAnim();
ConfigScope* m_scope; QString m_screen;
GlobalConfig* m_config = nullptr;
TokenConfig* m_tokens = nullptr;
AnimTokens* m_anim = nullptr; AnimTokens* m_anim = nullptr;
}; };