Phase 3 (slice): event-driven UI plumbing + Mute through controller

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>
This commit is contained in:
Uladzimir Karpenka
2026-06-04 11:01:39 +03:00
co-authored by Claude Opus 4.8
parent 2cf01036a3
commit b577a0d67a
2 changed files with 35 additions and 8 deletions
+30 -7
View File
@@ -149,6 +149,9 @@ type
// RadioController.pas. Поля состояния/движков теперь живут в
// FController; обращения идут как FController.FXxx. ----
FController: TRadioController;
// True пока OnControllerState обновляет виджеты — чтобы программное
// изменение слайдера/кнопки не вызывало повторно команду из OnChange.
FSyncingFromController: Boolean;
// ---- Network ----
FDevices: array of TDeviceItem;
@@ -368,6 +371,7 @@ type
// ---- Helpers ----
procedure DoInvoke(M: TThreadMethod); // маршалинг команд контроллера в UI-поток
procedure OnControllerState(Sender: TObject; Field: TRadioField); // контроллер → UI
procedure BuildUI;
function LeftPanelButtonLeft(PanelWidth, ColCount, ColIndex: Integer): Integer;
function LeftPanelButtonWidth(PanelWidth, ColCount, ColIndex: Integer): Integer;
@@ -1065,6 +1069,7 @@ begin
// освобождаются здесь же (в полях FController) — Фаза 1 миграции.
FController := TRadioController.Create;
FController.OnInvoke := DoInvoke;
FController.OnStateChanged := OnControllerState;
FController.FVfoA := 14200000;
FController.FVfoB := 7100000;
FController.FActiveVfo := 0;
@@ -1410,6 +1415,28 @@ begin
TThread.Synchronize(nil, M);
end;
// Контроллер изменил состояние — обновляем соответствующие виджеты.
// Выполняется в UI-потоке (команды контроллера вызываются из UI-потока либо
// маршалятся адаптерами). FSyncingFromController защищает от рекурсии, когда
// программное изменение виджета триггерит его OnChange.
procedure TMainForm.OnControllerState(Sender: TObject; Field: TRadioField);
begin
FSyncingFromController := True;
try
case Field of
rfMute:
begin
StyleButton(BtnMute, FController.FMuted);
if FController.FMuted then BtnMute.Caption := 'UNMUTE'
else BtnMute.Caption := 'MUTE';
BtnMute.Invalidate;
end;
end;
finally
FSyncingFromController := False;
end;
end;
procedure TMainForm.FormClose(Sender: TObject; var CloseAction: TCloseAction);
begin
// Сохраняем позицию здесь — окно ещё полностью видимо и стабильно.
@@ -5830,11 +5857,8 @@ end;
procedure TMainForm.BtnMuteClick(Sender: TObject);
begin
FController.FMuted := not FController.FMuted;
StyleButton(BtnMute, FController.FMuted);
if FController.FMuted then BtnMute.Caption := 'UNMUTE'
else BtnMute.Caption := 'MUTE';
if FController.FWDSPReady then FController.FDSPEngine.SetMute(FController.FMuted);
if FSyncingFromController then Exit;
FController.ToggleMute; // логика (движок) + OnControllerState обновит кнопку
end;
procedure TMainForm.UpdateNRButton;
@@ -6430,8 +6454,7 @@ end;
procedure TMainForm.SyncWebMute;
begin
if FWebSyncBool <> FController.FMuted then
BtnMuteClick(nil);
FController.SetMute(FWebSyncBool); // идемпотентно; OnControllerState обновит UI
end;
// ── CTUN ──────────────────────────────────────────────────────────────────
+5 -1
View File
@@ -397,7 +397,11 @@ procedure TRadioController.VolumeBy(Delta: Integer);
begin SetVolume(FVolume + Delta); end;
procedure TRadioController.SetMute(On_: Boolean);
begin FMuted := On_; Changed(rfMute); { TODO wiring } end;
begin
FMuted := On_;
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetMute(FMuted);
Changed(rfMute);
end;
procedure TRadioController.ToggleMute;
begin SetMute(not FMuted); end;