diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f6b6f2..1324261 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,10 @@ This section tracks work landing on `dev` toward **v0.2.0** (next minor; pre-1.0 - **`BridgeOpTest` wire-format contract.** Locks the four enum case values (`upsert` / `delete` / `replace` / `event`) against accidental rename — QML clients hardcode the strings, so a `value` change is a wire-protocol break and the test fails the build before it ships. +### Documentation + +- **`docs/native-dialogs.md`.** Documents the framework boundary §12 already implied: native UI affordances (file pickers, confirmations, system notifications) live on the QML side via `Qt.labs.platform`, not in PHP. Includes copy-pasteable examples of `FileDialog`, `MessageDialog`, `SystemTrayIcon`, and the trigger-vs-effect split for server-pushed-event-driven dialogs. + ## [0.1.2] — 2026-05-03 Bugfix release. Bundles the v0.1.1 follow-up that surfaced during the v0.1.2 cycle (bundled-mode supervisor cleanly SIGTERMs its child on host exit) with three non-breaking fixes from a post-v0.1.1 architecture audit. diff --git a/docs/README.md b/docs/README.md index 9d07dd3..2b716e3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -18,6 +18,7 @@ Developer documentation for [php-qml](../README.md). Design rationale and roadma - **[Makers](makers.md)** — `make:bridge:resource`, `make:bridge:command`, `make:bridge:window`. - **[Dev workflow](dev-workflow.md)** — hot reload (PHP + QML), dev console (`Ctrl+\``), editor configs, `bridge:doctor`. - **[Linux packaging](packaging-linux.md)** — `make appimage`, AppImageUpdate, performance budgets. +- **[Native dialogs](native-dialogs.md)** — file pickers, confirmations, system notifications: where they live (QML, not PHP) and how to use the platform-native components Qt already ships. ## Reference diff --git a/docs/native-dialogs.md b/docs/native-dialogs.md new file mode 100644 index 0000000..bd8573e --- /dev/null +++ b/docs/native-dialogs.md @@ -0,0 +1,113 @@ +# Native dialogs and notifications + +> **The rule:** native UI affordances — file pickers, message boxes, system notifications, tray icons — live on the **QML side**, not in PHP. PLAN.md §12 ([Native dialogs row](../PLAN.md#12-open-questions-and-risks)) treats this as a framework boundary, not an open question. + +## Why this boundary + +The Qt host owns the window, the input loop, and the platform integration. Anything that needs to talk to the OS's native chrome (a save-file dialog drawn by `kdialog` / `NSSavePanel` / `IFileSaveDialog`, a notification routed through `org.freedesktop.Notifications` / `NSUserNotificationCenter` / `ToastNotification`) is reachable from Qt and unreachable from PHP. PHP can produce data and decide *what* should happen; QML decides *how to surface it natively*. + +Trying to dispatch a "show me a save-file dialog" from a Symfony controller means either polling the Qt side over HTTP (slow, ugly) or shipping a second Qt↔PHP transport just for UI side-effects (unnecessary). Don't. + +## File pickers + +Use [`Qt.labs.platform.FileDialog`](https://doc.qt.io/qt-6/qml-qt-labs-platform-filedialog.html) — it draws the platform-native dialog, not Qt's fallback rendering. + +```qml +import QtQuick +import QtQuick.Controls +import Qt.labs.platform as Platform + +Button { + text: "Open document…" + onClicked: openDialog.open() + + Platform.FileDialog { + id: openDialog + title: "Open document" + nameFilters: ["JSON files (*.json)", "All files (*)"] + fileMode: Platform.FileDialog.OpenFile + onAccepted: console.log("user picked:", currentFile) + } +} +``` + +Save-file is the same component with `fileMode: Platform.FileDialog.SaveFile`. Multi-select uses `OpenFiles` and `currentFiles` (plural). + +The dialog returns a `file://` URL — convert it for filesystem use with the standard QML idiom `Qt.url.toLocalFile(currentFile)` or pass the URL straight to `QFile` / `QNetworkAccessManager` if upload-as-URL fits. + +## Confirmations / message boxes + +Use [`Qt.labs.platform.MessageDialog`](https://doc.qt.io/qt-6/qml-qt-labs-platform-messagedialog.html). Same idiom — declarative component, `open()` to show, signal handlers to react. + +```qml +Platform.MessageDialog { + id: confirmDelete + title: "Delete todo?" + text: `“${todo.title}” will be removed permanently.` + buttons: Platform.MessageDialog.Yes | Platform.MessageDialog.Cancel + onAccepted: todoModel.removeRow(index) +} +``` + +For a non-modal toast-style "Saved!" feel, use Qt Quick Controls' `ToolTip.show(text, timeoutMs)` rather than a MessageDialog — that's a *banner*, not a confirmation, and shouldn't grab focus. + +## Notifications + +For "X has finished" / "you have a new Y" *system tray* notifications, use [`Qt.labs.platform.SystemTrayIcon`](https://doc.qt.io/qt-6/qml-qt-labs-platform-systemtrayicon.html) with `showMessage(title, body, icon, msecs)`: + +```qml +Platform.SystemTrayIcon { + id: tray + visible: true + icon.source: "qrc:/icons/app.png" + + Connections { + target: ImportJobs + function onCompleted(job) { + tray.showMessage("Import finished", `${job.rowCount} rows`) + } + } +} +``` + +Caveats: + +- Cross-platform but **routed through the tray**. Users who hide the tray won't see the notification on Linux (the [XDG Notifications portal](https://flatpak.github.io/xdg-desktop-portal/) is the long-term fix; planned for a later release). +- macOS: notifications go to the Notification Center even if the tray isn't visible — works without caveat. +- Windows: same, via the action center. + +Richer notifications (action buttons, replies, persistent banners) need platform-specific code per OS and aren't in scope for v0.x. If your app needs them sooner, drop a `QtPlatformNotification` wrapper next to `BackendConnection` and surface it as a QML singleton — happy to merge. + +## Folder pickers + +Same `FileDialog` component, `fileMode: Platform.FileDialog.OpenDirectory`. The native dialog hides files automatically. + +## Standard paths + +Don't hard-code `~/Downloads` or `%USERPROFILE%`. Use [`Qt.labs.platform.StandardPaths`](https://doc.qt.io/qt-6/qml-qt-labs-platform-standardpaths.html): + +```qml +Platform.FileDialog { + folder: Platform.StandardPaths.writableLocation(Platform.StandardPaths.DocumentsLocation) +} +``` + +These resolve to the OS-correct location (`XDG_DOCUMENTS_DIR`, `~/Documents`, the Documents knownfolder, etc.) and stay correct under user policy / corporate config. + +## Anti-patterns + +- **Don't** `POST /api/show-dialog`. The PHP side has no hook into the user's window manager. +- **Don't** open a `QDialog` from a Doctrine listener. Doctrine listeners run inside the FrankenPHP child process; even if they could reach Qt, they'd block the worker. +- **Don't** roll a custom QML "FileDialog" using `Window` + `ListView` to browse the filesystem. It looks wrong on every OS, can't access OS-restricted paths (sandboxed downloads, Photos, etc.), and reinvents what Qt already ships. + +## When to publish a Mercure event vs open a dialog directly + +- **User initiates an action that needs confirmation** (delete, overwrite) → open the dialog from the QML handler that fired the action; the action only proceeds on `onAccepted`. +- **Server-side event the user should be told about** (background job done, push notification) → publish a Mercure event on `app://event/`; QML's `MercureClient` listener fires the dialog or `tray.showMessage`. + +The split keeps the *trigger* close to the user's intent and the *side-effect* declarative on the QML side. + +## See also + +- [QML API reference — `BackendConnection`, `MercureClient`](qml-api.md) — for handling server-pushed events that drive notifications. +- [Update semantics](update-semantics.md) — for the `commandFailed` / `commandTimedOut` signals that often want toast or dialog feedback.