From d94afe33aec647ba15fbedd63502b25122822e68 Mon Sep 17 00:00:00 2001 From: Dario Gabriel Lipicar Date: Fri, 26 Jun 2026 16:39:20 -0300 Subject: [PATCH] tutorial(cpp-ui): exercise the full .rep surface beyond slots MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Part 3's .rep previously declared only slots. Broaden it to cover the whole Qt Remote Objects contract a universal UI backend can author, so the tutorial teaches the breadth of .rep, not just call-and-return: - SLOT(void announceVersion()) — a void / fire-and-forget slot - PROP(QString versionEvent READONLY) — fed by a typed calc_module event subscription armed in onContextReady() - PROP(int computeCount READONLY) — a slot-driven counter (int PROP) - PROP(int memory READWRITE) — a register the QML view writes back - SIGNAL(computed(QString op, int result)) — a backend -> view push The backend gains onContextReady() (event subscription) and a record() helper (bumps computeCount, emits computed); the QML view gains a Connections block, Computations / Last-op labels, and a Memory register with Store/Clear buttons that assign backend.memory. Step 9's UI test drives each surface — the slot return value, the int PROP (Computations: 1), the signal payload (add = 8), and the READWRITE round-trip (Memory: 0 -> 8) — and the closing .rep pattern table now marks every row exercised except Model. Regenerated outputs/ (md + calc_ui_cpp sources + screenshots). Verified end to end via the doctest chain (Part 1->2->3): 69/69 steps, UI test 14/14. Co-Authored-By: Claude Opus 4.8 --- outputs/images/calc-cpp-buttons.png | Bin 13648 -> 24635 bytes outputs/images/calc-cpp-event.png | Bin 0 -> 21422 bytes outputs/images/calc-cpp-result.png | Bin 10956 -> 21798 bytes outputs/logos-calc-ui-cpp/src/calc_ui_cpp.rep | 25 ++ .../src/calc_ui_cpp_backend.cpp | 51 +++- .../src/calc_ui_cpp_backend.h | 15 + outputs/logos-calc-ui-cpp/src/qml/Main.qml | 78 +++++ outputs/tutorial-cpp-ui-app.md | 249 ++++++++++++++-- tests/tutorial-cpp-ui-app.test.yaml | 278 ++++++++++++++++-- 9 files changed, 630 insertions(+), 66 deletions(-) create mode 100644 outputs/images/calc-cpp-event.png diff --git a/outputs/images/calc-cpp-buttons.png b/outputs/images/calc-cpp-buttons.png index 858f5b1f979eeb985e2137d27361a6589d25371f..a587d00547566ab2c76be2deaa0a2a3d36aaa585 100644 GIT binary patch literal 24635 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�$Z_viA~ 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≧4@!PLY)`pET-z~*NA`VY0omp( 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 literal 13648 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 literal 0 HcmV?d00001 diff --git a/outputs/images/calc-cpp-result.png b/outputs/images/calc-cpp-result.png index 69f3a4d2d18445e7617563475e4364c41417d976..30222c392bec77b4913114598d41051ae6247669 100644 GIT binary patch literal 21798 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 F1OTWk133Ty literal 10956 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 + a Qt-typed callback (QString, because a + // UI plugin is api-style qt). Push each payload into the versionEvent + // PROP — Qt Remote Objects then auto-syncs it to the QML replica. + modules().calc_module.onVersionReady([this](const QString& version) { + setVersionEvent(version); + }); +} diff --git a/outputs/logos-calc-ui-cpp/src/calc_ui_cpp_backend.h b/outputs/logos-calc-ui-cpp/src/calc_ui_cpp_backend.h index 35dad83..4201e39 100644 --- a/outputs/logos-calc-ui-cpp/src/calc_ui_cpp_backend.h +++ b/outputs/logos-calc-ui-cpp/src/calc_ui_cpp_backend.h @@ -21,4 +21,19 @@ public: int factorial(int n) override; int fibonacci(int n) override; QString libVersion() override; + + // Tells calc_module to emit its `versionReady` event. + void announceVersion() override; + + // Fires once when ui-host hands the plugin its LogosAPI — the + // typed dependency surface is live, so we arm the event + // subscription here (before the view's first call). + void onContextReady() override; + +private: + // Feeds the non-slot surfaces of the .rep after each calculation: + // bumps the computeCount PROP (setComputeCount, generated) and + // emits the `computed` SIGNAL. The READWRITE `memory` PROP is + // driven from QML, so the backend doesn't have to touch it. + void record(const QString& op, int result); }; diff --git a/outputs/logos-calc-ui-cpp/src/qml/Main.qml b/outputs/logos-calc-ui-cpp/src/qml/Main.qml index 745c699..8b6379d 100644 --- a/outputs/logos-calc-ui-cpp/src/qml/Main.qml +++ b/outputs/logos-calc-ui-cpp/src/qml/Main.qml @@ -8,6 +8,9 @@ Item { property string result: "" property string errorText: "" + // Last payload from the backend's `computed` SIGNAL (see Connections below). + property string lastSignal: "(none)" + // Typed replica of the backend running in ui-host (generated from calc_ui_cpp.rep). readonly property var backend: logos.module("calc_ui_cpp") @@ -29,6 +32,17 @@ Item { root.ready = root.backend !== null && logos.isViewModuleReady("calc_ui_cpp") } + // SIGNAL from the .rep: the backend emits `computed(op, result)` after + // each calculation. The typed replica re-emits it, so we catch it with + // a Connections block — no logos.watch(), no property read. This is the + // backend → view push path, distinct from the slot return value above. + Connections { + target: root.backend + function onComputed(op, result) { + root.lastSignal = op + " = " + result + } + } + // logos.watch() delivers the result of a replica slot call via callbacks. // No QtRemoteObjects import needed — the bridge handles it. function callCalc(method, args) { @@ -123,6 +137,15 @@ Item { enabled: root.ready onClicked: root.callCalc("libVersion", []) } + + Button { + // Fires the event path: asks calc_module to emit + // versionReady. No logos.watch() — the result comes + // back through the versionEvent PROP, not a return value. + text: "Announce version (event)" + enabled: root.ready + onClicked: root.backend.announceVersion() + } } Rectangle { @@ -140,6 +163,61 @@ Item { } } + // Slot-driven PROP: bumped by the backend's record() after each + // calculation. A plain property read — auto-syncs, no polling. + Text { + text: "Computations: " + ((root.ready && root.backend) ? root.backend.computeCount : 0) + color: "#cdd6f4" + font.pixelSize: 14 + Layout.alignment: Qt.AlignHCenter + } + + // SIGNAL payload, captured by the Connections block above. + Text { + text: "Last op (signal): " + root.lastSignal + color: "#94e2d5" + font.pixelSize: 14 + Layout.alignment: Qt.AlignHCenter + } + + // READWRITE PROP: the memory register. The label *reads* + // backend.memory; the buttons *write* it. A write round-trips + // QML → replica → backend source → back to every replica, so the + // label updates once the new value syncs home. + RowLayout { + spacing: 12 + Layout.alignment: Qt.AlignHCenter + + Text { + text: "Memory: " + ((root.ready && root.backend) ? root.backend.memory : 0) + color: "#cdd6f4" + font.pixelSize: 14 + } + + Button { + text: "Store (MS)" + enabled: root.ready + onClicked: root.backend.memory = parseInt(root.result) || 0 + } + + Button { + text: "Clear (MC)" + enabled: root.ready + onClicked: root.backend.memory = 0 + } + } + + // Event-fed label: the versionEvent PROP auto-syncs from the + // backend's typed versionReady subscription. No polling — it + // updates the moment calc_module emits. + Text { + readonly property string ev: (root.ready && root.backend) ? root.backend.versionEvent : "" + text: "Version event: " + (ev.length > 0 ? ev : "(none yet)") + color: "#f9e2af" + font.pixelSize: 15 + Layout.alignment: Qt.AlignHCenter + } + Item { Layout.fillHeight: true } } } diff --git a/outputs/tutorial-cpp-ui-app.md b/outputs/tutorial-cpp-ui-app.md index 7350b69..32f67c7 100644 --- a/outputs/tutorial-cpp-ui-app.md +++ b/outputs/tutorial-cpp-ui-app.md @@ -6,9 +6,9 @@ You'll use the **universal authoring model**: set `"interface": "universal"` in **What you'll build:** A `calc_ui_cpp` module with: -- A `.rep` file defining the remote interface (slots) — the one Qt-typed contract you author +- A `.rep` file exercising the **full QtRO surface**, not just slots: value slots and a void slot, `PROP`s of different types (`QString`, `int`) and both modes (`READONLY` and `READWRITE`), and a `SIGNAL` — the one Qt-typed contract you author - A C++ `*Backend` class that derives the generated `SimpleSource` (implements the `.rep`) and `LogosUiPluginContext` (gives `modules()` Qt-typed callers, event subscriptions, and `onContextReady()`) -- A QML view that calls the backend via a typed replica using `logos.watch()` +- A QML view that drives each surface: `logos.watch()` for slot replies, plain property reads for auto-synced PROPs, a property *write* for the READWRITE memory register, a `Connections` block for the signal, and a label fed by a typed `calc_module` **event subscription** - Process isolation: backend crashes can't bring down the host app You write only the `.rep` and the `Backend`. The `*Plugin`/`*Interface` classes, the `initLogos`/`setBackend` wiring, and the typed SDK are generated. @@ -75,7 +75,9 @@ And because `metadata.json` sets `"interface": "universal"`, the builder also ge - **`CalcUiCppInterface`** — the Logos plugin interface (`name()`, `version()`) - **`CalcUiCppPlugin`** — the `Q_OBJECT` plugin with `Q_PLUGIN_METADATA`, `initLogos(LogosAPI*)`, and the `setBackend()` / `enableRemoting()` wiring — built around your `*Backend` -Your `*Backend` derives `LogosUiPluginContext`. A UI plugin is a view, not a module, so the context carries only the dependency surface: `modules()` typed method callers for your `dependencies`, typed **event subscriptions** (`modules().dep.on(...)`), and `onContextReady()` (fires when the backend is wired, so subscriptions are live before the view's first call). The dep wrappers are **Qt-typed** (`QString`, `int`, ...) to match the `.rep` slots — no std<->Qt conversions in the view. `calc_module` here is call-only, but for an event-driven PROP fed from a typed subscription see the worked [ui-typed-backend doc-test](https://github.com/logos-co/logos-module-builder/blob/main/doctests/ui-typed-backend.test.yaml). +Your `*Backend` derives `LogosUiPluginContext`. A UI plugin is a view, not a module, so the context carries only the dependency surface: `modules()` typed method callers for your `dependencies`, typed **event subscriptions** (`modules().dep.on(...)`), and `onContextReady()` (fires when the backend is wired, so subscriptions are live before the view's first call). The dep wrappers are **Qt-typed** (`QString`, `int`, ...) to match the `.rep` slots — no std<->Qt conversions in the view. + +The `.rep` is more than a list of slots, and this tutorial exercises the whole of it: value slots and a void slot, `PROP`s of different types and both modes (a slot-driven `READONLY` counter, a `READWRITE` memory register QML writes back, an event-fed `READONLY` string), and a `SIGNAL` the backend pushes to the view. The calculator slots *call* `calc_module`; `record()` feeds the PROP + SIGNAL after each call; and `onContextReady()` *subscribes* to `calc_module`'s `versionReady` event to feed the auto-syncing event PROP. ## Step 1: Scaffold @@ -161,20 +163,49 @@ Create `src/calc_ui_cpp.rep`: ```rep class CalcUiCpp { + // ── SLOTs — call-and-return; each reply reaches QML via logos.watch() ── SLOT(int add(int a, int b)) SLOT(int multiply(int a, int b)) SLOT(int factorial(int n)) SLOT(int fibonacci(int n)) SLOT(QString libVersion()) + + // Void slot — fire-and-forget. Asks calc_module to (re-)announce + // its version as a `versionReady` event; there's no return value + // to await, the answer comes back through the PROP below. + SLOT(void announceVersion()) + + // ── PROPs — auto-synced backend → every QML replica, no polling ── + // QString, event-fed: the typed `versionReady` subscription the + // backend arms in onContextReady() writes it. Starts empty. + PROP(QString versionEvent="" READONLY) + + // int, slot-driven: the backend bumps it after each calculation, + // so the view shows a live tally without ever polling. + PROP(int computeCount=0 READONLY) + + // int, READWRITE: a memory register the QML view both *reads* and + // *writes* (Store / Clear buttons), and the backend may set too. + // A write round-trips QML → replica → source → back to every replica. + PROP(int memory=0 READWRITE) + + // ── SIGNAL — backend → view push, distinct from a return value ── + // Emitted after each calculation. QML catches it with a + // Connections block (not logos.watch(), not a PROP read). + SIGNAL(computed(QString op, int result)) } ``` -This is the **single source of truth** for the remote interface, and the one Qt-typed file you author — the `.rep` uses Qt types (`QString`) because that's the Qt Remote Objects wire contract. `repc` generates: +This is the **single source of truth** for the remote interface, and the one Qt-typed file you author — the `.rep` uses Qt types (`QString`, `int`) because that's the Qt Remote Objects wire contract. `repc` generates: -- `rep_calc_ui_cpp_source.h` — `CalcUiCppSimpleSource` with virtual slots your `*Backend` overrides -- `rep_calc_ui_cpp_replica.h` — `CalcUiCppReplica` with typed methods the QML view calls +- `rep_calc_ui_cpp_source.h` — `CalcUiCppSimpleSource` with virtual slots your `*Backend` overrides, a `set(...)` setter + change signal for each PROP (`setVersionEvent`, `setComputeCount`, `setMemory`), and the `computed(...)` signal you `emit` +- `rep_calc_ui_cpp_replica.h` — `CalcUiCppReplica` with typed methods, the auto-synced `versionEvent` / `computeCount` / `memory` properties, and the `computed` signal the QML view reads, writes, and connects to -**SLOT** return values are delivered as `QRemoteObjectPendingReply` — use `logos.watch()` in QML to get them as JS Promises. You can also declare **PROP** entries (e.g. `PROP(QString status READWRITE)`) which auto-sync from the backend to the QML replica — see the [ui-typed-backend doc-test](https://github.com/logos-co/logos-module-builder/blob/main/doctests/ui-typed-backend.test.yaml) for a PROP fed from a typed module-event subscription. +One contract, four kinds of surface — slots are only the first: + +- **SLOT** return values arrive as `QRemoteObjectPendingReply` — `logos.watch()` turns them into JS Promises in QML. `add`/`multiply`/… return values; `announceVersion()` is a **void** slot (fire-and-forget, no reply to watch). +- **PROP** auto-syncs from the backend to every QML replica with no polling. We use three, of two types and two modes: `versionEvent` (QString, READONLY, fed by a module-**event subscription** in Step 5), `computeCount` (int, READONLY, bumped by each slot), and `memory` (int, **READWRITE** — QML assigns `backend.memory = …` and the new value round-trips through the backend source and back to the view). +- **SIGNAL** is a backend → view push that is neither a return value nor a synced property: `computed(op, result)` fires after each calculation and the QML view catches it with a `Connections` block. --- @@ -248,12 +279,31 @@ public: int factorial(int n) override; int fibonacci(int n) override; QString libVersion() override; + + // Tells calc_module to emit its `versionReady` event. + void announceVersion() override; + + // Fires once when ui-host hands the plugin its LogosAPI — the + // typed dependency surface is live, so we arm the event + // subscription here (before the view's first call). + void onContextReady() override; + +private: + // Feeds the non-slot surfaces of the .rep after each calculation: + // bumps the computeCount PROP (setComputeCount, generated) and + // emits the `computed` SIGNAL. The READWRITE `memory` PROP is + // driven from QML, so the backend doesn't have to touch it. + void record(const QString& op, int result); }; ``` -No `Q_OBJECT`, no `Q_PLUGIN_METADATA`, no `initLogos`, no `name()`/`version()` — the universal builder generates all of that. You only declare the `.rep` slot overrides. +No `Q_OBJECT`, no `Q_PLUGIN_METADATA`, no `initLogos`, no `name()`/`version()` — the universal builder generates all of that. You only declare the `.rep` slot overrides (plus any private helpers of your own, like `record`). -`LogosUiPluginContext` also gives this backend typed **event subscriptions** (`modules().dep.on(...)`) and `onContextReady()` — arm subscriptions there so they're live before the view's first call. `calc_module` is call-only here, but the [ui-typed-backend doc-test](https://github.com/logos-co/logos-module-builder/blob/main/doctests/ui-typed-backend.test.yaml) shows a `.rep` PROP fed from a typed module-event subscription registered in `onContextReady()`. +Beyond the call-and-return slots, the backend drives the rest of the `.rep` surface: + +- **PROPs** — each gets a generated `set()` setter on the `SimpleSource`. The backend calls `setComputeCount(...)` (and could call `setMemory(...)`); `memory` is READWRITE, so QML can write it too. +- **SIGNAL** — `computed(...)` is declared on the `SimpleSource`, so the backend just `emit`s it. +- **Event subscriptions** — `LogosUiPluginContext` gives typed `modules().dep.on(...)` and the `onContextReady()` hook. `onContextReady()` subscribes to `calc_module`'s `versionReady` event and pipes the payload into the `versionEvent` PROP, which Qt Remote Objects auto-syncs to the view. Arm subscriptions in `onContextReady()` (not the constructor) so they're live the moment the backend is wired. ### 5.2 `src/calc_ui_cpp_backend.cpp` @@ -266,22 +316,30 @@ No `Q_OBJECT`, no `Q_PLUGIN_METADATA`, no `initLogos`, no `name()`/`version()` int CalcUiCppBackend::add(int a, int b) { - return modules().calc_module.add(a, b); + int result = modules().calc_module.add(a, b); + record("add", result); + return result; } int CalcUiCppBackend::multiply(int a, int b) { - return modules().calc_module.multiply(a, b); + int result = modules().calc_module.multiply(a, b); + record("multiply", result); + return result; } int CalcUiCppBackend::factorial(int n) { - return modules().calc_module.factorial(n); + int result = modules().calc_module.factorial(n); + record("factorial", result); + return result; } int CalcUiCppBackend::fibonacci(int n) { - return modules().calc_module.fibonacci(n); + int result = modules().calc_module.fibonacci(n); + record("fibonacci", result); + return result; } QString CalcUiCppBackend::libVersion() @@ -290,14 +348,50 @@ QString CalcUiCppBackend::libVersion() // (api-style qt), matching the .rep slot — no conversion needed. return modules().calc_module.libVersion(); } + +void CalcUiCppBackend::record(const QString& op, int result) +{ + // PROP: bump the slot-driven counter. setComputeCount() is the + // generated setter; Qt Remote Objects syncs the new value to every + // replica, so the view's "Computations" label updates with no polling. + setComputeCount(computeCount() + 1); + + // SIGNAL: a backend → view push, distinct from the return value the + // QML side gets via logos.watch(). `computed` is declared on the + // generated SimpleSource, so we just emit it; the typed replica + // re-emits it and the view's Connections block catches it. + emit computed(op, result); +} + +void CalcUiCppBackend::announceVersion() +{ + // Fire-and-forget call into calc_module: it looks up the library + // version and emits it as a `versionReady` event. We don't read a + // return value here — the event comes back through the subscription + // armed in onContextReady() below. + modules().calc_module.libVersionNotify(); +} + +void CalcUiCppBackend::onContextReady() +{ + // Typed module-event subscription. `versionReady` is calc_module's + // event (Part 1's `logos_events:` block); the generated wrapper + // exposes it as on + a Qt-typed callback (QString, because a + // UI plugin is api-style qt). Push each payload into the versionEvent + // PROP — Qt Remote Objects then auto-syncs it to the QML replica. + modules().calc_module.onVersionReady([this](const QString& version) { + setVersionEvent(version); + }); +} ``` Key points: -- Each slot delegates straight to `calc_module` via `modules().calc_module.(...)` — the generated typed SDK, type-safe with no `QVariant`. -- Slots return values directly; they travel back to the QML replica via Qt Remote Objects. +- Each value slot delegates straight to `calc_module` via `modules().calc_module.(...)` — the generated typed SDK, type-safe with no `QVariant` — then calls `record()` to drive the PROP + SIGNAL surfaces before returning. Slot return values travel back to the QML replica via Qt Remote Objects. - `modules().calc_module.libVersion()` returns `QString` even though Part 1's `calc_module` declares it `std::string`. A UI plugin is Qt-typed (api-style `qt`), so the generated `modules().` wrapper exposes Qt types (`QString`, `int`, ...) that match the `.rep` slots directly — the std<->Qt conversion happens inside the generated wrapper, not in your view code. -- No `initLogos`, no manual `LogosModules` construction — `modules()` is wired by the generated plugin before any slot runs. +- **PROP vs. SIGNAL vs. return value — three ways to get data to the view.** `record()` shows two of them side by side: `setComputeCount(...)` writes a **PROP** that Qt Remote Objects auto-syncs (the view reads `backend.computeCount` with no polling), while `emit computed(...)` fires a **SIGNAL** the view catches in a `Connections` block. Both are independent of the slot's `logos.watch()` return value. The READWRITE `memory` PROP is the fourth path — there the *view* writes and the value syncs back. +- **Event subscription vs. method call.** `announceVersion()` makes `calc_module` *emit*; `onContextReady()` *subscribes* to that emission. The callback is Qt-typed (`const QString&`) and `setVersionEvent(...)` is the generated PROP setter — the value reaches QML with no polling and no return value to await. Arm the subscription in `onContextReady()`, never the constructor: `modules()` isn't wired until the framework calls it. +- No `initLogos`, no manual `LogosModules` construction — `modules()` is wired by the generated plugin before any slot runs or any event arrives. --- @@ -316,6 +410,9 @@ Item { property string result: "" property string errorText: "" + // Last payload from the backend's `computed` SIGNAL (see Connections below). + property string lastSignal: "(none)" + // Typed replica of the backend running in ui-host (generated from calc_ui_cpp.rep). readonly property var backend: logos.module("calc_ui_cpp") @@ -337,6 +434,17 @@ Item { root.ready = root.backend !== null && logos.isViewModuleReady("calc_ui_cpp") } + // SIGNAL from the .rep: the backend emits `computed(op, result)` after + // each calculation. The typed replica re-emits it, so we catch it with + // a Connections block — no logos.watch(), no property read. This is the + // backend → view push path, distinct from the slot return value above. + Connections { + target: root.backend + function onComputed(op, result) { + root.lastSignal = op + " = " + result + } + } + // logos.watch() delivers the result of a replica slot call via callbacks. // No QtRemoteObjects import needed — the bridge handles it. function callCalc(method, args) { @@ -431,6 +539,15 @@ Item { enabled: root.ready onClicked: root.callCalc("libVersion", []) } + + Button { + // Fires the event path: asks calc_module to emit + // versionReady. No logos.watch() — the result comes + // back through the versionEvent PROP, not a return value. + text: "Announce version (event)" + enabled: root.ready + onClicked: root.backend.announceVersion() + } } Rectangle { @@ -448,15 +565,74 @@ Item { } } + // Slot-driven PROP: bumped by the backend's record() after each + // calculation. A plain property read — auto-syncs, no polling. + Text { + text: "Computations: " + ((root.ready && root.backend) ? root.backend.computeCount : 0) + color: "#cdd6f4" + font.pixelSize: 14 + Layout.alignment: Qt.AlignHCenter + } + + // SIGNAL payload, captured by the Connections block above. + Text { + text: "Last op (signal): " + root.lastSignal + color: "#94e2d5" + font.pixelSize: 14 + Layout.alignment: Qt.AlignHCenter + } + + // READWRITE PROP: the memory register. The label *reads* + // backend.memory; the buttons *write* it. A write round-trips + // QML → replica → backend source → back to every replica, so the + // label updates once the new value syncs home. + RowLayout { + spacing: 12 + Layout.alignment: Qt.AlignHCenter + + Text { + text: "Memory: " + ((root.ready && root.backend) ? root.backend.memory : 0) + color: "#cdd6f4" + font.pixelSize: 14 + } + + Button { + text: "Store (MS)" + enabled: root.ready + onClicked: root.backend.memory = parseInt(root.result) || 0 + } + + Button { + text: "Clear (MC)" + enabled: root.ready + onClicked: root.backend.memory = 0 + } + } + + // Event-fed label: the versionEvent PROP auto-syncs from the + // backend's typed versionReady subscription. No polling — it + // updates the moment calc_module emits. + Text { + readonly property string ev: (root.ready && root.backend) ? root.backend.versionEvent : "" + text: "Version event: " + (ev.length > 0 ? ev : "(none yet)") + color: "#f9e2af" + font.pixelSize: 15 + Layout.alignment: Qt.AlignHCenter + } + Item { Layout.fillHeight: true } } } ``` -Key patterns: +Key patterns — one for each surface the `.rep` exposes: -- `logos.module("calc_ui_cpp")` — gets the typed replica (auto-synced properties) -- `logos.watch(backend.add(1, 2), ...)` — SLOT return value as JS Promise +- `logos.module("calc_ui_cpp")` — gets the typed replica (auto-synced properties, callable slots, connectable signals) +- **SLOT return value:** `logos.watch(backend.add(1, 2), ...)` — the reply as a JS Promise. +- **READONLY PROP (slot-driven):** `backend.computeCount` is read directly in a binding. The backend bumps it via `setComputeCount(...)` after each calculation, and Qt Remote Objects auto-syncs it — the label re-evaluates with no polling. +- **READWRITE PROP:** `backend.memory` is both read (the "Memory:" label) and **written** (`backend.memory = ...` in the Store/Clear buttons). Assigning the property on the replica pushes the value to the backend source; it syncs back to every replica, so the label updates once the write lands. +- **SIGNAL:** `Connections { target: root.backend; function onComputed(op, result) { ... } }` catches the backend's `computed` push. No return value, no property — a one-shot event the view reacts to. +- **Event-fed PROP:** `backend.versionEvent` is read directly — no `logos.watch()`, no polling. The "Announce version" button calls the void `announceVersion()` slot (which makes `calc_module` emit `versionReady`); the backend's subscription catches the event and writes the PROP, and Qt Remote Objects pushes the new value straight into this label. This is the event path, distinct from the call-and-return slots above. - **Readiness:** the backend lives in a separate `ui-host` process and connects asynchronously, so the replica isn't usable the instant the view loads. `logos.isViewModuleReady("calc_ui_cpp")` reports the current state and the `onViewModuleReadyChanged` signal fires when it changes. Because `isViewModuleReady()` is a `Q_INVOKABLE` method (not a property), don't bind it directly — a `readonly property bool ready: logos.isViewModuleReady(...)` would never re-evaluate. Use the `Connections` + `Component.onCompleted` pattern shown above, and gate the buttons with `enabled: root.ready`. - The `logos` object is injected by the host at runtime — no `QtRemoteObjects` import needed @@ -606,7 +782,15 @@ nix run . ![Result of 3 + 5 shows 8](images/calc-cpp-result.png) -The result `8` comes from `calc_module.add(3, 5)` executed in the C++ backend — proof the full path (QML replica → Qt Remote Objects → ui-host backend → typed SDK → `calc_module`) works end to end. +![Subscribed event payload reaches the view](images/calc-cpp-event.png) + +Every surface the `.rep` declares is now proven end to end from one click: + +- **SLOT return value** — the result `8` comes from `calc_module.add(3, 5)`, the call-and-return path (QML replica → Qt Remote Objects → ui-host backend → typed SDK → `calc_module`), delivered via `logos.watch()`. +- **READONLY PROP (slot-driven)** — `Computations: 1` is the `computeCount` PROP: the same `add` call ran `setComputeCount(...)` in the backend's `record()`, and Qt Remote Objects synced it to the view with no polling. +- **SIGNAL** — `Last op (signal): add = 8` is the `computed` signal the backend `emit`ted; the QML `Connections` block caught it. It carried the op name *and* the result, neither of which is a property or a return value. +- **READWRITE PROP** — `Memory: 0` → `Memory: 8` proves the *write* direction: the Store button assigned `backend.memory = 8` in QML, which pushed to the backend source and synced back to the label. Properties aren't read-only mirrors; QML can drive them too. +- **Event-fed PROP** — `Version event: 1.0.0` is the event path: clicking *Announce version* called `calc_module.libVersionNotify()`, which emitted `versionReady("1.0.0")`; the backend's `modules().calc_module.onVersionReady(...)` subscription — armed in `onContextReady()` — caught it and wrote the `versionEvent` PROP, which Qt Remote Objects synced into the view. The label was `(none yet)` until the event fired, so seeing the version proves the typed subscription delivered. --- @@ -721,22 +905,25 @@ node tests/ui-tests.mjs # in another terminal ## Comparison: .rep Interface Patterns -You declare each pattern in the `.rep` and implement it in your `*Backend` (which derives the generated `SimpleSource` + `LogosUiPluginContext`). There is no hand-written plugin — the `*Plugin`/`*Interface` glue is generated. +You declare each pattern in the `.rep` and implement it in your `*Backend` (which derives the generated `SimpleSource` + `LogosUiPluginContext`). There is no hand-written plugin — the `*Plugin`/`*Interface` glue is generated. Every row but **Model** is live in this tutorial's `.rep` (✓), and the UI test in Step 9 drives each one: -| Pattern | .rep declaration | Backend C++ (`CalcUiCppBackend`) | QML usage | -| ------------------- | ------------------------------------ | ------------------------------------------- | ---------------------------------------------------------------------- | -| **Return value** | `SLOT(int add(int a, int b))` | `int add(...) override { return ...; }` | `logos.watch(backend.add(1,2), cb)` | -| **Property** | `PROP(QString status READWRITE)` | `setStatus("Ready")` (inherited from SimpleSource) | `backend.status` (auto-syncs) | -| **Signal** | `SIGNAL(errorOccurred(QString msg))` | `emit errorOccurred("fail")` | `Connections { target: backend; function onErrorOccurred(msg) {...} }` | -| **Model** | (use Q_PROPERTY on backend) | `Q_PROPERTY(QAbstractItemModel* items ...)` | `logos.model("calc_ui_cpp", "items")` | -| **Event-fed PROP** | `PROP(QString last READONLY)` | `onContextReady()`: `modules().dep.on([this](...){ setLast(...); })` | `backend.last` (auto-syncs, no polling) | +| Pattern | .rep declaration | Backend C++ (`CalcUiCppBackend`) | QML usage | +| -------------------- | ----------------------------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------------- | +| **Return value** ✓ | `SLOT(int add(int a, int b))` | `int add(...) override { return ...; }` | `logos.watch(backend.add(1,2), cb)` | +| **Void slot** ✓ | `SLOT(void announceVersion())` | `void announceVersion() override { ... }` | `backend.announceVersion()` (fire-and-forget, no `watch`) | +| **READONLY PROP** ✓ | `PROP(int computeCount=0 READONLY)` | `setComputeCount(computeCount()+1)` (inherited setter) | `backend.computeCount` (auto-syncs, read-only) | +| **READWRITE PROP** ✓ | `PROP(int memory=0 READWRITE)` | `setMemory(...)` — or let QML write it | `backend.memory` (read) / `backend.memory = 8` (write, round-trips) | +| **Signal** ✓ | `SIGNAL(computed(QString op, int result))`| `emit computed("add", 8)` | `Connections { target: backend; function onComputed(op, result) {...} }` | +| **Event-fed PROP** ✓ | `PROP(QString versionEvent="" READONLY)` | `onContextReady()`: `modules().dep.on([this](...){ setVersionEvent(...); })` | `backend.versionEvent` (auto-syncs, no polling) | +| **Model** | (use Q_PROPERTY on backend) | `Q_PROPERTY(QAbstractItemModel* items ...)` | `logos.model("calc_ui_cpp", "items")` | -The last row uses the `LogosUiPluginContext` surface — Qt-typed `modules()` callers and event subscriptions armed in `onContextReady()`. `calc_module` is call-only here; see the [ui-typed-backend doc-test](https://github.com/logos-co/logos-module-builder/blob/main/doctests/ui-typed-backend.test.yaml) for a worked event-driven PROP. +The last live row uses the `LogosUiPluginContext` surface — Qt-typed `modules()` callers and event subscriptions armed in `onContextReady()`: `versionEvent` is a PROP fed by the `modules().calc_module.onVersionReady(...)` subscription, and the *Announce version* button drives it. **Model** is the one pattern shown but not built here — for a `QAbstractItemModel*` Q_PROPERTY remoted via `logos.model()`, see [Next Steps](#next-steps). ## Next Steps -- Add more `.rep` properties/signals for richer UI state -- Use `logos.model()` for list views backed by `QAbstractItemModel` +- This tutorial already exercises slots, PROPs of different types and modes, and a signal — extend them with more state for your own UI +- Add an **enum** to the `.rep` (`ENUM Status { Idle, Busy }` — no parentheses, unlike `SLOT`/`PROP`/`SIGNAL`) — the factory registers it under the `Logos.` QML URI, so `import Logos.CalcUiCpp` then `CalcUiCpp.Busy` works in bindings +- Use `logos.model()` for list views backed by a `QAbstractItemModel*` Q_PROPERTY — the **Model** row above, the one `.rep` pattern this tutorial doesn't build - Package as `.lgx` for distribution: `nix build .#lgx` - **Use the Logos Design System** in your QML — see [the design system step](#use-the-logos-design-system-in-your-qml). Browse components in the storybook (`cd repos/logos-design-system && nix run`); file issues at `logos-co/logos-design-system`. - See [logos-package-manager-ui](https://github.com/logos-co/logos-package-manager-ui) for a production example diff --git a/tests/tutorial-cpp-ui-app.test.yaml b/tests/tutorial-cpp-ui-app.test.yaml index af34539..ef3b18f 100644 --- a/tests/tutorial-cpp-ui-app.test.yaml +++ b/tests/tutorial-cpp-ui-app.test.yaml @@ -13,9 +13,9 @@ intro: | what_you_build: | A `calc_ui_cpp` module with: - - A `.rep` file defining the remote interface (slots) — the one Qt-typed contract you author + - A `.rep` file exercising the **full QtRO surface**, not just slots: value slots and a void slot, `PROP`s of different types (`QString`, `int`) and both modes (`READONLY` and `READWRITE`), and a `SIGNAL` — the one Qt-typed contract you author - A C++ `*Backend` class that derives the generated `SimpleSource` (implements the `.rep`) and `LogosUiPluginContext` (gives `modules()` Qt-typed callers, event subscriptions, and `onContextReady()`) - - A QML view that calls the backend via a typed replica using `logos.watch()` + - A QML view that drives each surface: `logos.watch()` for slot replies, plain property reads for auto-synced PROPs, a property *write* for the READWRITE memory register, a `Connections` block for the signal, and a label fed by a typed `calc_module` **event subscription** - Process isolation: backend crashes can't bring down the host app You write only the `.rep` and the `Backend`. The `*Plugin`/`*Interface` classes, the `initLogos`/`setBackend` wiring, and the typed SDK are generated. @@ -82,7 +82,9 @@ sections: - **`CalcUiCppInterface`** — the Logos plugin interface (`name()`, `version()`) - **`CalcUiCppPlugin`** — the `Q_OBJECT` plugin with `Q_PLUGIN_METADATA`, `initLogos(LogosAPI*)`, and the `setBackend()` / `enableRemoting()` wiring — built around your `*Backend` - Your `*Backend` derives `LogosUiPluginContext`. A UI plugin is a view, not a module, so the context carries only the dependency surface: `modules()` typed method callers for your `dependencies`, typed **event subscriptions** (`modules().dep.on(...)`), and `onContextReady()` (fires when the backend is wired, so subscriptions are live before the view's first call). The dep wrappers are **Qt-typed** (`QString`, `int`, ...) to match the `.rep` slots — no std<->Qt conversions in the view. `calc_module` here is call-only, but for an event-driven PROP fed from a typed subscription see the worked [ui-typed-backend doc-test](https://github.com/logos-co/logos-module-builder/blob/main/doctests/ui-typed-backend.test.yaml). + Your `*Backend` derives `LogosUiPluginContext`. A UI plugin is a view, not a module, so the context carries only the dependency surface: `modules()` typed method callers for your `dependencies`, typed **event subscriptions** (`modules().dep.on(...)`), and `onContextReady()` (fires when the backend is wired, so subscriptions are live before the view's first call). The dep wrappers are **Qt-typed** (`QString`, `int`, ...) to match the `.rep` slots — no std<->Qt conversions in the view. + + The `.rep` is more than a list of slots, and this tutorial exercises the whole of it: value slots and a void slot, `PROP`s of different types and both modes (a slot-driven `READONLY` counter, a `READWRITE` memory register QML writes back, an event-fed `READONLY` string), and a `SIGNAL` the backend pushes to the view. The calculator slots *call* `calc_module`; `record()` feeds the PROP + SIGNAL after each call; and `onContextReady()` *subscribes* to `calc_module`'s `versionReady` event to feed the auto-syncing event PROP. # ── Step 1: Scaffold ──────────────────────────────────────────────────────── - title: "Scaffold" @@ -171,19 +173,48 @@ sections: content: | class CalcUiCpp { + // ── SLOTs — call-and-return; each reply reaches QML via logos.watch() ── SLOT(int add(int a, int b)) SLOT(int multiply(int a, int b)) SLOT(int factorial(int n)) SLOT(int fibonacci(int n)) SLOT(QString libVersion()) + + // Void slot — fire-and-forget. Asks calc_module to (re-)announce + // its version as a `versionReady` event; there's no return value + // to await, the answer comes back through the PROP below. + SLOT(void announceVersion()) + + // ── PROPs — auto-synced backend → every QML replica, no polling ── + // QString, event-fed: the typed `versionReady` subscription the + // backend arms in onContextReady() writes it. Starts empty. + PROP(QString versionEvent="" READONLY) + + // int, slot-driven: the backend bumps it after each calculation, + // so the view shows a live tally without ever polling. + PROP(int computeCount=0 READONLY) + + // int, READWRITE: a memory register the QML view both *reads* and + // *writes* (Store / Clear buttons), and the backend may set too. + // A write round-trips QML → replica → source → back to every replica. + PROP(int memory=0 READWRITE) + + // ── SIGNAL — backend → view push, distinct from a return value ── + // Emitted after each calculation. QML catches it with a + // Connections block (not logos.watch(), not a PROP read). + SIGNAL(computed(QString op, int result)) } post_text: | - This is the **single source of truth** for the remote interface, and the one Qt-typed file you author — the `.rep` uses Qt types (`QString`) because that's the Qt Remote Objects wire contract. `repc` generates: + This is the **single source of truth** for the remote interface, and the one Qt-typed file you author — the `.rep` uses Qt types (`QString`, `int`) because that's the Qt Remote Objects wire contract. `repc` generates: - - `rep_calc_ui_cpp_source.h` — `CalcUiCppSimpleSource` with virtual slots your `*Backend` overrides - - `rep_calc_ui_cpp_replica.h` — `CalcUiCppReplica` with typed methods the QML view calls + - `rep_calc_ui_cpp_source.h` — `CalcUiCppSimpleSource` with virtual slots your `*Backend` overrides, a `set(...)` setter + change signal for each PROP (`setVersionEvent`, `setComputeCount`, `setMemory`), and the `computed(...)` signal you `emit` + - `rep_calc_ui_cpp_replica.h` — `CalcUiCppReplica` with typed methods, the auto-synced `versionEvent` / `computeCount` / `memory` properties, and the `computed` signal the QML view reads, writes, and connects to - **SLOT** return values are delivered as `QRemoteObjectPendingReply` — use `logos.watch()` in QML to get them as JS Promises. You can also declare **PROP** entries (e.g. `PROP(QString status READWRITE)`) which auto-sync from the backend to the QML replica — see the [ui-typed-backend doc-test](https://github.com/logos-co/logos-module-builder/blob/main/doctests/ui-typed-backend.test.yaml) for a PROP fed from a typed module-event subscription. + One contract, four kinds of surface — slots are only the first: + + - **SLOT** return values arrive as `QRemoteObjectPendingReply` — `logos.watch()` turns them into JS Promises in QML. `add`/`multiply`/… return values; `announceVersion()` is a **void** slot (fire-and-forget, no reply to watch). + - **PROP** auto-syncs from the backend to every QML replica with no polling. We use three, of two types and two modes: `versionEvent` (QString, READONLY, fed by a module-**event subscription** in Step 5), `computeCount` (int, READONLY, bumped by each slot), and `memory` (int, **READWRITE** — QML assigns `backend.memory = …` and the new value round-trips through the backend source and back to the view). + - **SIGNAL** is a backend → view push that is neither a return value nor a synced property: `computed(op, result)` fires after each calculation and the QML view catches it with a `Connections` block. # ── Step 4: CMakeLists.txt ──────────────────────────────────────────────────── - title: "`CMakeLists.txt`" @@ -261,11 +292,30 @@ sections: int factorial(int n) override; int fibonacci(int n) override; QString libVersion() override; + + // Tells calc_module to emit its `versionReady` event. + void announceVersion() override; + + // Fires once when ui-host hands the plugin its LogosAPI — the + // typed dependency surface is live, so we arm the event + // subscription here (before the view's first call). + void onContextReady() override; + + private: + // Feeds the non-slot surfaces of the .rep after each calculation: + // bumps the computeCount PROP (setComputeCount, generated) and + // emits the `computed` SIGNAL. The READWRITE `memory` PROP is + // driven from QML, so the backend doesn't have to touch it. + void record(const QString& op, int result); }; post_text: | - No `Q_OBJECT`, no `Q_PLUGIN_METADATA`, no `initLogos`, no `name()`/`version()` — the universal builder generates all of that. You only declare the `.rep` slot overrides. + No `Q_OBJECT`, no `Q_PLUGIN_METADATA`, no `initLogos`, no `name()`/`version()` — the universal builder generates all of that. You only declare the `.rep` slot overrides (plus any private helpers of your own, like `record`). - `LogosUiPluginContext` also gives this backend typed **event subscriptions** (`modules().dep.on(...)`) and `onContextReady()` — arm subscriptions there so they're live before the view's first call. `calc_module` is call-only here, but the [ui-typed-backend doc-test](https://github.com/logos-co/logos-module-builder/blob/main/doctests/ui-typed-backend.test.yaml) shows a `.rep` PROP fed from a typed module-event subscription registered in `onContextReady()`. + Beyond the call-and-return slots, the backend drives the rest of the `.rep` surface: + + - **PROPs** — each gets a generated `set()` setter on the `SimpleSource`. The backend calls `setComputeCount(...)` (and could call `setMemory(...)`); `memory` is READWRITE, so QML can write it too. + - **SIGNAL** — `computed(...)` is declared on the `SimpleSource`, so the backend just `emit`s it. + - **Event subscriptions** — `LogosUiPluginContext` gives typed `modules().dep.on(...)` and the `onContextReady()` hook. `onContextReady()` subscribes to `calc_module`'s `versionReady` event and pipes the payload into the `versionEvent` PROP, which Qt Remote Objects auto-syncs to the view. Arm subscriptions in `onContextReady()` (not the constructor) so they're live the moment the backend is wired. - title: "`src/calc_ui_cpp_backend.cpp`" file: @@ -280,22 +330,30 @@ sections: int CalcUiCppBackend::add(int a, int b) { - return modules().calc_module.add(a, b); + int result = modules().calc_module.add(a, b); + record("add", result); + return result; } int CalcUiCppBackend::multiply(int a, int b) { - return modules().calc_module.multiply(a, b); + int result = modules().calc_module.multiply(a, b); + record("multiply", result); + return result; } int CalcUiCppBackend::factorial(int n) { - return modules().calc_module.factorial(n); + int result = modules().calc_module.factorial(n); + record("factorial", result); + return result; } int CalcUiCppBackend::fibonacci(int n) { - return modules().calc_module.fibonacci(n); + int result = modules().calc_module.fibonacci(n); + record("fibonacci", result); + return result; } QString CalcUiCppBackend::libVersion() @@ -304,13 +362,49 @@ sections: // (api-style qt), matching the .rep slot — no conversion needed. return modules().calc_module.libVersion(); } + + void CalcUiCppBackend::record(const QString& op, int result) + { + // PROP: bump the slot-driven counter. setComputeCount() is the + // generated setter; Qt Remote Objects syncs the new value to every + // replica, so the view's "Computations" label updates with no polling. + setComputeCount(computeCount() + 1); + + // SIGNAL: a backend → view push, distinct from the return value the + // QML side gets via logos.watch(). `computed` is declared on the + // generated SimpleSource, so we just emit it; the typed replica + // re-emits it and the view's Connections block catches it. + emit computed(op, result); + } + + void CalcUiCppBackend::announceVersion() + { + // Fire-and-forget call into calc_module: it looks up the library + // version and emits it as a `versionReady` event. We don't read a + // return value here — the event comes back through the subscription + // armed in onContextReady() below. + modules().calc_module.libVersionNotify(); + } + + void CalcUiCppBackend::onContextReady() + { + // Typed module-event subscription. `versionReady` is calc_module's + // event (Part 1's `logos_events:` block); the generated wrapper + // exposes it as on + a Qt-typed callback (QString, because a + // UI plugin is api-style qt). Push each payload into the versionEvent + // PROP — Qt Remote Objects then auto-syncs it to the QML replica. + modules().calc_module.onVersionReady([this](const QString& version) { + setVersionEvent(version); + }); + } post_text: | Key points: - - Each slot delegates straight to `calc_module` via `modules().calc_module.(...)` — the generated typed SDK, type-safe with no `QVariant`. - - Slots return values directly; they travel back to the QML replica via Qt Remote Objects. + - Each value slot delegates straight to `calc_module` via `modules().calc_module.(...)` — the generated typed SDK, type-safe with no `QVariant` — then calls `record()` to drive the PROP + SIGNAL surfaces before returning. Slot return values travel back to the QML replica via Qt Remote Objects. - `modules().calc_module.libVersion()` returns `QString` even though Part 1's `calc_module` declares it `std::string`. A UI plugin is Qt-typed (api-style `qt`), so the generated `modules().` wrapper exposes Qt types (`QString`, `int`, ...) that match the `.rep` slots directly — the std<->Qt conversion happens inside the generated wrapper, not in your view code. - - No `initLogos`, no manual `LogosModules` construction — `modules()` is wired by the generated plugin before any slot runs. + - **PROP vs. SIGNAL vs. return value — three ways to get data to the view.** `record()` shows two of them side by side: `setComputeCount(...)` writes a **PROP** that Qt Remote Objects auto-syncs (the view reads `backend.computeCount` with no polling), while `emit computed(...)` fires a **SIGNAL** the view catches in a `Connections` block. Both are independent of the slot's `logos.watch()` return value. The READWRITE `memory` PROP is the fourth path — there the *view* writes and the value syncs back. + - **Event subscription vs. method call.** `announceVersion()` makes `calc_module` *emit*; `onContextReady()` *subscribes* to that emission. The callback is Qt-typed (`const QString&`) and `setVersionEvent(...)` is the generated PROP setter — the value reaches QML with no polling and no return value to await. Arm the subscription in `onContextReady()`, never the constructor: `modules()` isn't wired until the framework calls it. + - No `initLogos`, no manual `LogosModules` construction — `modules()` is wired by the generated plugin before any slot runs or any event arrives. # ── Step 6: QML View ────────────────────────────────────────────────────────── - title: "QML View" @@ -332,6 +426,9 @@ sections: property string result: "" property string errorText: "" + // Last payload from the backend's `computed` SIGNAL (see Connections below). + property string lastSignal: "(none)" + // Typed replica of the backend running in ui-host (generated from calc_ui_cpp.rep). readonly property var backend: logos.module("calc_ui_cpp") @@ -353,6 +450,17 @@ sections: root.ready = root.backend !== null && logos.isViewModuleReady("calc_ui_cpp") } + // SIGNAL from the .rep: the backend emits `computed(op, result)` after + // each calculation. The typed replica re-emits it, so we catch it with + // a Connections block — no logos.watch(), no property read. This is the + // backend → view push path, distinct from the slot return value above. + Connections { + target: root.backend + function onComputed(op, result) { + root.lastSignal = op + " = " + result + } + } + // logos.watch() delivers the result of a replica slot call via callbacks. // No QtRemoteObjects import needed — the bridge handles it. function callCalc(method, args) { @@ -447,6 +555,15 @@ sections: enabled: root.ready onClicked: root.callCalc("libVersion", []) } + + Button { + // Fires the event path: asks calc_module to emit + // versionReady. No logos.watch() — the result comes + // back through the versionEvent PROP, not a return value. + text: "Announce version (event)" + enabled: root.ready + onClicked: root.backend.announceVersion() + } } Rectangle { @@ -464,14 +581,73 @@ sections: } } + // Slot-driven PROP: bumped by the backend's record() after each + // calculation. A plain property read — auto-syncs, no polling. + Text { + text: "Computations: " + ((root.ready && root.backend) ? root.backend.computeCount : 0) + color: "#cdd6f4" + font.pixelSize: 14 + Layout.alignment: Qt.AlignHCenter + } + + // SIGNAL payload, captured by the Connections block above. + Text { + text: "Last op (signal): " + root.lastSignal + color: "#94e2d5" + font.pixelSize: 14 + Layout.alignment: Qt.AlignHCenter + } + + // READWRITE PROP: the memory register. The label *reads* + // backend.memory; the buttons *write* it. A write round-trips + // QML → replica → backend source → back to every replica, so the + // label updates once the new value syncs home. + RowLayout { + spacing: 12 + Layout.alignment: Qt.AlignHCenter + + Text { + text: "Memory: " + ((root.ready && root.backend) ? root.backend.memory : 0) + color: "#cdd6f4" + font.pixelSize: 14 + } + + Button { + text: "Store (MS)" + enabled: root.ready + onClicked: root.backend.memory = parseInt(root.result) || 0 + } + + Button { + text: "Clear (MC)" + enabled: root.ready + onClicked: root.backend.memory = 0 + } + } + + // Event-fed label: the versionEvent PROP auto-syncs from the + // backend's typed versionReady subscription. No polling — it + // updates the moment calc_module emits. + Text { + readonly property string ev: (root.ready && root.backend) ? root.backend.versionEvent : "" + text: "Version event: " + (ev.length > 0 ? ev : "(none yet)") + color: "#f9e2af" + font.pixelSize: 15 + Layout.alignment: Qt.AlignHCenter + } + Item { Layout.fillHeight: true } } } post_text: | - Key patterns: + Key patterns — one for each surface the `.rep` exposes: - - `logos.module("calc_ui_cpp")` — gets the typed replica (auto-synced properties) - - `logos.watch(backend.add(1, 2), ...)` — SLOT return value as JS Promise + - `logos.module("calc_ui_cpp")` — gets the typed replica (auto-synced properties, callable slots, connectable signals) + - **SLOT return value:** `logos.watch(backend.add(1, 2), ...)` — the reply as a JS Promise. + - **READONLY PROP (slot-driven):** `backend.computeCount` is read directly in a binding. The backend bumps it via `setComputeCount(...)` after each calculation, and Qt Remote Objects auto-syncs it — the label re-evaluates with no polling. + - **READWRITE PROP:** `backend.memory` is both read (the "Memory:" label) and **written** (`backend.memory = ...` in the Store/Clear buttons). Assigning the property on the replica pushes the value to the backend source; it syncs back to every replica, so the label updates once the write lands. + - **SIGNAL:** `Connections { target: root.backend; function onComputed(op, result) { ... } }` catches the backend's `computed` push. No return value, no property — a one-shot event the view reacts to. + - **Event-fed PROP:** `backend.versionEvent` is read directly — no `logos.watch()`, no polling. The "Announce version" button calls the void `announceVersion()` slot (which makes `calc_module` emit `versionReady`); the backend's subscription catches the event and writes the PROP, and Qt Remote Objects pushes the new value straight into this label. This is the event path, distinct from the call-and-return slots above. - **Readiness:** the backend lives in a separate `ui-host` process and connects asynchronously, so the replica isn't usable the instant the view loads. `logos.isViewModuleReady("calc_ui_cpp")` reports the current state and the `onViewModuleReadyChanged` signal fires when it changes. Because `isViewModuleReady()` is a `Q_INVOKABLE` method (not a property), don't bind it directly — a `readonly property bool ready: logos.isViewModuleReady(...)` would never re-evaluate. Use the `Connections` + `Component.onCompleted` pattern shown above, and gate the buttons with `enabled: root.ready`. - The `logos` object is injected by the host at runtime — no `QtRemoteObjects` import needed @@ -639,8 +815,45 @@ sections: texts: ["8"] timeout: 10000 screenshot: "calc-cpp-result.png" + - name: "Slot-driven READONLY PROP: compute count incremented" + action: wait_for + texts: ["Computations: 1"] + timeout: 5000 + - name: "SIGNAL delivered: computed(op, result) reaches the view" + action: wait_for + texts: ["Last op (signal): add = 8"] + timeout: 5000 + - name: "Memory register starts at zero" + action: wait_for + texts: ["Memory: 0"] + timeout: 5000 + - name: "Write the READWRITE PROP from QML" + action: click + target: "Store (MS)" + - name: "READWRITE PROP round-trips backend → view" + action: wait_for + texts: ["Memory: 8"] + timeout: 10000 + - name: "Event label starts empty" + action: wait_for + texts: ["Version event: (none yet)"] + timeout: 5000 + - name: "Trigger the versionReady event" + action: click + target: "Announce version (event)" + - name: "Subscribed event payload reaches the view" + action: wait_for + texts: ["Version event: 1.0.0"] + timeout: 10000 + screenshot: "calc-cpp-event.png" post_text: | - The result `8` comes from `calc_module.add(3, 5)` executed in the C++ backend — proof the full path (QML replica → Qt Remote Objects → ui-host backend → typed SDK → `calc_module`) works end to end. + Every surface the `.rep` declares is now proven end to end from one click: + + - **SLOT return value** — the result `8` comes from `calc_module.add(3, 5)`, the call-and-return path (QML replica → Qt Remote Objects → ui-host backend → typed SDK → `calc_module`), delivered via `logos.watch()`. + - **READONLY PROP (slot-driven)** — `Computations: 1` is the `computeCount` PROP: the same `add` call ran `setComputeCount(...)` in the backend's `record()`, and Qt Remote Objects synced it to the view with no polling. + - **SIGNAL** — `Last op (signal): add = 8` is the `computed` signal the backend `emit`ted; the QML `Connections` block caught it. It carried the op name *and* the result, neither of which is a property or a return value. + - **READWRITE PROP** — `Memory: 0` → `Memory: 8` proves the *write* direction: the Store button assigned `backend.memory = 8` in QML, which pushed to the backend source and synced back to the label. Properties aren't read-only mirrors; QML can drive them too. + - **Event-fed PROP** — `Version event: 1.0.0` is the event path: clicking *Announce version* called `calc_module.libVersionNotify()`, which emitted `versionReady("1.0.0")`; the backend's `modules().calc_module.onVersionReady(...)` subscription — armed in `onContextReady()` — caught it and wrote the `versionEvent` PROP, which Qt Remote Objects synced into the view. The label was `(none yet)` until the event fired, so seeing the version proves the typed subscription delivered. # ── Step 10: Live reloading (prose only) ────────────────────────────────────── - title: "Live reloading QML with `DEV_QML_PATH`" @@ -751,23 +964,26 @@ sections: # ── Comparison: .rep Interface Patterns (prose only) ────────────────────────── - title: "Comparison: .rep Interface Patterns" text: | - You declare each pattern in the `.rep` and implement it in your `*Backend` (which derives the generated `SimpleSource` + `LogosUiPluginContext`). There is no hand-written plugin — the `*Plugin`/`*Interface` glue is generated. + You declare each pattern in the `.rep` and implement it in your `*Backend` (which derives the generated `SimpleSource` + `LogosUiPluginContext`). There is no hand-written plugin — the `*Plugin`/`*Interface` glue is generated. Every row but **Model** is live in this tutorial's `.rep` (✓), and the UI test in Step 9 drives each one: - | Pattern | .rep declaration | Backend C++ (`CalcUiCppBackend`) | QML usage | - | ------------------- | ------------------------------------ | ------------------------------------------- | ---------------------------------------------------------------------- | - | **Return value** | `SLOT(int add(int a, int b))` | `int add(...) override { return ...; }` | `logos.watch(backend.add(1,2), cb)` | - | **Property** | `PROP(QString status READWRITE)` | `setStatus("Ready")` (inherited from SimpleSource) | `backend.status` (auto-syncs) | - | **Signal** | `SIGNAL(errorOccurred(QString msg))` | `emit errorOccurred("fail")` | `Connections { target: backend; function onErrorOccurred(msg) {...} }` | - | **Model** | (use Q_PROPERTY on backend) | `Q_PROPERTY(QAbstractItemModel* items ...)` | `logos.model("calc_ui_cpp", "items")` | - | **Event-fed PROP** | `PROP(QString last READONLY)` | `onContextReady()`: `modules().dep.on([this](...){ setLast(...); })` | `backend.last` (auto-syncs, no polling) | + | Pattern | .rep declaration | Backend C++ (`CalcUiCppBackend`) | QML usage | + | -------------------- | ----------------------------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------------- | + | **Return value** ✓ | `SLOT(int add(int a, int b))` | `int add(...) override { return ...; }` | `logos.watch(backend.add(1,2), cb)` | + | **Void slot** ✓ | `SLOT(void announceVersion())` | `void announceVersion() override { ... }` | `backend.announceVersion()` (fire-and-forget, no `watch`) | + | **READONLY PROP** ✓ | `PROP(int computeCount=0 READONLY)` | `setComputeCount(computeCount()+1)` (inherited setter) | `backend.computeCount` (auto-syncs, read-only) | + | **READWRITE PROP** ✓ | `PROP(int memory=0 READWRITE)` | `setMemory(...)` — or let QML write it | `backend.memory` (read) / `backend.memory = 8` (write, round-trips) | + | **Signal** ✓ | `SIGNAL(computed(QString op, int result))`| `emit computed("add", 8)` | `Connections { target: backend; function onComputed(op, result) {...} }` | + | **Event-fed PROP** ✓ | `PROP(QString versionEvent="" READONLY)` | `onContextReady()`: `modules().dep.on([this](...){ setVersionEvent(...); })` | `backend.versionEvent` (auto-syncs, no polling) | + | **Model** | (use Q_PROPERTY on backend) | `Q_PROPERTY(QAbstractItemModel* items ...)` | `logos.model("calc_ui_cpp", "items")` | - The last row uses the `LogosUiPluginContext` surface — Qt-typed `modules()` callers and event subscriptions armed in `onContextReady()`. `calc_module` is call-only here; see the [ui-typed-backend doc-test](https://github.com/logos-co/logos-module-builder/blob/main/doctests/ui-typed-backend.test.yaml) for a worked event-driven PROP. + The last live row uses the `LogosUiPluginContext` surface — Qt-typed `modules()` callers and event subscriptions armed in `onContextReady()`: `versionEvent` is a PROP fed by the `modules().calc_module.onVersionReady(...)` subscription, and the *Announce version* button drives it. **Model** is the one pattern shown but not built here — for a `QAbstractItemModel*` Q_PROPERTY remoted via `logos.model()`, see [Next Steps](#next-steps). # ── Next Steps (prose only) ─────────────────────────────────────────────────── - title: "Next Steps" text: | - - Add more `.rep` properties/signals for richer UI state - - Use `logos.model()` for list views backed by `QAbstractItemModel` + - This tutorial already exercises slots, PROPs of different types and modes, and a signal — extend them with more state for your own UI + - Add an **enum** to the `.rep` (`ENUM Status { Idle, Busy }` — no parentheses, unlike `SLOT`/`PROP`/`SIGNAL`) — the factory registers it under the `Logos.` QML URI, so `import Logos.CalcUiCpp` then `CalcUiCpp.Busy` works in bindings + - Use `logos.model()` for list views backed by a `QAbstractItemModel*` Q_PROPERTY — the **Model** row above, the one `.rep` pattern this tutorial doesn't build - Package as `.lgx` for distribution: `nix build .#lgx` - **Use the Logos Design System** in your QML — see [the design system step](#use-the-logos-design-system-in-your-qml). Browse components in the storybook (`cd repos/logos-design-system && nix run`); file issues at `logos-co/logos-design-system`. - See [logos-package-manager-ui](https://github.com/logos-co/logos-package-manager-ui) for a production example