Reusable Qt6/QML/Kirigami settings-app framework: the window chrome (sidebar, breadcrumbs, footer, page host) plus the orchestration (
PageController,PageRegistry,StagingDomain,ApplicationController) that any Kirigami settings app builds on, so each consumer writes only its own page controllers and QML pages.
A settings app on this framework declares a set of pages, each backed by a staging domain that holds the user's pending edits, and the shell handles the rest: sidebar navigation, breadcrumbs, a global dirty flag, batched Apply/Discard across every domain, deep-link reveal, and global search. The library provides:
StagingDomain is the unit of staged work: isDirty(), apply(), discard(), resetToDefaults(), plus async completion signals. PageController is a domain that also has a QML page and a sidebar row. Headless domains hold cross-cutting state (e.g. per-screen assignment maps) with no sidebar entry.PageRegistry holds the page tree (top-level entries plus drill-down or collapsible children), keyed by stable page id, and drives the sidebar via serialized Entry views.ApplicationController owns the registry and the registered domains, recomputes the global dirty flag, drives applyAll() / discardAll() as one transaction (sync or async), dispatches per-page resetToDefaults(), and latches deep-link anchors for PageHost to reveal.SearchController builds a ranked result index from the page registry (Page entries), authored anchors (addEntry()), and dynamic content (ISearchProvider). SearchRanker is the pure, headless, typo-tolerant scorer behind it.org.phosphor.control ships SettingsAppWindow, Sidebar, Breadcrumbs, UnsavedChangesFooter, PageHost, DiscardChangesDialog, and AboutPageShell, wired to the controllers above.DBusBridge for talking to a daemon, and LocalizedContext for bare-word i18n() in QML on Qt-only (non-KF6) builds.| Type | Purpose |
|---|---|
PhosphorControl::ApplicationController | Top-level orchestrator: owns the registry + domains, global dirty flag, batched applyAllAsync / discardAllAsync, current-page selection, deep-link anchor latch |
PhosphorControl::StagingDomain | Abstract unit of staged changes: isDirty(), apply(), discard(), resetToDefaults(), with applyResult / discardResult async signals |
PhosphorControl::PageController | A StagingDomain that also has a QML page and a sidebar row, with a stable, globally-unique page id |
PhosphorControl::PageRegistry | Catalogue of registered pages as a tree (Entry per page); read-only after startup, supports dynamic add but not removal |
PhosphorControl::SearchController | Global settings search: ranked results from page registry, authored anchors, and registered providers |
PhosphorControl::SearchRanker | Pure, stateless, typo-tolerant scoring + ranking; headless and Qt-free beyond QString |
PhosphorControl::ISearchProvider | App-implemented source of dynamic search entries (window rules, shaders, layouts, …) |
PhosphorControl::SearchEntry | One searchable item (Page / Section / Setting / Entity), addressed as "pageId" or "pageId#anchor" |
PhosphorControl::DBusBridge | Configured endpoint(s) for daemon calls: sync call() / async asyncCall(), with explicit-interface variants |
PhosphorControl::LocalizedContext | QML i18n context routing i18n()/i18nc()/i18np() through Qt translation, for Qt-only consumers without KF6 |
A consumer subclasses ApplicationController to declare its pages, subclasses PageController for each page, and points the QML SettingsAppWindow at the controller. Titles are translated by the caller, because the library deliberately provides no translation context for app strings.
#include <PhosphorControl/ApplicationController.h>
#include <PhosphorControl/PageController.h>
using namespace PhosphorControl;
// Each page stages its own edits.
class GeneralPage : public PageController
{
public:
explicit GeneralPage(QObject* parent = nullptr)
: PageController(QStringLiteral("general"), parent) {}
bool isDirty() const override { return m_dirty; }
void apply() override { /* persist */ Q_EMIT applyResult(true, {}); }
void discard() override { /* reload */ Q_EMIT discardResult(true, {}); }
};
// The app wires its pages up at construction.
class MySettings : public ApplicationController
{
public:
explicit MySettings(QObject* parent = nullptr)
: ApplicationController(parent)
{
auto* general = new GeneralPage(this);
registerPage(general, {}, MySettings::tr("General"),
QUrl(QStringLiteral("qrc:/qt/qml/MyApp/qml/GeneralPage.qml")));
}
};
// Main.qml — the chrome binds to the controller exposed from C++.
import org.phosphor.control
SettingsAppWindow {
controller: appController // your ApplicationController subclass
}The shell drives the dirty/Apply lifecycle from the domains' signals: UnsavedChangesFooter binds to ApplicationController::dirty / applying / discarding, so a synchronous page just emits applyResult(true, {}) at the end of apply() and the footer swaps "Save" → "Saving…" automatically.
See examples/minimal/ for a complete two-page app (General + About).
ApplicationController only invokes apply() / discard() on domains whose isDirty() is true, so implementations must not rely on side effects that need to run while clean (timestamp stamping, notifications). Every domain must emit applyResult / discardResult exactly once per batch. The async driver's pending counter parks the chrome's "Saving…" state until it lands or the 60 s timeout fires.ISettings against `phosphor-config` and pull global shortcuts from `phosphor-shortcuts`.PhosphorControl library links Qt6::Gui privately and avoids Qt6::Quick, so headless tests can construct ApplicationController / PageController / StagingDomain without a QML engine. The QML module is a separate target.SearchRanker is pure and unit-testable on its own (best-tier-per-field scoring, weighted title > keywords > subtitle, edit distance only as a "did you mean" fallback). SearchController composes it with the page registry and providers. The app supplies translated strings, and the index stores them verbatim.org.phosphor.control builds STATIC for in-tree consumers (linked directly). Out-of-tree find_package(PhosphorControl) consumers need the SHARED runtime plugin, gated behind -DPHOSPHOR_CONTROL_QML_INSTALL=ON (mirrors phosphor-animation).QtCore, QtDBus, QtGui (private), QtQml. The QML module adds QtQuickorg.phosphor.animation for the spring-physics motion profiles (panel fade, accordion expand/collapse, tint, pulse)ISettings implementation is wired against. This library does not duplicate that contract.