v0.2.0 (6/N): docs/native-dialogs.md — boundary doc + Qt.labs.platform examples
PLAN.md §12 noted "Native dialogs (file pickers, notifications) — where do they live?" as an open question with the bias "QML side via Qt". That bias was never written up; without the doc, a Symfony developer new to Qt would reasonably reach for "POST /api/show-dialog" or roll a custom QML "FileDialog" using Window + ListView. Both are wrong. The doc: - States the boundary plainly (native UI = QML side, never PHP) plus the architectural reason (PHP's process can't reach the user's window manager; Qt's can and already wraps every platform's native API). - Walks through Qt.labs.platform.FileDialog / MessageDialog / SystemTrayIcon / StandardPaths with copy-pasteable examples so apps don't need to discover Qt.labs the hard way. - Explains the trigger-vs-effect split: user-initiated confirmations open from the QML handler that fired the action; server-side events route through Mercure and let QML decide how to surface them (toast / dialog / tray notification). Anti-pattern callouts: don't dispatch dialogs from Doctrine listeners, don't add HTTP endpoints whose only job is to trigger UI side-effects, don't roll a custom QML file browser. Notifications caveat: Qt.labs.platform.SystemTrayIcon::showMessage covers the common case but routes through the tray. Richer notifications (action buttons, replies) need platform-specific code and are deferred — flagged in-doc. PLAN.md §13 also mentioned "ship a small Q_INVOKABLE helper for the common cases". Skipped: every common case Qt.labs.platform already covers, and a wrapper would just shadow upstream's API. If a future need surfaces a real gap (XDG portal notifications without tray, say), that's the time to add framework-side code; the doc will point at it. No code changes; doc-only. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
113
docs/native-dialogs.md
Normal file
113
docs/native-dialogs.md
Normal file
@@ -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/<name>`; 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.
|
||||
Reference in New Issue
Block a user