|
| 1 | +# View-Model ↔ Document Binding Prompt |
| 2 | + |
| 3 | +Use this prompt to guide new view-model bindings for future document elements. Keep property specifics out; focus on state design, controller-driven transitions, transactions, and synchronization patterns. Derive patterns from existing implementations (e.g., `TrackViewModelContextData`, `LabelViewModelContextData`, `TempoViewModelContextData`). |
| 4 | + |
| 5 | +## Core Goals |
| 6 | +- Mirror document collections to view-model collections with clear ownership and mapping tables in both directions. |
| 7 | +- Drive UI interactions through controllers; never mutate document state directly from QML. |
| 8 | +- Guard against infinite loops by conditioning view→doc updates on active states. |
| 9 | +- Wrap mutating operations in transactions with start/commit/abort semantics tied to state entry/exit. |
| 10 | + |
| 11 | +- Instrument every state with entry/exit logging (use `qCInfo` with a per-context logging category) to trace flows during debugging, even for states without handlers. |
| 12 | + |
| 13 | +## State Machine Design |
| 14 | +- Build a `QStateMachine` with explicit states for idle, rubber-band selection, move/drag, edit/adjust flows, and per-property operations as needed. |
| 15 | +- Keep transitions driven by signals emitted from interaction controllers and internal guards (e.g., started/not-started, commit/abort, finish). |
| 16 | +- Example (move flow, list rotation): |
| 17 | + - `idle` → `movePending` on `moveTransactionWillStart` (from controller). |
| 18 | + - `movePending` → `moveProcessing` on `moveTransactionStarted`; → `idle` on `moveTransactionNotStarted`. |
| 19 | + - `moveProcessing` → `moveCommitting` on `moveTransactionWillCommit`; → `moveAborting` on `moveTransactionWillAbort`. |
| 20 | + - `moveCommitting`/`moveAborting` → `idle` (reset guards). |
| 21 | +- Example (edit flow, text-like): |
| 22 | + - `idle` → `namePending` on `nameTransactionWillStart`. |
| 23 | + - `namePending` → `nameProgressing` on `nameTransactionStarted`; → `idle` if not started. |
| 24 | + - `nameProgressing` → `nameCommitting` on commit; → `nameAborting` on abort. |
| 25 | + - `nameCommitting`/`nameAborting` → `idle`. |
| 26 | +- Rubber-band selection: `idle` → `rubberBandDragging` on start, back to `idle` on finish. |
| 27 | + |
| 28 | +## Controller-Driven Transitions |
| 29 | +- Wire controller signals to emit local signals that the state machine consumes (two patterns): |
| 30 | + 1) **Started/Committed/Aborted**: drag/move and text edits use started/commit/abort triplets. |
| 31 | + 2) **Started/Finished**: toggle/slider/height edits use started/finished pairs. |
| 32 | +- Set the current target item/index when the controller signals a start; clear it on commit/finish/abort handlers. |
| 33 | +- For rotations or list moves: only propagate view→doc when in the move-processing state; otherwise apply doc→view rotations. |
| 34 | + |
| 35 | +## Transactions |
| 36 | +- Begin a transaction when entering the corresponding "pending" state. Abort immediately if the controller could not start. |
| 37 | +- On commit states: if the new value differs, write to the document and `commitTransaction` with a descriptive label; otherwise `abortTransaction`. |
| 38 | +- On abort states: always `abortTransaction` and reset local guards (`target`, flags like `moveChanged`). |
| 39 | +- Track whether any change occurred during move/drag (`moveChanged`) to avoid committing no-op transactions. |
| 40 | + |
| 41 | +## Synchronization Patterns |
| 42 | +- Maintain bidirectional maps: `doc -> view` and `view -> doc`. Insert/remove bindings on collection signals (`itemInserted`/`itemRemoved`), not "aboutTo" when you need the item fully constructed. |
| 43 | +- When binding a new item: |
| 44 | + - Create the view-model item, insert into both maps and the view-model collection at the correct index. |
| 45 | + - Connect doc→view signals to update view items, guarded by equality checks. |
| 46 | + - Connect view→doc signals but gate them with state checks (only honor during the relevant progressing/doing states; otherwise revert the view to the doc value). |
| 47 | + - Initialize view properties from the doc model after wiring connections. |
| 48 | +- Selection sync: listen to document selection model `itemSelected` and mark the view item selected; initialize selection for pre-selected items after binding. |
| 49 | +- Rotation sync: doc→view rotations apply when *not* moving; view→doc rotations apply only while the move state is active, and should mark a change flag. |
| 50 | + |
| 51 | +## Example Snippets |
| 52 | +- **Doc→View guarded update** (avoid loops): |
| 53 | + ```cpp |
| 54 | + connect(control, &ControlType::propertyChanged, viewItem, [=](auto value) { |
| 55 | + if (viewItem->property() == value) return; |
| 56 | + viewItem->setProperty(value); |
| 57 | + }); |
| 58 | + ``` |
| 59 | +- **View→Doc gated by state**: |
| 60 | + ```cpp |
| 61 | + connect(viewItem, &ViewType::propertyChanged, docItem, [=] { |
| 62 | + if (!stateMachine->configuration().contains(propertyProgressingState)) { |
| 63 | + viewItem->setProperty(docItem->property()); |
| 64 | + return; |
| 65 | + } |
| 66 | + // defer actual write to commit handler |
| 67 | + }); |
| 68 | + ``` |
| 69 | +- **Transaction commit handler**: |
| 70 | + ```cpp |
| 71 | + void ContextData::onNameCommittingStateEntered() { |
| 72 | + if (!target || nameTxId == Invalid) { target = {}; return; } |
| 73 | + auto viewItem = viewMap.value(target); |
| 74 | + if (viewItem->name() == target->name()) { |
| 75 | + tx->abortTransaction(nameTxId); |
| 76 | + } else { |
| 77 | + target->setName(viewItem->name()); |
| 78 | + tx->commitTransaction(nameTxId, tr("Renaming item")); |
| 79 | + } |
| 80 | + nameTxId = {}; target = {}; |
| 81 | + } |
| 82 | + ``` |
| 83 | +- **Rotate handling**: |
| 84 | + ```cpp |
| 85 | + connect(docList, &List::rotated, this, [=](int l, int m, int r) { |
| 86 | + if (stateMachine->configuration().contains(moveProcessingState)) return; |
| 87 | + viewList->rotate(l, m, r); |
| 88 | + }); |
| 89 | + connect(viewList, &ViewList::rotated, this, [=](int l, int m, int r) { |
| 90 | + if (!stateMachine->configuration().contains(moveProcessingState)) return; |
| 91 | + moveChanged = true; |
| 92 | + docList->rotate(l, m, r); |
| 93 | + }); |
| 94 | + ``` |
| 95 | + |
| 96 | +## Implementation Checklist |
| 97 | +- Define states and transitions before binding to controllers; start the state machine immediately. |
| 98 | +- Create controllers via context helper methods; hook all relevant signals to emit local transition signals and set the current target. |
| 99 | +- Bind document collections first, then replay existing selection to the view. |
| 100 | +- For each commit/finish handler: compare values, write document, commit transaction; otherwise abort. Always reset `target` and flags. |
| 101 | +- Keep all strings ASCII; add concise comments only where non-obvious. |
| 102 | + |
| 103 | +Use this prompt verbatim when extending bindings to new document elements to maintain consistent interaction, transaction, and synchronization behavior across the codebase. |
0 commit comments