Vertical slice to establish the Phase 3 pattern before replicating it
across all commands: logic moves into controller commands, the UI becomes
event-driven, and frontends call commands instead of poking widgets.
- TRadioController.SetMute now carries the real logic (state + FDSPEngine.
SetMute), then fires OnStateChanged(rfMute).
- MainForm subscribes via OnControllerState (controller -> UI). It restyles
BtnMute from controller state. FSyncingFromController guards against
re-entrancy when a programmatic widget change would retrigger OnChange.
- BtnMuteClick -> FController.ToggleMute; SyncWebMute -> FController.SetMute.
The button/web no longer own the mute logic or the restyle.
Proves the command/event/refresh loop end-to-end (UI click and web both
route through the controller). Remaining commands follow this pattern.
Builds clean (cocoa); behavior unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
NR/NB modes, SNB/ANF flags, and drive % were stored only in the widgets
themselves (Btn*.Tag, TrkDrive.Position) — the last hard blocker to a
headless/daemon mode, since without those controls the state had nowhere
to live and CAT/Web read them directly.
- NR/NB: BtnNR.Tag/BtnNB.Tag -> FController.FNRMode/FNBMode (Integer).
- SNB/ANF: BtnSNB.Tag/BtnANF.Tag (0/1) -> FController.FSNB/FANF (Boolean).
- Drive %: TrkDrive.Position -> new FController.FDrivePercent; the slider
is now pure input (TrkDriveChange pushes its value into the controller),
while FDriveLevel stays the derived hardware byte.
Buttons/sliders become pure view+input: click handlers and UpdateXButton
read/write the controller field; CAT getters, the web snapshot, and
GetSnapshot all source state from the controller, not the widgets.
No behavioral change. Builds clean (cocoa).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Decouple MainForm from CAT/Web by introducing a headless core,
TRadioController, as the single home for radio state and engine objects.
This is groundwork for swappable UIs, a daemon (Web-only) mode, and a
future hardware control panel — each becomes a frontend/adapter over the
same controller API.
RadioController.pas (new):
- Radio state + engine objects (Network/DSP/Audio/Settings/Channels) as
public fields named exactly as in MainForm, so wiring is mechanical.
- Command API: absolute Set* plus relative *By/Up/Down (for encoders).
- OnStateChanged(field) notifications and GetSnapshot for frontend sync.
- Invoke/OnInvoke (TUIInvokeEvent) thread-marshalling seam (GUI uses
TThread.Synchronize; daemon supplies its own).
- Commands are scaffolding for now (update field + fire event); engine
logic stays in MainForm and migrates in later phases (TODO-wiring).
MainForm.pas:
- Owns FController; creates it first in FormCreate, frees it last in
FormDestroy. Engines are still created/freed by MainForm into the
controller's fields (ownership move is a later phase).
- ~1400 FXxx references redirected to FController.FXxx (implementation
only); dead field declarations removed from the interface.
No behavioral change — pure state relocation. Builds clean (cocoa).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>