feat: add per monitor configs
Per monitor configs go in a separate file in the monitor dir, e.g. `monitors/<MONITOR_NAME>/shell.json`
This commit is contained in:
parent
a932a68a40
commit
71f27bb45f
11 changed files with 556 additions and 5 deletions
|
|
@ -6,6 +6,7 @@ qml_module(caelestia-config
|
||||||
configscope.cpp
|
configscope.cpp
|
||||||
appearanceconfig.cpp
|
appearanceconfig.cpp
|
||||||
tokens.cpp
|
tokens.cpp
|
||||||
|
monitorconfigmanager.cpp
|
||||||
backgroundconfig.hpp
|
backgroundconfig.hpp
|
||||||
barconfig.hpp
|
barconfig.hpp
|
||||||
borderconfig.hpp
|
borderconfig.hpp
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,30 @@ GlobalConfig::GlobalConfig(QObject* parent)
|
||||||
setupFileBackend(configDir() + QStringLiteral("shell.json"));
|
setupFileBackend(configDir() + QStringLiteral("shell.json"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GlobalConfig::GlobalConfig(GlobalConfig* fallback, const QString& filePath, QObject* parent)
|
||||||
|
: ConfigObject(parent)
|
||||||
|
, m_appearance(new AppearanceConfig(this))
|
||||||
|
, m_general(new GeneralConfig(this))
|
||||||
|
, m_background(new BackgroundConfig(this))
|
||||||
|
, m_bar(new BarConfig(this))
|
||||||
|
, m_border(new BorderConfig(this))
|
||||||
|
, m_dashboard(new DashboardConfig(this))
|
||||||
|
, m_controlCenter(new ControlCenterConfig(this))
|
||||||
|
, m_launcher(new LauncherConfig(this))
|
||||||
|
, m_notifs(new NotifsConfig(this))
|
||||||
|
, m_osd(new OsdConfig(this))
|
||||||
|
, m_session(new SessionConfig(this))
|
||||||
|
, m_winfo(new WInfoConfig(this))
|
||||||
|
, m_lock(new LockConfig(this))
|
||||||
|
, m_utilities(new UtilitiesConfig(this))
|
||||||
|
, m_sidebar(new SidebarConfig(this))
|
||||||
|
, m_services(new ServiceConfig(this))
|
||||||
|
, m_paths(new UserPaths(this)) {
|
||||||
|
setSparse(true);
|
||||||
|
setupFileBackend(filePath);
|
||||||
|
syncFromGlobal(fallback);
|
||||||
|
}
|
||||||
|
|
||||||
GlobalConfig::~GlobalConfig() {
|
GlobalConfig::~GlobalConfig() {
|
||||||
// Clear global instance
|
// Clear global instance
|
||||||
s_instance = nullptr;
|
s_instance = nullptr;
|
||||||
|
|
@ -67,7 +91,44 @@ void GlobalConfig::reload() {
|
||||||
|
|
||||||
Config::Config(ConfigScope* scope, QObject* parent)
|
Config::Config(ConfigScope* scope, QObject* parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
, m_scope(scope) {}
|
, m_scope(scope) {
|
||||||
|
connectScope();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Config::connectScope() {
|
||||||
|
if (!m_scope)
|
||||||
|
return;
|
||||||
|
connect(m_scope, &ConfigScope::configChanged, this, &Config::sourceChanged);
|
||||||
|
connect(m_scope, &ConfigScope::tokensChanged, this, &Config::sourceChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: return per-monitor sub-object if scope exists, otherwise global
|
||||||
|
#define CONFIG_ATTACHED_GETTER(Type, name) \
|
||||||
|
const Type* Config::name() const { \
|
||||||
|
if (m_scope && m_scope->config()) \
|
||||||
|
return m_scope->config()->name(); \
|
||||||
|
return GlobalConfig::instance()->name(); \
|
||||||
|
}
|
||||||
|
|
||||||
|
CONFIG_ATTACHED_GETTER(AppearanceConfig, appearance)
|
||||||
|
CONFIG_ATTACHED_GETTER(GeneralConfig, general)
|
||||||
|
CONFIG_ATTACHED_GETTER(BackgroundConfig, background)
|
||||||
|
CONFIG_ATTACHED_GETTER(BarConfig, bar)
|
||||||
|
CONFIG_ATTACHED_GETTER(BorderConfig, border)
|
||||||
|
CONFIG_ATTACHED_GETTER(DashboardConfig, dashboard)
|
||||||
|
CONFIG_ATTACHED_GETTER(ControlCenterConfig, controlCenter)
|
||||||
|
CONFIG_ATTACHED_GETTER(LauncherConfig, launcher)
|
||||||
|
CONFIG_ATTACHED_GETTER(NotifsConfig, notifs)
|
||||||
|
CONFIG_ATTACHED_GETTER(OsdConfig, osd)
|
||||||
|
CONFIG_ATTACHED_GETTER(SessionConfig, session)
|
||||||
|
CONFIG_ATTACHED_GETTER(WInfoConfig, winfo)
|
||||||
|
CONFIG_ATTACHED_GETTER(LockConfig, lock)
|
||||||
|
CONFIG_ATTACHED_GETTER(UtilitiesConfig, utilities)
|
||||||
|
CONFIG_ATTACHED_GETTER(SidebarConfig, sidebar)
|
||||||
|
CONFIG_ATTACHED_GETTER(ServiceConfig, services)
|
||||||
|
CONFIG_ATTACHED_GETTER(UserPaths, paths)
|
||||||
|
|
||||||
|
#undef CONFIG_ATTACHED_GETTER
|
||||||
|
|
||||||
Config* Config::qmlAttachedProperties(QObject* object) {
|
Config* Config::qmlAttachedProperties(QObject* object) {
|
||||||
return new Config(ConfigScope::find(object), object);
|
return new Config(ConfigScope::find(object), object);
|
||||||
|
|
|
||||||
|
|
@ -55,25 +55,69 @@ public:
|
||||||
~GlobalConfig() override;
|
~GlobalConfig() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
friend class MonitorConfigManager;
|
||||||
explicit GlobalConfig(QObject* parent = nullptr);
|
explicit GlobalConfig(QObject* parent = nullptr);
|
||||||
|
explicit GlobalConfig(GlobalConfig* fallback, const QString& filePath, QObject* parent);
|
||||||
};
|
};
|
||||||
|
|
||||||
class ConfigScope;
|
class ConfigScope;
|
||||||
|
|
||||||
class Config : public QObject {
|
class Config : public QObject {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
Q_MOC_INCLUDE("configscope.hpp")
|
||||||
QML_ELEMENT
|
QML_ELEMENT
|
||||||
QML_UNCREATABLE("")
|
QML_UNCREATABLE("")
|
||||||
QML_ATTACHED(Config)
|
QML_ATTACHED(Config)
|
||||||
|
|
||||||
|
Q_PROPERTY(ConfigScope* scope READ scope NOTIFY sourceChanged)
|
||||||
|
Q_PROPERTY(const AppearanceConfig* appearance READ appearance NOTIFY sourceChanged)
|
||||||
|
Q_PROPERTY(const GeneralConfig* general READ general NOTIFY sourceChanged)
|
||||||
|
Q_PROPERTY(const BackgroundConfig* background READ background NOTIFY sourceChanged)
|
||||||
|
Q_PROPERTY(const BarConfig* bar READ bar NOTIFY sourceChanged)
|
||||||
|
Q_PROPERTY(const BorderConfig* border READ border NOTIFY sourceChanged)
|
||||||
|
Q_PROPERTY(const DashboardConfig* dashboard READ dashboard NOTIFY sourceChanged)
|
||||||
|
Q_PROPERTY(const ControlCenterConfig* controlCenter READ controlCenter NOTIFY sourceChanged)
|
||||||
|
Q_PROPERTY(const LauncherConfig* launcher READ launcher NOTIFY sourceChanged)
|
||||||
|
Q_PROPERTY(const NotifsConfig* notifs READ notifs NOTIFY sourceChanged)
|
||||||
|
Q_PROPERTY(const OsdConfig* osd READ osd NOTIFY sourceChanged)
|
||||||
|
Q_PROPERTY(const SessionConfig* session READ session NOTIFY sourceChanged)
|
||||||
|
Q_PROPERTY(const WInfoConfig* winfo READ winfo NOTIFY sourceChanged)
|
||||||
|
Q_PROPERTY(const LockConfig* lock READ lock NOTIFY sourceChanged)
|
||||||
|
Q_PROPERTY(const UtilitiesConfig* utilities READ utilities NOTIFY sourceChanged)
|
||||||
|
Q_PROPERTY(const SidebarConfig* sidebar READ sidebar NOTIFY sourceChanged)
|
||||||
|
Q_PROPERTY(const ServiceConfig* services READ services NOTIFY sourceChanged)
|
||||||
|
Q_PROPERTY(const UserPaths* paths READ paths NOTIFY sourceChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit Config(ConfigScope* scope, QObject* parent = nullptr);
|
explicit Config(ConfigScope* scope, QObject* parent = nullptr);
|
||||||
|
|
||||||
[[nodiscard]] ConfigScope* scope() const { return m_scope; }
|
[[nodiscard]] ConfigScope* scope() const { return m_scope; }
|
||||||
|
|
||||||
|
[[nodiscard]] const AppearanceConfig* appearance() const;
|
||||||
|
[[nodiscard]] const GeneralConfig* general() const;
|
||||||
|
[[nodiscard]] const BackgroundConfig* background() const;
|
||||||
|
[[nodiscard]] const BarConfig* bar() const;
|
||||||
|
[[nodiscard]] const BorderConfig* border() const;
|
||||||
|
[[nodiscard]] const DashboardConfig* dashboard() const;
|
||||||
|
[[nodiscard]] const ControlCenterConfig* controlCenter() const;
|
||||||
|
[[nodiscard]] const LauncherConfig* launcher() const;
|
||||||
|
[[nodiscard]] const NotifsConfig* notifs() const;
|
||||||
|
[[nodiscard]] const OsdConfig* osd() const;
|
||||||
|
[[nodiscard]] const SessionConfig* session() const;
|
||||||
|
[[nodiscard]] const WInfoConfig* winfo() const;
|
||||||
|
[[nodiscard]] const LockConfig* lock() const;
|
||||||
|
[[nodiscard]] const UtilitiesConfig* utilities() const;
|
||||||
|
[[nodiscard]] const SidebarConfig* sidebar() const;
|
||||||
|
[[nodiscard]] const ServiceConfig* services() const;
|
||||||
|
[[nodiscard]] const UserPaths* paths() const;
|
||||||
|
|
||||||
static Config* qmlAttachedProperties(QObject* object);
|
static Config* qmlAttachedProperties(QObject* object);
|
||||||
|
|
||||||
|
Q_SIGNAL void sourceChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void connectScope();
|
||||||
|
|
||||||
ConfigScope* m_scope;
|
ConfigScope* m_scope;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,11 +50,13 @@ void ConfigObject::loadFromJson(const QJsonObject& obj) {
|
||||||
for (const auto& v : jsonArr)
|
for (const auto& v : jsonArr)
|
||||||
list.append(v.toString());
|
list.append(v.toString());
|
||||||
prop.write(this, QVariant::fromValue(list));
|
prop.write(this, QVariant::fromValue(list));
|
||||||
|
m_loadedKeys.insert(key);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For all other types, let Qt's variant conversion handle it
|
// For all other types, let Qt's variant conversion handle it
|
||||||
prop.write(this, jsonVal.toVariant());
|
prop.write(this, jsonVal.toVariant());
|
||||||
|
m_loadedKeys.insert(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -108,6 +110,159 @@ QJsonObject ConfigObject::toJsonObject() const {
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QJsonObject ConfigObject::toSparseJsonObject() const {
|
||||||
|
QJsonObject obj;
|
||||||
|
const auto* meta = metaObject();
|
||||||
|
|
||||||
|
for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) {
|
||||||
|
const auto prop = meta->property(i);
|
||||||
|
|
||||||
|
if (!prop.isReadable())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const auto key = QString::fromUtf8(prop.name());
|
||||||
|
const auto value = prop.read(this);
|
||||||
|
|
||||||
|
// Recurse into sub-objects — include only if they have loaded keys
|
||||||
|
if (value.canView<ConfigObject*>()) {
|
||||||
|
auto* const subObj = value.value<ConfigObject*>();
|
||||||
|
if (subObj) {
|
||||||
|
auto subJson = subObj->toSparseJsonObject();
|
||||||
|
if (!subJson.isEmpty())
|
||||||
|
obj.insert(key, subJson);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only include properties that were explicitly loaded
|
||||||
|
if (!m_loadedKeys.contains(key))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!prop.isWritable())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (prop.metaType().id() == QMetaType::QStringList) {
|
||||||
|
QJsonArray arr;
|
||||||
|
const auto strList = value.toStringList();
|
||||||
|
for (const auto& s : strList)
|
||||||
|
arr.append(s);
|
||||||
|
obj.insert(key, arr);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prop.metaType().id() == QMetaType::QVariantList) {
|
||||||
|
obj.insert(key, QJsonArray::fromVariantList(value.toList()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
obj.insert(key, QJsonValue::fromVariant(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConfigObject::clearLoadedKeys() {
|
||||||
|
m_loadedKeys.clear();
|
||||||
|
|
||||||
|
const auto* meta = metaObject();
|
||||||
|
for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) {
|
||||||
|
auto prop = meta->property(i);
|
||||||
|
auto value = prop.read(this);
|
||||||
|
auto* subObj = value.value<ConfigObject*>();
|
||||||
|
if (subObj)
|
||||||
|
subObj->clearLoadedKeys();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConfigObject::syncFromGlobal(ConfigObject* global) {
|
||||||
|
m_global = global;
|
||||||
|
|
||||||
|
// Connect batched change signal (single connection per ConfigObject pair)
|
||||||
|
connect(global, &ConfigObject::propertiesChanged, this, &ConfigObject::onGlobalPropertiesChanged);
|
||||||
|
|
||||||
|
// Initial sync: copy all non-loaded property values from global
|
||||||
|
const auto* meta = metaObject();
|
||||||
|
for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) {
|
||||||
|
auto prop = meta->property(i);
|
||||||
|
const auto key = QString::fromUtf8(prop.name());
|
||||||
|
|
||||||
|
auto current = prop.read(this);
|
||||||
|
auto* subObj = current.value<ConfigObject*>();
|
||||||
|
|
||||||
|
if (subObj) {
|
||||||
|
auto globalVal = prop.read(global);
|
||||||
|
auto* globalSub = globalVal.value<ConfigObject*>();
|
||||||
|
if (globalSub)
|
||||||
|
subObj->syncFromGlobal(globalSub);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!prop.isWritable())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!m_loadedKeys.contains(key))
|
||||||
|
prop.write(this, prop.read(global));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConfigObject::resyncFromGlobal() {
|
||||||
|
if (!m_global)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const auto* meta = metaObject();
|
||||||
|
for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) {
|
||||||
|
auto prop = meta->property(i);
|
||||||
|
const auto key = QString::fromUtf8(prop.name());
|
||||||
|
|
||||||
|
auto current = prop.read(this);
|
||||||
|
auto* subObj = current.value<ConfigObject*>();
|
||||||
|
|
||||||
|
if (subObj) {
|
||||||
|
subObj->resyncFromGlobal();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!prop.isWritable())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!m_loadedKeys.contains(key))
|
||||||
|
prop.write(this, prop.read(m_global));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConfigObject::onGlobalPropertiesChanged(const QMap<QString, QVariant>& changed) {
|
||||||
|
for (auto it = changed.begin(); it != changed.end(); ++it) {
|
||||||
|
if (m_loadedKeys.contains(it.key()))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
int idx = metaObject()->indexOfProperty(it.key().toUtf8().constData());
|
||||||
|
if (idx >= 0)
|
||||||
|
metaObject()->property(idx).write(this, it.value());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConfigObject::notifyPropertyChanged(const QString& name, const QVariant& value) {
|
||||||
|
m_pendingChanges.insert(name, value);
|
||||||
|
|
||||||
|
if (!m_batchTimer) {
|
||||||
|
m_batchTimer = new QTimer(this);
|
||||||
|
m_batchTimer->setSingleShot(true);
|
||||||
|
m_batchTimer->setInterval(0);
|
||||||
|
connect(m_batchTimer, &QTimer::timeout, this, &ConfigObject::emitBatchedChanges);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_batchTimer->start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConfigObject::emitBatchedChanges() {
|
||||||
|
if (m_pendingChanges.isEmpty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
auto changes = std::move(m_pendingChanges);
|
||||||
|
m_pendingChanges.clear();
|
||||||
|
Q_EMIT propertiesChanged(changes);
|
||||||
|
}
|
||||||
|
|
||||||
void ConfigObject::setupFileBackend(const QString& path) {
|
void ConfigObject::setupFileBackend(const QString& path) {
|
||||||
m_filePath = path;
|
m_filePath = path;
|
||||||
|
|
||||||
|
|
@ -126,7 +281,8 @@ void ConfigObject::setupFileBackend(const QString& path) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
file.write(QJsonDocument(toJsonObject()).toJson(QJsonDocument::Indented));
|
auto json = m_sparse ? toSparseJsonObject() : toJsonObject();
|
||||||
|
file.write(QJsonDocument(json).toJson(QJsonDocument::Indented));
|
||||||
});
|
});
|
||||||
|
|
||||||
m_cooldownTimer->setSingleShot(true);
|
m_cooldownTimer->setSingleShot(true);
|
||||||
|
|
@ -167,7 +323,12 @@ void ConfigObject::reloadFromFile() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearLoadedKeys();
|
||||||
loadFromJson(doc.object());
|
loadFromJson(doc.object());
|
||||||
|
|
||||||
|
// Re-sync non-loaded properties from global after reload
|
||||||
|
if (m_global)
|
||||||
|
resyncFromGlobal();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ConfigObject::onFileChanged() {
|
void ConfigObject::onFileChanged() {
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,12 @@
|
||||||
|
|
||||||
#include <qfilesystemwatcher.h>
|
#include <qfilesystemwatcher.h>
|
||||||
#include <qjsonobject.h>
|
#include <qjsonobject.h>
|
||||||
|
#include <qmap.h>
|
||||||
#include <qobject.h>
|
#include <qobject.h>
|
||||||
#include <qqmlintegration.h>
|
#include <qqmlintegration.h>
|
||||||
|
#include <qset.h>
|
||||||
#include <qtimer.h>
|
#include <qtimer.h>
|
||||||
|
#include <qvariant.h>
|
||||||
|
|
||||||
// Declares a serialized config property with getter, setter (change-detected), signal, and member.
|
// Declares a serialized config property with getter, setter (change-detected), signal, and member.
|
||||||
#define CONFIG_PROPERTY(Type, name, ...) \
|
#define CONFIG_PROPERTY(Type, name, ...) \
|
||||||
|
|
@ -15,8 +18,10 @@ public:
|
||||||
return m_##name; \
|
return m_##name; \
|
||||||
} \
|
} \
|
||||||
void set_##name(const Type& val) { \
|
void set_##name(const Type& val) { \
|
||||||
if (caelestia::config::ConfigObject::updateMember(m_##name, val)) \
|
if (caelestia::config::ConfigObject::updateMember(m_##name, val)) { \
|
||||||
Q_EMIT name##Changed(); \
|
Q_EMIT name##Changed(); \
|
||||||
|
notifyPropertyChanged(QStringLiteral(#name), QVariant::fromValue(m_##name)); \
|
||||||
|
} \
|
||||||
} \
|
} \
|
||||||
Q_SIGNAL void name##Changed(); \
|
Q_SIGNAL void name##Changed(); \
|
||||||
\
|
\
|
||||||
|
|
@ -45,6 +50,7 @@ public:
|
||||||
|
|
||||||
void loadFromJson(const QJsonObject& obj);
|
void loadFromJson(const QJsonObject& obj);
|
||||||
[[nodiscard]] QJsonObject toJsonObject() const;
|
[[nodiscard]] QJsonObject toJsonObject() const;
|
||||||
|
[[nodiscard]] QJsonObject toSparseJsonObject() const;
|
||||||
|
|
||||||
// File-backed config support. Call setupFileBackend() to enable
|
// File-backed config support. Call setupFileBackend() to enable
|
||||||
// automatic file watching, debounced saving, and reload.
|
// automatic file watching, debounced saving, and reload.
|
||||||
|
|
@ -52,6 +58,18 @@ public:
|
||||||
void saveToFile();
|
void saveToFile();
|
||||||
void reloadFromFile();
|
void reloadFromFile();
|
||||||
|
|
||||||
|
// Per-monitor overlay support (Qt Resolve Mask pattern).
|
||||||
|
// Eagerly syncs non-overridden properties from a global ConfigObject.
|
||||||
|
void syncFromGlobal(ConfigObject* global);
|
||||||
|
void resyncFromGlobal();
|
||||||
|
void clearLoadedKeys();
|
||||||
|
|
||||||
|
[[nodiscard]] bool isPropertyLoaded(const QString& name) const { return m_loadedKeys.contains(name); }
|
||||||
|
|
||||||
|
[[nodiscard]] bool isSparse() const { return m_sparse; }
|
||||||
|
|
||||||
|
void setSparse(bool sparse) { m_sparse = sparse; }
|
||||||
|
|
||||||
[[nodiscard]] bool recentlySaved() const { return m_recentlySaved; }
|
[[nodiscard]] bool recentlySaved() const { return m_recentlySaved; }
|
||||||
|
|
||||||
template <typename T> static bool updateMember(T& member, const T& value) {
|
template <typename T> static bool updateMember(T& member, const T& value) {
|
||||||
|
|
@ -66,15 +84,30 @@ public:
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Q_SIGNAL void propertiesChanged(const QMap<QString, QVariant>& changed);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void notifyPropertyChanged(const QString& name, const QVariant& value);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void onFileChanged();
|
void onFileChanged();
|
||||||
|
void onGlobalPropertiesChanged(const QMap<QString, QVariant>& changed);
|
||||||
|
void emitBatchedChanges();
|
||||||
|
|
||||||
QString m_filePath;
|
QString m_filePath;
|
||||||
bool m_recentlySaved = false;
|
bool m_recentlySaved = false;
|
||||||
// These are heap-allocated only when setupFileBackend is called
|
bool m_sparse = false;
|
||||||
|
|
||||||
|
// File backend (heap-allocated only when setupFileBackend is called)
|
||||||
QFileSystemWatcher* m_watcher = nullptr;
|
QFileSystemWatcher* m_watcher = nullptr;
|
||||||
QTimer* m_saveTimer = nullptr;
|
QTimer* m_saveTimer = nullptr;
|
||||||
QTimer* m_cooldownTimer = nullptr;
|
QTimer* m_cooldownTimer = nullptr;
|
||||||
|
|
||||||
|
// Per-monitor overlay state
|
||||||
|
ConfigObject* m_global = nullptr;
|
||||||
|
QSet<QString> m_loadedKeys;
|
||||||
|
QMap<QString, QVariant> m_pendingChanges;
|
||||||
|
QTimer* m_batchTimer = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace caelestia::config
|
} // namespace caelestia::config
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,42 @@
|
||||||
#include "configscope.hpp"
|
#include "configscope.hpp"
|
||||||
|
#include "monitorconfigmanager.hpp"
|
||||||
|
|
||||||
namespace caelestia::config {
|
namespace caelestia::config {
|
||||||
|
|
||||||
ConfigScope::ConfigScope(QQuickItem* parent)
|
ConfigScope::ConfigScope(QQuickItem* parent)
|
||||||
: QQuickItem(parent) {}
|
: QQuickItem(parent) {}
|
||||||
|
|
||||||
|
void ConfigScope::setScreen(const QString& screen) {
|
||||||
|
if (m_screen == screen)
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_screen = screen;
|
||||||
|
Q_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;
|
||||||
|
Q_EMIT configChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_tokens != newTokens) {
|
||||||
|
m_tokens = newTokens;
|
||||||
|
Q_EMIT tokensChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ConfigScope* ConfigScope::find(QObject* object) {
|
ConfigScope* ConfigScope::find(QObject* object) {
|
||||||
auto* item = qobject_cast<QQuickItem*>(object);
|
auto* item = qobject_cast<QQuickItem*>(object);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,42 @@
|
||||||
|
|
||||||
namespace caelestia::config {
|
namespace caelestia::config {
|
||||||
|
|
||||||
|
class GlobalConfig;
|
||||||
|
class TokenConfig;
|
||||||
|
|
||||||
class ConfigScope : public QQuickItem {
|
class ConfigScope : public QQuickItem {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
Q_MOC_INCLUDE("config.hpp")
|
||||||
|
Q_MOC_INCLUDE("tokens.hpp")
|
||||||
QML_ELEMENT
|
QML_ELEMENT
|
||||||
|
|
||||||
|
Q_PROPERTY(QString screen READ screen WRITE setScreen NOTIFY screenChanged)
|
||||||
|
Q_PROPERTY(GlobalConfig* config READ config NOTIFY configChanged)
|
||||||
|
Q_PROPERTY(TokenConfig* tokens READ tokens NOTIFY tokensChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit ConfigScope(QQuickItem* parent = nullptr);
|
explicit ConfigScope(QQuickItem* parent = nullptr);
|
||||||
|
|
||||||
|
[[nodiscard]] QString screen() const { return m_screen; }
|
||||||
|
|
||||||
|
void setScreen(const QString& screen);
|
||||||
|
|
||||||
|
[[nodiscard]] GlobalConfig* config() const { return m_config; }
|
||||||
|
|
||||||
|
[[nodiscard]] TokenConfig* tokens() const { return m_tokens; }
|
||||||
|
|
||||||
static ConfigScope* find(QObject* object);
|
static ConfigScope* find(QObject* object);
|
||||||
|
|
||||||
|
Q_SIGNAL void screenChanged();
|
||||||
|
Q_SIGNAL void configChanged();
|
||||||
|
Q_SIGNAL void tokensChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void resolveConfig();
|
||||||
|
|
||||||
|
QString m_screen;
|
||||||
|
GlobalConfig* m_config = nullptr;
|
||||||
|
TokenConfig* m_tokens = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace caelestia::config
|
} // namespace caelestia::config
|
||||||
|
|
|
||||||
69
plugin/src/Caelestia/Config/monitorconfigmanager.cpp
Normal file
69
plugin/src/Caelestia/Config/monitorconfigmanager.cpp
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
#include "monitorconfigmanager.hpp"
|
||||||
|
#include "config.hpp"
|
||||||
|
#include "tokens.hpp"
|
||||||
|
|
||||||
|
#include <qstandardpaths.h>
|
||||||
|
|
||||||
|
namespace caelestia::config {
|
||||||
|
|
||||||
|
MonitorConfigManager* MonitorConfigManager::s_instance = nullptr;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
QString monitorConfigDir(const QString& screen) {
|
||||||
|
return QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) +
|
||||||
|
QStringLiteral("/caelestia/monitors/") + screen + QStringLiteral("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
MonitorConfigManager::MonitorConfigManager(QObject* parent)
|
||||||
|
: QObject(parent) {
|
||||||
|
s_instance = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
MonitorConfigManager::~MonitorConfigManager() {
|
||||||
|
s_instance = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
MonitorConfigManager* MonitorConfigManager::instance() {
|
||||||
|
return s_instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
MonitorConfigManager* MonitorConfigManager::create(QQmlEngine* engine, QJSEngine*) {
|
||||||
|
return new MonitorConfigManager(engine);
|
||||||
|
}
|
||||||
|
|
||||||
|
GlobalConfig* MonitorConfigManager::configForScreen(const QString& screen) {
|
||||||
|
auto it = m_overlays.find(screen);
|
||||||
|
if (it != m_overlays.end() && it->config)
|
||||||
|
return it->config;
|
||||||
|
|
||||||
|
auto* global = GlobalConfig::instance();
|
||||||
|
if (!global)
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
auto dir = monitorConfigDir(screen);
|
||||||
|
auto* overlay = new GlobalConfig(global, dir + QStringLiteral("shell.json"), this);
|
||||||
|
|
||||||
|
m_overlays[screen].config = overlay;
|
||||||
|
return overlay;
|
||||||
|
}
|
||||||
|
|
||||||
|
TokenConfig* MonitorConfigManager::tokensForScreen(const QString& screen) {
|
||||||
|
auto it = m_overlays.find(screen);
|
||||||
|
if (it != m_overlays.end() && it->tokens)
|
||||||
|
return it->tokens;
|
||||||
|
|
||||||
|
auto* global = TokenConfig::instance();
|
||||||
|
if (!global)
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
auto dir = monitorConfigDir(screen);
|
||||||
|
auto* overlay = new TokenConfig(global, dir + QStringLiteral("shell-tokens.json"), this);
|
||||||
|
|
||||||
|
m_overlays[screen].tokens = overlay;
|
||||||
|
return overlay;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace caelestia::config
|
||||||
38
plugin/src/Caelestia/Config/monitorconfigmanager.hpp
Normal file
38
plugin/src/Caelestia/Config/monitorconfigmanager.hpp
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <qhash.h>
|
||||||
|
#include <qobject.h>
|
||||||
|
#include <qqmlengine.h>
|
||||||
|
|
||||||
|
namespace caelestia::config {
|
||||||
|
|
||||||
|
class GlobalConfig;
|
||||||
|
class TokenConfig;
|
||||||
|
|
||||||
|
class MonitorConfigManager : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
QML_ELEMENT
|
||||||
|
QML_SINGLETON
|
||||||
|
|
||||||
|
public:
|
||||||
|
static MonitorConfigManager* instance();
|
||||||
|
static MonitorConfigManager* create(QQmlEngine*, QJSEngine*);
|
||||||
|
|
||||||
|
[[nodiscard]] Q_INVOKABLE GlobalConfig* configForScreen(const QString& screen);
|
||||||
|
[[nodiscard]] Q_INVOKABLE TokenConfig* tokensForScreen(const QString& screen);
|
||||||
|
|
||||||
|
~MonitorConfigManager() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
explicit MonitorConfigManager(QObject* parent = nullptr);
|
||||||
|
|
||||||
|
struct ScreenOverlay {
|
||||||
|
GlobalConfig* config = nullptr;
|
||||||
|
TokenConfig* tokens = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
QHash<QString, ScreenOverlay> m_overlays;
|
||||||
|
static MonitorConfigManager* s_instance;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace caelestia::config
|
||||||
|
|
@ -46,6 +46,25 @@ TokenConfig::TokenConfig(QObject* parent)
|
||||||
setupFileBackend(configDir() + QStringLiteral("shell-tokens.json"));
|
setupFileBackend(configDir() + QStringLiteral("shell-tokens.json"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TokenConfig::TokenConfig(TokenConfig* fallback, const QString& filePath, QObject* parent)
|
||||||
|
: ConfigObject(parent)
|
||||||
|
, m_appearance(new AppearanceTokens(this))
|
||||||
|
, m_bar(new BarTokens(this))
|
||||||
|
, m_dashboard(new DashboardTokens(this))
|
||||||
|
, m_launcher(new LauncherTokens(this))
|
||||||
|
, m_notifs(new NotifsTokens(this))
|
||||||
|
, m_osd(new OsdTokens(this))
|
||||||
|
, m_session(new SessionTokens(this))
|
||||||
|
, m_sidebar(new SidebarTokens(this))
|
||||||
|
, m_utilities(new UtilitiesTokens(this))
|
||||||
|
, m_lock(new LockTokens(this))
|
||||||
|
, m_winfo(new WInfoTokens(this))
|
||||||
|
, m_controlCenter(new ControlCenterTokens(this)) {
|
||||||
|
setSparse(true);
|
||||||
|
setupFileBackend(filePath);
|
||||||
|
syncFromGlobal(fallback);
|
||||||
|
}
|
||||||
|
|
||||||
TokenConfig::~TokenConfig() {
|
TokenConfig::~TokenConfig() {
|
||||||
s_instance = nullptr;
|
s_instance = nullptr;
|
||||||
}
|
}
|
||||||
|
|
@ -70,7 +89,38 @@ void TokenConfig::reload() {
|
||||||
|
|
||||||
Tokens::Tokens(ConfigScope* scope, QObject* parent)
|
Tokens::Tokens(ConfigScope* scope, QObject* parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
, m_scope(scope) {}
|
, m_scope(scope) {
|
||||||
|
connectScope();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Tokens::connectScope() {
|
||||||
|
if (!m_scope)
|
||||||
|
return;
|
||||||
|
connect(m_scope, &ConfigScope::tokensChanged, this, &Tokens::sourceChanged);
|
||||||
|
connect(m_scope, &ConfigScope::configChanged, this, &Tokens::sourceChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
#define TOKENS_ATTACHED_GETTER(Type, name) \
|
||||||
|
const Type* Tokens::name() const { \
|
||||||
|
if (m_scope && m_scope->tokens()) \
|
||||||
|
return m_scope->tokens()->name(); \
|
||||||
|
return TokenConfig::instance()->name(); \
|
||||||
|
}
|
||||||
|
|
||||||
|
TOKENS_ATTACHED_GETTER(AppearanceTokens, appearance)
|
||||||
|
TOKENS_ATTACHED_GETTER(BarTokens, bar)
|
||||||
|
TOKENS_ATTACHED_GETTER(DashboardTokens, dashboard)
|
||||||
|
TOKENS_ATTACHED_GETTER(LauncherTokens, launcher)
|
||||||
|
TOKENS_ATTACHED_GETTER(NotifsTokens, notifs)
|
||||||
|
TOKENS_ATTACHED_GETTER(OsdTokens, osd)
|
||||||
|
TOKENS_ATTACHED_GETTER(SessionTokens, session)
|
||||||
|
TOKENS_ATTACHED_GETTER(SidebarTokens, sidebar)
|
||||||
|
TOKENS_ATTACHED_GETTER(UtilitiesTokens, utilities)
|
||||||
|
TOKENS_ATTACHED_GETTER(LockTokens, lock)
|
||||||
|
TOKENS_ATTACHED_GETTER(WInfoTokens, winfo)
|
||||||
|
TOKENS_ATTACHED_GETTER(ControlCenterTokens, controlCenter)
|
||||||
|
|
||||||
|
#undef TOKENS_ATTACHED_GETTER
|
||||||
|
|
||||||
Tokens* Tokens::qmlAttachedProperties(QObject* object) {
|
Tokens* Tokens::qmlAttachedProperties(QObject* object) {
|
||||||
return new Tokens(ConfigScope::find(object), object);
|
return new Tokens(ConfigScope::find(object), object);
|
||||||
|
|
|
||||||
|
|
@ -317,23 +317,57 @@ public:
|
||||||
~TokenConfig() override;
|
~TokenConfig() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
friend class MonitorConfigManager;
|
||||||
explicit TokenConfig(QObject* parent = nullptr);
|
explicit TokenConfig(QObject* parent = nullptr);
|
||||||
|
explicit TokenConfig(TokenConfig* fallback, const QString& filePath, QObject* parent);
|
||||||
};
|
};
|
||||||
|
|
||||||
class Tokens : public QObject {
|
class Tokens : public QObject {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
Q_MOC_INCLUDE("configscope.hpp")
|
||||||
QML_ELEMENT
|
QML_ELEMENT
|
||||||
QML_UNCREATABLE("")
|
QML_UNCREATABLE("")
|
||||||
QML_ATTACHED(Tokens)
|
QML_ATTACHED(Tokens)
|
||||||
|
|
||||||
|
Q_PROPERTY(ConfigScope* scope READ scope NOTIFY sourceChanged)
|
||||||
|
Q_PROPERTY(const AppearanceTokens* appearance READ appearance NOTIFY sourceChanged)
|
||||||
|
Q_PROPERTY(const BarTokens* bar READ bar NOTIFY sourceChanged)
|
||||||
|
Q_PROPERTY(const DashboardTokens* dashboard READ dashboard NOTIFY sourceChanged)
|
||||||
|
Q_PROPERTY(const LauncherTokens* launcher READ launcher NOTIFY sourceChanged)
|
||||||
|
Q_PROPERTY(const NotifsTokens* notifs READ notifs NOTIFY sourceChanged)
|
||||||
|
Q_PROPERTY(const OsdTokens* osd READ osd NOTIFY sourceChanged)
|
||||||
|
Q_PROPERTY(const SessionTokens* session READ session NOTIFY sourceChanged)
|
||||||
|
Q_PROPERTY(const SidebarTokens* sidebar READ sidebar NOTIFY sourceChanged)
|
||||||
|
Q_PROPERTY(const UtilitiesTokens* utilities READ utilities NOTIFY sourceChanged)
|
||||||
|
Q_PROPERTY(const LockTokens* lock READ lock NOTIFY sourceChanged)
|
||||||
|
Q_PROPERTY(const WInfoTokens* winfo READ winfo NOTIFY sourceChanged)
|
||||||
|
Q_PROPERTY(const ControlCenterTokens* controlCenter READ controlCenter NOTIFY sourceChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit Tokens(ConfigScope* scope, QObject* parent = nullptr);
|
explicit Tokens(ConfigScope* scope, QObject* parent = nullptr);
|
||||||
|
|
||||||
[[nodiscard]] ConfigScope* scope() const { return m_scope; }
|
[[nodiscard]] ConfigScope* scope() const { return m_scope; }
|
||||||
|
|
||||||
|
[[nodiscard]] const AppearanceTokens* appearance() const;
|
||||||
|
[[nodiscard]] const BarTokens* bar() const;
|
||||||
|
[[nodiscard]] const DashboardTokens* dashboard() const;
|
||||||
|
[[nodiscard]] const LauncherTokens* launcher() const;
|
||||||
|
[[nodiscard]] const NotifsTokens* notifs() const;
|
||||||
|
[[nodiscard]] const OsdTokens* osd() const;
|
||||||
|
[[nodiscard]] const SessionTokens* session() const;
|
||||||
|
[[nodiscard]] const SidebarTokens* sidebar() const;
|
||||||
|
[[nodiscard]] const UtilitiesTokens* utilities() const;
|
||||||
|
[[nodiscard]] const LockTokens* lock() const;
|
||||||
|
[[nodiscard]] const WInfoTokens* winfo() const;
|
||||||
|
[[nodiscard]] const ControlCenterTokens* controlCenter() const;
|
||||||
|
|
||||||
static Tokens* qmlAttachedProperties(QObject* object);
|
static Tokens* qmlAttachedProperties(QObject* object);
|
||||||
|
|
||||||
|
Q_SIGNAL void sourceChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void connectScope();
|
||||||
|
|
||||||
ConfigScope* m_scope;
|
ConfigScope* m_scope;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue