Files
ewsdr/ChannelsForm.pas
T
ew8bakandClaude Opus 4.8 ba2a75bb04 feat(dsp): режимы WFM, DIGI-U и DIGI-L
WFM использует новые wfmd/wfmmod из нашего форка WDSP. Демодулятор —
квадратурный дискриминатор на dsp_rate, поэтому при штатных 48 кГц
девиация 75 кГц не помещается в полосу. В WFM цепь RXA поднимается до
WFMDSPRate (наибольший FAudioRate*2^k <= 192 кГц, делящий SampleRate
нацело), rsmpin децимирует вход сам. Выше 192 кГц смысла нет: энергия
демодулированного тона выходит на полку (33.5k при 192 кГц против 31.8k
при 576 кГц), а FFT дорожает.

Две ловушки WDSP, найденные по дороге:

  * firmin.c: nfor = nc / size, idxmask = nfor - 1. Все fircore создаются
    с nc = max(2048, dsp_size), поэтому смена dsp_size без RXASetNC даёт
    nfor = 0 — пустой массив планов и падение в fftw_execute.

  * fir.c/bandpass.c планируют с FFTW_PATIENT, а wisdom.c пишет в
    wdspWisdom00 только степени двойки. При SampleRate 576 кГц наивный
    dsp_size = in_size = 12288 требовал FFT на 24576 точек — FFTW мерил
    план 25 секунд. Отсюда потолок 192 кГц и dsp_size = степень двойки.

RXA и TXA гейтятся раздельно: рейты TXA (48 кГц вход, 192 кГц выход) от
SampleRate не зависят, поэтому wfmmod уходит на широкий dsp_rate даже
когда приёмник остался на 48 кГц.

Заодно исправлен WDSP_SAM: было 12, а RXA_SAM = 10. Раньше 12 было вне
диапазона и WDSP молча игнорировал (SAM работал как DSB); после появления
RXA_WFM = 12 это включало бы WFM-демодулятор вместо SAM. txaMode
расходится с rxaMode после DRM (WFM: RXA 12, TXA 14) — отсюда ModeToWDSPTX.

DIGI-U/DIGI-L получили свою таблицу фильтров (3.0k…300) и полосу от нуля,
а не от среза на 100 Гц, как у SSB. В CAT они замаплены на коды Kenwood 6
(FSK) и 9 (FSK-R) — раньше эти коды сваливались в USB/LSB.

Новые индексы дописаны в конец (DIGU=8, DIGL=9, WFM=10): индекс режима
уходит в settings.json, band-cache, CAT и web-API. В VfoOverlay пришлось
развести хит-коды — режимы кодировались как -(I+1) и налезли на HIT_NR/
HIT_NB/HIT_SNB, а полосы кодировались значением BW, и WFM-овские
120000/160000/200000 налезли на HIT_AGC_PICK_BASE/HIT_DEV_BASE.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 16:59:27 +03:00

621 lines
19 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 ChannelsForm;
{ TChannelsForm — редактор списка каналов (channel memory).
Открывается по правой кнопке мыши на кнопке CH в блоке RX левой панели.
Список каналов слева (FlatListBox), поля редактирования справа.
Все изменения сохраняются немедленно в TChannelStore и на диск. }
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, Math,
Forms, Controls, Graphics, StdCtrls, ExtCtrls,
FlatButton, FlatCheckBox, FlatComboBox, FlatEdit,
FlatSpinEdit, FlatFloatSpinEdit, FlatListBox,
AppTheme, ChannelStore;
const
// Копии из MainForm: нужны для выпадающих списков
CH_MODE_COUNT = 11;
CH_MODE_NAMES: array[0..CH_MODE_COUNT-1] of string =
('LSB','USB','DSB','CWL','CWU','FM','AM','SAM','DIGU','DIGL','WFM');
CH_CTCSS_COUNT = 38;
CH_CTCSS_NAMES: array[0..CH_CTCSS_COUNT-1] of string = (
'67.0', '71.9', '74.4', '77.0', '79.7', '82.5', '85.4', '88.5',
'91.5', '94.8', '97.4', '100.0', '103.5', '107.2', '110.9', '114.8',
'118.8', '123.0', '127.3', '131.8', '136.5', '141.3', '146.2', '151.4',
'156.7', '162.2', '167.9', '173.8', '179.9', '186.2', '192.8', '203.5',
'210.7', '218.1', '225.7', '233.6', '241.8', '250.3');
type
TChannelsForm = class(TForm)
private
// --- Левая панель ---
FPanelLeft: TPanel;
FBtnAdd: TFlatButton;
FBtnDelete: TFlatButton;
FBtnUp: TFlatButton;
FBtnDown: TFlatButton;
FList: TFlatListBox;
// --- Правая панель (поля редактирования) ---
FPanelRight: TPanel;
// Labels (для перекраски темой)
FLbls: array of TLabel;
// Поля
FEdGroup: TFlatEdit;
FEdName: TFlatEdit;
FSpinRXFreq: TFlatFloatSpinEdit;
FSpinTXFreq: TFlatFloatSpinEdit;
FCmbMode: TFlatComboBox;
FBtnRptNone: TFlatButton;
FBtnRptMinus: TFlatButton;
FBtnRptPlus: TFlatButton;
FSpinRptOffset: TFlatFloatSpinEdit;
FChkCTCSS: TFlatCheckBox;
FCmbCTCSS: TFlatComboBox;
FSpinPower: TFlatSpinEdit;
// Данные
FStore: TChannelStore;
FUpdating: Boolean;
FOnChanged: TNotifyEvent;
FCurrentDefaults: TChannel; // значения по умолчанию для новых каналов
procedure BuildUI;
procedure StyleBtn(B: TFlatButton; AActive: Boolean = False);
function MakeLabel(AParent: TWinControl; const Cap: string;
X, Y, W, H: Integer): TLabel;
procedure LoadChannelToFields(Idx: Integer);
procedure SaveFieldsToChannel;
procedure RefreshList(KeepIdx: Integer = -1);
function SelectedIdx: Integer;
// Handlers — список
procedure OnListClick(Sender: TObject);
procedure OnListDblClick(Sender: TObject);
// Handlers — тулбар
procedure OnAddClick(Sender: TObject);
procedure OnDeleteClick(Sender: TObject);
procedure OnUpClick(Sender: TObject);
procedure OnDownClick(Sender: TObject);
// Handlers — поля
procedure OnFieldChange(Sender: TObject);
procedure OnRptBtnClick(Sender: TObject);
procedure OnCTCSSChkChange(Sender: TObject);
procedure OnCTCSSCmbChange(Sender: TObject);
// Handlers — форма
procedure OnFormClose(Sender: TObject; var CloseAction: TCloseAction);
public
constructor Create(AOwner: TComponent; AStore: TChannelStore;
AOnChanged: TNotifyEvent); reintroduce;
procedure ApplyTheme(const T: TAppTheme);
procedure SetCurrentDefaults(const Ch: TChannel);
property OnChannelsChanged: TNotifyEvent read FOnChanged write FOnChanged;
end;
implementation
const
// Геометрия
LPANEL_W = 185; // ширина левой панели
TOOLBAR_H = 32; // высота тулбара с кнопками Add/Del/Up/Down
BTN_H = 24;
// Правая панель
LBL_X = 8;
LBL_W = 95;
CTRL_X = 106;
ROW_H = 30; // высота строки
CTRL_H = 24; // высота контрола
constructor TChannelsForm.Create(AOwner: TComponent; AStore: TChannelStore;
AOnChanged: TNotifyEvent);
begin
inherited CreateNew(AOwner);
FStore := AStore;
FOnChanged := AOnChanged;
FUpdating := False;
TChannelStore.DefaultChannel(FCurrentDefaults);
Caption := 'Channels';
BorderStyle := bsSizeable;
Width := 720;
Height := 480;
Position := poScreenCenter;
KeyPreview := True;
OnClose := @OnFormClose;
BuildUI;
ApplyTheme(DarkTheme);
RefreshList(0);
end;
procedure TChannelsForm.BuildUI;
var
i: Integer;
BtnW: Integer;
RW: Integer; // ширина правой панели
Y: Integer;
begin
// ----- Левая панель -----
FPanelLeft := TPanel.Create(Self);
FPanelLeft.Parent := Self;
FPanelLeft.Align := alLeft;
FPanelLeft.Width := LPANEL_W;
FPanelLeft.BevelOuter := bvNone;
BtnW := (LPANEL_W - 4 - 3 * 2) div 4; // 4 кнопки с зазорами
FBtnAdd := TFlatButton.Create(Self);
FBtnAdd.Parent := FPanelLeft;
FBtnAdd.SetBounds(2, 4, BtnW, BTN_H);
FBtnAdd.Caption := 'ADD';
FBtnAdd.OnClick := @OnAddClick;
FBtnDelete := TFlatButton.Create(Self);
FBtnDelete.Parent := FPanelLeft;
FBtnDelete.SetBounds(2 + (BtnW + 2), 4, BtnW, BTN_H);
FBtnDelete.Caption := 'DEL';
FBtnDelete.OnClick := @OnDeleteClick;
FBtnUp := TFlatButton.Create(Self);
FBtnUp.Parent := FPanelLeft;
FBtnUp.SetBounds(2 + 2 * (BtnW + 2), 4, BtnW, BTN_H);
FBtnUp.Caption := '↑';
FBtnUp.OnClick := @OnUpClick;
FBtnDown := TFlatButton.Create(Self);
FBtnDown.Parent := FPanelLeft;
FBtnDown.SetBounds(2 + 3 * (BtnW + 2), 4, BtnW, BTN_H);
FBtnDown.Caption := '↓';
FBtnDown.OnClick := @OnDownClick;
FList := TFlatListBox.Create(Self);
FList.Parent := FPanelLeft;
FList.SetBounds(0, TOOLBAR_H, LPANEL_W, FPanelLeft.Height - TOOLBAR_H);
FList.Anchors := [akLeft, akTop, akRight, akBottom];
FList.OnClick := @OnListClick;
FList.OnDblClick := @OnListDblClick;
// ----- Правая панель -----
FPanelRight := TPanel.Create(Self);
FPanelRight.Parent := Self;
FPanelRight.Align := alClient;
FPanelRight.BevelOuter := bvNone;
SetLength(FLbls, 0);
RW := Width - LPANEL_W;
Y := 8;
// Group
MakeLabel(FPanelRight, 'Group', LBL_X, Y + 1, LBL_W, CTRL_H);
FEdGroup := TFlatEdit.Create(Self);
FEdGroup.Parent := FPanelRight;
FEdGroup.SetBounds(CTRL_X, Y, RW - CTRL_X - 8, CTRL_H);
FEdGroup.Anchors := [akLeft, akTop, akRight];
FEdGroup.OnChange := @OnFieldChange;
Inc(Y, ROW_H);
// Name
MakeLabel(FPanelRight, 'Name', LBL_X, Y + 1, LBL_W, CTRL_H);
FEdName := TFlatEdit.Create(Self);
FEdName.Parent := FPanelRight;
FEdName.SetBounds(CTRL_X, Y, RW - CTRL_X - 8, CTRL_H);
FEdName.Anchors := [akLeft, akTop, akRight];
FEdName.OnChange := @OnFieldChange;
Inc(Y, ROW_H);
// RX Freq
MakeLabel(FPanelRight, 'RX Freq', LBL_X, Y + 1, LBL_W, CTRL_H);
FSpinRXFreq := TFlatFloatSpinEdit.Create(Self);
FSpinRXFreq.Parent := FPanelRight;
FSpinRXFreq.SetBounds(CTRL_X, Y, RW - CTRL_X - 50, CTRL_H);
FSpinRXFreq.Anchors := [akLeft, akTop, akRight];
FSpinRXFreq.MinValue := 0.001;
FSpinRXFreq.MaxValue := 2000.0;
FSpinRXFreq.Increment := 0.001;
FSpinRXFreq.DecimalPlaces := 6;
FSpinRXFreq.Value := 145.5;
FSpinRXFreq.OnChange := @OnFieldChange;
MakeLabel(FPanelRight, 'MHz', RW - 46, Y + 1, 40, CTRL_H);
Inc(Y, ROW_H);
// TX Freq
MakeLabel(FPanelRight, 'TX Freq', LBL_X, Y + 1, LBL_W, CTRL_H);
FSpinTXFreq := TFlatFloatSpinEdit.Create(Self);
FSpinTXFreq.Parent := FPanelRight;
FSpinTXFreq.SetBounds(CTRL_X, Y, RW - CTRL_X - 50, CTRL_H);
FSpinTXFreq.Anchors := [akLeft, akTop, akRight];
FSpinTXFreq.MinValue := 0.001;
FSpinTXFreq.MaxValue := 2000.0;
FSpinTXFreq.Increment := 0.001;
FSpinTXFreq.DecimalPlaces := 6;
FSpinTXFreq.Value := 145.5;
FSpinTXFreq.OnChange := @OnFieldChange;
MakeLabel(FPanelRight, 'MHz', RW - 46, Y + 1, 40, CTRL_H);
Inc(Y, ROW_H);
// Mode
MakeLabel(FPanelRight, 'Mode', LBL_X, Y + 1, LBL_W, CTRL_H);
FCmbMode := TFlatComboBox.Create(Self);
FCmbMode.Parent := FPanelRight;
FCmbMode.SetBounds(CTRL_X, Y, 160, CTRL_H);
for i := 0 to CH_MODE_COUNT - 1 do FCmbMode.Items.Add(CH_MODE_NAMES[i]);
FCmbMode.ItemIndex := 5; // FM default
FCmbMode.OnChange := @OnFieldChange;
Inc(Y, ROW_H);
// RPTR direction
MakeLabel(FPanelRight, 'RPTR', LBL_X, Y + 1, LBL_W, CTRL_H);
FBtnRptNone := TFlatButton.Create(Self);
FBtnRptNone.Parent := FPanelRight;
FBtnRptNone.SetBounds(CTRL_X, Y, 55, CTRL_H);
FBtnRptNone.Caption := 'NONE';
FBtnRptNone.Tag := 0;
FBtnRptNone.OnClick := @OnRptBtnClick;
FBtnRptMinus := TFlatButton.Create(Self);
FBtnRptMinus.Parent := FPanelRight;
FBtnRptMinus.SetBounds(CTRL_X + 58, Y, 38, CTRL_H);
FBtnRptMinus.Caption := '';
FBtnRptMinus.Tag := 1;
FBtnRptMinus.OnClick := @OnRptBtnClick;
FBtnRptPlus := TFlatButton.Create(Self);
FBtnRptPlus.Parent := FPanelRight;
FBtnRptPlus.SetBounds(CTRL_X + 99, Y, 38, CTRL_H);
FBtnRptPlus.Caption := '+';
FBtnRptPlus.Tag := 2;
FBtnRptPlus.OnClick := @OnRptBtnClick;
Inc(Y, ROW_H);
// RPTR Offset
MakeLabel(FPanelRight, 'RPT Offset', LBL_X, Y + 1, LBL_W, CTRL_H);
FSpinRptOffset := TFlatFloatSpinEdit.Create(Self);
FSpinRptOffset.Parent := FPanelRight;
FSpinRptOffset.SetBounds(CTRL_X, Y, RW - CTRL_X - 50, CTRL_H);
FSpinRptOffset.Anchors := [akLeft, akTop, akRight];
FSpinRptOffset.MinValue := 0.0;
FSpinRptOffset.MaxValue := 100.0;
FSpinRptOffset.Increment := 0.025;
FSpinRptOffset.DecimalPlaces := 3;
FSpinRptOffset.Value := 0.6;
FSpinRptOffset.OnChange := @OnFieldChange;
MakeLabel(FPanelRight, 'MHz', RW - 46, Y + 1, 40, CTRL_H);
Inc(Y, ROW_H);
// CTCSS
MakeLabel(FPanelRight, 'CTCSS', LBL_X, Y + 1, LBL_W, CTRL_H);
FChkCTCSS := TFlatCheckBox.Create(Self);
FChkCTCSS.Parent := FPanelRight;
FChkCTCSS.SetBounds(CTRL_X, Y, 26, CTRL_H);
FChkCTCSS.Caption := '';
FChkCTCSS.Checked := False;
FChkCTCSS.OnChange := @OnCTCSSChkChange;
FCmbCTCSS := TFlatComboBox.Create(Self);
FCmbCTCSS.Parent := FPanelRight;
FCmbCTCSS.SetBounds(CTRL_X + 30, Y, RW - CTRL_X - 38, CTRL_H);
FCmbCTCSS.Anchors := [akLeft, akTop, akRight];
for i := 0 to CH_CTCSS_COUNT - 1 do FCmbCTCSS.Items.Add(CH_CTCSS_NAMES[i]);
FCmbCTCSS.ItemIndex := 0;
FCmbCTCSS.OnChange := @OnCTCSSCmbChange;
Inc(Y, ROW_H);
// Power
MakeLabel(FPanelRight, 'Power', LBL_X, Y + 1, LBL_W, CTRL_H);
FSpinPower := TFlatSpinEdit.Create(Self);
FSpinPower.Parent := FPanelRight;
FSpinPower.SetBounds(CTRL_X, Y, 80, CTRL_H);
FSpinPower.MinValue := 0;
FSpinPower.MaxValue := 100;
FSpinPower.Value := 100;
FSpinPower.OnChange := @OnFieldChange;
MakeLabel(FPanelRight, '%', CTRL_X + 84, Y + 1, 20, CTRL_H);
end;
function TChannelsForm.MakeLabel(AParent: TWinControl; const Cap: string;
X, Y, W, H: Integer): TLabel;
var L: TLabel;
begin
L := TLabel.Create(Self);
L.Parent := AParent;
L.Caption := Cap;
L.SetBounds(X, Y, W, H);
L.Font.Name := 'Courier New';
L.Font.Size := 8;
L.Font.Color := DarkTheme.TextDim;
// Сохраняем для последующей перекраски темой
SetLength(FLbls, Length(FLbls) + 1);
FLbls[High(FLbls)] := L;
Result := L;
end;
procedure TChannelsForm.StyleBtn(B: TFlatButton; AActive: Boolean);
var T: TAppTheme;
begin
T := DarkTheme;
B.Active := AActive;
B.ClrNorm := T.BtnNorm;
B.ClrActive := T.BtnActive;
B.ClrHot := T.BtnHot;
B.ClrBorder := IfThen(AActive, T.BtnBorderActive, T.BtnBorderNorm);
B.ClrText := T.BtnText;
B.ClrTextAct := T.BtnTextActive;
end;
procedure TChannelsForm.ApplyTheme(const T: TAppTheme);
var
i: Integer;
Ch: TChannel;
begin
Color := T.Panel;
FPanelLeft.Color := T.Panel;
FPanelRight.Color := T.Panel;
FList.SetAppTheme(T);
StyleBtn(FBtnAdd);
StyleBtn(FBtnDelete);
StyleBtn(FBtnUp);
StyleBtn(FBtnDown);
// RPT buttons — пересчитываем Active из текущего состояния
if SelectedIdx >= 0 then
begin
Ch := FStore.GetChannel(SelectedIdx);
StyleBtn(FBtnRptNone, Ch.RptDir = 0);
StyleBtn(FBtnRptMinus, Ch.RptDir = 1);
StyleBtn(FBtnRptPlus, Ch.RptDir = 2);
end
else
begin
StyleBtn(FBtnRptNone, True);
StyleBtn(FBtnRptMinus, False);
StyleBtn(FBtnRptPlus, False);
end;
FEdGroup.SetAppTheme(T);
FEdName.SetAppTheme(T);
FSpinRXFreq.SetAppTheme(T);
FSpinTXFreq.SetAppTheme(T);
FCmbMode.SetAppTheme(T);
FSpinRptOffset.SetAppTheme(T);
FChkCTCSS.SetAppTheme(T);
FCmbCTCSS.SetAppTheme(T);
FSpinPower.SetAppTheme(T);
for i := 0 to High(FLbls) do
begin
FLbls[i].Font.Color := T.TextDim;
FLbls[i].Color := T.Panel;
FLbls[i].ParentColor := False;
end;
end;
// ---------------------------------------------------------------------------
// Список каналов
// ---------------------------------------------------------------------------
function TChannelsForm.SelectedIdx: Integer;
begin
Result := FList.ItemIndex;
end;
procedure TChannelsForm.RefreshList(KeepIdx: Integer);
var
i, NewIdx: Integer;
Ch: TChannel;
begin
FList.Items.Clear;
for i := 0 to FStore.Count - 1 do
begin
Ch := FStore.GetChannel(i);
if Ch.Group <> '' then
FList.Items.Add(Ch.Group + ': ' + Ch.Name)
else
FList.Items.Add(Ch.Name);
end;
if FStore.Count = 0 then
begin
LoadChannelToFields(-1);
Exit;
end;
NewIdx := EnsureRange(KeepIdx, 0, FStore.Count - 1);
FList.ItemIndex := NewIdx;
LoadChannelToFields(NewIdx);
end;
procedure TChannelsForm.LoadChannelToFields(Idx: Integer);
var Ch: TChannel;
HasSel: Boolean;
begin
FUpdating := True;
try
HasSel := (Idx >= 0) and (Idx < FStore.Count);
FEdGroup.Enabled := HasSel;
FEdName.Enabled := HasSel;
FSpinRXFreq.Enabled := HasSel;
FSpinTXFreq.Enabled := HasSel;
FCmbMode.Enabled := HasSel;
FBtnRptNone.Enabled := HasSel;
FBtnRptMinus.Enabled := HasSel;
FBtnRptPlus.Enabled := HasSel;
FSpinRptOffset.Enabled := HasSel;
FChkCTCSS.Enabled := HasSel;
FCmbCTCSS.Enabled := HasSel;
FSpinPower.Enabled := HasSel;
if not HasSel then Exit;
Ch := FStore.GetChannel(Idx);
FEdGroup.Text := Ch.Group;
FEdName.Text := Ch.Name;
FSpinRXFreq.Value := Ch.RXFreq / 1000000.0;
FSpinTXFreq.Value := Ch.TXFreq / 1000000.0;
FCmbMode.ItemIndex := EnsureRange(Ch.Mode, 0, CH_MODE_COUNT - 1);
StyleBtn(FBtnRptNone, Ch.RptDir = 0);
StyleBtn(FBtnRptMinus, Ch.RptDir = 1);
StyleBtn(FBtnRptPlus, Ch.RptDir = 2);
FSpinRptOffset.Value := Ch.RptOffsetHz / 1000000.0;
FChkCTCSS.Checked := Ch.CTCSSOn;
FCmbCTCSS.ItemIndex := EnsureRange(Ch.CTCSSToneIdx, 0, CH_CTCSS_COUNT - 1);
FSpinPower.Value := Ch.Power;
finally
FUpdating := False;
end;
end;
procedure TChannelsForm.SaveFieldsToChannel;
var
Idx: Integer;
Ch: TChannel;
i: Integer;
begin
if FUpdating then Exit;
Idx := SelectedIdx;
if (Idx < 0) or (Idx >= FStore.Count) then Exit;
Ch := FStore.GetChannel(Idx);
Ch.Group := FEdGroup.Text;
Ch.Name := FEdName.Text;
Ch.RXFreq := FSpinRXFreq.Value * 1000000.0;
Ch.TXFreq := FSpinTXFreq.Value * 1000000.0;
Ch.Mode := EnsureRange(FCmbMode.ItemIndex, 0, CH_MODE_COUNT - 1);
// RptDir берётся из кнопок (Active)
if FBtnRptMinus.Active then Ch.RptDir := 1
else if FBtnRptPlus.Active then Ch.RptDir := 2
else Ch.RptDir := 0;
Ch.RptOffsetHz := FSpinRptOffset.Value * 1000000.0;
Ch.CTCSSOn := FChkCTCSS.Checked;
Ch.CTCSSToneIdx := EnsureRange(FCmbCTCSS.ItemIndex, 0, CH_CTCSS_COUNT - 1);
Ch.Power := FSpinPower.Value;
FStore.UpdateChannel(Idx, Ch);
FStore.Save;
// Обновляем имя в списке
FUpdating := True;
try
if Ch.Group <> '' then
FList.Items[Idx] := Ch.Group + ': ' + Ch.Name
else
FList.Items[Idx] := Ch.Name;
finally
FUpdating := False;
end;
if Assigned(FOnChanged) then FOnChanged(Self);
end;
// ---------------------------------------------------------------------------
// Обработчики кнопок тулбара
// ---------------------------------------------------------------------------
procedure TChannelsForm.OnListClick(Sender: TObject);
begin
if FUpdating then Exit;
LoadChannelToFields(FList.ItemIndex);
end;
procedure TChannelsForm.OnListDblClick(Sender: TObject);
begin
LoadChannelToFields(FList.ItemIndex);
end;
procedure TChannelsForm.OnAddClick(Sender: TObject);
var
Ch: TChannel;
NewIdx: Integer;
begin
Ch := FCurrentDefaults; // берём значения текущего состояния радио
FStore.AddChannel(Ch);
FStore.Save;
NewIdx := FStore.Count - 1;
RefreshList(NewIdx);
if Assigned(FOnChanged) then FOnChanged(Self);
// Фокус на имя для быстрого переименования
if FEdName.Enabled then FEdName.SetFocus;
FEdName.SelectAll;
end;
procedure TChannelsForm.OnDeleteClick(Sender: TObject);
var Idx: Integer;
begin
Idx := SelectedIdx;
if (Idx < 0) or (Idx >= FStore.Count) then Exit;
FStore.DeleteChannel(Idx);
FStore.Save;
RefreshList(EnsureRange(Idx, 0, FStore.Count - 1));
if Assigned(FOnChanged) then FOnChanged(Self);
end;
procedure TChannelsForm.OnUpClick(Sender: TObject);
var Idx: Integer;
begin
Idx := SelectedIdx;
if Idx < 1 then Exit;
FStore.MoveUp(Idx);
FStore.Save;
RefreshList(Idx - 1);
if Assigned(FOnChanged) then FOnChanged(Self);
end;
procedure TChannelsForm.OnDownClick(Sender: TObject);
var Idx: Integer;
begin
Idx := SelectedIdx;
if (Idx < 0) or (Idx >= FStore.Count - 1) then Exit;
FStore.MoveDown(Idx);
FStore.Save;
RefreshList(Idx + 1);
if Assigned(FOnChanged) then FOnChanged(Self);
end;
// ---------------------------------------------------------------------------
// Обработчики полей редактирования
// ---------------------------------------------------------------------------
procedure TChannelsForm.OnFieldChange(Sender: TObject);
begin
SaveFieldsToChannel;
end;
procedure TChannelsForm.OnRptBtnClick(Sender: TObject);
var Dir: Integer;
begin
Dir := (Sender as TFlatButton).Tag;
StyleBtn(FBtnRptNone, Dir = 0);
StyleBtn(FBtnRptMinus, Dir = 1);
StyleBtn(FBtnRptPlus, Dir = 2);
SaveFieldsToChannel;
end;
procedure TChannelsForm.OnCTCSSChkChange(Sender: TObject);
begin
SaveFieldsToChannel;
end;
procedure TChannelsForm.OnCTCSSCmbChange(Sender: TObject);
begin
SaveFieldsToChannel;
end;
procedure TChannelsForm.OnFormClose(Sender: TObject; var CloseAction: TCloseAction);
begin
CloseAction := caHide; // скрываем, не уничтожаем — указатель в MainForm остаётся валидным
end;
procedure TChannelsForm.SetCurrentDefaults(const Ch: TChannel);
begin
FCurrentDefaults := Ch;
end;
end.