Files
ewsdr/SettingsForm.pas
T
ew8bakandClaude Sonnet 4.6 efc21b92e6 Add configurable VFO frequency range with GHz support
- Settings > Display: new "VFO Display" control to select max frequency
  range (999 MHz / 9.999 GHz / 99.999 GHz), persisted as freq_mhz_digits
- FreqDisplay: extend digit map [0..10] to support up to 11 digit positions;
  keyboard navigation adapts to MinMhzDigits dynamically
- MainForm/LayoutTopVfoBlock: VFO panel width scales automatically with
  selected digit count using dynamic text measurement
- Web VFO: fix broken HTML (class attr not closed before data-s — wheel and
  hover were completely non-functional); rewrite fmtVfo to use variable MHz
  digit count synced from server state; fix missing </span> for .vfo-hz
- WebServer: publish freq_mhz_digits in state JSON; updated from
  ApplyFreqMhzDigits so desktop and web stay in sync
- Fix Int32 overflow on frequencies above ~2.15 GHz: CATEngine.Pad V param
  Int64, FormatFreq/FormatFreqSV Mhz var Int64 (Mhz*1000000 overflowed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 13:13:52 +03:00

2909 lines
102 KiB
ObjectPascal
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, AppTheme, Settings;
type
// PA: MaxPower в Вт, BandCal[0..10] — калибровка 38.8..100.0 на диапазон
TOnPASettingsChange = procedure(MaxPower: Double;
const BandCal: array of Double) of object;
// TX-настройки целиком (любая правка → один callback с актуальной структурой)
TOnTXSettingsChange = procedure(const T: TTXSettings) of object;
TOnFreqMhzDigitsChange = procedure(Digits: Integer) 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;
TOnThemeChange = procedure(LightTheme: Boolean) 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;
TOnAlexChange = procedure(const A: TAlexSettings) of object;
TOnXvtrChange = procedure(const X: TXvtrSettings) 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;
FPageTransmit: TScrollBox;
FNavAudio: TFlatButton;
FNavDisplay: TFlatButton;
FNavSpectrum: TFlatButton;
FNavWaterfall: TFlatButton;
FNavPA: TFlatButton;
FNavAdvanced: TFlatButton;
FNavCAT: TFlatButton;
FNavTransmit: 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;
FChkLightTheme: TCheckBox;
FCmbFreqMhzDigits: TComboBox;
// ---- PA Settings tab controls ----
FEdMaxPower: TSpinEdit;
FEdBandCal: array[0..10] of TFloatSpinEdit;
// ---- Transmit tab controls ----
// Текущее состояние, отдаётся в OnTXChange целиком при любой правке.
FTX: TTXSettings;
// Mic source / options
FCmbTXMicSource: TComboBox;
FChkTXMicBoost: TCheckBox;
FChkTXMicBias: TCheckBox;
FChkTXMicLineIn: TCheckBox;
FChkTXMicPTT: TCheckBox;
FEdTXMicGain: TFloatSpinEdit;
FEdTXAttOnTX: TSpinEdit;
// Tune (TUN button)
FEdTUNLevel: TSpinEdit;
FEdTUNFreq: TSpinEdit;
// Filter
FEdTXFiltLow: TSpinEdit;
FEdTXFiltHigh: TSpinEdit;
FCmbTXFiltNC: TComboBox;
FCmbTXFiltMP: TComboBox;
FCmbTXFiltWindow: TComboBox;
// Compressor
FChkTXCompOn: TCheckBox;
FEdTXCompGain: TFloatSpinEdit;
// Leveler
FChkTXLevOn: TCheckBox;
FEdTXLevTop: TFloatSpinEdit;
FEdTXLevDecay: TSpinEdit;
// ALC
FChkTXALCOn: TCheckBox;
FEdTXALCMaxGain: TFloatSpinEdit;
FEdTXALCDecay: TSpinEdit;
// Phase Rotator
FChkTXPHROn: TCheckBox;
FEdTXPHRStages: TSpinEdit;
FEdTXPHRFreq: TFloatSpinEdit;
// EQ
FChkTXEQOn: TCheckBox;
FCmbTXEQBands: TComboBox;
FEdTXEQGain: array[0..10] of TFloatSpinEdit;
FEdTXEQFreq: array[0..10] of TFloatSpinEdit;
// AM
FEdTXAMCarrier: TFloatSpinEdit;
// FM
FEdTXFMDev: TFloatSpinEdit;
FEdTXFMLow: TSpinEdit;
FEdTXFMHigh: TSpinEdit;
FCmbTXFMEmph: TComboBox;
// CTCSS
FChkTXCTCSSOn: TCheckBox;
FEdTXCTCSSFreq: TFloatSpinEdit;
// TX Display (отдельный analyzer)
FCmbTXFFT: TComboBox;
FCmbTXWindow: TComboBox;
FCmbTXSpecDet: TComboBox;
FCmbTXSpecAvg: TComboBox;
FEdTXSpecAvgTime: TFloatSpinEdit;
FCmbTXWfDet: TComboBox;
FCmbTXWfAvg: TComboBox;
FEdTXWfAvgTime: TFloatSpinEdit;
// TX Grid (отдельные от RX grid в Display)
FEdTXRefLevel: TEdit;
FEdTXRange: TEdit;
FCmbTXGridStep: TComboBox;
// ---- Alex / Antenna tab controls ----
FPageAlex: TScrollBox;
FNavAlex: TFlatButton;
// Per-band antenna radio buttons (bands 0..10 = 160m..6m)
FAlexRxAnt: array[0..10, 1..3] of TRadioButton;
FAlexTxAnt: array[0..10, 1..3] of TRadioButton;
// Per-band secondary RX (взаимно исключающие checkboxes)
FAlexRxByps: array[0..10] of TCheckBox; // BYPS: Ext1+Bypass
FAlexRxExt1: array[0..10] of TCheckBox; // EXT1: Ext2+Bypass
FAlexRxXvtr: array[0..10] of TCheckBox; // XVTR: XvtrDDC+Bypass
// Per-band Do Not TX
FAlexDoNotTx: array[0..10] of TCheckBox;
// Global TX routing flags
FAlexRxBypassOnTx: TCheckBox;
FAlexExt1OnTx: TCheckBox;
FAlexGndBpf2OnTx: TCheckBox;
FAlexEnableXvtrHf: TCheckBox;
// Current Alex config state
FAlexConfig: TAlexSettings;
FAlexBoardType: Integer;
// Callback
FOnAlexChange: TOnAlexChange;
// ---- XVTR (Transverter) tab controls ----
FPageXvtr: TScrollBox;
FNavXvtr: TFlatButton;
FXvtrEn: array[0..CFG_XVTR_COUNT-1] of TCheckBox;
FXvtrName: array[0..CFG_XVTR_COUNT-1] of TEdit;
FXvtrBegin: array[0..CFG_XVTR_COUNT-1] of TFloatSpinEdit; // MHz
FXvtrEnd: array[0..CFG_XVTR_COUNT-1] of TFloatSpinEdit; // MHz
FXvtrLOOff: array[0..CFG_XVTR_COUNT-1] of TFloatSpinEdit; // MHz
FXvtrLOErr: array[0..CFG_XVTR_COUNT-1] of TFloatSpinEdit; // Hz (целые)
FXvtrRXGain: array[0..CFG_XVTR_COUNT-1] of TFloatSpinEdit; // dB
FXvtrRXOnly: array[0..CFG_XVTR_COUNT-1] of TCheckBox;
FXvtrPower: array[0..CFG_XVTR_COUNT-1] of TSpinEdit; // 0..100
FXvtrRF: array[0..CFG_XVTR_COUNT-1] of TCheckBox;
FXvtrDisPA: array[0..CFG_XVTR_COUNT-1] of TCheckBox;
FXvtrRXAnt: array[0..CFG_XVTR_COUNT-1] of TComboBox; // 0=def..3=ANT3
FChkXvtrTunPwr: TCheckBox;
FXvtrConfig: TXvtrSettings;
FOnXvtrChange: TOnXvtrChange;
// ---- 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;
FOnThemeChange: TOnThemeChange;
FOnFreqMhzDigitsChange: TOnFreqMhzDigitsChange;
FOnTXChange: TOnTXSettingsChange;
FLoading: Boolean;
procedure BuildUI;
procedure BuildAudioTab;
procedure BuildDisplayTab;
procedure BuildRX1Tab;
procedure BuildPATab;
procedure BuildCATTab;
procedure BuildTransmitTab;
procedure BuildAlexTab;
procedure CollectAlexFromUI;
procedure FireAlexChange;
procedure OnAlexAnyChange(Sender: TObject);
procedure OnAlexRxOnlyChange(Sender: TObject);
procedure BuildXvtrTab;
procedure CollectXvtrFromUI;
procedure FireXvtrChange;
procedure OnXvtrAnyChange(Sender: TObject);
procedure UpdateXvtrRowEnabled(Idx: Integer);
procedure CollectTXFromUI;
procedure FireTXChange;
procedure OnTXAnyChange(Sender: TObject);
procedure ApplyTheme(const T: TAppTheme);
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 OnLightThemeChkChange(Sender: TObject);
procedure OnFreqMhzDigitsCmbChange(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 LoadLightTheme(ALight: Boolean);
procedure LoadFreqMhzDigits(Digits: 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);
procedure LoadTXSettings(const T: TTXSettings);
procedure LoadAlexSettings(const A: TAlexSettings; BoardType: Integer);
procedure LoadXvtrSettings(const X: TXvtrSettings);
property OnCATChange: TOnCATChange read FOnCATChange write FOnCATChange;
property OnAlexChange: TOnAlexChange read FOnAlexChange write FOnAlexChange;
property OnXvtrChange: TOnXvtrChange read FOnXvtrChange write FOnXvtrChange;
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;
property OnThemeChange: TOnThemeChange read FOnThemeChange write FOnThemeChange;
property OnFreqMhzDigitsChange: TOnFreqMhzDigitsChange read FOnFreqMhzDigitsChange write FOnFreqMhzDigitsChange;
property OnTXChange: TOnTXSettingsChange read FOnTXChange write FOnTXChange;
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 := 1180;
Height := 720;
Position := poScreenCenter;
BorderStyle := bsSizeable;
Color := CLR_BG;
Font.Name := UI_FONT;
Font.Size := 9;
Font.Color := CLR_TEXT;
FLoading := False;
FRXDevCount := 0;
BuildUI;
ApplyTheme(DarkTheme);
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);
FNavTransmit := MakeNavButton('Transmit', 218);
FNavPA := MakeNavButton('Power Amplifier', 254);
FNavAdvanced := MakeNavButton('Advanced', 290);
FNavCAT := MakeNavButton('CAT', 326);
FNavAlex := MakeNavButton('Antenna / Alex', 362);
FNavXvtr := MakeNavButton('Transverter', 398);
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;
FPageTransmit := MakeScrollPage;
FPagePA := MakeScrollPage;
FPageAdvanced := MakeScrollPage;
FPageCAT := MakeScrollPage;
FPageAlex := MakeScrollPage;
FPageXvtr := MakeScrollPage;
FAlexBoardType := 0;
TSettingsManager.DefaultAlex(FAlexConfig);
TSettingsManager.DefaultXvtr(FXvtrConfig);
BuildAudioTab;
BuildDisplayTab;
BuildRX1Tab;
BuildTransmitTab;
BuildPATab;
BuildCATTab;
BuildAlexTab;
BuildXvtrTab;
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 = FNavTransmit then SelectPage(FPageTransmit, FNavTransmit)
else if Sender = FNavPA then SelectPage(FPagePA, FNavPA)
else if Sender = FNavAdvanced then SelectPage(FPageAdvanced, FNavAdvanced)
else if Sender = FNavCAT then SelectPage(FPageCAT, FNavCAT)
else if Sender = FNavAlex then SelectPage(FPageAlex, FNavAlex)
else if Sender = FNavXvtr then SelectPage(FPageXvtr, FNavXvtr);
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;
FPageTransmit.Visible := APage = FPageTransmit;
FPagePA.Visible := APage = FPagePA;
FPageAdvanced.Visible := APage = FPageAdvanced;
FPageCAT.Visible := APage = FPageCAT;
FPageAlex.Visible := APage = FPageAlex;
FPageXvtr.Visible := APage = FPageXvtr;
FNavAudio.Active := ANav = FNavAudio;
FNavDisplay.Active := ANav = FNavDisplay;
FNavSpectrum.Active := ANav = FNavSpectrum;
FNavWaterfall.Active:= ANav = FNavWaterfall;
FNavTransmit.Active := ANav = FNavTransmit;
FNavPA.Active := ANav = FNavPA;
FNavAdvanced.Active := ANav = FNavAdvanced;
FNavCAT.Active := ANav = FNavCAT;
FNavAlex.Active := ANav = FNavAlex;
FNavXvtr.Active := ANav = FNavXvtr;
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;
Grp := MakeGroupPanel(FPageDisplay, 'Appearance', MARGIN, 292, 650, 68);
FChkLightTheme := TCheckBox.Create(Self);
FChkLightTheme.Parent := Grp;
FChkLightTheme.Caption := 'Light theme';
FChkLightTheme.SetBounds(PAD, R1, CHKW, 22);
FChkLightTheme.Checked := False;
FChkLightTheme.Font.Color := CLR_TEXT;
FChkLightTheme.Font.Name := UI_FONT;
FChkLightTheme.Font.Size := 9;
FChkLightTheme.OnChange := OnLightThemeChkChange;
Grp := MakeGroupPanel(FPageDisplay, 'VFO Display', MARGIN, 378, 650, 92);
MakeLbl(Grp, 'Frequency range', PAD, R1 + 5, LW);
FCmbFreqMhzDigits := MakeCombo(Grp, CX, R1, 240, OnFreqMhzDigitsCmbChange);
FCmbFreqMhzDigits.Items.Add('up to 999 MHz (3 digits)');
FCmbFreqMhzDigits.Items.Add('up to 9.999 GHz (4 digits)');
FCmbFreqMhzDigits.Items.Add('up to 99.999 GHz (5 digits)');
FCmbFreqMhzDigits.ItemIndex := 0;
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;
// ---------------------------------------------------------------------------
// BuildTransmitTab — все настройки TX-тракта (по образцу Thetis Setup→Transmit)
// ---------------------------------------------------------------------------
procedure TSettingsForm.BuildTransmitTab;
const
MARGIN = 22;
PAD = 18;
LW = 150;
CX = PAD + LW + 12;
STEP = 32;
R1 = 42;
SPW = 96;
CMB_W = 200;
var
Grp: TPanel;
Y, i: Integer;
function MkSpin(AParent: TWinControl; AX, AY: Integer; AMin, AMax, AVal: Integer): TSpinEdit;
begin
Result := TSpinEdit.Create(Self);
Result.Parent := AParent;
Result.SetBounds(AX, AY, SPW, BTN_H);
Result.MinValue := AMin;
Result.MaxValue := AMax;
Result.Value := AVal;
Result.Color := CLR_INPUT;
Result.Font.Color := CLR_INPUT_TEXT;
Result.Font.Name := UI_FONT;
Result.Font.Size := 9;
Result.OnChange := OnTXAnyChange;
end;
function MkFloat(AParent: TWinControl; AX, AY: Integer;
AVal, AMin, AMax, AInc: Double): TFloatSpinEdit;
begin
Result := TFloatSpinEdit.Create(Self);
Result.Parent := AParent;
Result.SetBounds(AX, AY, SPW, BTN_H);
Result.MinValue := AMin;
Result.MaxValue := AMax;
Result.Increment := AInc;
Result.Value := AVal;
Result.DecimalPlaces := 2;
Result.Color := CLR_INPUT;
Result.Font.Color := CLR_INPUT_TEXT;
Result.Font.Name := UI_FONT;
Result.Font.Size := 9;
Result.OnChange := OnTXAnyChange;
end;
function MkChk(AParent: TWinControl; AX, AY, AW: Integer; const ACap: string): TCheckBox;
begin
Result := TCheckBox.Create(Self);
Result.Parent := AParent;
Result.Caption := ACap;
Result.SetBounds(AX, AY, AW, 22);
Result.Font.Color := CLR_TEXT;
Result.Font.Name := UI_FONT;
Result.Font.Size := 9;
Result.OnChange := OnTXAnyChange;
end;
function MkCmb(AParent: TWinControl; AX, AY, AW: Integer): TComboBox;
begin
Result := MakeCombo(AParent, AX, AY, AW, OnTXAnyChange);
end;
begin
with TLabel.Create(Self) do
begin
Parent := FPageTransmit;
Caption := 'Transmit';
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 := FPageTransmit;
Caption := 'TX-цепь WDSP: микрофон, фильтр, динамическая обработка, модуляция.';
SetBounds(MARGIN, 48, 700, 20);
Font.Name := UI_FONT;
Font.Size := 9;
Font.Color := CLR_TEXTDIM;
end;
// ---- Mic source / options ----
Y := 86;
Grp := MakeGroupPanel(FPageTransmit, 'Microphone', MARGIN, Y, 700, 220);
MakeLbl(Grp, 'Mic source', PAD, R1 + 5, LW);
FCmbTXMicSource := MkCmb(Grp, CX, R1, CMB_W);
FCmbTXMicSource.Items.Add('Radio (HW mic)');
FCmbTXMicSource.Items.Add('Sound card');
FCmbTXMicSource.ItemIndex := 0;
FChkTXMicBoost := MkChk(Grp, PAD, R1 + STEP, 200, 'Mic boost +20 dB');
FChkTXMicBias := MkChk(Grp, PAD + 200, R1 + STEP, 200, 'Mic bias');
FChkTXMicLineIn := MkChk(Grp, PAD + 400, R1 + STEP, 160, 'Line in');
FChkTXMicPTT := MkChk(Grp, PAD, R1 + 2 * STEP, 220, 'Mic-jack PTT');
MakeLbl(Grp, 'Mic gain (dB)', PAD, R1 + 3 * STEP + 5, LW);
FEdTXMicGain := MkFloat(Grp, CX, R1 + 3 * STEP, 10.0, -20.0, 60.0, 0.5);
// ATT on TX — RX-ADC ослабление во время передачи (DUC Specific bytes 57/58/59).
// Thetis-style default 31 dB. Помещаем на той же строке, что и Mic gain.
MakeLbl(Grp, 'ATT on TX (dB)', PAD + 320, R1 + 3 * STEP + 5, 110);
FEdTXAttOnTX := MkSpin(Grp, PAD + 320 + 120, R1 + 3 * STEP, 0, 31, 31);
// ---- Tune (TUN button) ----
Inc(Y, 220 + 12);
Grp := MakeGroupPanel(FPageTransmit, 'Tune (TUN)', MARGIN, Y, 700, 100);
MakeLbl(Grp, 'Tune level (%)', PAD, R1 + 5, LW);
FEdTUNLevel := MkSpin(Grp, CX, R1, 0, 100, 10);
MakeLbl(Grp, 'Tone freq (Hz)', PAD + 320, R1 + 5, 110);
FEdTUNFreq := MkSpin(Grp, PAD + 320 + 120, R1, 100, 5000, 750);
// ---- TX bandpass filter ----
Inc(Y, 100 + 12);
Grp := MakeGroupPanel(FPageTransmit, 'TX Filter (bandpass)', MARGIN, Y, 700, 200);
MakeLbl(Grp, 'Low cut (Hz)', PAD, R1 + 5, LW);
FEdTXFiltLow := MkSpin(Grp, CX, R1, 0, 8000, 100);
MakeLbl(Grp, 'High cut (Hz)', PAD + 320, R1 + 5, 90);
FEdTXFiltHigh := MkSpin(Grp, PAD + 320 + 100, R1, 100, 12000, 2800);
MakeLbl(Grp, 'FIR length', PAD, R1 + STEP + 5, LW);
FCmbTXFiltNC := MkCmb(Grp, CX, R1 + STEP, 140);
FCmbTXFiltNC.Items.AddStrings(['256','512','1024','2048','4096']);
FCmbTXFiltNC.ItemIndex := 2;
MakeLbl(Grp, 'Phase mode', PAD, R1 + 2 * STEP + 5, LW);
FCmbTXFiltMP := MkCmb(Grp, CX, R1 + 2 * STEP, 200);
FCmbTXFiltMP.Items.AddStrings(['Linear Phase','Low Latency']);
FCmbTXFiltMP.ItemIndex := 1;
MakeLbl(Grp, 'Window', PAD, R1 + 3 * STEP + 5, LW);
FCmbTXFiltWindow := MkCmb(Grp, CX, R1 + 3 * STEP, 240);
for i := 0 to High(WINDOW_NAMES) do
FCmbTXFiltWindow.Items.Add(WINDOW_NAMES[i]);
FCmbTXFiltWindow.ItemIndex := 1;
// ---- Compressor / Leveler / ALC / Phase Rotator ----
Inc(Y, 200 + 12);
Grp := MakeGroupPanel(FPageTransmit, 'Dynamic processing', MARGIN, Y, 700, 220);
FChkTXCompOn := MkChk(Grp, PAD, R1, 220, 'Compressor');
MakeLbl(Grp, 'Gain (dB)', PAD + 230, R1 + 5, 80);
FEdTXCompGain := MkFloat(Grp, PAD + 320, R1, 5.0, 0.0, 20.0, 0.5);
FChkTXLevOn := MkChk(Grp, PAD, R1 + STEP, 220, 'Leveler');
MakeLbl(Grp, 'Top (dB)', PAD + 230, R1 + STEP + 5, 80);
FEdTXLevTop := MkFloat(Grp, PAD + 320, R1 + STEP, 5.0, 0.0, 20.0, 0.5);
MakeLbl(Grp, 'Decay (ms)', PAD + 430, R1 + STEP + 5, 80);
FEdTXLevDecay := MkSpin(Grp, PAD + 520, R1 + STEP, 1, 5000, 500);
FChkTXALCOn := MkChk(Grp, PAD, R1 + 2 * STEP, 220, 'ALC');
MakeLbl(Grp, 'Max gain', PAD + 230, R1 + 2 * STEP + 5, 80);
FEdTXALCMaxGain := MkFloat(Grp, PAD + 320, R1 + 2 * STEP, 0.0, -20.0, 20.0, 0.5);
MakeLbl(Grp, 'Decay (ms)', PAD + 430, R1 + 2 * STEP + 5, 80);
FEdTXALCDecay := MkSpin(Grp, PAD + 520, R1 + 2 * STEP, 1, 5000, 10);
FChkTXPHROn := MkChk(Grp, PAD, R1 + 3 * STEP, 220, 'Phase Rotator');
MakeLbl(Grp, 'Stages', PAD + 230, R1 + 3 * STEP + 5, 80);
FEdTXPHRStages := MkSpin(Grp, PAD + 320, R1 + 3 * STEP, 2, 16, 8);
MakeLbl(Grp, 'Corner (Hz)', PAD + 430, R1 + 3 * STEP + 5, 80);
FEdTXPHRFreq := MkFloat(Grp, PAD + 520, R1 + 3 * STEP, 338.0, 50.0, 3000.0, 1.0);
// ---- TX EQ ----
Inc(Y, 220 + 12);
Grp := MakeGroupPanel(FPageTransmit, 'TX Equalizer', MARGIN, Y, 700, 320);
FChkTXEQOn := MkChk(Grp, PAD, R1, 200, 'Enable EQ');
MakeLbl(Grp, 'Bands', PAD + 230, R1 + 5, 60);
FCmbTXEQBands := MkCmb(Grp, PAD + 290, R1, 80);
FCmbTXEQBands.Items.AddStrings(['3','10']);
FCmbTXEQBands.ItemIndex := 1;
// 11 строк: [0]=Preamp, [1..10]=полосы. Freq+Gain в одной строке.
for i := 0 to 10 do
begin
if i = 0 then
MakeLbl(Grp, 'Preamp (dB)', PAD, R1 + STEP + i * 22 + 3, 100)
else
MakeLbl(Grp, Format('Band %d', [i]), PAD, R1 + STEP + i * 22 + 3, 80);
FEdTXEQFreq[i] := MkFloat(Grp, PAD + 90, R1 + STEP + i * 22, 0.0, 0.0, 20000.0, 10.0);
FEdTXEQFreq[i].Width := 100;
MakeLbl(Grp, 'gain', PAD + 200, R1 + STEP + i * 22 + 3, 30);
FEdTXEQGain[i] := MkFloat(Grp, PAD + 230, R1 + STEP + i * 22, 0.0, -15.0, 15.0, 0.5);
FEdTXEQGain[i].Width := 80;
end;
// ---- AM / FM / CTCSS ----
Inc(Y, 320 + 12);
Grp := MakeGroupPanel(FPageTransmit, 'AM / FM / CTCSS', MARGIN, Y, 700, 220);
MakeLbl(Grp, 'AM carrier level', PAD, R1 + 5, LW);
FEdTXAMCarrier := MkFloat(Grp, CX, R1, 0.5, 0.0, 1.0, 0.05);
MakeLbl(Grp, 'FM deviation (Hz)', PAD, R1 + STEP + 5, LW);
FEdTXFMDev := MkFloat(Grp, CX, R1 + STEP, 5000.0, 1000.0, 25000.0, 100.0);
MakeLbl(Grp, 'FM AF low cut', PAD, R1 + 2 * STEP + 5, LW);
FEdTXFMLow := MkSpin(Grp, CX, R1 + 2 * STEP, 0, 5000, 300);
MakeLbl(Grp, 'FM AF high cut', PAD + 320, R1 + 2 * STEP + 5, 100);
FEdTXFMHigh := MkSpin(Grp, PAD + 320 + 110, R1 + 2 * STEP, 100, 8000, 3000);
MakeLbl(Grp, 'FM emphasis', PAD, R1 + 3 * STEP + 5, LW);
FCmbTXFMEmph := MkCmb(Grp, CX, R1 + 3 * STEP, 180);
FCmbTXFMEmph.Items.AddStrings(['Pre-EQ','Post-EQ']);
FCmbTXFMEmph.ItemIndex := 1;
FChkTXCTCSSOn := MkChk(Grp, PAD, R1 + 4 * STEP, 220, 'CTCSS tone');
MakeLbl(Grp, 'Freq (Hz)', PAD + 230, R1 + 4 * STEP + 5, 80);
FEdTXCTCSSFreq := MkFloat(Grp, PAD + 320, R1 + 4 * STEP, 100.0, 50.0, 300.0, 0.1);
// ---- TX Display (отдельный analyzer для TX-спектра/водопада) ----
Inc(Y, 220 + 12);
Grp := MakeGroupPanel(FPageTransmit, 'TX Display (Analyzer)', MARGIN, Y, 700, 290);
MakeLbl(Grp, 'FFT size', PAD, R1 + 5, LW);
FCmbTXFFT := MkCmb(Grp, CX, R1, 140);
for i := 0 to High(FFT_NAMES) do FCmbTXFFT.Items.Add(FFT_NAMES[i]);
FCmbTXFFT.ItemIndex := 2;
MakeLbl(Grp, 'Window', PAD, R1 + STEP + 5, LW);
FCmbTXWindow := MkCmb(Grp, CX, R1 + STEP, 240);
for i := 0 to High(WINDOW_NAMES) do FCmbTXWindow.Items.Add(WINDOW_NAMES[i]);
FCmbTXWindow.ItemIndex := 2;
MakeLbl(Grp, 'Spec detector',PAD, R1 + 2 * STEP + 5, LW);
FCmbTXSpecDet := MkCmb(Grp, CX, R1 + 2 * STEP, 200);
for i := 0 to High(DETECTOR_NAMES) do FCmbTXSpecDet.Items.Add(DETECTOR_NAMES[i]);
FCmbTXSpecDet.ItemIndex := 0;
MakeLbl(Grp, 'Spec average', PAD, R1 + 3 * STEP + 5, LW);
FCmbTXSpecAvg := MkCmb(Grp, CX, R1 + 3 * STEP, 200);
for i := 0 to High(AVGMODE_NAMES) do FCmbTXSpecAvg.Items.Add(AVGMODE_NAMES[i]);
FCmbTXSpecAvg.ItemIndex := 3;
MakeLbl(Grp, 'Avg time (ms)', PAD + 430, R1 + 3 * STEP + 5, 100);
FEdTXSpecAvgTime := MkFloat(Grp, PAD + 540, R1 + 3 * STEP, 30.0, 1.0, 5000.0, 1.0);
MakeLbl(Grp, 'WF detector', PAD, R1 + 4 * STEP + 5, LW);
FCmbTXWfDet := MkCmb(Grp, CX, R1 + 4 * STEP, 200);
for i := 0 to High(DETECTOR_NAMES) do FCmbTXWfDet.Items.Add(DETECTOR_NAMES[i]);
FCmbTXWfDet.ItemIndex := 0;
MakeLbl(Grp, 'WF average', PAD, R1 + 5 * STEP + 5, LW);
FCmbTXWfAvg := MkCmb(Grp, CX, R1 + 5 * STEP, 200);
for i := 0 to High(WF_AVGMODE_NAMES) do FCmbTXWfAvg.Items.Add(WF_AVGMODE_NAMES[i]);
FCmbTXWfAvg.ItemIndex := 3;
MakeLbl(Grp, 'Avg time (ms)', PAD + 430, R1 + 5 * STEP + 5, 100);
FEdTXWfAvgTime := MkFloat(Grp, PAD + 540, R1 + 5 * STEP, 120.0, 1.0, 5000.0, 1.0);
// ---- TX Grid (RefLevel / Range / GridStep — отдельные от RX) ----
Inc(Y, 290 + 12);
Grp := MakeGroupPanel(FPageTransmit, 'TX Grid', MARGIN, Y, 700, 170);
MakeLbl(Grp, 'Reference level', PAD, R1 + 5, LW);
FEdTXRefLevel := TEdit.Create(Self);
FEdTXRefLevel.Parent := Grp;
FEdTXRefLevel.SetBounds(CX, R1, 96, BTN_H);
FEdTXRefLevel.Color := CLR_INPUT;
FEdTXRefLevel.Font.Color := CLR_INPUT_TEXT;
FEdTXRefLevel.Font.Name := UI_FONT;
FEdTXRefLevel.Font.Size := 9;
FEdTXRefLevel.Text := '0';
FEdTXRefLevel.OnExit := OnTXAnyChange;
MakeLbl(Grp, 'Range (dB)', PAD, R1 + STEP + 5, LW);
FEdTXRange := TEdit.Create(Self);
FEdTXRange.Parent := Grp;
FEdTXRange.SetBounds(CX, R1 + STEP, 96, BTN_H);
FEdTXRange.Color := CLR_INPUT;
FEdTXRange.Font.Color := CLR_INPUT_TEXT;
FEdTXRange.Font.Name := UI_FONT;
FEdTXRange.Font.Size := 9;
FEdTXRange.Text := '80';
FEdTXRange.OnExit := OnTXAnyChange;
MakeLbl(Grp, 'Grid step', PAD, R1 + 2 * STEP + 5, LW);
FCmbTXGridStep := MkCmb(Grp, CX, R1 + 2 * STEP, 130);
for i := 0 to High(GRID_STEP_NAMES) do FCmbTXGridStep.Items.Add(GRID_STEP_NAMES[i]);
FCmbTXGridStep.ItemIndex := 1;
end;
procedure TSettingsForm.CollectTXFromUI;
var i: Integer;
begin
if FCmbTXMicSource <> nil then
FTX.MicSource := Max(0, FCmbTXMicSource.ItemIndex);
if FChkTXMicBoost <> nil then FTX.MicBoost := FChkTXMicBoost.Checked;
if FChkTXMicBias <> nil then FTX.MicBias := FChkTXMicBias.Checked;
if FChkTXMicLineIn <> nil then FTX.MicLineIn := FChkTXMicLineIn.Checked;
if FChkTXMicPTT <> nil then FTX.MicPTTEnabled := FChkTXMicPTT.Checked;
if FEdTXMicGain <> nil then FTX.MicGainDB := FEdTXMicGain.Value;
if FEdTXAttOnTX <> nil then FTX.AttOnTX := FEdTXAttOnTX.Value;
if FEdTUNLevel <> nil then FTX.TUNLevel := FEdTUNLevel.Value;
if FEdTUNFreq <> nil then FTX.TUNFreq := FEdTUNFreq.Value;
if FEdTXFiltLow <> nil then FTX.FilterLow := FEdTXFiltLow.Value;
if FEdTXFiltHigh <> nil then FTX.FilterHigh := FEdTXFiltHigh.Value;
if FCmbTXFiltNC <> nil then
FTX.FilterNC := StrToIntDef(FCmbTXFiltNC.Text, 1024);
if FCmbTXFiltMP <> nil then FTX.FilterMP := Max(0, FCmbTXFiltMP.ItemIndex);
if FCmbTXFiltWindow<> nil then FTX.FilterWindow := Max(0, FCmbTXFiltWindow.ItemIndex);
if FChkTXCompOn <> nil then FTX.CompressorOn := FChkTXCompOn.Checked;
if FEdTXCompGain <> nil then FTX.CompressorGain := FEdTXCompGain.Value;
if FChkTXLevOn <> nil then FTX.LevelerOn := FChkTXLevOn.Checked;
if FEdTXLevTop <> nil then FTX.LevelerTop := FEdTXLevTop.Value;
if FEdTXLevDecay <> nil then FTX.LevelerDecay := FEdTXLevDecay.Value;
if FChkTXALCOn <> nil then FTX.ALCOn := FChkTXALCOn.Checked;
if FEdTXALCMaxGain <> nil then FTX.ALCMaxGain := FEdTXALCMaxGain.Value;
if FEdTXALCDecay <> nil then FTX.ALCDecay := FEdTXALCDecay.Value;
if FChkTXPHROn <> nil then FTX.PhaseRotOn := FChkTXPHROn.Checked;
if FEdTXPHRStages <> nil then FTX.PhaseRotStages := FEdTXPHRStages.Value;
if FEdTXPHRFreq <> nil then FTX.PhaseRotFreq := FEdTXPHRFreq.Value;
if FChkTXEQOn <> nil then FTX.EQOn := FChkTXEQOn.Checked;
if FCmbTXEQBands <> nil then
if FCmbTXEQBands.ItemIndex = 0 then FTX.EQNumBands := 3 else FTX.EQNumBands := 10;
for i := 0 to 10 do
begin
if FEdTXEQGain[i] <> nil then FTX.EQGains[i] := FEdTXEQGain[i].Value;
if FEdTXEQFreq[i] <> nil then FTX.EQFreqs[i] := FEdTXEQFreq[i].Value;
end;
if FEdTXAMCarrier <> nil then FTX.AMCarrierLevel := FEdTXAMCarrier.Value;
if FEdTXFMDev <> nil then FTX.FMDeviation := FEdTXFMDev.Value;
if FEdTXFMLow <> nil then FTX.FMLowCut := FEdTXFMLow.Value;
if FEdTXFMHigh <> nil then FTX.FMHighCut := FEdTXFMHigh.Value;
if FCmbTXFMEmph <> nil then FTX.FMEmphPosition := Max(0, FCmbTXFMEmph.ItemIndex);
if FChkTXCTCSSOn <> nil then FTX.CTCSSOn := FChkTXCTCSSOn.Checked;
if FEdTXCTCSSFreq <> nil then FTX.CTCSSFreq := FEdTXCTCSSFreq.Value;
if (FCmbTXFFT <> nil) and (FCmbTXFFT.ItemIndex >= 0) then
FTX.TXFFTSize := FFT_SIZES[FCmbTXFFT.ItemIndex];
if FCmbTXWindow <> nil then FTX.TXWindowType := Max(0, FCmbTXWindow.ItemIndex);
if FCmbTXSpecDet <> nil then FTX.TXSpecDetector := Max(0, FCmbTXSpecDet.ItemIndex);
if (FCmbTXSpecAvg <> nil) and (FCmbTXSpecAvg.ItemIndex >= 0) then
FTX.TXSpecAvgMode := AVGMODE_VALUES[FCmbTXSpecAvg.ItemIndex];
if FEdTXSpecAvgTime <> nil then FTX.TXSpecAvgTimeMS := FEdTXSpecAvgTime.Value;
if FCmbTXWfDet <> nil then FTX.TXWfDetector := Max(0, FCmbTXWfDet.ItemIndex);
if (FCmbTXWfAvg <> nil) and (FCmbTXWfAvg.ItemIndex >= 0) then
FTX.TXWfAvgMode := WF_AVGMODE_VALUES[FCmbTXWfAvg.ItemIndex];
if FEdTXWfAvgTime <> nil then FTX.TXWfAvgTimeMS := FEdTXWfAvgTime.Value;
if FEdTXRefLevel <> nil then
begin
Val(FEdTXRefLevel.Text, FTX.TXSpecRefLevel, i);
if i <> 0 then FTX.TXSpecRefLevel := 0.0;
end;
if FEdTXRange <> nil then
begin
Val(FEdTXRange.Text, FTX.TXSpecRange, i);
if i <> 0 then FTX.TXSpecRange := 80.0;
end;
if (FCmbTXGridStep <> nil) and (FCmbTXGridStep.ItemIndex >= 0)
and (FCmbTXGridStep.ItemIndex <= High(GRID_STEPS)) then
FTX.TXSpecGridStep := GRID_STEPS[FCmbTXGridStep.ItemIndex];
end;
procedure TSettingsForm.FireTXChange;
begin
if FLoading then Exit;
CollectTXFromUI;
if Assigned(FOnTXChange) then
FOnTXChange(FTX);
end;
procedure TSettingsForm.OnTXAnyChange(Sender: TObject);
begin
FireTXChange;
end;
procedure TSettingsForm.LoadTXSettings(const T: TTXSettings);
var i: Integer;
begin
FLoading := True;
try
FTX := T;
if FCmbTXMicSource <> nil then
FCmbTXMicSource.ItemIndex := EnsureRange(T.MicSource, 0, FCmbTXMicSource.Items.Count - 1);
if FChkTXMicBoost <> nil then FChkTXMicBoost.Checked := T.MicBoost;
if FChkTXMicBias <> nil then FChkTXMicBias.Checked := T.MicBias;
if FChkTXMicLineIn <> nil then FChkTXMicLineIn.Checked := T.MicLineIn;
if FChkTXMicPTT <> nil then FChkTXMicPTT.Checked := T.MicPTTEnabled;
if FEdTXMicGain <> nil then FEdTXMicGain.Value := T.MicGainDB;
if FEdTXAttOnTX <> nil then FEdTXAttOnTX.Value := EnsureRange(T.AttOnTX, 0, 31);
if FEdTUNLevel <> nil then FEdTUNLevel.Value := EnsureRange(T.TUNLevel, 0, 100);
if FEdTUNFreq <> nil then FEdTUNFreq.Value := EnsureRange(T.TUNFreq, 100, 5000);
if FEdTXFiltLow <> nil then FEdTXFiltLow.Value := T.FilterLow;
if FEdTXFiltHigh <> nil then FEdTXFiltHigh.Value := T.FilterHigh;
if FCmbTXFiltNC <> nil then FCmbTXFiltNC.Text := IntToStr(T.FilterNC);
if FCmbTXFiltMP <> nil then
FCmbTXFiltMP.ItemIndex := EnsureRange(T.FilterMP, 0, FCmbTXFiltMP.Items.Count - 1);
if FCmbTXFiltWindow<> nil then
FCmbTXFiltWindow.ItemIndex := EnsureRange(T.FilterWindow, 0, FCmbTXFiltWindow.Items.Count - 1);
if FChkTXCompOn <> nil then FChkTXCompOn.Checked := T.CompressorOn;
if FEdTXCompGain <> nil then FEdTXCompGain.Value := T.CompressorGain;
if FChkTXLevOn <> nil then FChkTXLevOn.Checked := T.LevelerOn;
if FEdTXLevTop <> nil then FEdTXLevTop.Value := T.LevelerTop;
if FEdTXLevDecay <> nil then FEdTXLevDecay.Value := T.LevelerDecay;
if FChkTXALCOn <> nil then FChkTXALCOn.Checked := T.ALCOn;
if FEdTXALCMaxGain <> nil then FEdTXALCMaxGain.Value := T.ALCMaxGain;
if FEdTXALCDecay <> nil then FEdTXALCDecay.Value := T.ALCDecay;
if FChkTXPHROn <> nil then FChkTXPHROn.Checked := T.PhaseRotOn;
if FEdTXPHRStages <> nil then FEdTXPHRStages.Value := T.PhaseRotStages;
if FEdTXPHRFreq <> nil then FEdTXPHRFreq.Value := T.PhaseRotFreq;
if FChkTXEQOn <> nil then FChkTXEQOn.Checked := T.EQOn;
if FCmbTXEQBands <> nil then
if T.EQNumBands <= 3 then FCmbTXEQBands.ItemIndex := 0
else FCmbTXEQBands.ItemIndex := 1;
for i := 0 to 10 do
begin
if FEdTXEQGain[i] <> nil then FEdTXEQGain[i].Value := T.EQGains[i];
if FEdTXEQFreq[i] <> nil then FEdTXEQFreq[i].Value := T.EQFreqs[i];
end;
if FEdTXAMCarrier <> nil then FEdTXAMCarrier.Value := T.AMCarrierLevel;
if FEdTXFMDev <> nil then FEdTXFMDev.Value := T.FMDeviation;
if FEdTXFMLow <> nil then FEdTXFMLow.Value := T.FMLowCut;
if FEdTXFMHigh <> nil then FEdTXFMHigh.Value := T.FMHighCut;
if FCmbTXFMEmph <> nil then
FCmbTXFMEmph.ItemIndex := EnsureRange(T.FMEmphPosition, 0, FCmbTXFMEmph.Items.Count - 1);
if FChkTXCTCSSOn <> nil then FChkTXCTCSSOn.Checked := T.CTCSSOn;
if FEdTXCTCSSFreq <> nil then FEdTXCTCSSFreq.Value := T.CTCSSFreq;
if FCmbTXFFT <> nil then
begin
FCmbTXFFT.ItemIndex := 2;
for i := 0 to High(FFT_SIZES) do
if FFT_SIZES[i] = T.TXFFTSize then
begin
FCmbTXFFT.ItemIndex := i;
Break;
end;
end;
if FCmbTXWindow <> nil then
FCmbTXWindow.ItemIndex := EnsureRange(T.TXWindowType, 0, FCmbTXWindow.Items.Count - 1);
if FCmbTXSpecDet <> nil then
FCmbTXSpecDet.ItemIndex := EnsureRange(T.TXSpecDetector, 0, FCmbTXSpecDet.Items.Count - 1);
if FCmbTXSpecAvg <> nil then
begin
FCmbTXSpecAvg.ItemIndex := 3;
for i := 0 to High(AVGMODE_VALUES) do
if AVGMODE_VALUES[i] = T.TXSpecAvgMode then
begin
FCmbTXSpecAvg.ItemIndex := i;
Break;
end;
end;
if FEdTXSpecAvgTime <> nil then FEdTXSpecAvgTime.Value := T.TXSpecAvgTimeMS;
if FCmbTXWfDet <> nil then
FCmbTXWfDet.ItemIndex := EnsureRange(T.TXWfDetector, 0, FCmbTXWfDet.Items.Count - 1);
if FCmbTXWfAvg <> nil then
begin
FCmbTXWfAvg.ItemIndex := 3;
for i := 0 to High(WF_AVGMODE_VALUES) do
if WF_AVGMODE_VALUES[i] = T.TXWfAvgMode then
begin
FCmbTXWfAvg.ItemIndex := i;
Break;
end;
end;
if FEdTXWfAvgTime <> nil then FEdTXWfAvgTime.Value := T.TXWfAvgTimeMS;
if FEdTXRefLevel <> nil then FEdTXRefLevel.Text := FormatFloat('0.#', T.TXSpecRefLevel);
if FEdTXRange <> nil then FEdTXRange.Text := FormatFloat('0.#', T.TXSpecRange);
if FCmbTXGridStep <> nil then
begin
FCmbTXGridStep.ItemIndex := 1; // дефолт 10 dB
for i := 0 to High(GRID_STEPS) do
if Abs(GRID_STEPS[i] - T.TXSpecGridStep) < 0.5 then
begin
FCmbTXGridStep.ItemIndex := i;
Break;
end;
end;
finally
FLoading := False;
end;
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(const T: TAppTheme);
procedure StyleBtn(B: TFlatButton);
begin
B.ClrNorm := T.Panel;
B.ClrBorder := T.Panel;
B.ClrHot := T.BG;
B.ClrActive := T.Panel;
B.ClrText := T.Text;
B.ClrTextAct := T.FreqActive;
B.Invalidate;
end;
procedure WalkControls(C: TWinControl);
var i: Integer; Ctrl: TControl;
begin
for i := 0 to C.ControlCount - 1 do
begin
Ctrl := C.Controls[i];
if Ctrl is TFlatButton then
StyleBtn(TFlatButton(Ctrl))
else if Ctrl is TScrollBox then
begin
TScrollBox(Ctrl).Color := T.BG;
WalkControls(TScrollBox(Ctrl));
end
else if Ctrl is TPanel then
begin
if TPanel(Ctrl).Height = 1 then
TPanel(Ctrl).Color := T.Border
else
TPanel(Ctrl).Color := T.Panel;
WalkControls(TPanel(Ctrl));
end
else if Ctrl is TLabel then
begin
if Ctrl.Font.Size >= 12 then
TLabel(Ctrl).Font.Color := T.Text
else if fsBold in Ctrl.Font.Style then
TLabel(Ctrl).Font.Color := T.FreqActive
else
TLabel(Ctrl).Font.Color := T.TextDim;
end
else if Ctrl is TCheckBox then
TCheckBox(Ctrl).Font.Color := T.Text
else if Ctrl is TListBox then
begin
TListBox(Ctrl).Color := T.BG;
TListBox(Ctrl).Font.Color := T.Text;
end
else if Ctrl is TWinControl then
WalkControls(TWinControl(Ctrl));
end;
end;
begin
Color := T.BG;
Font.Color := T.Text;
WalkControls(Self);
Invalidate;
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.OnLightThemeChkChange(Sender: TObject);
begin
if FLoading then Exit;
if FChkLightTheme.Checked then ApplyTheme(LightTheme)
else ApplyTheme(DarkTheme);
if Assigned(FOnThemeChange) then
FOnThemeChange(FChkLightTheme.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.LoadLightTheme(ALight: Boolean);
begin
FLoading := True;
try
FChkLightTheme.Checked := ALight;
finally
FLoading := False;
end;
if ALight then ApplyTheme(LightTheme)
else ApplyTheme(DarkTheme);
end;
procedure TSettingsForm.LoadFreqMhzDigits(Digits: Integer);
begin
FLoading := True;
try
FCmbFreqMhzDigits.ItemIndex := EnsureRange(Digits - 3, 0, 2);
finally
FLoading := False;
end;
end;
procedure TSettingsForm.OnFreqMhzDigitsCmbChange(Sender: TObject);
begin
if FLoading then Exit;
if Assigned(FOnFreqMhzDigitsChange) then
FOnFreqMhzDigitsChange(FCmbFreqMhzDigits.ItemIndex + 3);
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;
// ---------------------------------------------------------------------------
// BuildAlexTab
// ---------------------------------------------------------------------------
procedure TSettingsForm.BuildAlexTab;
const
MARGIN = 22;
PAD = 12;
HDR1_H = 22; // height of "RX Ant / TX Ant" header row
HDR2_H = 20; // height of "1 2 3 / BYPS EXT1 XVTR" sub-header row
ROW_H = 24;
BANDS = 11;
BAND_X = PAD;
RX1_X = PAD + 50;
RX2_X = PAD + 76;
RX3_X = PAD + 102;
BYPS_X = PAD + 136;
EXT1_X = PAD + 192;
XVTR_X = PAD + 248;
TX1_X = PAD + 308;
TX2_X = PAD + 334;
TX3_X = PAD + 360;
DNTX_X = PAD + 398;
GRP_W = DNTX_X + 80;
GRP_H = GRP_TITLE_H + HDR1_H + HDR2_H + BANDS * ROW_H + PAD;
DATA_Y0 = GRP_TITLE_H + HDR1_H + HDR2_H + 4;
TXGRP_H = GRP_TITLE_H + 4 * 28 + PAD;
BAND_NAMES: array[0..10] of string =
('160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m');
var
Grp, GrpTX: TPanel;
b: Integer;
Y: Integer;
Chk: TCheckBox;
RxGrp, TxGrp: TPanel;
// In Lazarus, TRadioButton has no GroupIndex; buttons are grouped by parent.
// We use small invisible panels as parents so each band's RX and TX groups
// are independent of each other.
function MkRbGroup(AParent: TWinControl; AX, AY, AW: Integer): TPanel;
begin
Result := TPanel.Create(Self);
Result.Parent := AParent;
Result.SetBounds(AX, AY, AW, 20);
Result.BevelOuter := bvNone;
Result.Color := CLR_PANEL;
Result.Caption := '';
end;
function MkRb(AParent: TWinControl; AX: Integer): TRadioButton;
begin
Result := TRadioButton.Create(Self);
Result.Parent := AParent;
Result.Caption := '';
Result.SetBounds(AX, 0, 22, 20);
Result.Font.Color := CLR_TEXT;
Result.Font.Name := UI_FONT;
Result.Font.Size := 9;
Result.OnChange := OnAlexAnyChange;
end;
function MkCh(AParent: TWinControl; const ACap: string;
AX, AY, AW: Integer): TCheckBox;
begin
Result := TCheckBox.Create(Self);
Result.Parent := AParent;
Result.Caption := ACap;
Result.SetBounds(AX, AY, AW, 20);
Result.Font.Color := CLR_TEXT;
Result.Font.Name := UI_FONT;
Result.Font.Size := 9;
Result.OnChange := OnAlexAnyChange;
end;
begin
FLoading := True;
try
with TLabel.Create(Self) do
begin
Parent := FPageAlex;
Caption := 'Antenna / Alex';
SetBounds(MARGIN, 18, 500, 28);
Font.Name := UI_FONT; Font.Size := 16;
Font.Style := [fsBold]; Font.Color := CLR_TEXT;
end;
with TLabel.Create(Self) do
begin
Parent := FPageAlex;
Caption := 'Per-band RX/TX antenna and secondary path routing for the Alex filter board.';
SetBounds(MARGIN, 48, 680, 20);
Font.Name := UI_FONT; Font.Size := 9; Font.Color := CLR_TEXTDIM;
end;
// ---- Antenna grid group ----
Grp := MakeGroupPanel(FPageAlex, 'Antenna Selection', MARGIN, 86, GRP_W, GRP_H);
// Header row 1 — group labels
MakeLbl(Grp, 'Band', BAND_X, GRP_TITLE_H + 2, 44);
MakeLbl(Grp, 'RX Antenna', RX1_X, GRP_TITLE_H + 2, 80);
MakeLbl(Grp, 'Secondary RX', BYPS_X, GRP_TITLE_H + 2, 170);
MakeLbl(Grp, 'TX Antenna', TX1_X, GRP_TITLE_H + 2, 80);
MakeLbl(Grp, 'No TX', DNTX_X, GRP_TITLE_H + 2, 56);
// Header row 2 — sub-column labels
MakeLbl(Grp, '1', RX1_X + 3, GRP_TITLE_H + HDR1_H + 2, 20);
MakeLbl(Grp, '2', RX2_X + 3, GRP_TITLE_H + HDR1_H + 2, 20);
MakeLbl(Grp, '3', RX3_X + 3, GRP_TITLE_H + HDR1_H + 2, 20);
MakeLbl(Grp, 'BYPS', BYPS_X, GRP_TITLE_H + HDR1_H + 2, 52);
MakeLbl(Grp, 'EXT1', EXT1_X, GRP_TITLE_H + HDR1_H + 2, 52);
MakeLbl(Grp, 'XVTR', XVTR_X, GRP_TITLE_H + HDR1_H + 2, 52);
MakeLbl(Grp, '1', TX1_X + 3, GRP_TITLE_H + HDR1_H + 2, 20);
MakeLbl(Grp, '2', TX2_X + 3, GRP_TITLE_H + HDR1_H + 2, 20);
MakeLbl(Grp, '3', TX3_X + 3, GRP_TITLE_H + HDR1_H + 2, 20);
// Data rows (bands 0..10 = 160m..6m)
for b := 0 to BANDS - 1 do
begin
Y := DATA_Y0 + b * ROW_H;
MakeLbl(Grp, BAND_NAMES[b], BAND_X, Y + 2, 44);
// RX antenna — 3 radio buttons in a dedicated panel so they are mutually
// exclusive only within that band row (Lazarus groups by parent, not GroupIndex)
RxGrp := MkRbGroup(Grp, RX1_X, Y, RX3_X - RX1_X + 26);
FAlexRxAnt[b, 1] := MkRb(RxGrp, 0);
FAlexRxAnt[b, 2] := MkRb(RxGrp, RX2_X - RX1_X);
FAlexRxAnt[b, 3] := MkRb(RxGrp, RX3_X - RX1_X);
FAlexRxAnt[b, 1].Checked := True;
// Secondary RX checkboxes — mutually exclusive, handled in OnAlexRxOnlyChange
// Tag encodes: b=BYPS, b+100=EXT1, b+200=XVTR
Chk := MkCh(Grp, '', BYPS_X, Y, 22);
Chk.Tag := b;
Chk.OnChange := OnAlexRxOnlyChange;
FAlexRxByps[b] := Chk;
Chk := MkCh(Grp, '', EXT1_X, Y, 22);
Chk.Tag := b + 100;
Chk.OnChange := OnAlexRxOnlyChange;
FAlexRxExt1[b] := Chk;
Chk := MkCh(Grp, '', XVTR_X, Y, 22);
Chk.Tag := b + 200;
Chk.OnChange := OnAlexRxOnlyChange;
FAlexRxXvtr[b] := Chk;
// TX antenna — same pattern: dedicated panel per band row
TxGrp := MkRbGroup(Grp, TX1_X, Y, TX3_X - TX1_X + 26);
FAlexTxAnt[b, 1] := MkRb(TxGrp, 0);
FAlexTxAnt[b, 2] := MkRb(TxGrp, TX2_X - TX1_X);
FAlexTxAnt[b, 3] := MkRb(TxGrp, TX3_X - TX1_X);
FAlexTxAnt[b, 1].Checked := True;
// Do Not TX checkbox
FAlexDoNotTx[b] := MkCh(Grp, '', DNTX_X, Y, 22);
end;
// ---- TX Routing flags group ----
GrpTX := MakeGroupPanel(FPageAlex, 'TX Routing',
MARGIN, 86 + GRP_H + 12, 480, TXGRP_H);
FAlexRxBypassOnTx := MkCh(GrpTX, 'Rx BYPASS on TX',
PAD, GRP_TITLE_H + 8, 440);
FAlexExt1OnTx := MkCh(GrpTX, 'Ext 1 on TX',
PAD, GRP_TITLE_H + 8 + 28, 440);
FAlexGndBpf2OnTx := MkCh(GrpTX, 'Ground BPF2 on TX (ANAN-8000DLE)',
PAD, GRP_TITLE_H + 8 + 56, 440);
FAlexEnableXvtrHf := MkCh(GrpTX, 'Enable XVTR HF',
PAD, GRP_TITLE_H + 8 + 84, 440);
finally
FLoading := False;
end;
end;
// ---------------------------------------------------------------------------
// Alex event handlers and data collectors
// ---------------------------------------------------------------------------
procedure TSettingsForm.CollectAlexFromUI;
var b, a: Integer;
begin
for b := 0 to 10 do
begin
FAlexConfig.RxAnt[b] := 1;
for a := 1 to 3 do
if (FAlexRxAnt[b, a] <> nil) and FAlexRxAnt[b, a].Checked then
begin
FAlexConfig.RxAnt[b] := a;
Break;
end;
FAlexConfig.TxAnt[b] := 1;
for a := 1 to 3 do
if (FAlexTxAnt[b, a] <> nil) and FAlexTxAnt[b, a].Checked then
begin
FAlexConfig.TxAnt[b] := a;
Break;
end;
if (FAlexRxXvtr[b] <> nil) and FAlexRxXvtr[b].Checked then
FAlexConfig.RxOnly[b] := 3
else if (FAlexRxExt1[b] <> nil) and FAlexRxExt1[b].Checked then
FAlexConfig.RxOnly[b] := 2
else if (FAlexRxByps[b] <> nil) and FAlexRxByps[b].Checked then
FAlexConfig.RxOnly[b] := 1
else
FAlexConfig.RxOnly[b] := 0;
FAlexConfig.DoNotTx[b] := (FAlexDoNotTx[b] <> nil) and FAlexDoNotTx[b].Checked;
end;
FAlexConfig.RxBypassOnTx := (FAlexRxBypassOnTx <> nil) and FAlexRxBypassOnTx.Checked;
FAlexConfig.Ext1OnTx := (FAlexExt1OnTx <> nil) and FAlexExt1OnTx.Checked;
FAlexConfig.GndBpf2OnTx := (FAlexGndBpf2OnTx <> nil) and FAlexGndBpf2OnTx.Checked;
FAlexConfig.EnableXvtrHf := (FAlexEnableXvtrHf <> nil) and FAlexEnableXvtrHf.Checked;
end;
procedure TSettingsForm.FireAlexChange;
begin
if FLoading then Exit;
CollectAlexFromUI;
if Assigned(FOnAlexChange) then
FOnAlexChange(FAlexConfig);
end;
procedure TSettingsForm.OnAlexAnyChange(Sender: TObject);
begin
FireAlexChange;
end;
procedure TSettingsForm.OnAlexRxOnlyChange(Sender: TObject);
var
Tag, b: Integer;
begin
if FLoading then Exit;
Tag := TCheckBox(Sender).Tag;
if Tag >= 200 then
begin
b := Tag - 200; // XVTR
if TCheckBox(Sender).Checked then
begin
FLoading := True;
FAlexRxByps[b].Checked := False;
FAlexRxExt1[b].Checked := False;
FLoading := False;
end;
end
else if Tag >= 100 then
begin
b := Tag - 100; // EXT1
if TCheckBox(Sender).Checked then
begin
FLoading := True;
FAlexRxByps[b].Checked := False;
FAlexRxXvtr[b].Checked := False;
FLoading := False;
end;
end
else
begin
b := Tag; // BYPS
if TCheckBox(Sender).Checked then
begin
FLoading := True;
FAlexRxExt1[b].Checked := False;
FAlexRxXvtr[b].Checked := False;
FLoading := False;
end;
end;
FireAlexChange;
end;
procedure TSettingsForm.LoadAlexSettings(const A: TAlexSettings; BoardType: Integer);
var b, ant: Integer;
begin
FLoading := True;
try
FAlexConfig := A;
FAlexBoardType := BoardType;
for b := 0 to 10 do
begin
for ant := 1 to 3 do
if FAlexRxAnt[b, ant] <> nil then
FAlexRxAnt[b, ant].Checked := (A.RxAnt[b] = ant);
for ant := 1 to 3 do
if FAlexTxAnt[b, ant] <> nil then
FAlexTxAnt[b, ant].Checked := (A.TxAnt[b] = ant);
if FAlexRxByps[b] <> nil then FAlexRxByps[b].Checked := (A.RxOnly[b] = 1);
if FAlexRxExt1[b] <> nil then FAlexRxExt1[b].Checked := (A.RxOnly[b] = 2);
if FAlexRxXvtr[b] <> nil then FAlexRxXvtr[b].Checked := (A.RxOnly[b] = 3);
if FAlexDoNotTx[b] <> nil then FAlexDoNotTx[b].Checked := A.DoNotTx[b];
end;
if FAlexRxBypassOnTx <> nil then FAlexRxBypassOnTx.Checked := A.RxBypassOnTx;
if FAlexExt1OnTx <> nil then FAlexExt1OnTx.Checked := A.Ext1OnTx;
if FAlexGndBpf2OnTx <> nil then FAlexGndBpf2OnTx.Checked := A.GndBpf2OnTx;
if FAlexEnableXvtrHf <> nil then FAlexEnableXvtrHf.Checked := A.EnableXvtrHf;
finally
FLoading := False;
end;
end;
// ===========================================================================
// XVTR (Transverter) tab
// ===========================================================================
procedure TSettingsForm.BuildXvtrTab;
const
MARGIN = 22;
ROW_H = 30;
HDR_H = 38; // место под двухстрочные заголовки столбцов
// X-координаты колонок (внутри Grp). Подобраны так чтобы помещаться
// в content-area формы (~880px при ширине 1080).
EN_X = 14;
NAME_X = 40; NAME_W = 104; // подпись на band-кнопке (макс. 15 символов)
BEG_X = 152; BEG_W = 80; // 3 десятичных, "144.000"
END_X = 240; END_W = 80;
LO_X = 328; LO_W = 90; // 6 десятичных, "116.000000"
ERR_X = 426; ERR_W = 70; // целое Hz
GAIN_X = 504; GAIN_W = 62; // 1 десятичная, dB
RXO_X = 574; RXO_W = 56; // chk центрирован
PWR_X = 638; PWR_W = 58; // целое %
RF_X = 704; RF_W = 56;
PA_X = 768; PA_W = 66;
ANT_X = 842; ANT_W = 78; // combo default/ANT1/ANT2/ANT3
GRP_W = ANT_X + ANT_W + 14; // 934
var
Grp, GrpOpts: TPanel;
i, Y, GrpY: Integer;
L: TLabel;
function MakeFlt(AParent: TWinControl; AX, AY, AW: Integer;
AMin, AMax, AStep: Double; ADec: Integer): TFloatSpinEdit;
begin
Result := TFloatSpinEdit.Create(Self);
Result.Parent := AParent;
Result.SetBounds(AX, AY, AW, BTN_H);
Result.MinValue := AMin;
Result.MaxValue := AMax;
Result.Increment := AStep;
Result.DecimalPlaces := ADec;
Result.Color := CLR_INPUT;
Result.Font.Color := CLR_INPUT_TEXT;
Result.Font.Name := UI_FONT;
Result.Font.Size := 9;
Result.OnChange := OnXvtrAnyChange;
end;
function MakeInt(AParent: TWinControl; AX, AY, AW: Integer;
AMin, AMax: Integer): TSpinEdit;
begin
Result := TSpinEdit.Create(Self);
Result.Parent := AParent;
Result.SetBounds(AX, AY, AW, BTN_H);
Result.MinValue := AMin;
Result.MaxValue := AMax;
Result.Color := CLR_INPUT;
Result.Font.Color := CLR_INPUT_TEXT;
Result.Font.Name := UI_FONT;
Result.Font.Size := 9;
Result.OnChange := OnXvtrAnyChange;
end;
// Чекбокс без подписи (только квадрат). Подпись пишем отдельно над колонкой.
function MakeChk(AParent: TWinControl; AX, AY: Integer): TCheckBox;
begin
Result := TCheckBox.Create(Self);
Result.Parent := AParent;
Result.Caption := '';
Result.SetBounds(AX, AY, 22, 20);
Result.Font.Color := CLR_TEXT;
Result.Font.Name := UI_FONT;
Result.Font.Size := 9;
Result.OnChange := OnXvtrAnyChange;
end;
function MakeEdt(AParent: TWinControl; AX, AY, AW: Integer): TEdit;
begin
Result := TEdit.Create(Self);
Result.Parent := AParent;
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.MaxLength := 15;
Result.OnChange := OnXvtrAnyChange;
end;
function MakeCmb(AParent: TWinControl; AX, AY, AW: Integer): TComboBox;
begin
Result := TComboBox.Create(Self);
Result.Parent := AParent;
Result.SetBounds(AX, AY, AW, 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 := OnXvtrAnyChange;
end;
// Заголовок столбца, центрирован по ширине AW от AX.
function MakeHdr(AParent: TWinControl; const Cap: string;
AX, AY, AW: Integer): TLabel;
begin
Result := TLabel.Create(Self);
Result.Parent := AParent;
Result.Caption := Cap;
Result.AutoSize := False;
Result.SetBounds(AX, AY, AW, 28);
Result.Alignment := taCenter;
Result.Layout := tlCenter;
Result.WordWrap := True;
Result.Font.Color := CLR_TEXTDIM;
Result.Font.Name := UI_FONT;
Result.Font.Size := 9;
end;
begin
FLoading := True;
try
// --- Заголовок страницы ---
L := TLabel.Create(Self);
L.Parent := FPageXvtr;
L.Caption := 'Transverters';
L.SetBounds(MARGIN, 18, 500, 28);
L.Font.Name := UI_FONT; L.Font.Size := 16;
L.Font.Style := [fsBold]; L.Font.Color := CLR_TEXT;
L := TLabel.Create(Self);
L.Parent := FPageXvtr;
L.Caption := 'Configure up to 8 transverter (XVTR) bands. When a band is selected on the main form, ' +
'the VFO shows the displayed (RF) frequency.' + LineEnding +
'The radio is internally tuned to the IF: f_IF = f_displayed - LO + LO Error.';
L.SetBounds(MARGIN, 50, GRP_W, 34);
L.Font.Name := UI_FONT; L.Font.Size := 9; L.Font.Color := CLR_TEXTDIM;
L.WordWrap := True;
// --- Group "Slots" ---
GrpY := 100;
Grp := MakeGroupPanel(FPageXvtr, 'Slots',
MARGIN, GrpY, GRP_W, GRP_TITLE_H + HDR_H + CFG_XVTR_COUNT * ROW_H + 14);
// Заголовки колонок (под GRP_TITLE_H + небольшой отступ)
Y := GRP_TITLE_H + 6;
MakeHdr(Grp, 'EN', EN_X + 2, Y, 28);
MakeHdr(Grp, 'Label', NAME_X, Y, NAME_W);
MakeHdr(Grp, 'Begin' + LineEnding + '(MHz)', BEG_X, Y, BEG_W);
MakeHdr(Grp, 'End' + LineEnding + '(MHz)', END_X, Y, END_W);
MakeHdr(Grp, 'LO' + LineEnding + '(MHz)', LO_X, Y, LO_W);
MakeHdr(Grp, 'LO Error' + LineEnding + 'Hz', ERR_X, Y, ERR_W);
MakeHdr(Grp, 'RX Gain' + LineEnding + 'dB', GAIN_X, Y, GAIN_W);
MakeHdr(Grp, 'RX' + LineEnding + 'Only', RXO_X, Y, RXO_W);
MakeHdr(Grp, 'TX Pwr' + LineEnding + '%', PWR_X, Y, PWR_W);
MakeHdr(Grp, 'XVTR' + LineEnding + 'RF', RF_X, Y, RF_W);
MakeHdr(Grp, 'Disable' + LineEnding + 'PA', PA_X, Y, PA_W);
MakeHdr(Grp, 'RX' + LineEnding + 'Antenna', ANT_X, Y, ANT_W);
// Ряды слотов
for i := 0 to CFG_XVTR_COUNT - 1 do
begin
Y := GRP_TITLE_H + HDR_H + i * ROW_H + 2;
// Чекбокс EN центрируем по 24px ширине EN-колонки
FXvtrEn[i] := MakeChk(Grp, EN_X + 5, Y + 2);
FXvtrName[i] := MakeEdt(Grp, NAME_X, Y, NAME_W);
FXvtrBegin[i] := MakeFlt(Grp, BEG_X, Y, BEG_W, 0.0, 50000.0, 0.001, 3);
FXvtrEnd[i] := MakeFlt(Grp, END_X, Y, END_W, 0.0, 50000.0, 0.001, 3);
FXvtrLOOff[i] := MakeFlt(Grp, LO_X, Y, LO_W, -50000.0, 50000.0, 0.001, 6);
FXvtrLOErr[i] := MakeFlt(Grp, ERR_X, Y, ERR_W, -99999.0, 99999.0, 1.0, 0);
FXvtrRXGain[i]:= MakeFlt(Grp, GAIN_X, Y, GAIN_W, -60.0, 60.0, 0.1, 1);
// Чекбокс RX Only центрируем
FXvtrRXOnly[i]:= MakeChk(Grp, RXO_X + (RXO_W - 22) div 2, Y + 2);
FXvtrPower[i] := MakeInt(Grp, PWR_X, Y, PWR_W, 0, 100);
FXvtrRF[i] := MakeChk(Grp, RF_X + (RF_W - 22) div 2, Y + 2);
FXvtrDisPA[i] := MakeChk(Grp, PA_X + (PA_W - 22) div 2, Y + 2);
FXvtrRXAnt[i] := MakeCmb(Grp, ANT_X, Y, ANT_W);
FXvtrRXAnt[i].Items.Add('default');
FXvtrRXAnt[i].Items.Add('ANT1');
FXvtrRXAnt[i].Items.Add('ANT2');
FXvtrRXAnt[i].Items.Add('ANT3');
FXvtrRXAnt[i].ItemIndex := 0;
FXvtrEn[i].Tag := i;
FXvtrEn[i].OnClick := OnXvtrAnyChange;
end;
// --- Group "Options" ---
GrpY := GrpY + GRP_TITLE_H + HDR_H + CFG_XVTR_COUNT * ROW_H + 14 + 18;
GrpOpts := MakeGroupPanel(FPageXvtr, 'Options',
MARGIN, GrpY, GRP_W, GRP_TITLE_H + 36);
FChkXvtrTunPwr := TCheckBox.Create(Self);
FChkXvtrTunPwr.Parent := GrpOpts;
FChkXvtrTunPwr.Caption := 'Use XVTR power for TUN '
+ '(TUN button uses the per-slot TX Power instead of the global TUN Level)';
FChkXvtrTunPwr.SetBounds(14, GRP_TITLE_H + 8, GRP_W - 28, 20);
FChkXvtrTunPwr.Font.Color := CLR_TEXT;
FChkXvtrTunPwr.Font.Name := UI_FONT;
FChkXvtrTunPwr.Font.Size := 9;
FChkXvtrTunPwr.OnChange := OnXvtrAnyChange;
// --- Подсказка про колонки внизу ---
L := TLabel.Create(Self);
L.Parent := FPageXvtr;
L.Caption :=
'EN - enable the slot. Label - band button caption. Begin/End - displayed VFO range. ' +
'LO - local oscillator. LO Error - fine correction in Hz.' + LineEnding +
'RX Gain - transverter RX gain offset. RX Only - block TX. TX Pwr - drive limit. ' +
'XVTR RF - use the main HF RF path.' + LineEnding +
'Disable PA - keep the internal PA in RX position during TX. ' +
'RX Antenna - override the per-band antenna for this slot.';
L.SetBounds(MARGIN, GrpY + GRP_TITLE_H + 36 + 16, GRP_W, 52);
L.Font.Name := UI_FONT; L.Font.Size := 9; L.Font.Color := CLR_TEXTDIM;
L.WordWrap := True;
// Применяем начальное состояние disabled-строк
for i := 0 to CFG_XVTR_COUNT - 1 do
UpdateXvtrRowEnabled(i);
finally
FLoading := False;
end;
end;
procedure TSettingsForm.UpdateXvtrRowEnabled(Idx: Integer);
var En: Boolean;
begin
if (Idx < 0) or (Idx >= CFG_XVTR_COUNT) then Exit;
if FXvtrEn[Idx] = nil then Exit;
En := FXvtrEn[Idx].Checked;
if FXvtrName[Idx] <> nil then FXvtrName[Idx].Enabled := En;
if FXvtrBegin[Idx] <> nil then FXvtrBegin[Idx].Enabled := En;
if FXvtrEnd[Idx] <> nil then FXvtrEnd[Idx].Enabled := En;
if FXvtrLOOff[Idx] <> nil then FXvtrLOOff[Idx].Enabled := En;
if FXvtrLOErr[Idx] <> nil then FXvtrLOErr[Idx].Enabled := En;
if FXvtrRXGain[Idx]<> nil then FXvtrRXGain[Idx].Enabled:= En;
if FXvtrRXOnly[Idx]<> nil then FXvtrRXOnly[Idx].Enabled:= En;
if FXvtrPower[Idx] <> nil then FXvtrPower[Idx].Enabled := En;
if FXvtrRF[Idx] <> nil then FXvtrRF[Idx].Enabled := En;
if FXvtrDisPA[Idx] <> nil then FXvtrDisPA[Idx].Enabled := En;
if FXvtrRXAnt[Idx] <> nil then FXvtrRXAnt[Idx].Enabled := En;
end;
procedure TSettingsForm.CollectXvtrFromUI;
var i: Integer;
begin
for i := 0 to CFG_XVTR_COUNT - 1 do
begin
if FXvtrEn[i] <> nil then FXvtrConfig.Entries[i].Enabled := FXvtrEn[i].Checked;
if FXvtrName[i] <> nil then FXvtrConfig.Entries[i].ButtonText := Copy(FXvtrName[i].Text, 1, 15);
if FXvtrBegin[i] <> nil then FXvtrConfig.Entries[i].FreqBegin := FXvtrBegin[i].Value * 1.0e6;
if FXvtrEnd[i] <> nil then FXvtrConfig.Entries[i].FreqEnd := FXvtrEnd[i].Value * 1.0e6;
if FXvtrLOOff[i] <> nil then FXvtrConfig.Entries[i].LOOffset := FXvtrLOOff[i].Value * 1.0e6;
if FXvtrLOErr[i] <> nil then FXvtrConfig.Entries[i].LOError := FXvtrLOErr[i].Value;
if FXvtrRXGain[i]<> nil then FXvtrConfig.Entries[i].RXGain := FXvtrRXGain[i].Value;
if FXvtrRXOnly[i]<> nil then FXvtrConfig.Entries[i].RXOnly := FXvtrRXOnly[i].Checked;
if FXvtrPower[i] <> nil then FXvtrConfig.Entries[i].TXPower := FXvtrPower[i].Value;
if FXvtrRF[i] <> nil then FXvtrConfig.Entries[i].XVTRRF := FXvtrRF[i].Checked;
if FXvtrDisPA[i] <> nil then FXvtrConfig.Entries[i].DisablePA := FXvtrDisPA[i].Checked;
if (FXvtrRXAnt[i]<> nil) and (FXvtrRXAnt[i].ItemIndex >= 0) then
FXvtrConfig.Entries[i].RXAntenna := Byte(FXvtrRXAnt[i].ItemIndex);
end;
if FChkXvtrTunPwr <> nil then
FXvtrConfig.UseXVTRTunePower := FChkXvtrTunPwr.Checked;
end;
procedure TSettingsForm.FireXvtrChange;
var i: Integer;
begin
if FLoading then Exit;
CollectXvtrFromUI;
// Перерисовать enabled-состояние строк (если изменилась галка En)
for i := 0 to CFG_XVTR_COUNT - 1 do
UpdateXvtrRowEnabled(i);
if Assigned(FOnXvtrChange) then
FOnXvtrChange(FXvtrConfig);
end;
procedure TSettingsForm.OnXvtrAnyChange(Sender: TObject);
begin
FireXvtrChange;
end;
procedure TSettingsForm.LoadXvtrSettings(const X: TXvtrSettings);
var i: Integer;
begin
FLoading := True;
try
FXvtrConfig := X;
for i := 0 to CFG_XVTR_COUNT - 1 do
begin
if FXvtrEn[i] <> nil then FXvtrEn[i].Checked := X.Entries[i].Enabled;
if FXvtrName[i] <> nil then FXvtrName[i].Text := X.Entries[i].ButtonText;
if FXvtrBegin[i] <> nil then FXvtrBegin[i].Value := X.Entries[i].FreqBegin / 1.0e6;
if FXvtrEnd[i] <> nil then FXvtrEnd[i].Value := X.Entries[i].FreqEnd / 1.0e6;
if FXvtrLOOff[i] <> nil then FXvtrLOOff[i].Value := X.Entries[i].LOOffset / 1.0e6;
if FXvtrLOErr[i] <> nil then FXvtrLOErr[i].Value := X.Entries[i].LOError;
if FXvtrRXGain[i]<> nil then FXvtrRXGain[i].Value := X.Entries[i].RXGain;
if FXvtrRXOnly[i]<> nil then FXvtrRXOnly[i].Checked:= X.Entries[i].RXOnly;
if FXvtrPower[i] <> nil then FXvtrPower[i].Value := X.Entries[i].TXPower;
if FXvtrRF[i] <> nil then FXvtrRF[i].Checked := X.Entries[i].XVTRRF;
if FXvtrDisPA[i] <> nil then FXvtrDisPA[i].Checked := X.Entries[i].DisablePA;
if FXvtrRXAnt[i] <> nil then
FXvtrRXAnt[i].ItemIndex := EnsureRange(X.Entries[i].RXAntenna, 0, 3);
UpdateXvtrRowEnabled(i);
end;
if FChkXvtrTunPwr <> nil then FChkXvtrTunPwr.Checked := X.UseXVTRTunePower;
finally
FLoading := False;
end;
end;
end.