mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
776 lines
24 KiB
ObjectPascal
776 lines
24 KiB
ObjectPascal
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, RadioModes, DpiUtils;
|
||
|
||
const
|
||
// Копии из MainForm: нужны для выпадающих списков
|
||
CH_MODE_COUNT = RadioModes.MODE_COUNT;
|
||
CH_MODE_NAMES: array[0..CH_MODE_COUNT-1] of string =
|
||
('LSB','USB','DSB','CWL','CWU','FM','AM','SAM','DIGU','DIGL','WFM','DMR','FM RAW');
|
||
|
||
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;
|
||
FBtnClose: TFlatButton;
|
||
FList: TFlatListBox;
|
||
|
||
// --- Правая панель (поля редактирования) ---
|
||
FPanelRight: TPanel;
|
||
FFooterPanel: TPanel;
|
||
FCardIdentity: TPanel;
|
||
FCardRadio: TPanel;
|
||
FCardRepeater: TPanel;
|
||
FLeftTitle: TLabel;
|
||
FLeftHint: TLabel;
|
||
FRightTitle: TLabel;
|
||
FRightHint: TLabel;
|
||
|
||
// 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; // значения по умолчанию для новых каналов
|
||
FTheme: TAppTheme;
|
||
|
||
procedure BuildUI;
|
||
procedure StyleBtn(B: TFlatButton; AActive: Boolean = False);
|
||
function MakeLabel(AParent: TWinControl; const Cap: string;
|
||
X, Y, W, H: Integer): TLabel;
|
||
function MakeGroupPanel(const Cap: string; Y, H: Integer): TPanel;
|
||
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 OnCloseClick(Sender: TObject);
|
||
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 = 240;
|
||
TOOLBAR_Y = 66;
|
||
TOOLBAR_H = 30;
|
||
BTN_H = 26;
|
||
FORM_W = 820;
|
||
FORM_H = 540;
|
||
PAGE_MARGIN = 18;
|
||
|
||
// Правая панель
|
||
LBL_X = 14;
|
||
LBL_W = 106;
|
||
CTRL_X = 126;
|
||
ROW_H = 28;
|
||
CTRL_H = 24;
|
||
|
||
constructor TChannelsForm.Create(AOwner: TComponent; AStore: TChannelStore;
|
||
AOnChanged: TNotifyEvent);
|
||
var
|
||
WorkArea: TRect;
|
||
MaxW, MaxH: Integer;
|
||
begin
|
||
inherited CreateNew(AOwner);
|
||
FStore := AStore;
|
||
FOnChanged := AOnChanged;
|
||
FUpdating := False;
|
||
FTheme := DarkTheme;
|
||
TChannelStore.DefaultChannel(FCurrentDefaults);
|
||
|
||
Scaled := False;
|
||
Caption := 'Channels';
|
||
BorderStyle := bsSizeable;
|
||
if AOwner is TCustomForm then
|
||
begin
|
||
WorkArea := TCustomForm(AOwner).BoundsRect;
|
||
WorkArea := Screen.MonitorFromRect(WorkArea).WorkareaRect;
|
||
Position := poOwnerFormCenter;
|
||
end
|
||
else
|
||
begin
|
||
WorkArea := Screen.PrimaryMonitor.WorkareaRect;
|
||
Position := poScreenCenter;
|
||
end;
|
||
MaxW := WorkArea.Right - WorkArea.Left - DpiScale(48);
|
||
MaxH := WorkArea.Bottom - WorkArea.Top - DpiScale(48);
|
||
Width := Min(DpiScale(FORM_W), MaxW);
|
||
Height := Min(DpiScale(FORM_H), MaxH);
|
||
Constraints.MinWidth := Min(DpiScale(680), Width);
|
||
Constraints.MinHeight := Min(DpiScale(530), Height);
|
||
KeyPreview := True;
|
||
OnClose := @OnFormClose;
|
||
|
||
BuildUI;
|
||
ApplyTheme(DarkTheme);
|
||
RefreshList(0);
|
||
end;
|
||
|
||
procedure TChannelsForm.BuildUI;
|
||
var
|
||
i, BtnW, CardW, Y: Integer;
|
||
L: TLabel;
|
||
|
||
procedure PlaceRightUnit(AParent: TWinControl; const Cap: string;
|
||
ATop: Integer);
|
||
begin
|
||
L := MakeLabel(AParent, Cap, 0, ATop + 1, 42, CTRL_H);
|
||
L.SetBounds(AParent.ClientWidth - DpiScale(50), DpiScale(ATop + 1),
|
||
DpiScale(42), DpiScale(CTRL_H));
|
||
L.Anchors := [akTop, akRight];
|
||
end;
|
||
|
||
begin
|
||
SetLength(FLbls, 0);
|
||
|
||
// ----- Левая панель -----
|
||
FPanelLeft := TPanel.Create(Self);
|
||
FPanelLeft.Parent := Self;
|
||
FPanelLeft.Align := alLeft;
|
||
FPanelLeft.Width := DpiScale(LPANEL_W);
|
||
FPanelLeft.BevelOuter := bvNone;
|
||
|
||
FLeftTitle := MakeLabel(FPanelLeft, 'CHANNEL MEMORY',
|
||
14, 12, LPANEL_W - 28, 24);
|
||
FLeftTitle.Font.Size := 9;
|
||
FLeftTitle.Font.Style := [fsBold];
|
||
FLeftTitle.Font.Color := FTheme.Text;
|
||
|
||
FLeftHint := MakeLabel(FPanelLeft, 'Saved frequencies and modes',
|
||
14, 37, LPANEL_W - 28, 18);
|
||
FLeftHint.Font.Size := 8;
|
||
|
||
BtnW := (DpiScale(LPANEL_W - 24) - 3 * DpiScale(6)) div 4;
|
||
FBtnAdd := TFlatButton.Create(Self);
|
||
FBtnAdd.Parent := FPanelLeft;
|
||
FBtnAdd.SetBounds(DpiScale(12), DpiScale(TOOLBAR_Y),
|
||
BtnW, DpiScale(BTN_H));
|
||
FBtnAdd.Caption := 'ADD';
|
||
FBtnAdd.OnClick := @OnAddClick;
|
||
|
||
FBtnDelete := TFlatButton.Create(Self);
|
||
FBtnDelete.Parent := FPanelLeft;
|
||
FBtnDelete.SetBounds(FBtnAdd.Left + BtnW + DpiScale(6),
|
||
FBtnAdd.Top, BtnW, FBtnAdd.Height);
|
||
FBtnDelete.Caption := 'DEL';
|
||
FBtnDelete.OnClick := @OnDeleteClick;
|
||
|
||
FBtnUp := TFlatButton.Create(Self);
|
||
FBtnUp.Parent := FPanelLeft;
|
||
FBtnUp.SetBounds(FBtnDelete.Left + BtnW + DpiScale(6),
|
||
FBtnAdd.Top, BtnW, FBtnAdd.Height);
|
||
FBtnUp.Caption := '↑';
|
||
FBtnUp.OnClick := @OnUpClick;
|
||
|
||
FBtnDown := TFlatButton.Create(Self);
|
||
FBtnDown.Parent := FPanelLeft;
|
||
FBtnDown.SetBounds(FBtnUp.Left + BtnW + DpiScale(6),
|
||
FBtnAdd.Top, BtnW, FBtnAdd.Height);
|
||
FBtnDown.Caption := '↓';
|
||
FBtnDown.OnClick := @OnDownClick;
|
||
|
||
FList := TFlatListBox.Create(Self);
|
||
FList.Parent := FPanelLeft;
|
||
FList.SetBounds(DpiScale(12), DpiScale(TOOLBAR_Y + TOOLBAR_H + 10),
|
||
FPanelLeft.ClientWidth - DpiScale(24),
|
||
FPanelLeft.ClientHeight - DpiScale(TOOLBAR_Y + TOOLBAR_H + 22));
|
||
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;
|
||
|
||
FFooterPanel := TPanel.Create(Self);
|
||
FFooterPanel.Parent := FPanelRight;
|
||
FFooterPanel.Align := alBottom;
|
||
FFooterPanel.Height := DpiScale(50);
|
||
FFooterPanel.BevelOuter := bvNone;
|
||
|
||
FBtnClose := TFlatButton.Create(Self);
|
||
FBtnClose.Parent := FFooterPanel;
|
||
FBtnClose.Caption := 'Close';
|
||
FBtnClose.SetBounds(FFooterPanel.ClientWidth - DpiScale(110),
|
||
DpiScale(10), DpiScale(92), DpiScale(30));
|
||
FBtnClose.Anchors := [akTop, akRight];
|
||
FBtnClose.OnClick := @OnCloseClick;
|
||
|
||
FRightTitle := MakeLabel(FPanelRight, 'CHANNEL DETAILS',
|
||
PAGE_MARGIN, 10, 420, 22);
|
||
FRightTitle.Font.Size := 9;
|
||
FRightTitle.Font.Style := [fsBold];
|
||
FRightTitle.Font.Color := FTheme.Text;
|
||
FRightTitle.Anchors := [akLeft, akTop, akRight];
|
||
|
||
FRightHint := MakeLabel(FPanelRight,
|
||
'Changes are saved automatically', PAGE_MARGIN, 31, 420, 18);
|
||
FRightHint.Font.Size := 8;
|
||
FRightHint.Anchors := [akLeft, akTop, akRight];
|
||
|
||
FCardIdentity := MakeGroupPanel('IDENTITY', 56, 90);
|
||
CardW := FCardIdentity.ClientWidth;
|
||
Y := 34;
|
||
|
||
MakeLabel(FCardIdentity, 'Group', LBL_X, Y + 1, LBL_W, CTRL_H);
|
||
FEdGroup := TFlatEdit.Create(Self);
|
||
FEdGroup.Parent := FCardIdentity;
|
||
FEdGroup.SetBounds(DpiScale(CTRL_X), DpiScale(Y),
|
||
CardW - DpiScale(CTRL_X + 14), DpiScale(CTRL_H));
|
||
FEdGroup.Anchors := [akLeft, akTop, akRight];
|
||
FEdGroup.OnChange := @OnFieldChange;
|
||
Inc(Y, ROW_H);
|
||
|
||
MakeLabel(FCardIdentity, 'Name', LBL_X, Y + 1, LBL_W, CTRL_H);
|
||
FEdName := TFlatEdit.Create(Self);
|
||
FEdName.Parent := FCardIdentity;
|
||
FEdName.SetBounds(DpiScale(CTRL_X), DpiScale(Y),
|
||
CardW - DpiScale(CTRL_X + 14), DpiScale(CTRL_H));
|
||
FEdName.Anchors := [akLeft, akTop, akRight];
|
||
FEdName.OnChange := @OnFieldChange;
|
||
|
||
FCardRadio := MakeGroupPanel('RADIO', 156, 120);
|
||
CardW := FCardRadio.ClientWidth;
|
||
Y := 34;
|
||
|
||
MakeLabel(FCardRadio, 'RX frequency', LBL_X, Y + 1, LBL_W, CTRL_H);
|
||
FSpinRXFreq := TFlatFloatSpinEdit.Create(Self);
|
||
FSpinRXFreq.Parent := FCardRadio;
|
||
FSpinRXFreq.SetBounds(DpiScale(CTRL_X), DpiScale(Y),
|
||
CardW - DpiScale(CTRL_X + 56), DpiScale(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;
|
||
PlaceRightUnit(FCardRadio, 'MHz', Y);
|
||
Inc(Y, ROW_H);
|
||
|
||
MakeLabel(FCardRadio, 'TX frequency', LBL_X, Y + 1, LBL_W, CTRL_H);
|
||
FSpinTXFreq := TFlatFloatSpinEdit.Create(Self);
|
||
FSpinTXFreq.Parent := FCardRadio;
|
||
FSpinTXFreq.SetBounds(DpiScale(CTRL_X), DpiScale(Y),
|
||
CardW - DpiScale(CTRL_X + 56), DpiScale(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;
|
||
PlaceRightUnit(FCardRadio, 'MHz', Y);
|
||
Inc(Y, ROW_H);
|
||
|
||
MakeLabel(FCardRadio, 'Mode', LBL_X, Y + 1, LBL_W, CTRL_H);
|
||
FCmbMode := TFlatComboBox.Create(Self);
|
||
FCmbMode.Parent := FCardRadio;
|
||
FCmbMode.SetBounds(DpiScale(CTRL_X), DpiScale(Y),
|
||
DpiScale(180), DpiScale(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;
|
||
|
||
FCardRepeater := MakeGroupPanel('REPEATER / TRANSMIT', 286, 148);
|
||
CardW := FCardRepeater.ClientWidth;
|
||
Y := 34;
|
||
|
||
MakeLabel(FCardRepeater, 'Direction', LBL_X, Y + 1, LBL_W, CTRL_H);
|
||
FBtnRptNone := TFlatButton.Create(Self);
|
||
FBtnRptNone.Parent := FCardRepeater;
|
||
FBtnRptNone.SetBounds(DpiScale(CTRL_X), DpiScale(Y),
|
||
DpiScale(72), DpiScale(CTRL_H));
|
||
FBtnRptNone.Caption := 'NONE';
|
||
FBtnRptNone.Tag := 0;
|
||
FBtnRptNone.OnClick := @OnRptBtnClick;
|
||
|
||
FBtnRptMinus := TFlatButton.Create(Self);
|
||
FBtnRptMinus.Parent := FCardRepeater;
|
||
FBtnRptMinus.SetBounds(DpiScale(CTRL_X + 78), DpiScale(Y),
|
||
DpiScale(46), DpiScale(CTRL_H));
|
||
FBtnRptMinus.Caption := '−';
|
||
FBtnRptMinus.Tag := 1;
|
||
FBtnRptMinus.OnClick := @OnRptBtnClick;
|
||
|
||
FBtnRptPlus := TFlatButton.Create(Self);
|
||
FBtnRptPlus.Parent := FCardRepeater;
|
||
FBtnRptPlus.SetBounds(DpiScale(CTRL_X + 130), DpiScale(Y),
|
||
DpiScale(46), DpiScale(CTRL_H));
|
||
FBtnRptPlus.Caption := '+';
|
||
FBtnRptPlus.Tag := 2;
|
||
FBtnRptPlus.OnClick := @OnRptBtnClick;
|
||
Inc(Y, ROW_H);
|
||
|
||
MakeLabel(FCardRepeater, 'Offset', LBL_X, Y + 1, LBL_W, CTRL_H);
|
||
FSpinRptOffset := TFlatFloatSpinEdit.Create(Self);
|
||
FSpinRptOffset.Parent := FCardRepeater;
|
||
FSpinRptOffset.SetBounds(DpiScale(CTRL_X), DpiScale(Y),
|
||
CardW - DpiScale(CTRL_X + 56), DpiScale(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;
|
||
PlaceRightUnit(FCardRepeater, 'MHz', Y);
|
||
Inc(Y, ROW_H);
|
||
|
||
MakeLabel(FCardRepeater, 'CTCSS tone', LBL_X, Y + 1, LBL_W, CTRL_H);
|
||
FChkCTCSS := TFlatCheckBox.Create(Self);
|
||
FChkCTCSS.Parent := FCardRepeater;
|
||
FChkCTCSS.SetBounds(DpiScale(CTRL_X), DpiScale(Y),
|
||
DpiScale(28), DpiScale(CTRL_H));
|
||
FChkCTCSS.Caption := '';
|
||
FChkCTCSS.Checked := False;
|
||
FChkCTCSS.OnChange := @OnCTCSSChkChange;
|
||
|
||
FCmbCTCSS := TFlatComboBox.Create(Self);
|
||
FCmbCTCSS.Parent := FCardRepeater;
|
||
FCmbCTCSS.SetBounds(DpiScale(CTRL_X + 34), DpiScale(Y),
|
||
CardW - DpiScale(CTRL_X + 48), DpiScale(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);
|
||
|
||
MakeLabel(FCardRepeater, 'Power', LBL_X, Y + 1, LBL_W, CTRL_H);
|
||
FSpinPower := TFlatSpinEdit.Create(Self);
|
||
FSpinPower.Parent := FCardRepeater;
|
||
FSpinPower.SetBounds(DpiScale(CTRL_X), DpiScale(Y),
|
||
DpiScale(92), DpiScale(CTRL_H));
|
||
FSpinPower.MinValue := 0;
|
||
FSpinPower.MaxValue := 100;
|
||
FSpinPower.Value := 100;
|
||
FSpinPower.OnChange := @OnFieldChange;
|
||
MakeLabel(FCardRepeater, '%', CTRL_X + 98, Y + 1, 24, 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.AutoSize := False;
|
||
L.SetBounds(DpiScale(X), DpiScale(Y), DpiScale(W), DpiScale(H));
|
||
L.Layout := tlCenter;
|
||
L.Font.Size := 8;
|
||
L.Font.Color := FTheme.TextDim;
|
||
// Сохраняем для последующей перекраски темой
|
||
SetLength(FLbls, Length(FLbls) + 1);
|
||
FLbls[High(FLbls)] := L;
|
||
Result := L;
|
||
end;
|
||
|
||
function TChannelsForm.MakeGroupPanel(const Cap: string; Y, H: Integer): TPanel;
|
||
var
|
||
Title: TLabel;
|
||
Divider: TPanel;
|
||
begin
|
||
Result := TPanel.Create(Self);
|
||
Result.Parent := FPanelRight;
|
||
Result.SetBounds(DpiScale(PAGE_MARGIN), DpiScale(Y),
|
||
FPanelRight.ClientWidth - DpiScale(2 * PAGE_MARGIN), DpiScale(H));
|
||
Result.Anchors := [akLeft, akTop, akRight];
|
||
Result.BevelOuter := bvNone;
|
||
Result.Color := FTheme.Panel;
|
||
|
||
Title := MakeLabel(Result, Cap, 12, 2, 300, 24);
|
||
Title.Font.Size := 8;
|
||
Title.Font.Style := [fsBold];
|
||
Title.Font.Color := FTheme.FreqActive;
|
||
|
||
Divider := TPanel.Create(Self);
|
||
Divider.Parent := Result;
|
||
Divider.SetBounds(0, DpiScale(28), Result.ClientWidth, 1);
|
||
Divider.Anchors := [akLeft, akTop, akRight];
|
||
Divider.BevelOuter := bvNone;
|
||
Divider.Color := FTheme.Border;
|
||
end;
|
||
|
||
procedure TChannelsForm.StyleBtn(B: TFlatButton; AActive: Boolean);
|
||
begin
|
||
// Прямо созданные TFlatButton не проходят через MakeFlatBtn, поэтому
|
||
// явно даём им тот же системный шрифт, что и остальному интерфейсу.
|
||
B.Font.Assign(Font);
|
||
B.Font.Size := 8;
|
||
B.Font.Style := [];
|
||
B.Active := AActive;
|
||
B.ClrNorm := FTheme.BtnNorm;
|
||
B.ClrActive := FTheme.BtnActive;
|
||
B.ClrHot := FTheme.BtnHot;
|
||
B.ClrBorder := IfThen(AActive,
|
||
FTheme.BtnBorderActive, FTheme.BtnBorderNorm);
|
||
B.ClrText := FTheme.BtnText;
|
||
B.ClrTextAct := FTheme.BtnTextActive;
|
||
B.Invalidate;
|
||
end;
|
||
|
||
procedure TChannelsForm.ApplyTheme(const T: TAppTheme);
|
||
var
|
||
i: Integer;
|
||
Ch: TChannel;
|
||
|
||
procedure ThemeCard(P: TPanel);
|
||
var
|
||
n: Integer;
|
||
begin
|
||
if P = nil then Exit;
|
||
P.Color := T.Panel;
|
||
for n := 0 to P.ControlCount - 1 do
|
||
if (P.Controls[n] is TPanel) and (P.Controls[n].Height = 1) then
|
||
TPanel(P.Controls[n]).Color := T.Border;
|
||
end;
|
||
|
||
begin
|
||
FTheme := T;
|
||
Color := T.BG;
|
||
FPanelLeft.Color := T.Panel;
|
||
FPanelRight.Color := T.BG;
|
||
FFooterPanel.Color := T.BG;
|
||
ThemeCard(FCardIdentity);
|
||
ThemeCard(FCardRadio);
|
||
ThemeCard(FCardRepeater);
|
||
|
||
FList.SetAppTheme(T);
|
||
|
||
StyleBtn(FBtnAdd);
|
||
StyleBtn(FBtnDelete);
|
||
StyleBtn(FBtnUp);
|
||
StyleBtn(FBtnDown);
|
||
StyleBtn(FBtnClose);
|
||
|
||
// 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
|
||
if FLbls[i] = FLeftTitle then
|
||
FLbls[i].Font.Color := T.Text
|
||
else if FLbls[i] = FRightTitle then
|
||
FLbls[i].Font.Color := T.Text
|
||
else if fsBold in FLbls[i].Font.Style then
|
||
FLbls[i].Font.Color := T.FreqActive
|
||
else
|
||
FLbls[i].Font.Color := T.TextDim;
|
||
FLbls[i].Transparent := True;
|
||
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, MODE_MIN, MODE_MAX);
|
||
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;
|
||
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, MODE_MIN, MODE_MAX);
|
||
// 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.OnCloseClick(Sender: TObject);
|
||
begin
|
||
Close;
|
||
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.
|