unit SettingsForm;

{
  SettingsForm.pas — диалог настроек EWSDR SDR приложения.

  Вкладки: General | Audio | Display | DSP | Transmit | PA Settings | CAT
  Display имеет подвкладки: General | RX1 | RX2 | TX

  Все изменения применяются МГНОВЕННО через events:
    OnDisplayChange  — изменены FFT/Window/Detector/Avg параметры
    OnWaterfallChange — изменены параметры водопада
    OnGridChange     — изменена сетка спектра
    OnAudioChange    — изменено аудио устройство/буфер
    OnSampleRateChange — изменён audio output sample rate (только PortAudio, DDC не трогается)

  Для применения настроек: создайте форму, зарегистрируйте callbacks,
  потом вызовите Show или ShowModal.
}

{$IFDEF FPC}
  {$MODE Delphi}
{$ENDIF}

interface

uses
  Classes, SysUtils, Math,
  Forms, Controls, Graphics, Dialogs,
  StdCtrls, ExtCtrls, Spin,
  FlatButton, AudioOutput, AudioInput;

type
  // PA: MaxPower в Вт, BandCal[0..10] — калибровка 38.8..100.0 на диапазон
  TOnPASettingsChange = procedure(MaxPower: Double;
                                  const BandCal: array of Double) of object;

  TOnDisplayParamChange = procedure(FFTSize, WindowType, SpecDetector,
    SpecAvgMode: Integer; SpecAvgTimeMS: Double) of object;
  TOnWaterfallParamChange = procedure(WfDetector, WfAvgMode: Integer;
    WfAvgTimeMS, WfHigh, WfLow, WfAGCOffset: Double) of object;
  TOnGridParamChange = procedure(RefLevel, Range, GridStep: Double) of object;
  TOnAudioDevChange = procedure(DevIndex: Integer; const DevName: string) of object;
  TOnAudioInDevChange = procedure(DevIndex: Integer; const DevName: string) of object;
  TOnVisibilityChange = procedure(ShowSpectrum, ShowWaterfall: Boolean) of object;
  TOnFPSChange = procedure(FPS: Integer) of object;
  TOnCATChange = procedure(
    const SerEnabled: array of Boolean;
    const SerPort: array of string;
    const SerBaud: array of Integer;
    const SerDataBits: array of Integer;
    const SerStopBits: array of Integer;
    const SerParity: array of Integer;
    TcpEnabled: Boolean; TcpPort: Integer) of object;

  { TSettingsForm }
  TSettingsForm = class(TForm)
  private
    // ---- Layout ----
    FNavPanel:       TPanel;
    FContentPanel:   TPanel;
    FPageAudio:      TScrollBox;
    FPageDisplay:    TScrollBox;
    FPageSpectrum:   TScrollBox;
    FPageWaterfall:  TScrollBox;
    FPagePA:         TScrollBox;
    FPageAdvanced:   TScrollBox;
    FPageCAT:        TScrollBox;
    FNavAudio:       TFlatButton;
    FNavDisplay:     TFlatButton;
    FNavSpectrum:    TFlatButton;
    FNavWaterfall:   TFlatButton;
    FNavPA:          TFlatButton;
    FNavAdvanced:    TFlatButton;
    FNavCAT:         TFlatButton;

    // ---- Audio tab controls ----
    FCmbRXDev:       TComboBox;
    FRXDevIndices:   array[0..63] of Integer;
    FRXDevCount:     Integer;
    FCmbTXDev:       TComboBox;
    FTXDevIndices:   array[0..63] of Integer;
    FTXDevCount:     Integer;

    // ---- RX1 sub-tab controls ----
    FCmbFFTSize:     TComboBox;
    FCmbWindow:      TComboBox;
    FCmbSpecDet:     TComboBox;
    FCmbSpecAvg:     TComboBox;
    FSpecAvgTime:    TFloatSpinEdit;
    FCmbWfDet:       TComboBox;
    FCmbWfAvg:       TComboBox;
    FWfAvgTime:      TFloatSpinEdit;
    FWfHigh:         TFloatSpinEdit;
    FWfLow:          TFloatSpinEdit;
    FWfAGCOffset:    TFloatSpinEdit;
    FEdRefLevel:     TEdit;
    FEdRange:        TEdit;
    FCmbGridStep:    TComboBox;

    // ---- General display tab controls ----
    FChkShowSpectrum:    TCheckBox;
    FChkShowWaterfall:   TCheckBox;
    FCmbFPS:             TComboBox;

    // ---- PA Settings tab controls ----
    FEdMaxPower:     TSpinEdit;
    FEdBandCal:      array[0..10] of TFloatSpinEdit;

    // ---- CAT tab controls ----
    FCATSerialEn:       array[0..3] of TCheckBox;
    FCATSerialPort:     array[0..3] of TEdit;
    FCATSerialBaud:     array[0..3] of TComboBox;
    FCATSerialDataBits: array[0..3] of TComboBox;
    FCATSerialStopBits: array[0..3] of TComboBox;
    FCATSerialParity:   array[0..3] of TComboBox;
    FCATTcpEn:          TCheckBox;
    FCATTcpPort:        TSpinEdit;

    // ---- Close button ----
    FBtnClose:       TFlatButton;

    // ---- Callbacks ----
    FOnCATChange:        TOnCATChange;
    FOnPAChange:         TOnPASettingsChange;
    FOnDisplayChange:    TOnDisplayParamChange;
    FOnWaterfallChange:  TOnWaterfallParamChange;
    FOnGridChange:       TOnGridParamChange;
    FOnAudioDevChange:   TOnAudioDevChange;
    FOnAudioInDevChange: TOnAudioInDevChange;
    FOnVisibilityChange: TOnVisibilityChange;
    FOnFPSChange:        TOnFPSChange;

    FLoading: Boolean;

    procedure BuildUI;
    procedure BuildAudioTab;
    procedure BuildDisplayTab;
    procedure BuildRX1Tab;
    procedure BuildPATab;
    procedure BuildCATTab;
    procedure ApplyTheme;
    procedure NavClick(Sender: TObject);
    procedure SelectPage(APage: TWinControl; ANav: TFlatButton);

    function MakeLbl(AParent: TWinControl; const Cap: string;
      ALeft, ATop, AW: Integer): TLabel;
    function MakeCombo(AParent: TWinControl; ALeft, ATop, AW: Integer;
      OnChange: TNotifyEvent): TComboBox;
    function MakeGroupPanel(AParent: TWinControl; const Cap: string;
      ALeft, ATop, AW, AH: Integer): TPanel;
    function MakeScrollPage: TScrollBox;
    function MakeNavButton(const Cap: string; ATop: Integer): TFlatButton;

    procedure OnFFTSizeChange(Sender: TObject);
    procedure OnWindowChange(Sender: TObject);
    procedure OnSpecDetChange(Sender: TObject);
    procedure OnSpecAvgModeChange(Sender: TObject);
    procedure OnSpecAvgTimeChange(Sender: TObject);
    procedure OnWfDetChange(Sender: TObject);
    procedure OnWfAvgModeChange(Sender: TObject);
    procedure OnWfAvgTimeChange(Sender: TObject);
    procedure OnWfThresholdChange(Sender: TObject);
    procedure OnRefLevelChange(Sender: TObject);
    procedure OnRangeChange(Sender: TObject);
    procedure OnGridStepChange(Sender: TObject);
    procedure OnRXDevChange(Sender: TObject);
    procedure OnTXDevChange(Sender: TObject);
    procedure OnVisibilityChkChange(Sender: TObject);
    procedure OnFPSCmbChange(Sender: TObject);
    procedure OnMaxPowerChange(Sender: TObject);
    procedure OnBandCalChange(Sender: TObject);
    procedure OnCATSerialChange(Sender: TObject);
    procedure OnCATTcpChange(Sender: TObject);
    procedure FireCATChange;
    procedure BtnCloseClick(Sender: TObject);

    procedure FireDisplayChange;
    procedure FireWaterfallChange;
    procedure FireGridChange;

    function  GetSpecAvgTimeMS: Double;
    function  GetWfAvgTimeMS:   Double;
    function  GetWfHigh:        Double;
    function  GetWfLow:         Double;
    function  GetWfAGCOffset:   Double;
    function  GetRefLevel:     Double;
    function  GetRange:        Double;
    function  GetGridStep:     Double;
    function  GetFFTSize:      Integer;

  public
    constructor Create(AOwner: TComponent); override;

    procedure LoadValues(
      FFTSize, WinType, SpecDet, SpecAvgMode: Integer;
      SpecAvgTimeMS: Double;
      WfDet, WfAvgMode: Integer;
      WfAvgTimeMS, WfHigh, WfLow, WfAGCOffset: Double;
      RefLevel, Range, GridStep: Double;
      const AudioOutDevName: string;
      const AudioInDevName: string);

    procedure RefreshAudioDevices(AudioOut: TAudioOutput; AudioIn: TAudioInput);
    procedure LoadVisibility(ShowSpectrum, ShowWaterfall: Boolean);
    procedure LoadFPS(FPS: Integer);
    procedure LoadPASettings(MaxPower: Double; const BandCal: array of Double);
    procedure LoadCATSettings(
      const SerEnabled: array of Boolean;
      const SerPort: array of string;
      const SerBaud, SerDataBits, SerStopBits, SerParity: array of Integer;
      TcpEnabled: Boolean; TcpPort: Integer);

    property OnCATChange:        TOnCATChange            read FOnCATChange       write FOnCATChange;
    property OnPAChange:         TOnPASettingsChange     read FOnPAChange        write FOnPAChange;
    property OnDisplayChange:    TOnDisplayParamChange   read FOnDisplayChange   write FOnDisplayChange;
    property OnWaterfallChange:  TOnWaterfallParamChange read FOnWaterfallChange write FOnWaterfallChange;
    property OnGridChange:       TOnGridParamChange      read FOnGridChange      write FOnGridChange;
    property OnAudioDevChange:   TOnAudioDevChange       read FOnAudioDevChange  write FOnAudioDevChange;
    property OnAudioInDevChange: TOnAudioInDevChange     read FOnAudioInDevChange write FOnAudioInDevChange;
    property OnVisibilityChange: TOnVisibilityChange     read FOnVisibilityChange write FOnVisibilityChange;
    property OnFPSChange:        TOnFPSChange            read FOnFPSChange       write FOnFPSChange;
  end;

implementation

const
  CLR_BG      = TColor($00121212);
  CLR_PANEL   = TColor($001A1A1A);
  CLR_TEXT    = TColor($00E0E0E0);
  CLR_TEXTDIM = TColor($00888888);
  CLR_BORDER  = TColor($00303030);
  CLR_ACCENT  = TColor($0040FF80);
  CLR_INPUT   = TColor($00FFFFFF);
  CLR_INPUT_TEXT = TColor($00202020);
  {$IFDEF MSWINDOWS}
  UI_FONT     = 'Segoe UI';
  {$ELSE}
  UI_FONT     = 'Sans';
  {$ENDIF}
  BTN_H       = 24;
  GRP_TITLE_H = 22;  // высота заголовка в group-панелях (title label + divider)

  CAT_BAUD_VALS: array[0..7] of Integer = (1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200);
  CAT_BAUD_NAMES: array[0..7] of string = ('1200','2400','4800','9600','19200','38400','57600','115200');

  FFT_SIZES: array[0..6] of Integer = (4096, 8192, 16384, 32768, 65536, 131072, 262144);
  FFT_NAMES: array[0..6] of string  = ('4096','8192','16384','32768','65536','131072','262144');

  WINDOW_NAMES: array[0..6] of string = (
    'Rectangular', '4-term Blackman-Harris', 'Hann',
    'Flat-top', 'Hamming', 'Kaiser', '7-term Blackman-Harris');

  DETECTOR_NAMES: array[0..4] of string = (
    'Peak', 'Rosenfell', 'Average', 'Sample', 'RMS');

  AVGMODE_NAMES: array[0..3] of string = (
    'None', 'Recursive', 'Time Window', 'Log Recursive');
  AVGMODE_VALUES: array[0..3] of Integer = (0, 1, 2, 3);
  WF_AVGMODE_NAMES: array[0..3] of string = (
    'None', 'Recursive', 'Time Window', 'Log Recursive');
  WF_AVGMODE_VALUES: array[0..3] of Integer = (0, 1, 2, 3);

  GRID_STEPS:  array[0..4] of Double = (5.0, 10.0, 20.0, 30.0, 40.0);
  GRID_STEP_NAMES: array[0..4] of string = ('5 dB','10 dB','20 dB','30 dB','40 dB');

  FPS_VALUES: array[0..12] of Integer = (5, 10, 15, 20, 25, 30, 40, 50, 60, 75, 80, 90, 100);
  FPS_NAMES:  array[0..12] of string  = ('5 fps','10 fps','15 fps','20 fps','25 fps','30 fps',
                                         '40 fps','50 fps','60 fps','75 fps','80 fps','90 fps','100 fps');

  PA_BAND_NAMES: array[0..10] of string =
    ('160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m');

{ TSettingsForm }

constructor TSettingsForm.Create(AOwner: TComponent);
begin
  inherited CreateNew(AOwner);
  Caption      := 'Settings';
  Width        := 900;
  Height       := 660;
  Position     := poScreenCenter;
  BorderStyle  := bsSizeable;
  Color        := CLR_BG;
  Font.Name    := UI_FONT;
  Font.Size    := 9;
  Font.Color   := CLR_TEXT;
  FLoading     := False;
  FRXDevCount  := 0;
  BuildUI;
  ApplyTheme;
end;

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

function TSettingsForm.MakeLbl(AParent: TWinControl; const Cap: string;
  ALeft, ATop, AW: Integer): TLabel;
begin
  Result           := TLabel.Create(Self);
  Result.Parent    := AParent;
  Result.Caption   := Cap;
  Result.Left      := ALeft;
  Result.Top       := ATop;
  if AW > 0 then Result.Width := AW;
  Result.Font.Color := CLR_TEXTDIM;
  Result.Font.Name  := UI_FONT;
  Result.Font.Size  := 9;
end;

function TSettingsForm.MakeCombo(AParent: TWinControl; ALeft, ATop, AW: Integer;
  OnChange: TNotifyEvent): TComboBox;
begin
  Result          := TComboBox.Create(Self);
  Result.Parent   := AParent;
  Result.Left     := ALeft;
  Result.Top      := ATop;
  Result.Width    := AW;
  Result.Height   := BTN_H;
  Result.Style    := csDropDownList;
  Result.Color    := CLR_INPUT;
  Result.Font.Color := CLR_INPUT_TEXT;
  Result.Font.Name  := UI_FONT;
  Result.Font.Size  := 9;
  Result.OnChange := OnChange;
end;

// Создаёт панель-группу с заголовком и разделителем (вместо TGroupBox).
// На Windows TGroupBox рисует фрейм системными цветами, TPanel — нет.
function TSettingsForm.MakeGroupPanel(AParent: TWinControl; const Cap: string;
  ALeft, ATop, AW, AH: Integer): TPanel;
var
  TitleLbl: TLabel;
  Divider:  TPanel;
begin
  Result := TPanel.Create(Self);
  Result.Parent := AParent;
  Result.SetBounds(ALeft, ATop, AW, AH);
  Result.BevelOuter := bvNone;
  Result.Color := CLR_PANEL;

  TitleLbl := TLabel.Create(Self);
  TitleLbl.Parent := Result;
  TitleLbl.Caption := Cap;
  TitleLbl.SetBounds(8, 5, AW - 16, 14);
  TitleLbl.Font.Color := CLR_ACCENT;
  TitleLbl.Font.Name  := UI_FONT;
  TitleLbl.Font.Size  := 9;
  TitleLbl.Font.Style := [fsBold];

  Divider := TPanel.Create(Self);
  Divider.Parent := Result;
  Divider.SetBounds(0, GRP_TITLE_H, AW + 200, 1);
  Divider.BevelOuter := bvNone;
  Divider.Color := CLR_BORDER;
end;

function TSettingsForm.MakeScrollPage: TScrollBox;
begin
  Result := TScrollBox.Create(Self);
  Result.Parent      := FContentPanel;
  Result.Align       := alClient;
  Result.BorderStyle := bsNone;
  Result.Color       := CLR_BG;
  Result.AutoScroll  := True;
  Result.Visible     := False;
end;

function TSettingsForm.MakeNavButton(const Cap: string; ATop: Integer): TFlatButton;
begin
  Result := TFlatButton.Create(Self);
  Result.Parent      := FNavPanel;
  Result.Caption     := Cap;
  Result.SetBounds(10, ATop, 166, 32);
  Result.Anchors     := [akLeft, akTop, akRight];
  Result.OnClick     := NavClick;
  Result.ClrNorm     := CLR_PANEL;
  Result.ClrBorder   := CLR_PANEL;
  Result.ClrHot      := TColor($00242424);
  Result.ClrActive   := TColor($00204030);
  Result.ClrText     := CLR_TEXT;
  Result.ClrTextAct  := CLR_ACCENT;
  Result.Font.Name   := UI_FONT;
  Result.Font.Size   := 9;
  Result.Font.Style  := [];
end;

// ---------------------------------------------------------------------------
// BuildUI
// ---------------------------------------------------------------------------

procedure TSettingsForm.BuildUI;
begin
  FNavPanel := TPanel.Create(Self);
  FNavPanel.Parent := Self;
  FNavPanel.Align := alLeft;
  FNavPanel.Width := 188;
  FNavPanel.BevelOuter := bvNone;
  FNavPanel.Color := TColor($00151515);

  with TLabel.Create(Self) do
  begin
    Parent := FNavPanel;
    Caption := 'Settings';
    SetBounds(14, 14, 160, 24);
    Font.Name := UI_FONT;
    Font.Size := 14;
    Font.Style := [fsBold];
    Font.Color := CLR_TEXT;
  end;

  with TLabel.Create(Self) do
  begin
    Parent := FNavPanel;
    Caption := 'EWSDR';
    SetBounds(14, 39, 160, 18);
    Font.Name := UI_FONT;
    Font.Size := 9;
    Font.Color := CLR_TEXTDIM;
  end;

  FNavAudio     := MakeNavButton('Audio', 74);
  FNavDisplay   := MakeNavButton('Display', 110);
  FNavSpectrum  := MakeNavButton('Spectrum', 146);
  FNavWaterfall := MakeNavButton('Waterfall', 182);
  FNavPA        := MakeNavButton('Power Amplifier', 218);
  FNavAdvanced  := MakeNavButton('Advanced', 254);
  FNavCAT       := MakeNavButton('CAT', 290);

  FContentPanel := TPanel.Create(Self);
  FContentPanel.Parent := Self;
  FContentPanel.Align := alClient;
  FContentPanel.BevelOuter := bvNone;
  FContentPanel.Color := CLR_BG;

  FPageAudio     := MakeScrollPage;
  FPageDisplay   := MakeScrollPage;
  FPageSpectrum  := MakeScrollPage;
  FPageWaterfall := MakeScrollPage;
  FPagePA        := MakeScrollPage;
  FPageAdvanced  := MakeScrollPage;
  FPageCAT       := MakeScrollPage;

  BuildAudioTab;
  BuildDisplayTab;
  BuildRX1Tab;
  BuildPATab;
  BuildCATTab;

  FBtnClose          := TFlatButton.Create(Self);
  FBtnClose.Parent   := Self;
  FBtnClose.Caption  := 'Close';
  FBtnClose.SetBounds(Width - 106, Height - 40, 92, 28);
  FBtnClose.Anchors  := [akRight, akBottom];
  FBtnClose.OnClick  := BtnCloseClick;
  FBtnClose.ClrNorm   := CLR_PANEL;
  FBtnClose.ClrBorder := TColor($00404040);
  FBtnClose.ClrHot    := TColor($00303030);
  FBtnClose.ClrActive := TColor($00003300);
  FBtnClose.ClrText   := CLR_TEXT;
  FBtnClose.ClrTextAct:= CLR_ACCENT;
  FBtnClose.Font.Name := UI_FONT;
  FBtnClose.Font.Size := 9;

  SelectPage(FPageAudio, FNavAudio);
end;

procedure TSettingsForm.NavClick(Sender: TObject);
begin
  if Sender = FNavAudio then SelectPage(FPageAudio, FNavAudio)
  else if Sender = FNavDisplay then SelectPage(FPageDisplay, FNavDisplay)
  else if Sender = FNavSpectrum then SelectPage(FPageSpectrum, FNavSpectrum)
  else if Sender = FNavWaterfall then SelectPage(FPageWaterfall, FNavWaterfall)
  else if Sender = FNavPA then SelectPage(FPagePA, FNavPA)
  else if Sender = FNavAdvanced then SelectPage(FPageAdvanced, FNavAdvanced)
  else if Sender = FNavCAT then SelectPage(FPageCAT, FNavCAT);
end;

procedure TSettingsForm.SelectPage(APage: TWinControl; ANav: TFlatButton);
begin
  FPageAudio.Visible := APage = FPageAudio;
  FPageDisplay.Visible := APage = FPageDisplay;
  FPageSpectrum.Visible := APage = FPageSpectrum;
  FPageWaterfall.Visible := APage = FPageWaterfall;
  FPagePA.Visible := APage = FPagePA;
  FPageAdvanced.Visible := APage = FPageAdvanced;
  FPageCAT.Visible := APage = FPageCAT;

  FNavAudio.Active := ANav = FNavAudio;
  FNavDisplay.Active := ANav = FNavDisplay;
  FNavSpectrum.Active := ANav = FNavSpectrum;
  FNavWaterfall.Active := ANav = FNavWaterfall;
  FNavPA.Active := ANav = FNavPA;
  FNavAdvanced.Active := ANav = FNavAdvanced;
  FNavCAT.Active := ANav = FNavCAT;
end;

// ---------------------------------------------------------------------------
// BuildAudioTab
// ---------------------------------------------------------------------------

procedure TSettingsForm.BuildAudioTab;
const
  MARGIN = 22;
  PAD    = 18;
  R1     = 42;
  STEP   = 42;
  LW     = 150;
  CX     = PAD + LW + 12;
  CMB_W  = 460;

var
  Grp: TPanel;
begin
  with TLabel.Create(Self) do
  begin
    Parent := FPageAudio;
    Caption := 'Audio';
    SetBounds(MARGIN, 18, 400, 28);
    Font.Name := UI_FONT;
    Font.Size := 16;
    Font.Style := [fsBold];
    Font.Color := CLR_TEXT;
  end;

  with TLabel.Create(Self) do
  begin
    Parent := FPageAudio;
    Caption := 'Input and output devices used by the receiver and transmitter.';
    SetBounds(MARGIN, 48, 560, 20);
    Font.Name := UI_FONT;
    Font.Size := 9;
    Font.Color := CLR_TEXTDIM;
  end;

  Grp := MakeGroupPanel(FPageAudio, 'Audio Devices', MARGIN, 86, 650, 142);

  MakeLbl(Grp, 'TX input device', PAD, R1 + 5, LW);
  FCmbTXDev := MakeCombo(Grp, CX, R1, CMB_W, OnTXDevChange);
  FCmbTXDev.Items.Add('None');

  MakeLbl(Grp, 'RX output device', PAD, R1 + STEP + 5, LW);
  FCmbRXDev := MakeCombo(Grp, CX, R1 + STEP, CMB_W, OnRXDevChange);
  FCmbRXDev.Items.Add('Default');
end;

// ---------------------------------------------------------------------------
// BuildDisplayTab
// ---------------------------------------------------------------------------

procedure TSettingsForm.BuildDisplayTab;
const
  MARGIN = 22;
  PAD    = 18;
  R1     = 42;
  CHKW   = 220;
  LW     = 150;
  CX     = PAD + LW + 12;

var
  i:   Integer;
  Grp: TPanel;
begin
  with TLabel.Create(Self) do
  begin
    Parent := FPageDisplay;
    Caption := 'Display';
    SetBounds(MARGIN, 18, 400, 28);
    Font.Name := UI_FONT;
    Font.Size := 16;
    Font.Style := [fsBold];
    Font.Color := CLR_TEXT;
  end;

  with TLabel.Create(Self) do
  begin
    Parent := FPageDisplay;
    Caption := 'General visual options. Spectrum and waterfall processing are split into their own pages.';
    SetBounds(MARGIN, 48, 680, 20);
    Font.Name := UI_FONT;
    Font.Size := 9;
    Font.Color := CLR_TEXTDIM;
  end;

  Grp := MakeGroupPanel(FPageDisplay, 'Visible Panes', MARGIN, 86, 650, 86);

  FChkShowSpectrum := TCheckBox.Create(Self);
  FChkShowSpectrum.Parent := Grp;
  FChkShowSpectrum.Caption := 'Spectrum';
  FChkShowSpectrum.SetBounds(PAD, R1, CHKW, 22);
  FChkShowSpectrum.Checked := True;
  FChkShowSpectrum.Font.Color := CLR_TEXT;
  FChkShowSpectrum.Font.Name := UI_FONT;
  FChkShowSpectrum.Font.Size := 9;
  FChkShowSpectrum.OnChange := OnVisibilityChkChange;

  FChkShowWaterfall := TCheckBox.Create(Self);
  FChkShowWaterfall.Parent := Grp;
  FChkShowWaterfall.Caption := 'Waterfall';
  FChkShowWaterfall.SetBounds(PAD + CHKW + 20, R1, CHKW, 22);
  FChkShowWaterfall.Checked := True;
  FChkShowWaterfall.Font.Color := CLR_TEXT;
  FChkShowWaterfall.Font.Name := UI_FONT;
  FChkShowWaterfall.Font.Size := 9;
  FChkShowWaterfall.OnChange := OnVisibilityChkChange;

  Grp := MakeGroupPanel(FPageDisplay, 'Performance', MARGIN, 186, 650, 92);

  MakeLbl(Grp, 'Refresh rate', PAD, R1 + 5, LW);
  FCmbFPS := MakeCombo(Grp, CX, R1, 140, OnFPSCmbChange);
  for i := 0 to High(FPS_NAMES) do
    FCmbFPS.Items.Add(FPS_NAMES[i]);
  FCmbFPS.ItemIndex := 8;
end;

// ---------------------------------------------------------------------------
// BuildRX1Tab
// ---------------------------------------------------------------------------

procedure TSettingsForm.BuildRX1Tab;
const
  MARGIN = 22;
  PAD    = 18;
  LW     = 150;
  CX     = PAD + LW + 12;
  CMB_W  = 230;
  CMB_W_LONG = 300;
  SPW    = 96;
  EDT_W  = 96;
  STEP   = 42;
  R1     = 42;

var
  Grp:    TPanel;
  i:      Integer;

  function MkFloat(AX, AY, AW: Integer; AValue, AMin, AMax, AInc: Double;
                   AOnChange: TNotifyEvent): TFloatSpinEdit;
  begin
    Result := TFloatSpinEdit.Create(Self);
    Result.Parent := Grp;
    Result.SetBounds(AX, AY, AW, BTN_H);
    Result.Value := AValue; Result.MinValue := AMin;
    Result.MaxValue := AMax; Result.Increment := AInc;
    Result.DecimalPlaces := 1;
    Result.Color := CLR_INPUT; Result.Font.Color := CLR_INPUT_TEXT;
    Result.Font.Name := UI_FONT; Result.Font.Size := 9;
    Result.OnChange := AOnChange;
  end;

  function MkEdit(AX, AY, AW: Integer; const AText: string;
                  AOnExit: TNotifyEvent): TEdit;
  begin
    Result := TEdit.Create(Self);
    Result.Parent := Grp;
    Result.SetBounds(AX, AY, AW, BTN_H);
    Result.Color := CLR_INPUT; Result.Font.Color := CLR_INPUT_TEXT;
    Result.Font.Name := UI_FONT; Result.Font.Size := 9;
    Result.Text := AText; Result.OnExit := AOnExit;
  end;

begin
  with TLabel.Create(Self) do
  begin
    Parent := FPageSpectrum;
    Caption := 'Spectrum';
    SetBounds(MARGIN, 18, 400, 28);
    Font.Name := UI_FONT;
    Font.Size := 16;
    Font.Style := [fsBold];
    Font.Color := CLR_TEXT;
  end;

  with TLabel.Create(Self) do
  begin
    Parent := FPageSpectrum;
    Caption := 'Analyzer, detector, averaging and grid parameters for RX1.';
    SetBounds(MARGIN, 48, 620, 20);
    Font.Name := UI_FONT;
    Font.Size := 9;
    Font.Color := CLR_TEXTDIM;
  end;

  Grp := MakeGroupPanel(FPageSpectrum, 'Analyzer', MARGIN, 86, 650, 128);
  MakeLbl(Grp, 'FFT Size:', PAD, R1 + 5, LW);
  FCmbFFTSize := MakeCombo(Grp, CX, R1, CMB_W, OnFFTSizeChange);
  for i := 0 to High(FFT_NAMES) do FCmbFFTSize.Items.Add(FFT_NAMES[i]);
  MakeLbl(Grp, 'Window:', PAD, R1 + STEP + 5, LW);
  FCmbWindow := MakeCombo(Grp, CX, R1 + STEP, CMB_W_LONG, OnWindowChange);
  for i := 0 to High(WINDOW_NAMES) do FCmbWindow.Items.Add(WINDOW_NAMES[i]);

  Grp := MakeGroupPanel(FPageSpectrum, 'Detection && Averaging', MARGIN, 230, 650, 170);
  MakeLbl(Grp, 'Detector:', PAD, R1 + 5, LW);
  FCmbSpecDet := MakeCombo(Grp, CX, R1, CMB_W, OnSpecDetChange);
  for i := 0 to High(DETECTOR_NAMES) do FCmbSpecDet.Items.Add(DETECTOR_NAMES[i]);
  MakeLbl(Grp, 'Avg Mode:', PAD, R1 + STEP + 5, LW);
  FCmbSpecAvg := MakeCombo(Grp, CX, R1 + STEP, CMB_W, OnSpecAvgModeChange);
  for i := 0 to High(AVGMODE_NAMES) do FCmbSpecAvg.Items.Add(AVGMODE_NAMES[i]);
  MakeLbl(Grp, 'Avg Time (ms):', PAD, R1 + 2 * STEP + 5, LW);
  FSpecAvgTime := MkFloat(CX, R1 + 2 * STEP, SPW, 30.0, 0.0, 5000.0, 10.0, OnSpecAvgTimeChange);

  Grp := MakeGroupPanel(FPageSpectrum, 'Grid', MARGIN, 416, 650, 170);
  MakeLbl(Grp, 'Reference level:', PAD, R1 + 5, LW);
  FEdRefLevel := MkEdit(CX, R1, EDT_W, '-20', OnRefLevelChange);
  MakeLbl(Grp, 'Range (dB):', PAD, R1 + STEP + 5, LW);
  FEdRange := MkEdit(CX, R1 + STEP, EDT_W, '110', OnRangeChange);
  MakeLbl(Grp, 'Grid step:', PAD, R1 + 2 * STEP + 5, LW);
  FCmbGridStep := MakeCombo(Grp, CX, R1 + 2 * STEP, 130, OnGridStepChange);
  for i := 0 to High(GRID_STEP_NAMES) do FCmbGridStep.Items.Add(GRID_STEP_NAMES[i]);
  FCmbGridStep.ItemIndex := 1;

  with TLabel.Create(Self) do
  begin
    Parent := FPageWaterfall;
    Caption := 'Waterfall';
    SetBounds(MARGIN, 18, 400, 28);
    Font.Name := UI_FONT;
    Font.Size := 16;
    Font.Style := [fsBold];
    Font.Color := CLR_TEXT;
  end;

  with TLabel.Create(Self) do
  begin
    Parent := FPageWaterfall;
    Caption := 'Waterfall detector, smoothing and color scale thresholds.';
    SetBounds(MARGIN, 48, 620, 20);
    Font.Name := UI_FONT;
    Font.Size := 9;
    Font.Color := CLR_TEXTDIM;
  end;

  Grp := MakeGroupPanel(FPageWaterfall, 'Detection && Averaging', MARGIN, 86, 650, 170);
  MakeLbl(Grp, 'Detector:', PAD, R1 + 5, LW);
  FCmbWfDet := MakeCombo(Grp, CX, R1, CMB_W, OnWfDetChange);
  for i := 0 to High(DETECTOR_NAMES) do FCmbWfDet.Items.Add(DETECTOR_NAMES[i]);
  MakeLbl(Grp, 'Avg Mode:', PAD, R1 + STEP + 5, LW);
  FCmbWfAvg := MakeCombo(Grp, CX, R1 + STEP, CMB_W, OnWfAvgModeChange);
  for i := 0 to High(WF_AVGMODE_NAMES) do FCmbWfAvg.Items.Add(WF_AVGMODE_NAMES[i]);
  MakeLbl(Grp, 'Avg Time (ms):', PAD, R1 + 2 * STEP + 5, LW);
  FWfAvgTime := MkFloat(CX, R1 + 2 * STEP, SPW, 120.0, 0.0, 5000.0, 10.0, OnWfAvgTimeChange);

  Grp := MakeGroupPanel(FPageWaterfall, 'Color Scale', MARGIN, 272, 650, 170);
  MakeLbl(Grp, 'High (dBm):', PAD, R1 + 5, LW);
  FWfHigh := MkFloat(CX, R1, SPW, -80.0, -160.0, 20.0, 1.0, OnWfThresholdChange);
  MakeLbl(Grp, 'Low (dBm):', PAD, R1 + STEP + 5, LW);
  FWfLow := MkFloat(CX, R1 + STEP, SPW, -130.0, -180.0, 0.0, 1.0, OnWfThresholdChange);
  MakeLbl(Grp, 'AGC Offset (dB):', PAD, R1 + 2 * STEP + 5, LW);
  FWfAGCOffset := MkFloat(CX, R1 + 2 * STEP, SPW, 0.0, -40.0, 40.0, 0.5, OnWfThresholdChange);
end;

// ---------------------------------------------------------------------------
// BuildPATab
// ---------------------------------------------------------------------------

procedure TSettingsForm.BuildPATab;
const
  MARGIN  = 22;
  LBL_W   = 46;
  ED_W    = 72;
  COL_W   = LBL_W + ED_W + 8;
  COLS    = 4;
  ROW_H   = 32;
  GRP_PAD = 18;
  R1      = 42;
  GRP_H   = 222;

  function MakeFloatSpin(AParent: TWinControl; ALeft, ATop: Integer;
                         AChange: TNotifyEvent): TFloatSpinEdit;
  var E: TFloatSpinEdit;
  begin
    E               := TFloatSpinEdit.Create(Self);
    E.Parent        := AParent;
    E.SetBounds(ALeft, ATop, ED_W, BTN_H + 2);
    E.Color         := CLR_INPUT;
    E.Font.Color    := CLR_INPUT_TEXT;
    E.Font.Name     := UI_FONT;
    E.Font.Size     := 9;
    E.MinValue      := 38.8;
    E.MaxValue      := 100.0;
    E.Increment     := 0.1;
    E.DecimalPlaces := 1;
    E.Value         := 100.0;
    E.OnChange      := AChange;
    Result          := E;
  end;

var
  Grp:      TPanel;
  Lbl:      TLabel;
  Spin:     TSpinEdit;
  i, r, c, X, Y: Integer;
begin
  with TLabel.Create(Self) do
  begin
    Parent := FPagePA;
    Caption := 'Power Amplifier';
    SetBounds(MARGIN, 18, 400, 28);
    Font.Name := UI_FONT;
    Font.Size := 16;
    Font.Style := [fsBold];
    Font.Color := CLR_TEXT;
  end;

  with TLabel.Create(Self) do
  begin
    Parent := FPagePA;
    Caption := 'Output limit and per-band calibration values.';
    SetBounds(MARGIN, 48, 620, 20);
    Font.Name := UI_FONT;
    Font.Size := 9;
    Font.Color := CLR_TEXTDIM;
  end;

  Grp := MakeGroupPanel(FPagePA, 'Power && Calibration', MARGIN, 86, 650, GRP_H);

  // Max Power
  Lbl            := TLabel.Create(Self);
  Lbl.Parent     := Grp;
  Lbl.Caption    := 'Max power (W)';
  Lbl.SetBounds(GRP_PAD, R1 + 6, 132, 18);
  Lbl.Font.Color := CLR_TEXTDIM;
  Lbl.Font.Name  := UI_FONT;
  Lbl.Font.Size  := 9;

  Spin              := TSpinEdit.Create(Self);
  Spin.Parent       := Grp;
  Spin.SetBounds(GRP_PAD + 150, R1, ED_W, BTN_H + 2);
  Spin.Color        := CLR_INPUT;
  Spin.Font.Color   := CLR_INPUT_TEXT;
  Spin.Font.Name    := UI_FONT;
  Spin.Font.Size    := 9;
  Spin.MinValue     := 5;
  Spin.MaxValue     := 200;
  Spin.Increment    := 1;
  Spin.Value        := 100;
  Spin.OnChange     := OnMaxPowerChange;
  FEdMaxPower       := Spin;

  // Calibration label
  Lbl            := TLabel.Create(Self);
  Lbl.Parent     := Grp;
  Lbl.Caption    := 'Calibration per band, 38.8 ... 100.0';
  Lbl.SetBounds(GRP_PAD, R1 + ROW_H + 12, 320, 18);
  Lbl.Font.Color := CLR_TEXTDIM;
  Lbl.Font.Name  := UI_FONT;
  Lbl.Font.Size  := 9;

  // Band grid 4 columns × 3 rows
  for i := 0 to 10 do
  begin
    r := i div COLS;
    c := i mod COLS;
    X := GRP_PAD + c * (COL_W + 4);
    Y := R1 + ROW_H + 34 + r * ROW_H;

    Lbl            := TLabel.Create(Self);
    Lbl.Parent     := Grp;
    Lbl.Caption    := PA_BAND_NAMES[i] + ':';
    Lbl.SetBounds(X, Y + 5, LBL_W, 18);
    Lbl.Font.Color := CLR_TEXTDIM;
    Lbl.Font.Name  := UI_FONT;
    Lbl.Font.Size  := 9;

    FEdBandCal[i]     := MakeFloatSpin(Grp, X + LBL_W, Y, OnBandCalChange);
    FEdBandCal[i].Tag := i;
  end;

  with TLabel.Create(Self) do
  begin
    Parent := FPageAdvanced;
    Caption := 'Advanced';
    SetBounds(MARGIN, 18, 400, 28);
    Font.Name := UI_FONT;
    Font.Size := 16;
    Font.Style := [fsBold];
    Font.Color := CLR_TEXT;
  end;

  with TLabel.Create(Self) do
  begin
    Parent := FPageAdvanced;
    Caption := 'Transmit, CAT and DSP-only settings will appear here when they are wired to runtime settings.';
    SetBounds(MARGIN, 48, 680, 20);
    Font.Name := UI_FONT;
    Font.Size := 9;
    Font.Color := CLR_TEXTDIM;
  end;
end;

procedure TSettingsForm.ApplyTheme;
begin
  Color := CLR_BG;
  if Assigned(FNavPanel) then FNavPanel.Color := TColor($00151515);
  if Assigned(FContentPanel) then FContentPanel.Color := CLR_BG;
  if Assigned(FPageAudio) then FPageAudio.Color := CLR_BG;
  if Assigned(FPageDisplay) then FPageDisplay.Color := CLR_BG;
  if Assigned(FPageSpectrum) then FPageSpectrum.Color := CLR_BG;
  if Assigned(FPageWaterfall) then FPageWaterfall.Color := CLR_BG;
  if Assigned(FPagePA) then FPagePA.Color := CLR_BG;
  if Assigned(FPageAdvanced) then FPageAdvanced.Color := CLR_BG;
  if Assigned(FPageCAT) then FPageCAT.Color := CLR_BG;
end;

// ---------------------------------------------------------------------------
// LoadValues
// ---------------------------------------------------------------------------

procedure TSettingsForm.LoadValues(
  FFTSize, WinType, SpecDet, SpecAvgMode: Integer;
  SpecAvgTimeMS: Double;
  WfDet, WfAvgMode: Integer;
  WfAvgTimeMS, WfHigh, WfLow, WfAGCOffset: Double;
  RefLevel, Range, GridStep: Double;
  const AudioOutDevName: string;
  const AudioInDevName: string);
var
  i, Idx: Integer;
begin
  FLoading := True;
  try
    Idx := 5;
    for i := 0 to High(FFT_SIZES) do
      if FFT_SIZES[i] = FFTSize then begin Idx := i; Break; end;
    FCmbFFTSize.ItemIndex := Idx;

    if (WinType >= 0) and (WinType < FCmbWindow.Items.Count) then
      FCmbWindow.ItemIndex := WinType
    else
      FCmbWindow.ItemIndex := 2;

    if (SpecDet >= 0) and (SpecDet < FCmbSpecDet.Items.Count) then
      FCmbSpecDet.ItemIndex := SpecDet
    else
      FCmbSpecDet.ItemIndex := 0;

    Idx := 3;
    for i := 0 to High(AVGMODE_VALUES) do
      if AVGMODE_VALUES[i] = SpecAvgMode then begin Idx := i; Break; end;
    FCmbSpecAvg.ItemIndex := Idx;
    FSpecAvgTime.Value := EnsureRange(SpecAvgTimeMS, 0.0, 5000.0);

    if (WfDet >= 0) and (WfDet < FCmbWfDet.Items.Count) then
      FCmbWfDet.ItemIndex := WfDet
    else
      FCmbWfDet.ItemIndex := 0;

    Idx := 3;
    for i := 0 to High(WF_AVGMODE_VALUES) do
      if WF_AVGMODE_VALUES[i] = WfAvgMode then begin Idx := i; Break; end;
    FCmbWfAvg.ItemIndex := Idx;
    FWfAvgTime.Value   := EnsureRange(WfAvgTimeMS, 0.0, 5000.0);
    FWfHigh.Value      := EnsureRange(WfHigh, -160.0, 20.0);
    FWfLow.Value       := EnsureRange(WfLow, -180.0, 0.0);
    FWfAGCOffset.Value := EnsureRange(WfAGCOffset, -40.0, 40.0);

    FEdRefLevel.Text := FormatFloat('0.#', RefLevel);
    FEdRange.Text    := FormatFloat('0.#', Range);
    Idx := 1;
    for i := 0 to High(GRID_STEPS) do
      if Abs(GRID_STEPS[i] - GridStep) < 0.5 then begin Idx := i; Break; end;
    FCmbGridStep.ItemIndex := Idx;

    if AudioOutDevName = '' then
      FCmbRXDev.ItemIndex := 0
    else
    begin
      Idx := FCmbRXDev.Items.IndexOf(AudioOutDevName);
      if Idx >= 0 then FCmbRXDev.ItemIndex := Idx
      else FCmbRXDev.ItemIndex := 0;
    end;

    if AudioInDevName = '' then
      FCmbTXDev.ItemIndex := 0
    else
    begin
      Idx := FCmbTXDev.Items.IndexOf(AudioInDevName);
      if Idx >= 0 then FCmbTXDev.ItemIndex := Idx
      else FCmbTXDev.ItemIndex := 0;
    end;
  finally
    FLoading := False;
  end;
end;

// ---------------------------------------------------------------------------
// RefreshAudioDevices
// ---------------------------------------------------------------------------

procedure TSettingsForm.RefreshAudioDevices(AudioOut: TAudioOutput; AudioIn: TAudioInput);
var
  Names:    TStringList;
  Cnt, i:   Integer;
  OldRXSel: string;
  OldTXSel: string;
begin
  Names := TStringList.Create;
  try
    if AudioOut <> nil then
    begin
      OldRXSel := FCmbRXDev.Text;
      FCmbRXDev.Items.Clear;
      FCmbRXDev.Items.Add('Default');
      FRXDevCount := 0;

      Cnt := 0;
      Names.Clear;
      if AudioOut.EnumOutputDevices(Names, FRXDevIndices, Cnt) then
      begin
        FRXDevCount := Cnt;
        for i := 0 to Cnt - 1 do
          FCmbRXDev.Items.Add(Names[i]);
      end;
      if (FRXDevCount = 0) and (AudioOut.LastError <> '') then
        FCmbRXDev.Items.Add('PortAudio: ' + AudioOut.LastError);

      i := FCmbRXDev.Items.IndexOf(OldRXSel);
      if i >= 0 then FCmbRXDev.ItemIndex := i
      else FCmbRXDev.ItemIndex := 0;
    end;

    if AudioIn <> nil then
    begin
      OldTXSel := FCmbTXDev.Text;
      FCmbTXDev.Items.Clear;
      FCmbTXDev.Items.Add('None');
      FTXDevCount := 0;

      Cnt := 0;
      Names.Clear;
      if AudioIn.EnumInputDevices(Names, FTXDevIndices, Cnt) then
      begin
        FTXDevCount := Cnt;
        for i := 0 to Cnt - 1 do
          FCmbTXDev.Items.Add(Names[i]);
      end;
      if (FTXDevCount = 0) and (AudioIn.LastError <> '') then
        FCmbTXDev.Items.Add('PortAudio: ' + AudioIn.LastError);

      i := FCmbTXDev.Items.IndexOf(OldTXSel);
      if i >= 0 then FCmbTXDev.ItemIndex := i
      else FCmbTXDev.ItemIndex := 0;
    end;
  finally
    Names.Free;
  end;
end;

// ---------------------------------------------------------------------------
// Value accessors
// ---------------------------------------------------------------------------

function TSettingsForm.GetFFTSize: Integer;
var Idx: Integer;
begin
  Idx := FCmbFFTSize.ItemIndex;
  if (Idx >= 0) and (Idx <= High(FFT_SIZES)) then
    Result := FFT_SIZES[Idx]
  else
    Result := 4096;
end;

function TSettingsForm.GetSpecAvgTimeMS: Double;
begin
  Result := EnsureRange(FSpecAvgTime.Value, 0.0, 5000.0);
end;

function TSettingsForm.GetWfAvgTimeMS: Double;
begin
  Result := EnsureRange(FWfAvgTime.Value, 0.0, 5000.0);
end;

function TSettingsForm.GetWfHigh: Double;
begin
  Result := EnsureRange(FWfHigh.Value, -160.0, 20.0);
end;

function TSettingsForm.GetWfLow: Double;
begin
  Result := EnsureRange(FWfLow.Value, -180.0, 0.0);
end;

function TSettingsForm.GetWfAGCOffset: Double;
begin
  Result := EnsureRange(FWfAGCOffset.Value, -40.0, 40.0);
end;

function TSettingsForm.GetRefLevel: Double;
var V: Double; Err: Integer;
begin
  Val(FEdRefLevel.Text, V, Err);
  if Err <> 0 then V := -20.0;
  Result := EnsureRange(V, -200.0, 0.0);
end;

function TSettingsForm.GetRange: Double;
var V: Double; Err: Integer;
begin
  Val(FEdRange.Text, V, Err);
  if Err <> 0 then V := 110.0;
  Result := EnsureRange(V, 10.0, 200.0);
end;

function TSettingsForm.GetGridStep: Double;
var Idx: Integer;
begin
  Idx := FCmbGridStep.ItemIndex;
  if (Idx >= 0) and (Idx <= High(GRID_STEPS)) then
    Result := GRID_STEPS[Idx]
  else
    Result := 10.0;
end;

// ---------------------------------------------------------------------------
// Fire helpers
// ---------------------------------------------------------------------------

procedure TSettingsForm.FireDisplayChange;
var
  AvgIdx: Integer;
  AvgVal: Integer;
begin
  if FLoading then Exit;
  if not Assigned(FOnDisplayChange) then Exit;
  AvgIdx := FCmbSpecAvg.ItemIndex;
  if (AvgIdx >= 0) and (AvgIdx <= High(AVGMODE_VALUES)) then
    AvgVal := AVGMODE_VALUES[AvgIdx]
  else
    AvgVal := 3;
  FOnDisplayChange(
    GetFFTSize,
    FCmbWindow.ItemIndex,
    FCmbSpecDet.ItemIndex,
    AvgVal,
    GetSpecAvgTimeMS);
end;

procedure TSettingsForm.FireWaterfallChange;
var
  AvgIdx: Integer;
  AvgVal: Integer;
begin
  if FLoading then Exit;
  if not Assigned(FOnWaterfallChange) then Exit;
  AvgIdx := FCmbWfAvg.ItemIndex;
  if (AvgIdx >= 0) and (AvgIdx <= High(WF_AVGMODE_VALUES)) then
    AvgVal := WF_AVGMODE_VALUES[AvgIdx]
  else
    AvgVal := 1;
  FOnWaterfallChange(
    FCmbWfDet.ItemIndex,
    AvgVal,
    GetWfAvgTimeMS,
    GetWfHigh,
    GetWfLow,
    GetWfAGCOffset);
end;

procedure TSettingsForm.FireGridChange;
begin
  if FLoading then Exit;
  if not Assigned(FOnGridChange) then Exit;
  FOnGridChange(GetRefLevel, GetRange, GetGridStep);
end;

// ---------------------------------------------------------------------------
// Event handlers
// ---------------------------------------------------------------------------

procedure TSettingsForm.OnFFTSizeChange(Sender: TObject);
begin FireDisplayChange; end;

procedure TSettingsForm.OnWindowChange(Sender: TObject);
begin FireDisplayChange; end;

procedure TSettingsForm.OnSpecDetChange(Sender: TObject);
begin FireDisplayChange; end;

procedure TSettingsForm.OnSpecAvgModeChange(Sender: TObject);
begin FireDisplayChange; end;

procedure TSettingsForm.OnSpecAvgTimeChange(Sender: TObject);
begin FireDisplayChange; end;

procedure TSettingsForm.OnWfDetChange(Sender: TObject);
begin FireWaterfallChange; end;

procedure TSettingsForm.OnWfAvgModeChange(Sender: TObject);
begin FireWaterfallChange; end;

procedure TSettingsForm.OnWfAvgTimeChange(Sender: TObject);
begin FireWaterfallChange; end;

procedure TSettingsForm.OnWfThresholdChange(Sender: TObject);
begin FireWaterfallChange; end;

procedure TSettingsForm.OnRefLevelChange(Sender: TObject);
begin FireGridChange; end;

procedure TSettingsForm.OnRangeChange(Sender: TObject);
begin FireGridChange; end;

procedure TSettingsForm.OnGridStepChange(Sender: TObject);
begin FireGridChange; end;

procedure TSettingsForm.OnRXDevChange(Sender: TObject);
var
  Idx:     Integer;
  DevIdx:  Integer;
  DevName: string;
begin
  if FLoading then Exit;
  if not Assigned(FOnAudioDevChange) then Exit;
  Idx := FCmbRXDev.ItemIndex;
  if Idx <= 0 then
    FOnAudioDevChange(-1, '')
  else
  begin
    DevName := FCmbRXDev.Items[Idx];
    DevIdx := Idx - 1;
    if (DevIdx >= 0) and (DevIdx < FRXDevCount) then
      FOnAudioDevChange(FRXDevIndices[DevIdx], DevName)
    else
      FOnAudioDevChange(-1, '');
  end;
end;

procedure TSettingsForm.OnTXDevChange(Sender: TObject);
var
  Idx:     Integer;
  DevIdx:  Integer;
  DevName: string;
begin
  if FLoading then Exit;
  if not Assigned(FOnAudioInDevChange) then Exit;
  Idx := FCmbTXDev.ItemIndex;
  if Idx <= 0 then
    FOnAudioInDevChange(-1, '')
  else
  begin
    DevName := FCmbTXDev.Items[Idx];
    DevIdx := Idx - 1;
    if (DevIdx >= 0) and (DevIdx < FTXDevCount) then
      FOnAudioInDevChange(FTXDevIndices[DevIdx], DevName)
    else
      FOnAudioInDevChange(-1, '');
  end;
end;

procedure TSettingsForm.LoadVisibility(ShowSpectrum, ShowWaterfall: Boolean);
begin
  FLoading := True;
  try
    FChkShowSpectrum.Checked  := ShowSpectrum;
    FChkShowWaterfall.Checked := ShowWaterfall;
  finally
    FLoading := False;
  end;
end;

procedure TSettingsForm.OnVisibilityChkChange(Sender: TObject);
begin
  if FLoading then Exit;
  if not Assigned(FOnVisibilityChange) then Exit;
  FOnVisibilityChange(FChkShowSpectrum.Checked, FChkShowWaterfall.Checked);
end;

procedure TSettingsForm.OnFPSCmbChange(Sender: TObject);
var
  Idx: Integer;
begin
  if FLoading then Exit;
  if not Assigned(FOnFPSChange) then Exit;
  Idx := FCmbFPS.ItemIndex;
  if (Idx >= 0) and (Idx <= High(FPS_VALUES)) then
    FOnFPSChange(FPS_VALUES[Idx]);
end;

procedure TSettingsForm.LoadFPS(FPS: Integer);
var
  i, Idx: Integer;
begin
  FLoading := True;
  try
    Idx := 8;
    for i := 0 to High(FPS_VALUES) do
      if FPS_VALUES[i] = FPS then begin Idx := i; Break; end;
    FCmbFPS.ItemIndex := Idx;
  finally
    FLoading := False;
  end;
end;

procedure TSettingsForm.LoadPASettings(MaxPower: Double;
  const BandCal: array of Double);
var i, n: Integer;
begin
  FLoading := True;
  try
    FEdMaxPower.Value := Round(EnsureRange(MaxPower, 5.0, 200.0));
    n := Min(10, High(BandCal));
    for i := 0 to n do
      FEdBandCal[i].Value := EnsureRange(BandCal[i], 38.8, 100.0);
  finally
    FLoading := False;
  end;
end;

procedure TSettingsForm.OnMaxPowerChange(Sender: TObject);
var
  BandCal: array[0..10] of Double;
  i: Integer;
begin
  if FLoading then Exit;
  if not Assigned(FOnPAChange) then Exit;
  for i := 0 to 10 do
    BandCal[i] := FEdBandCal[i].Value;
  FOnPAChange(FEdMaxPower.Value, BandCal);
end;

procedure TSettingsForm.OnBandCalChange(Sender: TObject);
var
  BandCal: array[0..10] of Double;
  i: Integer;
begin
  if FLoading then Exit;
  if not Assigned(FOnPAChange) then Exit;
  for i := 0 to 10 do
    BandCal[i] := FEdBandCal[i].Value;
  FOnPAChange(FEdMaxPower.Value, BandCal);
end;

procedure TSettingsForm.BtnCloseClick(Sender: TObject);
begin
  Close;
end;

// ---------------------------------------------------------------------------
// BuildCATTab
// ---------------------------------------------------------------------------

procedure TSettingsForm.BuildCATTab;
const
  MARGIN  = 22;
  PAD     = 18;
  LW      = 120;
  CX      = PAD + LW + 8;
  GRP_W   = 320;
  GRP_H   = 240;
  GRP_GAP = 16;
  CMB_W   = 140;
  EDT_W   = 160;
  R1      = 42;
  STEP    = 32;
var
  i, col, row, gx, gy, j: Integer;
  Grp:  TPanel;
  Chk:  TCheckBox;
  Ed:   TEdit;
  Cmb:  TComboBox;
  Spin: TSpinEdit;
begin
  with TLabel.Create(Self) do
  begin
    Parent := FPageCAT;
    Caption := 'CAT';
    SetBounds(MARGIN, 18, 400, 28);
    Font.Name := UI_FONT; Font.Size := 16;
    Font.Style := [fsBold]; Font.Color := CLR_TEXT;
  end;
  with TLabel.Create(Self) do
  begin
    Parent := FPageCAT;
    Caption := 'Computer Aided Transceiver — serial port and TCP server settings.';
    SetBounds(MARGIN, 48, 680, 20);
    Font.Name := UI_FONT; Font.Size := 9; Font.Color := CLR_TEXTDIM;
  end;

  for i := 0 to 3 do
  begin
    row := i div 2;
    col := i mod 2;
    gx  := MARGIN + col * (GRP_W + GRP_GAP);
    gy  := 86 + row * (GRP_H + GRP_GAP);

    Grp := MakeGroupPanel(FPageCAT, 'Serial Port ' + IntToStr(i + 1), gx, gy, GRP_W, GRP_H);

    Chk            := TCheckBox.Create(Self);
    Chk.Parent     := Grp;
    Chk.Caption    := 'Enable';
    Chk.SetBounds(PAD, R1, GRP_W - PAD * 2, 22);
    Chk.Font.Color := CLR_TEXT;
    Chk.Font.Name  := UI_FONT; Chk.Font.Size := 9;
    Chk.Tag        := i;
    Chk.OnChange   := OnCATSerialChange;
    FCATSerialEn[i] := Chk;

    MakeLbl(Grp, 'Port:', PAD, R1 + STEP + 6, LW);
    Ed            := TEdit.Create(Self);
    Ed.Parent     := Grp;
    Ed.SetBounds(CX, R1 + STEP, EDT_W, BTN_H);
    Ed.Color      := CLR_INPUT; Ed.Font.Color := CLR_INPUT_TEXT;
    Ed.Font.Name  := UI_FONT; Ed.Font.Size := 9;
    Ed.Tag        := i;
    Ed.OnChange   := OnCATSerialChange;
    {$IFDEF MSWINDOWS}
    Ed.Text := 'COM' + IntToStr(i + 1);
    {$ELSE}
    Ed.Text := '/dev/ttyS' + IntToStr(i);
    {$ENDIF}
    FCATSerialPort[i] := Ed;

    MakeLbl(Grp, 'Baud rate:', PAD, R1 + 2 * STEP + 6, LW);
    Cmb := MakeCombo(Grp, CX, R1 + 2 * STEP, CMB_W, OnCATSerialChange);
    Cmb.Tag := i;
    for j := 0 to High(CAT_BAUD_NAMES) do Cmb.Items.Add(CAT_BAUD_NAMES[j]);
    Cmb.ItemIndex := 3; // 9600
    FCATSerialBaud[i] := Cmb;

    MakeLbl(Grp, 'Data bits:', PAD, R1 + 3 * STEP + 6, LW);
    Cmb := MakeCombo(Grp, CX, R1 + 3 * STEP, CMB_W, OnCATSerialChange);
    Cmb.Tag := i;
    Cmb.Items.Add('7'); Cmb.Items.Add('8');
    Cmb.ItemIndex := 1; // 8
    FCATSerialDataBits[i] := Cmb;

    MakeLbl(Grp, 'Stop bits:', PAD, R1 + 4 * STEP + 6, LW);
    Cmb := MakeCombo(Grp, CX, R1 + 4 * STEP, CMB_W, OnCATSerialChange);
    Cmb.Tag := i;
    Cmb.Items.Add('1'); Cmb.Items.Add('2');
    Cmb.ItemIndex := 0; // 1
    FCATSerialStopBits[i] := Cmb;

    MakeLbl(Grp, 'Parity:', PAD, R1 + 5 * STEP + 6, LW);
    Cmb := MakeCombo(Grp, CX, R1 + 5 * STEP, CMB_W, OnCATSerialChange);
    Cmb.Tag := i;
    Cmb.Items.Add('None'); Cmb.Items.Add('Odd'); Cmb.Items.Add('Even');
    Cmb.ItemIndex := 0; // None
    FCATSerialParity[i] := Cmb;
  end;

  // TCP server group below the two rows of serial port panels
  gy := 86 + 2 * (GRP_H + GRP_GAP);
  Grp := MakeGroupPanel(FPageCAT, 'TCP CAT Server', MARGIN, gy, GRP_W * 2 + GRP_GAP, 116);

  FCATTcpEn            := TCheckBox.Create(Self);
  FCATTcpEn.Parent     := Grp;
  FCATTcpEn.Caption    := 'Enable TCP server (default port 19090)';
  FCATTcpEn.SetBounds(PAD, R1, 300, 22);
  FCATTcpEn.Font.Color := CLR_TEXT;
  FCATTcpEn.Font.Name  := UI_FONT; FCATTcpEn.Font.Size := 9;
  FCATTcpEn.OnChange   := OnCATTcpChange;

  MakeLbl(Grp, 'Port:', PAD, R1 + STEP + 6, LW);
  Spin            := TSpinEdit.Create(Self);
  Spin.Parent     := Grp;
  Spin.SetBounds(CX, R1 + STEP, 100, BTN_H + 2);
  Spin.Color      := CLR_INPUT; Spin.Font.Color := CLR_INPUT_TEXT;
  Spin.Font.Name  := UI_FONT; Spin.Font.Size := 9;
  Spin.MinValue   := 1; Spin.MaxValue := 65535;
  Spin.Value      := 19090;
  Spin.OnChange   := OnCATTcpChange;
  FCATTcpPort := Spin;
end;

// ---------------------------------------------------------------------------
// CAT event handlers
// ---------------------------------------------------------------------------

procedure TSettingsForm.FireCATChange;
var
  SerEn:   array[0..3] of Boolean;
  SerPort: array[0..3] of string;
  SerBaud, SerData, SerStop, SerPar: array[0..3] of Integer;
  i, Idx: Integer;
begin
  if FLoading then Exit;
  if not Assigned(FOnCATChange) then Exit;
  for i := 0 to 3 do
  begin
    SerEn[i]   := FCATSerialEn[i].Checked;
    SerPort[i] := FCATSerialPort[i].Text;
    Idx := FCATSerialBaud[i].ItemIndex;
    if (Idx >= 0) and (Idx <= High(CAT_BAUD_VALS)) then
      SerBaud[i] := CAT_BAUD_VALS[Idx]
    else
      SerBaud[i] := 9600;
    if FCATSerialDataBits[i].ItemIndex = 0 then SerData[i] := 7 else SerData[i] := 8;
    if FCATSerialStopBits[i].ItemIndex = 1 then SerStop[i] := 2 else SerStop[i] := 1;
    SerPar[i] := FCATSerialParity[i].ItemIndex;
  end;
  FOnCATChange(SerEn, SerPort, SerBaud, SerData, SerStop, SerPar,
    FCATTcpEn.Checked, FCATTcpPort.Value);
end;

procedure TSettingsForm.OnCATSerialChange(Sender: TObject);
begin FireCATChange; end;

procedure TSettingsForm.OnCATTcpChange(Sender: TObject);
begin FireCATChange; end;

// ---------------------------------------------------------------------------
// LoadCATSettings
// ---------------------------------------------------------------------------

procedure TSettingsForm.LoadCATSettings(
  const SerEnabled: array of Boolean;
  const SerPort: array of string;
  const SerBaud, SerDataBits, SerStopBits, SerParity: array of Integer;
  TcpEnabled: Boolean; TcpPort: Integer);
var
  i, j, Idx: Integer;
begin
  FLoading := True;
  try
    for i := 0 to 3 do
    begin
      if i <= High(SerEnabled) then FCATSerialEn[i].Checked := SerEnabled[i];
      if i <= High(SerPort)    then FCATSerialPort[i].Text  := SerPort[i];
      if i <= High(SerBaud) then
      begin
        Idx := 3;
        for j := 0 to High(CAT_BAUD_VALS) do
          if CAT_BAUD_VALS[j] = SerBaud[i] then begin Idx := j; Break; end;
        FCATSerialBaud[i].ItemIndex := Idx;
      end;
      if i <= High(SerDataBits) then
      begin
        if SerDataBits[i] = 7 then FCATSerialDataBits[i].ItemIndex := 0
        else FCATSerialDataBits[i].ItemIndex := 1;
      end;
      if i <= High(SerStopBits) then
      begin
        if SerStopBits[i] = 2 then FCATSerialStopBits[i].ItemIndex := 1
        else FCATSerialStopBits[i].ItemIndex := 0;
      end;
      if i <= High(SerParity) then
        FCATSerialParity[i].ItemIndex := EnsureRange(SerParity[i], 0, 2);
    end;
    FCATTcpEn.Checked := TcpEnabled;
    FCATTcpPort.Value  := EnsureRange(TcpPort, 1, 65535);
  finally
    FLoading := False;
  end;
end;

end.
