@@ -332,39 +332,54 @@ RgbaImageRenderer).
332332- ` BackgroundTaskTracker ` — tracks async operations, checks completions per frame, logs errors
333333- ` TextInputState ` — single-line text input with ` OnCommit ` (async), ` OnCancel ` ,
334334 ` OnTextChanged ` , ` OnKeyOverride ` callbacks
335+ - ` SignalBus ` — thread-safe typed deferred signal bus; widgets ` PostSignal<T>() ` during
336+ event handling, hosts ` Subscribe<T>() ` at startup, ` ProcessPending() ` delivers once per frame
337+ - ` DropdownMenuState ` — generic popup dropdown menu with keyboard navigation, custom entry
338+ support, and full-screen backdrop dismiss
339+
340+ ** Signal bus pattern** (replaces callback properties):
341+ 1 . Widget calls ` PostSignal(new SomeSignal(...)) ` during click/key handling — just enqueues
342+ 2 . ` OnPostFrame ` calls ` bus.ProcessPending(tracker) ` — delivers all pending signals
343+ 3 . Sync handlers run inline; async handlers submitted to ` BackgroundTaskTracker `
344+ 4 . Signals are ` readonly record struct ` value types defined in ` GuiSignals.cs `
345+ 5 . Built-in signals in DIR.Lib: ` ActivateTextInputSignal ` , ` DeactivateTextInputSignal ` ,
346+ ` RequestExitSignal ` , ` RequestRedrawSignal `
347+ 6 . App signals in Abstractions: ` DiscoverDevicesSignal ` , ` AddOtaSignal ` , ` EditSiteSignal ` ,
348+ ` CreateProfileSignal ` , ` AssignDeviceSignal ` , ` UpdateProfileSignal ` , ` BuildScheduleSignal ` ,
349+ ` ToggleFullscreenSignal ` , ` PlateSolveSignal `
335350
336351** Click handling pattern** :
3373521 . Widgets call ` RegisterClickable(x, y, w, h, hitResult, onClick) ` during render
338- 2 . Self-contained actions (filter cycling , list selection) pass an ` OnClick ` delegate
353+ 2 . Self-contained actions (offset stepping , list selection) pass an ` OnClick ` delegate
3393543 . ` HitTestAndDispatch(px, py) ` invokes ` OnClick ` and returns the ` HitResult `
340- 4 . SDL-dependent actions (text input focus) are handled by ` GuiEventHandlers `
355+ 4 . Cross-component actions use ` PostSignal ` (text input focus, profile save, discovery)
341356
342357** Keyboard handling pattern** :
3433581 . ` Program.cs ` maps SDL scancodes to ` InputKey ` via ` SdlInputMapping ` (in ` TianWen.UI.Shared ` )
3443592 . ` GuiEventHandlers.HandleKeyDown ` routes to active text input first
3453603 . If not consumed, ` Program.cs ` routes to ` ActiveTab.HandleKeyDown(inputKey, modifiers) `
3463614 . Each tab overrides ` HandleKeyDown ` for tab-specific shortcuts (pure state mutations)
347- 5 . DI-dependent actions use ` Action ` / ` Func<Task> ` callbacks set by the host at wiring time
362+ 5 . Cross-component actions use ` PostSignal ` (delivered in ` OnPostFrame ` )
348363
349364** Mouse wheel pattern** :
350365- ` GuiEventHandlers.HandleMouseWheel ` delegates to ` ActiveTab.HandleMouseWheel(scrollY, x, y) `
351366- Each tab overrides to handle its own scroll zones (target list, config panel, etc.)
352367
353368** Async operations** :
354369- All background work goes through ` BackgroundTaskTracker.Run(Func<Task>, description) `
355- - ` Program.cs ` calls ` tracker.ProcessCompletions(logger) ` each frame — logs errors, triggers redraw
370+ - ` Program.cs ` calls ` bus.ProcessPending(tracker) ` then ` tracker.ProcessCompletions(logger) `
371+ each frame — async signal handlers are submitted to tracker automatically
356372- ` TextInputState.OnCommit ` is ` Func<string, Task>? ` — submitted to tracker on Enter
357- - Tab callbacks (` OnDiscover ` , ` OnAddOta ` , ` OnAssignDevice ` ) are ` Func<Task> ` — submitted via tracker
358373- Shutdown: ` tracker.DrainAsync() ` awaits all pending tasks
359374- Zero fire-and-forget ` _ = Task.Run(...) ` in the codebase
360375
361376** Inheritance hierarchy** :
362377```
363- PixelWidgetBase<TSurface> (Abstractions — renderer-agnostic)
378+ PixelWidgetBase<TSurface> (DIR.Lib — renderer-agnostic, has Bus + PostSignal )
364379 └─ VkTabBase (Gui — pins TSurface = VulkanContext)
365380 ├─ VkGuiRenderer (sidebar, status bar, content dispatch)
366381 ├─ VkPlannerTab (planner with autocomplete, scheduling viz)
367- ├─ VkEquipmentTab (profile/device management)
382+ ├─ VkEquipmentTab (profile/device management, filter editing )
368383 └─ VkSessionTab (session config + observation list)
369384 └─ VkImageRenderer (Shared — FITS viewer toolbar, file list, histogram)
370385```
@@ -376,10 +391,10 @@ then falls through to the active tab.
376391
377392** Event handling** (` GuiEventHandlers ` ):
378393- Generic event routing only — zero tab-specific logic in the routing methods
379- - Constructor wires all callbacks (OnCommit, OnDiscover, etc.) with DI-dependent closures
380- - SDL bridge: ` StartTextInput ` / ` StopTextInput ` for IME lifecycle
394+ - Constructor subscribes DI-dependent signal handlers on the ` SignalBus `
395+ - Per-field ` TextInputState ` callbacks wired for planner search, site editing, profile creation
381396- ` Program.cs ` is a thin event loop + composition root
382- - Text input focus tracked via ` GuiAppState.ActiveTextInput ` (single source of truth)
397+ - Text input focus managed via ` ActivateTextInputSignal ` / ` DeactivateTextInputSignal `
383398
384399** SDL input mapping** (` TianWen.UI.Shared/SdlInputMapping.cs ` ):
385400- C# 14 extension blocks on ` Scancode ` and ` Keymod `
@@ -409,6 +424,42 @@ camera settings + observation list with frame estimates.
409424- Default exposure from f-ratio: ` 5 × f² ` seconds, clamped [ 10, 600]
410425- Estimated frame count per target: ` windowSeconds / (subExposure + 10s overhead) `
411426
427+ ### Filter Wheel Configuration
428+
429+ ** Design principle** : Seed on discovery, TianWen owns the data, sync back on connect.
430+ Filter names and focus offsets are stored as URI query params (` filter1 ` , ` offset1 ` , ...)
431+ on the filter wheel device URI in the profile. All drivers read from URI params first,
432+ with driver-specific fallbacks per slot.
433+
434+ | Driver | Slot count source | Name/offset fallback |
435+ | --------| ------------------| ---------------------|
436+ | ASCOM | ` Names.Length ` (COM) | COM ` Names[i] ` / ` FocusOffsets[i] ` |
437+ | ZWO | ` NumberOfSlots ` (hardware) | ` "Filter {N}" ` / ` 0 ` (seeded at discovery) |
438+ | Alpaca | REST API ` names ` array | API values |
439+ | Fake | preset count (LRGB/Narrowband/Simple by device ID, max 8) | preset values |
440+ | Manual | always 1 | ` InstalledFilter.Name ` |
441+
442+ ** ` Filter ` type** (` TianWen.Lib/Imaging/Filter.cs ` ):
443+ - ` readonly record struct Filter(string Name, string ShortName, string DisplayName, Bandpass) `
444+ - ` Name ` : code name (` HydrogenAlpha ` ), ` ShortName ` : abbreviation (` Hα ` ), ` DisplayName ` : user-friendly (` H-Alpha ` )
445+ - ` FromName() ` uses ` [GeneratedRegex] ` patterns, supports unicode α/β, handles dual-band filters
446+ - ` InstalledFilter.CustomName ` preserves unknown filter names (e.g. "Optolong L-Ultimate")
447+
448+ ** Equipment tab filter editing** (` VkEquipmentTab ` ):
449+ - Filter table below filter wheel slot (expand/collapse via ` [+] ` /` [-] ` header)
450+ - Filter name: click opens ` DropdownMenuState ` with ` CommonFilterNames ` + "Custom..." entry
451+ - Custom entry shows inline ` TextInputState ` for arbitrary names
452+ - Focus offset: ` [-] ` / ` [+] ` stepper buttons
453+ - All edits are in-memory (` EquipmentTabState.EditingFilters ` ); Save/Cancel buttons appear when dirty
454+ - Save commits via ` UpdateProfileSignal ` through the signal bus
455+
456+ ** ` EquipmentActions ` ** (` TianWen.UI.Abstractions/EquipmentActions.cs ` ):
457+ - ` GetFilterConfig(ProfileData, otaIndex) ` — reads filters from URI params
458+ - ` SetFilterConfig(ProfileData, otaIndex, filters) ` — writes filters to URI params
459+ - ` UpdateOTA(ProfileData, otaIndex, ...) ` — updates OTA name/focal length/aperture/optical design
460+ - ` FilterDisplayName(InstalledFilter) ` — returns ` DisplayName ` (custom name for unknowns)
461+ - ` CommonFilterNames ` — shared list used by dropdown and CLI
462+
412463### Fake Camera Sensor Presets
413464
414465` FakeCameraDriver ` selects sensor specs by device ID (1-based, mod 9), alternating
0 commit comments