M8 Features

This commit is contained in:
Joey Yakimowich-Payne 2026-01-30 06:50:36 -07:00
commit d178e8765b

View file

@ -159,15 +159,136 @@ A Qt6-based node editor GUI for warppipe using the QtNodes (nodeeditor) library.
- [x] Screenshot tests gated behind `WARPPIPE_GUI_VISUAL_TESTS` CMake option (default OFF)
- [x] `ctest --test-dir build` runs model + GUI tests
- [ ] Milestone 8 (Optional) - Advanced Features
- [ ] Milestone 8a - Undo/Redo, Clipboard, and Keyboard Shortcuts
- [ ] Integrate `QUndoStack` via `BasicGraphicsScene::undoStack()`
- [ ] Undo/Redo already works for connection create/delete (built-in QtNodes `ConnectCommand`/`DisconnectCommand`)
- [ ] Verify Ctrl+Z / Ctrl+Shift+Z (or Ctrl+Y) work out of the box for connections
- [ ] Implement `DeleteVirtualNodeCommand : QUndoCommand`
- [ ] `redo()`: destroy virtual node via `Client::RemoveNode()`
- [ ] `undo()`: re-create virtual node via `Client::CreateVirtualSink/Source()` with same name/channels/rate
- [ ] Store node position and restore on undo
- [ ] Implement `deleteSelection()` for Del key
- [ ] Collect selected `NodeGraphicsObject` items from `m_scene->selectedItems()`
- [ ] Virtual nodes → push `DeleteVirtualNodeCommand` onto undo stack
- [ ] Non-virtual nodes → push `QtNodes::DeleteCommand` (removes from graph only, not PipeWire)
- [ ] Connection-only selection → push `QtNodes::DeleteCommand`
- [ ] Implement `copySelection()` (Ctrl+C)
- [ ] Serialize selected virtual nodes to JSON: stable_id, name, media_class, channels, rate, position
- [ ] Include links between selected nodes (source stable_id:port_name → target stable_id:port_name)
- [ ] Set `QClipboard` with custom MIME type `application/warppipe-virtual-graph`
- [ ] Implement `pasteSelection()` (Ctrl+V)
- [ ] Parse clipboard JSON, create new virtual nodes with " Copy" name suffix
- [ ] Position pasted nodes at offset from originals
- [ ] Deferred link resolution via `PendingPasteLink` queue (nodes may not exist yet)
- [ ] `tryResolvePendingLinks()` called on node add to wire up deferred links
- [ ] Implement `duplicateSelection()` (Ctrl+D) — copy + paste with (40, 40) offset
- [ ] Register keyboard shortcuts on `m_view`:
- [ ] Del → `deleteSelection()`
- [ ] Ctrl+C → `copySelection()`
- [ ] Ctrl+V → `pasteSelection()`
- [ ] Ctrl+D → `duplicateSelection()`
- [ ] Ctrl+L → auto-arrange + zoom fit
- [ ] Remove default QtNodes copy/paste actions to avoid conflicts
- [ ] Add tests for undo/redo command state (push command → undo → verify node re-created → redo → verify deleted)
- [ ] Milestone 8b - View and Layout Enhancements
- [ ] Add "Zoom Fit All" context menu action → `m_view->zoomFitAll()`
- [ ] Add "Zoom Fit Selected" context menu action → `m_view->zoomFitSelected()`
- [ ] Add "Save Layout As..." context menu action
- [ ] `QFileDialog::getSaveFileName()` → save layout JSON to custom path
- [ ] Reuse existing `saveLayout()` serialization, write to chosen path
- [ ] Add "Reset Layout" context menu action
- [ ] Clear saved positions, run `autoArrange()`, save, zoom fit
- [ ] Add "Refresh Graph" context menu action
- [ ] Reset model, re-sync from client, zoom fit
- [ ] Persist view state in layout JSON:
- [ ] Save view scale + center position (`m_view->getScale()`, `m_view->mapToScene(viewport center)`)
- [ ] Restore on load: `m_view->setupScale()` + `m_view->centerOn()`
- [ ] Fallback to `zoomFitAll()` when no saved view state
- [ ] Persist ghost nodes in layout JSON:
- [ ] Serialize ghost node stable_id, name, description, input/output ports (id + name), position
- [ ] Serialize ghost connections (out_stable_id, out_port_index, in_stable_id, in_port_index)
- [ ] Restore ghosts from layout on load (before live sync)
- [ ] Add middle-click center: `eventFilter` on viewport catches `MiddleButton``m_view->centerOn(mapToScene(pos))`
- [ ] Add tests for view state save/load round-trip and ghost persistence
- [ ] Milestone 8c - Sidebar and Preset System
- [ ] Add `QSplitter` between graph view and sidebar panel
- [ ] Graph view (stretch factor 1) on left, sidebar (stretch factor 0) on right
- [ ] Persist splitter sizes in layout JSON, restore on load
- [ ] Default sizes: graph 1200, sidebar 320
- [ ] Add `QTabWidget` sidebar with styled tabs (dark theme)
- [ ] Tab styling: dark background, selected tab has accent underline
- [ ] Initially one tab: "PRESETS" (meters/mixer tabs added in M8d/M8e)
- [ ] Implement `PresetManager` class:
- [ ] `savePreset(path)` → serialize to JSON:
- [ ] Virtual devices: name, description, media_class, channels, rate
- [ ] Routing: links by stable_id:port_name pairs
- [ ] UI layout: node positions, view state
- [ ] `loadPreset(path)` → apply from JSON:
- [ ] Create missing virtual devices
- [ ] Re-create links from routing entries
- [ ] Apply layout positions
- [ ] Save on quit via `QCoreApplication::aboutToQuit` signal
- [ ] Add "Save Preset..." context menu action → `QFileDialog::getSaveFileName()`
- [ ] Add "Load Preset..." context menu action → `QFileDialog::getOpenFileName()`
- [ ] Add tests for preset save/load round-trip
- [ ] Milestone 8d - Volume/Mute Controls (requires core API: `SetNodeVolume()`)
- [ ] Add `NodeVolumeState` struct: `{ float volume; bool mute; }`
- [ ] Add `ClickSlider : QSlider` — click jumps to position instead of page-stepping
- [ ] Add inline volume widget per node via `nodeData(NodeRole::Widget)`:
- [ ] Horizontal `ClickSlider` (0-100) + mute `QToolButton`
- [ ] Calls `Client::SetNodeVolume(nodeId, volume, mute)` on change
- [ ] Styled: dark background, green slider fill, rounded mute button
- [ ] Implement `VolumeChangeCommand : QUndoCommand`
- [ ] Stores previous + next `NodeVolumeState`, node ID
- [ ] `undo()` → apply previous state; `redo()` → apply next state
- [ ] Push on slider release or mute toggle (not during drag)
- [ ] Track volume states in model: `QHash<uint32_t, NodeVolumeState> m_nodeVolumeState`
- [ ] `setNodeVolumeState()` — update state + sync inline widget
- [ ] `nodeVolumeState()` — read current state
- [ ] Emit `nodeVolumeChanged(nodeId, previous, current)` signal
- [ ] Add "MIXER" tab to sidebar `QTabWidget`:
- [ ] `QScrollArea` with horizontal layout of channel strips
- [ ] Per-node strip: `AudioLevelMeter` + vertical `ClickSlider` (fader) + Mute (M) + Solo (S) buttons + node label
- [ ] Solo logic: when any node is soloed, all non-soloed nodes are muted
- [ ] Volume fader changes push `VolumeChangeCommand` onto undo stack
- [ ] `refreshMixerStrip()` — create strip when node appears
- [ ] `removeMixerStrip()` — destroy strip when node removed
- [ ] `updateMixerState()` — sync fader/mute from model state
- [ ] Include volume/mute states in preset save/load (`persistent_volumes`, `persistent_mutes`)
- [ ] Add tests for VolumeChangeCommand undo/redo and mixer strip lifecycle
- [ ] Milestone 8e - Audio Level Meters (requires core API: `MeterPeak()`, `NodeMeterPeak()`, `EnsureNodeMeter()`)
- [ ] Implement `AudioLevelMeter : QWidget`
- [ ] Custom `paintEvent`: vertical bar from bottom, background `(24,24,28)`
- [ ] Color thresholds: green (0-0.7), yellow (0.7-0.9), red (0.9-1.0)
- [ ] Peak hold indicator: white horizontal line, holds 6 frames then decays at 0.02/frame
- [ ] `setLevel(float)` — clamp 0-1, update hold, call `update()`
- [ ] `sizeHint()` → 40×160
- [ ] Add "METERS" tab to sidebar `QTabWidget`:
- [ ] "MASTER OUTPUT" label + master `AudioLevelMeter`
- [ ] "NODE METERS" label + scrollable list of per-node meter rows
- [ ] Per-node row: elided label + compact `AudioLevelMeter` (fixed 26px wide, min 70px tall)
- [ ] Add 30fps meter update timer (33ms, `Qt::PreciseTimer`)
- [ ] Poll `Client::MeterPeak()` → master meter
- [ ] Poll `Client::NodeMeterPeak(nodeId)` → per-node meters + mixer meters
- [ ] Skip updates when widget is not visible (`isVisible()` check)
- [ ] Auto-manage per-node meters:
- [ ] Create meter when node has active links (`ensureNodeMeter()`)
- [ ] Remove meter when node removed or all links removed (`removeNodeMeter()`)
- [ ] Skip meter nodes (filter by name prefix)
- [ ] Add tests for AudioLevelMeter level clamping, hold/decay logic
- [ ] Milestone 8f (Optional) - Architecture and Routing Rules
- [ ] Event-driven updates: replace 500ms polling with signal/slot if core adds registry callbacks
- [ ] `nodeAdded(NodeInfo)`, `nodeRemoved(uint32_t)`, `nodeChanged(NodeInfo)`
- [ ] `linkAdded(LinkInfo)`, `linkRemoved(uint32_t)`
- [ ] Keep polling as fallback if signals not available
- [ ] Link intent system: remember intended links by stable key, restore when nodes reappear
- [ ] `rememberLinkIntent(LinkInfo)` — store stable_id:port_name pairs
- [ ] `tryRestoreLinks()` — called on node add, resolves stored intents
- [ ] Persist link intents in layout JSON
- [ ] Add routing rule UI (separate panel or dialog)
- [ ] List existing rules from `Client::ListRouteRules()`
- [ ] Add/remove rules with RuleMatch fields
- [ ] Show which nodes are affected by rules
- [ ] Add volume/mute controls (if warppipe adds port parameters API)
- [ ] Add audio level meters (requires PipeWire param monitoring)
- [ ] Add config save/load UI (call `Client::SaveConfig()/LoadConfig()`)
- [ ] Add presets system (save/restore full node+link+rule configuration)
---