mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
Add channel memory: CH button, dropdown, editor form, smart CTCSS/RPT auto-clear
- New ChannelStore.pas: TChannel record + JSON persistence (channels.json) - New ChannelsForm.pas: full channel editor (list + fields, pre-fills from current radio state) - CH button in RX block: left-click opens dropdown, right-click opens editor - Active channel: button highlights with channel name, deactivates on VFO move - ApplyChannel: auto-switches XVTR/HF band based on channel frequency; saves old band state before switching to prevent cache corruption - SaveCurrentBand: skip save when VFO > 61 MHz (prevents HF cache corruption from VHF channel application without XVTR) - DeactivateXvtr: save full XVTR state (was only saving LastFreq) - CTCSS/RPT from channel marked as auto: cleared automatically when tuning away from channel frequency; survives band switches via FXvtrSettings auto-flags - TXvtrEntry: add LastCTCSSAutoActive, LastFMRptAutoActive fields Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,272 @@
|
|||||||
|
unit ChannelStore;
|
||||||
|
|
||||||
|
{ TChannelStore — хранилище каналов (channel memory).
|
||||||
|
Формат файла channels.json (рядом с hpsdr_settings.json). }
|
||||||
|
|
||||||
|
{$mode objfpc}{$H+}
|
||||||
|
|
||||||
|
interface
|
||||||
|
|
||||||
|
uses
|
||||||
|
SysUtils, Classes, Math, fpJSON, jsonparser, jsonscanner;
|
||||||
|
|
||||||
|
const
|
||||||
|
CHANNELS_FILE = 'channels.json';
|
||||||
|
|
||||||
|
type
|
||||||
|
TChannel = record
|
||||||
|
Group: string;
|
||||||
|
Name: string;
|
||||||
|
RXFreq: Double; // Hz
|
||||||
|
TXFreq: Double; // Hz
|
||||||
|
Mode: Integer; // 0=LSB 1=USB 2=DSB 3=CWL 4=CWU 5=FM 6=AM 7=SAM
|
||||||
|
RptDir: Integer; // 0=none 1=minus 2=plus
|
||||||
|
RptOffsetHz: Double; // Hz
|
||||||
|
CTCSSOn: Boolean;
|
||||||
|
CTCSSToneIdx: Integer; // 0..37
|
||||||
|
Power: Integer; // 0..100
|
||||||
|
end;
|
||||||
|
|
||||||
|
TChannelStore = class
|
||||||
|
private
|
||||||
|
FChannels: array of TChannel;
|
||||||
|
FFilePath: string;
|
||||||
|
function JI(O: TJSONObject; const K: string; Def: Integer): Integer;
|
||||||
|
function JD(O: TJSONObject; const K: string; Def: Double): Double;
|
||||||
|
function JB(O: TJSONObject; const K: string; Def: Boolean): Boolean;
|
||||||
|
function JS(O: TJSONObject; const K: string; const Def: string): string;
|
||||||
|
public
|
||||||
|
constructor Create(const AFilePath: string = CHANNELS_FILE);
|
||||||
|
procedure Load;
|
||||||
|
procedure Save;
|
||||||
|
function Count: Integer;
|
||||||
|
function GetChannel(Idx: Integer): TChannel;
|
||||||
|
procedure AddChannel(const Ch: TChannel);
|
||||||
|
procedure UpdateChannel(Idx: Integer; const Ch: TChannel);
|
||||||
|
procedure DeleteChannel(Idx: Integer);
|
||||||
|
procedure MoveUp(Idx: Integer);
|
||||||
|
procedure MoveDown(Idx: Integer);
|
||||||
|
class procedure DefaultChannel(out Ch: TChannel);
|
||||||
|
end;
|
||||||
|
|
||||||
|
implementation
|
||||||
|
|
||||||
|
class procedure TChannelStore.DefaultChannel(out Ch: TChannel);
|
||||||
|
begin
|
||||||
|
FillChar(Ch, SizeOf(Ch), 0);
|
||||||
|
Ch.Group := '';
|
||||||
|
Ch.Name := 'New Channel';
|
||||||
|
Ch.RXFreq := 145500000.0;
|
||||||
|
Ch.TXFreq := 145500000.0;
|
||||||
|
Ch.Mode := 5; // FM
|
||||||
|
Ch.RptDir := 0;
|
||||||
|
Ch.RptOffsetHz := 600000.0;
|
||||||
|
Ch.CTCSSOn := False;
|
||||||
|
Ch.CTCSSToneIdx := 0;
|
||||||
|
Ch.Power := 100;
|
||||||
|
end;
|
||||||
|
|
||||||
|
constructor TChannelStore.Create(const AFilePath: string);
|
||||||
|
begin
|
||||||
|
inherited Create;
|
||||||
|
FFilePath := AFilePath;
|
||||||
|
SetLength(FChannels, 0);
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TChannelStore.Count: Integer;
|
||||||
|
begin
|
||||||
|
Result := Length(FChannels);
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TChannelStore.GetChannel(Idx: Integer): TChannel;
|
||||||
|
begin
|
||||||
|
if (Idx >= 0) and (Idx < Length(FChannels)) then
|
||||||
|
Result := FChannels[Idx]
|
||||||
|
else
|
||||||
|
DefaultChannel(Result);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TChannelStore.AddChannel(const Ch: TChannel);
|
||||||
|
var N: Integer;
|
||||||
|
begin
|
||||||
|
N := Length(FChannels);
|
||||||
|
SetLength(FChannels, N + 1);
|
||||||
|
FChannels[N] := Ch;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TChannelStore.UpdateChannel(Idx: Integer; const Ch: TChannel);
|
||||||
|
begin
|
||||||
|
if (Idx >= 0) and (Idx < Length(FChannels)) then
|
||||||
|
FChannels[Idx] := Ch;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TChannelStore.DeleteChannel(Idx: Integer);
|
||||||
|
var i: Integer;
|
||||||
|
begin
|
||||||
|
if (Idx < 0) or (Idx >= Length(FChannels)) then Exit;
|
||||||
|
for i := Idx to Length(FChannels) - 2 do
|
||||||
|
FChannels[i] := FChannels[i + 1];
|
||||||
|
SetLength(FChannels, Length(FChannels) - 1);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TChannelStore.MoveUp(Idx: Integer);
|
||||||
|
var Tmp: TChannel;
|
||||||
|
begin
|
||||||
|
if (Idx < 1) or (Idx >= Length(FChannels)) then Exit;
|
||||||
|
Tmp := FChannels[Idx];
|
||||||
|
FChannels[Idx] := FChannels[Idx - 1];
|
||||||
|
FChannels[Idx - 1] := Tmp;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TChannelStore.MoveDown(Idx: Integer);
|
||||||
|
var Tmp: TChannel;
|
||||||
|
begin
|
||||||
|
if (Idx < 0) or (Idx >= Length(FChannels) - 1) then Exit;
|
||||||
|
Tmp := FChannels[Idx];
|
||||||
|
FChannels[Idx] := FChannels[Idx + 1];
|
||||||
|
FChannels[Idx + 1] := Tmp;
|
||||||
|
end;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// JSON helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function TChannelStore.JI(O: TJSONObject; const K: string; Def: Integer): Integer;
|
||||||
|
var D: TJSONData;
|
||||||
|
begin
|
||||||
|
D := O.Find(K);
|
||||||
|
if D <> nil then try Result := D.AsInteger; except Result := Def; end
|
||||||
|
else Result := Def;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TChannelStore.JD(O: TJSONObject; const K: string; Def: Double): Double;
|
||||||
|
var D: TJSONData;
|
||||||
|
begin
|
||||||
|
D := O.Find(K);
|
||||||
|
if D <> nil then try Result := D.AsFloat; except Result := Def; end
|
||||||
|
else Result := Def;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TChannelStore.JB(O: TJSONObject; const K: string; Def: Boolean): Boolean;
|
||||||
|
var D: TJSONData;
|
||||||
|
begin
|
||||||
|
D := O.Find(K);
|
||||||
|
if D <> nil then try Result := D.AsBoolean; except Result := Def; end
|
||||||
|
else Result := Def;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TChannelStore.JS(O: TJSONObject; const K: string; const Def: string): string;
|
||||||
|
var D: TJSONData;
|
||||||
|
begin
|
||||||
|
D := O.Find(K);
|
||||||
|
if D <> nil then try Result := D.AsString; except Result := Def; end
|
||||||
|
else Result := Def;
|
||||||
|
end;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Load / Save
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
procedure TChannelStore.Load;
|
||||||
|
var
|
||||||
|
F: TFileStream;
|
||||||
|
P: TJSONParser;
|
||||||
|
Root: TJSONData;
|
||||||
|
Arr: TJSONData;
|
||||||
|
Item: TJSONData;
|
||||||
|
O: TJSONObject;
|
||||||
|
Ch: TChannel;
|
||||||
|
i: Integer;
|
||||||
|
begin
|
||||||
|
SetLength(FChannels, 0);
|
||||||
|
if not FileExists(FFilePath) then Exit;
|
||||||
|
try
|
||||||
|
F := TFileStream.Create(FFilePath, fmOpenRead or fmShareDenyNone);
|
||||||
|
try
|
||||||
|
P := TJSONParser.Create(F, [joUTF8]);
|
||||||
|
try
|
||||||
|
Root := P.Parse;
|
||||||
|
if (Root <> nil) and (Root is TJSONObject) then
|
||||||
|
begin
|
||||||
|
Arr := TJSONObject(Root).Find('channels');
|
||||||
|
if (Arr <> nil) and (Arr is TJSONArray) then
|
||||||
|
begin
|
||||||
|
SetLength(FChannels, TJSONArray(Arr).Count);
|
||||||
|
for i := 0 to TJSONArray(Arr).Count - 1 do
|
||||||
|
begin
|
||||||
|
Item := TJSONArray(Arr).Items[i];
|
||||||
|
DefaultChannel(Ch);
|
||||||
|
if Item is TJSONObject then
|
||||||
|
begin
|
||||||
|
O := TJSONObject(Item);
|
||||||
|
Ch.Group := JS(O, 'group', '');
|
||||||
|
Ch.Name := JS(O, 'name', 'Channel');
|
||||||
|
Ch.RXFreq := JD(O, 'rx_freq', 145500000.0);
|
||||||
|
Ch.TXFreq := JD(O, 'tx_freq', 145500000.0);
|
||||||
|
Ch.Mode := EnsureRange(JI(O, 'mode', 5), 0, 7);
|
||||||
|
Ch.RptDir := EnsureRange(JI(O, 'rpt_dir', 0), 0, 2);
|
||||||
|
Ch.RptOffsetHz:= JD(O, 'rpt_offset', 600000.0);
|
||||||
|
Ch.CTCSSOn := JB(O, 'ctcss_on', False);
|
||||||
|
Ch.CTCSSToneIdx := EnsureRange(JI(O, 'ctcss_idx', 0), 0, 37);
|
||||||
|
Ch.Power := EnsureRange(JI(O, 'power', 100), 0, 100);
|
||||||
|
end;
|
||||||
|
FChannels[i] := Ch;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
Root.Free;
|
||||||
|
end;
|
||||||
|
finally
|
||||||
|
P.Free;
|
||||||
|
end;
|
||||||
|
finally
|
||||||
|
F.Free;
|
||||||
|
end;
|
||||||
|
except
|
||||||
|
SetLength(FChannels, 0);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TChannelStore.Save;
|
||||||
|
var
|
||||||
|
F: TFileStream;
|
||||||
|
Root: TJSONObject;
|
||||||
|
Arr: TJSONArray;
|
||||||
|
Obj: TJSONObject;
|
||||||
|
i: Integer;
|
||||||
|
S: string;
|
||||||
|
begin
|
||||||
|
Root := TJSONObject.Create;
|
||||||
|
try
|
||||||
|
Arr := TJSONArray.Create;
|
||||||
|
Root.Add('channels', Arr);
|
||||||
|
for i := 0 to Length(FChannels) - 1 do
|
||||||
|
begin
|
||||||
|
Obj := TJSONObject.Create;
|
||||||
|
Obj.Add('group', FChannels[i].Group);
|
||||||
|
Obj.Add('name', FChannels[i].Name);
|
||||||
|
Obj.Add('rx_freq', FChannels[i].RXFreq);
|
||||||
|
Obj.Add('tx_freq', FChannels[i].TXFreq);
|
||||||
|
Obj.Add('mode', FChannels[i].Mode);
|
||||||
|
Obj.Add('rpt_dir', FChannels[i].RptDir);
|
||||||
|
Obj.Add('rpt_offset', FChannels[i].RptOffsetHz);
|
||||||
|
Obj.Add('ctcss_on', FChannels[i].CTCSSOn);
|
||||||
|
Obj.Add('ctcss_idx', FChannels[i].CTCSSToneIdx);
|
||||||
|
Obj.Add('power', FChannels[i].Power);
|
||||||
|
Arr.Add(Obj);
|
||||||
|
end;
|
||||||
|
try
|
||||||
|
S := Root.FormatJSON([], 2);
|
||||||
|
F := TFileStream.Create(FFilePath, fmCreate);
|
||||||
|
try
|
||||||
|
if Length(S) > 0 then F.WriteBuffer(S[1], Length(S));
|
||||||
|
finally
|
||||||
|
F.Free;
|
||||||
|
end;
|
||||||
|
except
|
||||||
|
end;
|
||||||
|
finally
|
||||||
|
Root.Free;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
end.
|
||||||
@@ -0,0 +1,620 @@
|
|||||||
|
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 = 8;
|
||||||
|
CH_MODE_NAMES: array[0..CH_MODE_COUNT-1] of string =
|
||||||
|
('LSB','USB','DSB','CWL','CWU','FM','AM','SAM');
|
||||||
|
|
||||||
|
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.
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
unit FMRepeater;
|
||||||
|
|
||||||
|
{ FM repeater offset helpers — constants, default offsets, auto-direction logic. }
|
||||||
|
|
||||||
|
{$mode objfpc}{$H+}
|
||||||
|
|
||||||
|
interface
|
||||||
|
|
||||||
|
const
|
||||||
|
RPT_NONE = 0;
|
||||||
|
RPT_MINUS = 1;
|
||||||
|
RPT_PLUS = 2;
|
||||||
|
|
||||||
|
RPT_2M_BEGIN = 144000000.0;
|
||||||
|
RPT_2M_END = 148000000.0;
|
||||||
|
RPT_70CM_BEGIN = 430000000.0;
|
||||||
|
RPT_70CM_END = 440000000.0;
|
||||||
|
RPT_DEFAULT_2M = 600000.0; // 0.600 MHz
|
||||||
|
RPT_DEFAULT_70CM = 7600000.0; // 7.600 MHz
|
||||||
|
|
||||||
|
// European 2m repeater input segment: auto-enable minus direction
|
||||||
|
RPT_AUTO_MINUS_LO = 145600000.0; // 145.600 MHz
|
||||||
|
RPT_AUTO_MINUS_HI = 145787500.0; // 145.7875 MHz
|
||||||
|
|
||||||
|
{ Returns default offset (Hz) for the given visible frequency.
|
||||||
|
70 cm band → 7.600 MHz; everything else → 0.600 MHz. }
|
||||||
|
function RptDefaultOffsetHz(FreqHz: Double): Double;
|
||||||
|
|
||||||
|
{ Returns RPT_MINUS when FreqHz falls in the 2 m auto-minus segment,
|
||||||
|
RPT_NONE otherwise. }
|
||||||
|
function RptAutoDir(FreqHz: Double): Integer;
|
||||||
|
|
||||||
|
{ Formats OffsetHz as MHz with 3 decimal places, e.g. "0.600". }
|
||||||
|
function RptFormatOffset(OffsetHz: Double): string;
|
||||||
|
|
||||||
|
{ Parses a MHz string ("0.600", "7.6", "7,600") to Hz.
|
||||||
|
Returns 0 on parse error or out-of-range value. }
|
||||||
|
function RptParseOffset(const S: string): Double;
|
||||||
|
|
||||||
|
implementation
|
||||||
|
|
||||||
|
uses SysUtils;
|
||||||
|
|
||||||
|
function RptDefaultOffsetHz(FreqHz: Double): Double;
|
||||||
|
begin
|
||||||
|
if (FreqHz >= RPT_70CM_BEGIN) and (FreqHz <= RPT_70CM_END) then
|
||||||
|
Result := RPT_DEFAULT_70CM
|
||||||
|
else
|
||||||
|
Result := RPT_DEFAULT_2M;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function RptAutoDir(FreqHz: Double): Integer;
|
||||||
|
begin
|
||||||
|
if (FreqHz >= RPT_AUTO_MINUS_LO) and (FreqHz <= RPT_AUTO_MINUS_HI) then
|
||||||
|
Result := RPT_MINUS
|
||||||
|
else
|
||||||
|
Result := RPT_NONE;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function RptFormatOffset(OffsetHz: Double): string;
|
||||||
|
begin
|
||||||
|
Result := Format('%.3f', [OffsetHz / 1000000.0]);
|
||||||
|
end;
|
||||||
|
|
||||||
|
function RptParseOffset(const S: string): Double;
|
||||||
|
var
|
||||||
|
V: Double;
|
||||||
|
Code: Integer;
|
||||||
|
Norm: string;
|
||||||
|
begin
|
||||||
|
Norm := StringReplace(Trim(S), ',', '.', [rfReplaceAll]);
|
||||||
|
Val(Norm, V, Code);
|
||||||
|
if (Code = 0) and (V > 0.0) and (V <= 100.0) then
|
||||||
|
Result := V * 1000000.0
|
||||||
|
else
|
||||||
|
Result := 0.0;
|
||||||
|
end;
|
||||||
|
|
||||||
|
end.
|
||||||
+3
-1
@@ -42,7 +42,9 @@ type
|
|||||||
property ClrBorder: TColor read FClrBorder write FClrBorder;
|
property ClrBorder: TColor read FClrBorder write FClrBorder;
|
||||||
property ClrText: TColor read FClrText write FClrText;
|
property ClrText: TColor read FClrText write FClrText;
|
||||||
property ClrTextAct:TColor read FClrTextAct write FClrTextAct;
|
property ClrTextAct:TColor read FClrTextAct write FClrTextAct;
|
||||||
property OnClick: TNotifyEvent read FOnClick write FOnClick;
|
property OnClick: TNotifyEvent read FOnClick write FOnClick;
|
||||||
|
property OnMouseDown;
|
||||||
|
property OnMouseUp;
|
||||||
property Caption;
|
property Caption;
|
||||||
property Font;
|
property Font;
|
||||||
property Enabled;
|
property Enabled;
|
||||||
|
|||||||
+302
-17
@@ -38,7 +38,8 @@ uses
|
|||||||
PlatformUtils,
|
PlatformUtils,
|
||||||
WinFirewall,
|
WinFirewall,
|
||||||
BoardUtils, WisdomBuilder, UISync,
|
BoardUtils, WisdomBuilder, UISync,
|
||||||
FlatEdit, FMRepeater;
|
FlatEdit, FMRepeater,
|
||||||
|
ChannelStore, ChannelsForm;
|
||||||
|
|
||||||
const
|
const
|
||||||
CLR_BG = TColor($00101010);
|
CLR_BG = TColor($00101010);
|
||||||
@@ -257,7 +258,8 @@ type
|
|||||||
FFMStepIdx: Integer;
|
FFMStepIdx: Integer;
|
||||||
FFMRptDir: Integer; // RPT_NONE / RPT_MINUS / RPT_PLUS
|
FFMRptDir: Integer; // RPT_NONE / RPT_MINUS / RPT_PLUS
|
||||||
FFMRptOffsetHz: Double;
|
FFMRptOffsetHz: Double;
|
||||||
FFMRptAutoActive: Boolean; // True = текущий minus включён автоматически
|
FFMRptAutoActive: Boolean; // True = RPT включён автоматически (авто или из канала)
|
||||||
|
FFMCTCSSAutoActive: Boolean; // True = CTCSS включён из канала (не вручную)
|
||||||
FWfManualHigh: Double;
|
FWfManualHigh: Double;
|
||||||
FWfManualLow: Double;
|
FWfManualLow: Double;
|
||||||
FWfAGCOffset: Double;
|
FWfAGCOffset: Double;
|
||||||
@@ -403,6 +405,11 @@ type
|
|||||||
BtnTUN: TFlatButton;
|
BtnTUN: TFlatButton;
|
||||||
|
|
||||||
PanelRXBlock: TPanel;
|
PanelRXBlock: TPanel;
|
||||||
|
BtnChannels: TFlatButton;
|
||||||
|
FChannelsDropDown: TFlatDropDown;
|
||||||
|
FChannelStore: TChannelStore;
|
||||||
|
FChannelsForm: TObject; // TChannelsForm (cast при использовании)
|
||||||
|
FActiveChannelIdx: Integer; // -1 = нет активного канала
|
||||||
|
|
||||||
// ---- Right panel ----
|
// ---- Right panel ----
|
||||||
PanelRight: TPanel;
|
PanelRight: TPanel;
|
||||||
@@ -517,6 +524,14 @@ type
|
|||||||
procedure BtnFMStepSelClick(Sender: TObject);
|
procedure BtnFMStepSelClick(Sender: TObject);
|
||||||
procedure OnCTCSSDropDownSelect(Sender: TObject; Idx: Integer);
|
procedure OnCTCSSDropDownSelect(Sender: TObject; Idx: Integer);
|
||||||
procedure OnStepDropDownSelect(Sender: TObject; Idx: Integer);
|
procedure OnStepDropDownSelect(Sender: TObject; Idx: Integer);
|
||||||
|
procedure BtnChannelsClick(Sender: TObject);
|
||||||
|
procedure BtnChannelsMouseDown(Sender: TObject; Button: TMouseButton;
|
||||||
|
Shift: TShiftState; X, Y: Integer);
|
||||||
|
procedure OnChannelsDropDownSelect(Sender: TObject; Idx: Integer);
|
||||||
|
procedure RefreshChannelsDropDown;
|
||||||
|
procedure OnChannelStoreChanged(Sender: TObject);
|
||||||
|
procedure ApplyChannel(const Ch: TChannel);
|
||||||
|
procedure CheckChannelActive;
|
||||||
procedure PbSpectrumMouseDown(Sender: TObject; Button: TMouseButton;
|
procedure PbSpectrumMouseDown(Sender: TObject; Button: TMouseButton;
|
||||||
Shift: TShiftState; X, Y: Integer);
|
Shift: TShiftState; X, Y: Integer);
|
||||||
procedure PbSpectrumMouseMove(Sender: TObject; Shift: TShiftState;
|
procedure PbSpectrumMouseMove(Sender: TObject; Shift: TShiftState;
|
||||||
@@ -974,15 +989,20 @@ begin
|
|||||||
FXvtrSettings.Entries[FCurrentXvtr].LastCTCSSToneIdx := FFMCTCSSToneIdx;
|
FXvtrSettings.Entries[FCurrentXvtr].LastCTCSSToneIdx := FFMCTCSSToneIdx;
|
||||||
FXvtrSettings.Entries[FCurrentXvtr].LastFMStepOn := FFMStepOn;
|
FXvtrSettings.Entries[FCurrentXvtr].LastFMStepOn := FFMStepOn;
|
||||||
FXvtrSettings.Entries[FCurrentXvtr].LastFMStepIdx := FFMStepIdx;
|
FXvtrSettings.Entries[FCurrentXvtr].LastFMStepIdx := FFMStepIdx;
|
||||||
FXvtrSettings.Entries[FCurrentXvtr].LastFMRptDir := FFMRptDir;
|
FXvtrSettings.Entries[FCurrentXvtr].LastFMRptDir := FFMRptDir;
|
||||||
FXvtrSettings.Entries[FCurrentXvtr].LastFMRptOffsetHz := FFMRptOffsetHz;
|
FXvtrSettings.Entries[FCurrentXvtr].LastFMRptOffsetHz := FFMRptOffsetHz;
|
||||||
FXvtrSettings.Entries[FCurrentXvtr].LastCTun := FCTun;
|
FXvtrSettings.Entries[FCurrentXvtr].LastCTun := FCTun;
|
||||||
FXvtrSettings.Entries[FCurrentXvtr].LastAGCMode := FAGCMode;
|
FXvtrSettings.Entries[FCurrentXvtr].LastAGCMode := FAGCMode;
|
||||||
FXvtrSettings.Entries[FCurrentXvtr].LastAGCTop := FAGCTop;
|
FXvtrSettings.Entries[FCurrentXvtr].LastAGCTop := FAGCTop;
|
||||||
|
FXvtrSettings.Entries[FCurrentXvtr].LastCTCSSAutoActive := FFMCTCSSAutoActive;
|
||||||
|
FXvtrSettings.Entries[FCurrentXvtr].LastFMRptAutoActive := FFMRptAutoActive;
|
||||||
end;
|
end;
|
||||||
FSettings.SaveXvtr(FDevMAC, FXvtrSettings);
|
FSettings.SaveXvtr(FDevMAC, FXvtrSettings);
|
||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
|
// Не сохраняем, если VFO ушёл в VHF/UHF область (например, после применения
|
||||||
|
// канала на 2м без XVTR) — иначе HF-кэш диапазона будет перезаписан VHF-частотой.
|
||||||
|
if FVfoA > 61000000 then Exit;
|
||||||
FBandCache[FCurrentBand] := MakeBandSettings;
|
FBandCache[FCurrentBand] := MakeBandSettings;
|
||||||
FSettings.SaveBand(FDevMAC, FCurrentBand, FBandCache[FCurrentBand]);
|
FSettings.SaveBand(FDevMAC, FCurrentBand, FBandCache[FCurrentBand]);
|
||||||
end;
|
end;
|
||||||
@@ -1135,9 +1155,10 @@ begin
|
|||||||
FFMSQLevel := 30;
|
FFMSQLevel := 30;
|
||||||
FFMStepOn := True;
|
FFMStepOn := True;
|
||||||
FFMStepIdx := FM_STEP_DEF;
|
FFMStepIdx := FM_STEP_DEF;
|
||||||
FFMRptDir := RPT_NONE;
|
FFMRptDir := RPT_NONE;
|
||||||
FFMRptOffsetHz := RPT_DEFAULT_2M;
|
FFMRptOffsetHz := RPT_DEFAULT_2M;
|
||||||
FFMRptAutoActive := False;
|
FFMRptAutoActive := False;
|
||||||
|
FFMCTCSSAutoActive := False;
|
||||||
FSpectrumBufCount := 1024;
|
FSpectrumBufCount := 1024;
|
||||||
FWaterfallBufCount := 1024;
|
FWaterfallBufCount := 1024;
|
||||||
FWaterfallFrameInterval := 2;
|
FWaterfallFrameInterval := 2;
|
||||||
@@ -1152,6 +1173,8 @@ begin
|
|||||||
FTXSpecRange := 80.0;
|
FTXSpecRange := 80.0;
|
||||||
FTXSpecGridStep := 10.0;
|
FTXSpecGridStep := 10.0;
|
||||||
FSettingsForm := nil;
|
FSettingsForm := nil;
|
||||||
|
FChannelsForm := nil;
|
||||||
|
FActiveChannelIdx := -1;
|
||||||
FillChar(FDevMAC, SizeOf(FDevMAC), 0);
|
FillChar(FDevMAC, SizeOf(FDevMAC), 0);
|
||||||
|
|
||||||
// Инициализируем кэш диапазонов умолчаниями
|
// Инициализируем кэш диапазонов умолчаниями
|
||||||
@@ -1162,6 +1185,10 @@ begin
|
|||||||
// Загружаем JSON настройки
|
// Загружаем JSON настройки
|
||||||
FSettings := TSettingsManager.Create;
|
FSettings := TSettingsManager.Create;
|
||||||
FSettings.Load;
|
FSettings.Load;
|
||||||
|
|
||||||
|
// Загружаем список каналов
|
||||||
|
FChannelStore := TChannelStore.Create(CHANNELS_FILE);
|
||||||
|
FChannelStore.Load;
|
||||||
if FSettings.LoadStartupPreview(StartupVfoA, StartupVfoB, StartupRate) then
|
if FSettings.LoadStartupPreview(StartupVfoA, StartupVfoB, StartupRate) then
|
||||||
begin
|
begin
|
||||||
FVfoA := StartupVfoA;
|
FVfoA := StartupVfoA;
|
||||||
@@ -1411,6 +1438,7 @@ begin
|
|||||||
FSettings.SaveStartupPreview(FVfoA, FVfoB, FSampleRate);
|
FSettings.SaveStartupPreview(FVfoA, FVfoB, FSampleRate);
|
||||||
FSettings.Save;
|
FSettings.Save;
|
||||||
FSettings.Free;
|
FSettings.Free;
|
||||||
|
FreeAndNil(FChannelStore);
|
||||||
FWebServer.Stop;
|
FWebServer.Stop;
|
||||||
FWebServer.Free;
|
FWebServer.Free;
|
||||||
if Assigned(FCATTcp) then begin FCATTcp.Stop; FreeAndNil(FCATTcp); end;
|
if Assigned(FCATTcp) then begin FCATTcp.Stop; FreeAndNil(FCATTcp); end;
|
||||||
@@ -1719,6 +1747,17 @@ begin
|
|||||||
BtnCTun.Tag := 0;
|
BtnCTun.Tag := 0;
|
||||||
StyleButton(BtnCTun, FCTun);
|
StyleButton(BtnCTun, FCTun);
|
||||||
|
|
||||||
|
BtnChannels := MakeBtn(PanelRXBlock, 'Channel',
|
||||||
|
LeftPanelButtonLeft(LEFT_W, 3, 1), 16,
|
||||||
|
LeftPanelButtonWidth(LEFT_W, 3, 1),
|
||||||
|
BTN_H, BtnChannelsClick);
|
||||||
|
BtnChannels.OnMouseDown := BtnChannelsMouseDown;
|
||||||
|
StyleButton(BtnChannels, False);
|
||||||
|
|
||||||
|
FChannelsDropDown := TFlatDropDown.Create(BtnChannels, PanelLeft, 1, BTN_SM);
|
||||||
|
FChannelsDropDown.OnSelect := OnChannelsDropDownSelect;
|
||||||
|
RefreshChannelsDropDown;
|
||||||
|
|
||||||
MakeLbl(PanelRXBlock, 'VOL', 4, 50);
|
MakeLbl(PanelRXBlock, 'VOL', 4, 50);
|
||||||
TrkVolume := TFlatSlider.Create(Self);
|
TrkVolume := TFlatSlider.Create(Self);
|
||||||
TrkVolume.Parent := PanelRXBlock; TrkVolume.Left := 34; TrkVolume.Top := 46;
|
TrkVolume.Parent := PanelRXBlock; TrkVolume.Left := 34; TrkVolume.Top := 46;
|
||||||
@@ -2599,6 +2638,8 @@ begin
|
|||||||
if BtnFMRptPlus <> nil then StyleButton(BtnFMRptPlus, BtnFMRptPlus.Active);
|
if BtnFMRptPlus <> nil then StyleButton(BtnFMRptPlus, BtnFMRptPlus.Active);
|
||||||
if FCTCSSDropDown <> nil then FCTCSSDropDown.ApplyStyle(StyleButton, T.Panel);
|
if FCTCSSDropDown <> nil then FCTCSSDropDown.ApplyStyle(StyleButton, T.Panel);
|
||||||
if FStepDropDown <> nil then FStepDropDown.ApplyStyle(StyleButton, T.Panel);
|
if FStepDropDown <> nil then FStepDropDown.ApplyStyle(StyleButton, T.Panel);
|
||||||
|
if BtnChannels <> nil then StyleButton(BtnChannels, False);
|
||||||
|
if FChannelsDropDown <> nil then FChannelsDropDown.ApplyStyle(StyleButton, T.Panel);
|
||||||
StyleButton(BtnNR, BtnNR.Active);
|
StyleButton(BtnNR, BtnNR.Active);
|
||||||
StyleButton(BtnNB, BtnNB.Active);
|
StyleButton(BtnNB, BtnNB.Active);
|
||||||
StyleButton(BtnSNB, BtnSNB.Active);
|
StyleButton(BtnSNB, BtnSNB.Active);
|
||||||
@@ -4593,6 +4634,26 @@ begin
|
|||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
// Если нет активного канала, но CTCSS/RPT были установлены каналом (авто-флаги),
|
||||||
|
// сбрасываем их при первом же изменении частоты
|
||||||
|
if FActiveChannelIdx < 0 then
|
||||||
|
begin
|
||||||
|
if FFMCTCSSAutoActive then
|
||||||
|
begin
|
||||||
|
FFMCTCSSAutoActive := False;
|
||||||
|
FFMCTCSSOn := False;
|
||||||
|
if BtnFMCTCSS <> nil then StyleButton(BtnFMCTCSS, False);
|
||||||
|
if FWDSPReady then FDSPEngine.SetTXCTCSS(False, CTCSS_TONES[FFMCTCSSToneIdx]);
|
||||||
|
end;
|
||||||
|
if FFMRptAutoActive then
|
||||||
|
begin
|
||||||
|
FFMRptDir := RPT_NONE;
|
||||||
|
FFMRptAutoActive := False;
|
||||||
|
if BtnFMRptMinus <> nil then StyleButton(BtnFMRptMinus, False);
|
||||||
|
if BtnFMRptPlus <> nil then StyleButton(BtnFMRptPlus, False);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
// Auto-enable repeater minus for 2 m repeater input segment
|
// Auto-enable repeater minus for 2 m repeater input segment
|
||||||
if FMode = MODE_FM then UpdateRptAutoState;
|
if FMode = MODE_FM then UpdateRptAutoState;
|
||||||
|
|
||||||
@@ -4613,6 +4674,8 @@ begin
|
|||||||
FWaterfallDirty := True;
|
FWaterfallDirty := True;
|
||||||
PbWaterfall.Invalidate;
|
PbWaterfall.Invalidate;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
CheckChannelActive;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -5206,6 +5269,7 @@ procedure TMainForm.CloseCTCSSPopup;
|
|||||||
begin
|
begin
|
||||||
if FCTCSSDropDown <> nil then FCTCSSDropDown.ClosePopup;
|
if FCTCSSDropDown <> nil then FCTCSSDropDown.ClosePopup;
|
||||||
CloseFMStepPopup;
|
CloseFMStepPopup;
|
||||||
|
if FChannelsDropDown <> nil then FChannelsDropDown.ClosePopup;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TMainForm.BtnFMRptMinusClick(Sender: TObject);
|
procedure TMainForm.BtnFMRptMinusClick(Sender: TObject);
|
||||||
@@ -5299,7 +5363,8 @@ end;
|
|||||||
|
|
||||||
procedure TMainForm.CloseFMStepPopup;
|
procedure TMainForm.CloseFMStepPopup;
|
||||||
begin
|
begin
|
||||||
if FStepDropDown <> nil then FStepDropDown.ClosePopup;
|
if FStepDropDown <> nil then FStepDropDown.ClosePopup;
|
||||||
|
if FChannelsDropDown <> nil then FChannelsDropDown.ClosePopup;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TMainForm.SetFMStep(Idx: Integer);
|
procedure TMainForm.SetFMStep(Idx: Integer);
|
||||||
@@ -5331,6 +5396,208 @@ begin
|
|||||||
SaveCurrentBand;
|
SaveCurrentBand;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Channels dropdown / store
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
procedure TMainForm.RefreshChannelsDropDown;
|
||||||
|
var
|
||||||
|
names: array of string;
|
||||||
|
i: Integer;
|
||||||
|
begin
|
||||||
|
if (FChannelStore = nil) or (FChannelsDropDown = nil) then Exit;
|
||||||
|
SetLength(names, FChannelStore.Count);
|
||||||
|
for i := 0 to FChannelStore.Count - 1 do
|
||||||
|
names[i] := FChannelStore.GetChannel(i).Name;
|
||||||
|
FChannelsDropDown.SetItems(names);
|
||||||
|
if (FActiveChannelIdx >= 0) and (FActiveChannelIdx < FChannelStore.Count) then
|
||||||
|
FChannelsDropDown.SetItemIndex(FActiveChannelIdx)
|
||||||
|
else if BtnChannels <> nil then
|
||||||
|
BtnChannels.Caption := 'Channel';
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.OnChannelStoreChanged(Sender: TObject);
|
||||||
|
begin
|
||||||
|
RefreshChannelsDropDown;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.BtnChannelsClick(Sender: TObject);
|
||||||
|
begin
|
||||||
|
if FChannelStore.Count = 0 then Exit;
|
||||||
|
FCTCSSDropDown.ClosePopup;
|
||||||
|
FStepDropDown.ClosePopup;
|
||||||
|
FChannelsDropDown.Toggle;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.BtnChannelsMouseDown(Sender: TObject; Button: TMouseButton;
|
||||||
|
Shift: TShiftState; X, Y: Integer);
|
||||||
|
var
|
||||||
|
Def: TChannel;
|
||||||
|
begin
|
||||||
|
if Button <> mbRight then Exit;
|
||||||
|
FChannelsDropDown.ClosePopup;
|
||||||
|
if FChannelsForm = nil then
|
||||||
|
FChannelsForm := TChannelsForm.Create(Self, FChannelStore, OnChannelStoreChanged);
|
||||||
|
// Передаём текущее состояние радио как значения по умолчанию для нового канала
|
||||||
|
TChannelStore.DefaultChannel(Def);
|
||||||
|
Def.RXFreq := FVfoA;
|
||||||
|
Def.TXFreq := ActiveTXFreqHz;
|
||||||
|
Def.Mode := FMode;
|
||||||
|
Def.RptDir := FFMRptDir;
|
||||||
|
Def.RptOffsetHz := FFMRptOffsetHz;
|
||||||
|
Def.CTCSSOn := FFMCTCSSOn;
|
||||||
|
Def.CTCSSToneIdx := FFMCTCSSToneIdx;
|
||||||
|
Def.Power := 100;
|
||||||
|
Def.Name := '';
|
||||||
|
TChannelsForm(FChannelsForm).SetCurrentDefaults(Def);
|
||||||
|
TChannelsForm(FChannelsForm).ApplyTheme(DarkTheme);
|
||||||
|
TChannelsForm(FChannelsForm).Show;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.OnChannelsDropDownSelect(Sender: TObject; Idx: Integer);
|
||||||
|
var Ch: TChannel;
|
||||||
|
begin
|
||||||
|
if (FChannelStore = nil) or (Idx < 0) or (Idx >= FChannelStore.Count) then Exit;
|
||||||
|
Ch := FChannelStore.GetChannel(Idx);
|
||||||
|
FActiveChannelIdx := Idx;
|
||||||
|
StyleButton(BtnChannels, True);
|
||||||
|
BtnChannels.Caption := Ch.Name;
|
||||||
|
BtnChannels.Repaint;
|
||||||
|
ApplyChannel(Ch);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.ApplyChannel(const Ch: TChannel);
|
||||||
|
var
|
||||||
|
i, NewBand, XvtrIdx: Integer;
|
||||||
|
begin
|
||||||
|
// Сохраняем текущее состояние до любых изменений
|
||||||
|
SaveCurrentBand;
|
||||||
|
|
||||||
|
// Ищем подходящий XVTR-слот по частоте канала
|
||||||
|
XvtrIdx := -1;
|
||||||
|
for i := 0 to CFG_XVTR_COUNT - 1 do
|
||||||
|
if FXvtrSettings.Entries[i].Enabled and
|
||||||
|
(Ch.RXFreq >= FXvtrSettings.Entries[i].FreqBegin) and
|
||||||
|
(Ch.RXFreq <= FXvtrSettings.Entries[i].FreqEnd) then
|
||||||
|
begin
|
||||||
|
XvtrIdx := i;
|
||||||
|
Break;
|
||||||
|
end;
|
||||||
|
|
||||||
|
NewBand := -1;
|
||||||
|
|
||||||
|
if XvtrIdx >= 0 then
|
||||||
|
begin
|
||||||
|
// Канал на трансвертерном диапазоне — активируем XVTR если не активен
|
||||||
|
if FCurrentXvtr <> XvtrIdx then
|
||||||
|
ActivateXvtrBand(XvtrIdx);
|
||||||
|
end
|
||||||
|
else
|
||||||
|
begin
|
||||||
|
// Канал на HF (или неизвестная частота)
|
||||||
|
// Если были на XVTR — выходим из него, иначе ApplyVfoA заклэмпит частоту
|
||||||
|
if FCurrentXvtr >= 0 then
|
||||||
|
DeactivateXvtr;
|
||||||
|
NewBand := FreqToBandIdx(Ch.RXFreq);
|
||||||
|
if (NewBand >= 0) and (NewBand <> FCurrentBand) then
|
||||||
|
begin
|
||||||
|
StyleButton(BtnBand[FCurrentBand], False);
|
||||||
|
FCurrentBand := NewBand;
|
||||||
|
StyleButton(BtnBand[FCurrentBand], True);
|
||||||
|
FDriveLevel := CalcDriveByte;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
// Режим (после ActivateXvtrBand/DeactivateXvtr FMode обновился — сравниваем снова)
|
||||||
|
if FMode <> Ch.Mode then
|
||||||
|
begin
|
||||||
|
FMode := Ch.Mode;
|
||||||
|
for i := 0 to MODE_COUNT - 1 do StyleButton(BtnMode[i], i = FMode);
|
||||||
|
if FWDSPReady then
|
||||||
|
begin
|
||||||
|
FDSPEngine.SetMode(FMode);
|
||||||
|
if FTuning then FDSPEngine.SetTXTone(True, FTXSettings.TUNFreq, 1.0);
|
||||||
|
end;
|
||||||
|
UpdateFilterButtons;
|
||||||
|
ApplyModeFilter;
|
||||||
|
SyncSpecViewFreq;
|
||||||
|
end;
|
||||||
|
|
||||||
|
// FM-параметры
|
||||||
|
if FMode = MODE_FM then
|
||||||
|
begin
|
||||||
|
FFMCTCSSOn := Ch.CTCSSOn;
|
||||||
|
FFMCTCSSAutoActive := Ch.CTCSSOn; // CTCSS из канала — авто, сбросится при уходе с частоты
|
||||||
|
SetFMCTCSSTone(Ch.CTCSSToneIdx);
|
||||||
|
if BtnFMCTCSS <> nil then
|
||||||
|
begin
|
||||||
|
StyleButton(BtnFMCTCSS, FFMCTCSSOn);
|
||||||
|
BtnFMCTCSS.Repaint;
|
||||||
|
end;
|
||||||
|
if FWDSPReady then
|
||||||
|
FDSPEngine.SetTXCTCSS(FFMCTCSSOn, CTCSS_TONES[FFMCTCSSToneIdx]);
|
||||||
|
FFMRptDir := Ch.RptDir;
|
||||||
|
FFMRptOffsetHz := Ch.RptOffsetHz;
|
||||||
|
FFMRptAutoActive := Ch.RptDir <> RPT_NONE; // RPT из канала — авто, сбросится при уходе
|
||||||
|
if BtnFMRptMinus <> nil then StyleButton(BtnFMRptMinus, FFMRptDir = RPT_MINUS);
|
||||||
|
if BtnFMRptPlus <> nil then StyleButton(BtnFMRptPlus, FFMRptDir = RPT_PLUS);
|
||||||
|
if EdFMRptOffset <> nil then EdFMRptOffset.Text := RptFormatOffset(FFMRptOffsetHz);
|
||||||
|
end;
|
||||||
|
|
||||||
|
// Частота VFO A
|
||||||
|
ApplyVfoA(Round(Ch.RXFreq));
|
||||||
|
|
||||||
|
// Сохраняем состояние
|
||||||
|
if XvtrIdx >= 0 then
|
||||||
|
SaveCurrentBand // сохраняем в XVTR-слот (CTCSS/RPT/Mode из канала)
|
||||||
|
else if NewBand >= 0 then
|
||||||
|
SaveCurrentBand; // сохраняем в HF band-кэш
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.CheckChannelActive;
|
||||||
|
var Ch: TChannel;
|
||||||
|
begin
|
||||||
|
if FActiveChannelIdx < 0 then Exit;
|
||||||
|
if (FChannelStore = nil) or (FActiveChannelIdx >= FChannelStore.Count) then
|
||||||
|
begin
|
||||||
|
FActiveChannelIdx := -1;
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
Ch := FChannelStore.GetChannel(FActiveChannelIdx);
|
||||||
|
if Round(FVfoA) <> Round(Ch.RXFreq) then
|
||||||
|
begin
|
||||||
|
FActiveChannelIdx := -1;
|
||||||
|
StyleButton(BtnChannels, False);
|
||||||
|
BtnChannels.Caption := 'Channel';
|
||||||
|
BtnChannels.Repaint;
|
||||||
|
|
||||||
|
// Сбрасываем CTCSS если он был включён каналом (не вручную)
|
||||||
|
if FFMCTCSSAutoActive then
|
||||||
|
begin
|
||||||
|
FFMCTCSSAutoActive := False;
|
||||||
|
FFMCTCSSOn := False;
|
||||||
|
if BtnFMCTCSS <> nil then
|
||||||
|
begin
|
||||||
|
StyleButton(BtnFMCTCSS, False);
|
||||||
|
BtnFMCTCSS.Repaint;
|
||||||
|
end;
|
||||||
|
if FWDSPReady then
|
||||||
|
FDSPEngine.SetTXCTCSS(False, CTCSS_TONES[FFMCTCSSToneIdx]);
|
||||||
|
end;
|
||||||
|
|
||||||
|
// Сбрасываем RPT если он был включён каналом (не вручную), затем
|
||||||
|
// даём UpdateRptAutoState переоценить авто-MINUS для текущей частоты
|
||||||
|
if FFMRptAutoActive then
|
||||||
|
begin
|
||||||
|
FFMRptDir := RPT_NONE;
|
||||||
|
FFMRptAutoActive := False;
|
||||||
|
if BtnFMRptMinus <> nil then StyleButton(BtnFMRptMinus, False);
|
||||||
|
if BtnFMRptPlus <> nil then StyleButton(BtnFMRptPlus, False);
|
||||||
|
UpdateRptAutoState;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TMainForm.SetFMCTCSSTone(Idx: Integer);
|
procedure TMainForm.SetFMCTCSSTone(Idx: Integer);
|
||||||
begin
|
begin
|
||||||
FFMCTCSSToneIdx := Idx;
|
FFMCTCSSToneIdx := Idx;
|
||||||
@@ -5341,7 +5608,8 @@ end;
|
|||||||
|
|
||||||
procedure TMainForm.BtnFMCTCSSClick(Sender: TObject);
|
procedure TMainForm.BtnFMCTCSSClick(Sender: TObject);
|
||||||
begin
|
begin
|
||||||
FFMCTCSSOn := not FFMCTCSSOn;
|
FFMCTCSSOn := not FFMCTCSSOn;
|
||||||
|
FFMCTCSSAutoActive := False; // ручное действие снимает авто-флаг
|
||||||
StyleButton(BtnFMCTCSS, FFMCTCSSOn);
|
StyleButton(BtnFMCTCSS, FFMCTCSSOn);
|
||||||
BtnFMCTCSS.Repaint; // Flush visual state before WDSP call
|
BtnFMCTCSS.Repaint; // Flush visual state before WDSP call
|
||||||
if FWDSPReady then
|
if FWDSPReady then
|
||||||
@@ -6808,15 +7076,17 @@ begin
|
|||||||
if FWDSPReady then FDSPEngine.SetMode(FMode);
|
if FWDSPReady then FDSPEngine.SetMode(FMode);
|
||||||
UpdateFilterButtons; // показывает/скрывает FM-панели, обновляет SQL UI
|
UpdateFilterButtons; // показывает/скрывает FM-панели, обновляет SQL UI
|
||||||
ApplyModeFilter;
|
ApplyModeFilter;
|
||||||
FFMCTCSSOn := E.LastCTCSSOn;
|
FFMCTCSSOn := E.LastCTCSSOn;
|
||||||
|
FFMCTCSSAutoActive := E.LastCTCSSAutoActive;
|
||||||
if BtnFMCTCSS <> nil then StyleButton(BtnFMCTCSS, FFMCTCSSOn);
|
if BtnFMCTCSS <> nil then StyleButton(BtnFMCTCSS, FFMCTCSSOn);
|
||||||
SetFMCTCSSTone(E.LastCTCSSToneIdx);
|
SetFMCTCSSTone(E.LastCTCSSToneIdx);
|
||||||
ApplyFMSquelch;
|
ApplyFMSquelch;
|
||||||
FFMStepOn := E.LastFMStepOn;
|
FFMStepOn := E.LastFMStepOn;
|
||||||
SetFMStep(E.LastFMStepIdx);
|
SetFMStep(E.LastFMStepIdx);
|
||||||
if BtnFMStep <> nil then StyleButton(BtnFMStep, FFMStepOn);
|
if BtnFMStep <> nil then StyleButton(BtnFMStep, FFMStepOn);
|
||||||
FFMRptDir := E.LastFMRptDir;
|
FFMRptDir := E.LastFMRptDir;
|
||||||
FFMRptOffsetHz := E.LastFMRptOffsetHz;
|
FFMRptAutoActive := E.LastFMRptAutoActive;
|
||||||
|
FFMRptOffsetHz := E.LastFMRptOffsetHz;
|
||||||
if FFMRptOffsetHz <= 0 then
|
if FFMRptOffsetHz <= 0 then
|
||||||
FFMRptOffsetHz := RptDefaultOffsetHz(E.FreqBegin);
|
FFMRptOffsetHz := RptDefaultOffsetHz(E.FreqBegin);
|
||||||
if BtnFMRptMinus <> nil then StyleButton(BtnFMRptMinus, FFMRptDir = RPT_MINUS);
|
if BtnFMRptMinus <> nil then StyleButton(BtnFMRptMinus, FFMRptDir = RPT_MINUS);
|
||||||
@@ -6871,10 +7141,25 @@ procedure TMainForm.DeactivateXvtr;
|
|||||||
var i: Integer;
|
var i: Integer;
|
||||||
begin
|
begin
|
||||||
if FCurrentXvtr < 0 then Exit;
|
if FCurrentXvtr < 0 then Exit;
|
||||||
// Запоминаем LastFreq для bandstack
|
// Сохраняем полное состояние XVTR-слота (раньше сохранялся только LastFreq)
|
||||||
if FCurrentXvtr < CFG_XVTR_COUNT then
|
if FCurrentXvtr < CFG_XVTR_COUNT then
|
||||||
begin
|
begin
|
||||||
FXvtrSettings.Entries[FCurrentXvtr].LastFreq := FVfoA;
|
FXvtrSettings.Entries[FCurrentXvtr].LastFreq := FVfoA;
|
||||||
|
FXvtrSettings.Entries[FCurrentXvtr].LastMode := FMode;
|
||||||
|
FXvtrSettings.Entries[FCurrentXvtr].LastFilterIdx := FFilter;
|
||||||
|
FXvtrSettings.Entries[FCurrentXvtr].LastFMSQOn := FFMSQOn;
|
||||||
|
FXvtrSettings.Entries[FCurrentXvtr].LastFMSQLevel := FFMSQLevel;
|
||||||
|
FXvtrSettings.Entries[FCurrentXvtr].LastCTCSSOn := FFMCTCSSOn;
|
||||||
|
FXvtrSettings.Entries[FCurrentXvtr].LastCTCSSToneIdx := FFMCTCSSToneIdx;
|
||||||
|
FXvtrSettings.Entries[FCurrentXvtr].LastFMStepOn := FFMStepOn;
|
||||||
|
FXvtrSettings.Entries[FCurrentXvtr].LastFMStepIdx := FFMStepIdx;
|
||||||
|
FXvtrSettings.Entries[FCurrentXvtr].LastFMRptDir := FFMRptDir;
|
||||||
|
FXvtrSettings.Entries[FCurrentXvtr].LastFMRptOffsetHz := FFMRptOffsetHz;
|
||||||
|
FXvtrSettings.Entries[FCurrentXvtr].LastCTun := FCTun;
|
||||||
|
FXvtrSettings.Entries[FCurrentXvtr].LastAGCMode := FAGCMode;
|
||||||
|
FXvtrSettings.Entries[FCurrentXvtr].LastAGCTop := FAGCTop;
|
||||||
|
FXvtrSettings.Entries[FCurrentXvtr].LastCTCSSAutoActive := FFMCTCSSAutoActive;
|
||||||
|
FXvtrSettings.Entries[FCurrentXvtr].LastFMRptAutoActive := FFMRptAutoActive;
|
||||||
if FDevConnected then
|
if FDevConnected then
|
||||||
begin
|
begin
|
||||||
FSettings.SaveXvtr(FDevMAC, FXvtrSettings);
|
FSettings.SaveXvtr(FDevMAC, FXvtrSettings);
|
||||||
|
|||||||
+14
-6
@@ -152,8 +152,10 @@ type
|
|||||||
LastCTun: Boolean;
|
LastCTun: Boolean;
|
||||||
LastAGCMode: Integer;
|
LastAGCMode: Integer;
|
||||||
LastAGCTop: Integer;
|
LastAGCTop: Integer;
|
||||||
LastFMRptDir: Integer;
|
LastFMRptDir: Integer;
|
||||||
LastFMRptOffsetHz: Double;
|
LastFMRptOffsetHz: Double;
|
||||||
|
LastCTCSSAutoActive: Boolean; // CTCSS был включён каналом (авто)
|
||||||
|
LastFMRptAutoActive: Boolean; // RPT был включён каналом (авто)
|
||||||
end;
|
end;
|
||||||
|
|
||||||
TXvtrSettings = record
|
TXvtrSettings = record
|
||||||
@@ -1182,8 +1184,10 @@ begin
|
|||||||
X.Entries[i].LastCTun := False;
|
X.Entries[i].LastCTun := False;
|
||||||
X.Entries[i].LastAGCMode := 1;
|
X.Entries[i].LastAGCMode := 1;
|
||||||
X.Entries[i].LastAGCTop := 90;
|
X.Entries[i].LastAGCTop := 90;
|
||||||
X.Entries[i].LastFMRptDir := 0;
|
X.Entries[i].LastFMRptDir := 0;
|
||||||
X.Entries[i].LastFMRptOffsetHz := 0.0;
|
X.Entries[i].LastFMRptOffsetHz := 0.0;
|
||||||
|
X.Entries[i].LastCTCSSAutoActive := False;
|
||||||
|
X.Entries[i].LastFMRptAutoActive := False;
|
||||||
end;
|
end;
|
||||||
// Шаблоны (пользователь должен сам Enable, чтобы трансверторы появились)
|
// Шаблоны (пользователь должен сам Enable, чтобы трансверторы появились)
|
||||||
X.Entries[0].ButtonText := '2m';
|
X.Entries[0].ButtonText := '2m';
|
||||||
@@ -1240,8 +1244,10 @@ begin
|
|||||||
X.Entries[i].LastCTun := JB(EObj, 'last_ctun', X.Entries[i].LastCTun);
|
X.Entries[i].LastCTun := JB(EObj, 'last_ctun', X.Entries[i].LastCTun);
|
||||||
X.Entries[i].LastAGCMode := EnsureRange(JI(EObj, 'last_agc_mode', X.Entries[i].LastAGCMode), 0, 4);
|
X.Entries[i].LastAGCMode := EnsureRange(JI(EObj, 'last_agc_mode', X.Entries[i].LastAGCMode), 0, 4);
|
||||||
X.Entries[i].LastAGCTop := EnsureRange(JI(EObj, 'last_agc_top', X.Entries[i].LastAGCTop), 0, 120);
|
X.Entries[i].LastAGCTop := EnsureRange(JI(EObj, 'last_agc_top', X.Entries[i].LastAGCTop), 0, 120);
|
||||||
X.Entries[i].LastFMRptDir := EnsureRange(JI(EObj, 'last_fmrpt_dir', X.Entries[i].LastFMRptDir), 0, 2);
|
X.Entries[i].LastFMRptDir := EnsureRange(JI(EObj, 'last_fmrpt_dir', X.Entries[i].LastFMRptDir), 0, 2);
|
||||||
X.Entries[i].LastFMRptOffsetHz := JD(EObj, 'last_fmrpt_offset', X.Entries[i].LastFMRptOffsetHz);
|
X.Entries[i].LastFMRptOffsetHz := JD(EObj, 'last_fmrpt_offset', X.Entries[i].LastFMRptOffsetHz);
|
||||||
|
X.Entries[i].LastCTCSSAutoActive := JB(EObj, 'last_ctcss_auto', X.Entries[i].LastCTCSSAutoActive);
|
||||||
|
X.Entries[i].LastFMRptAutoActive := JB(EObj, 'last_fmrpt_auto', X.Entries[i].LastFMRptAutoActive);
|
||||||
// Валидация: LastFreq должен быть в [FreqBegin..FreqEnd]
|
// Валидация: LastFreq должен быть в [FreqBegin..FreqEnd]
|
||||||
if (X.Entries[i].LastFreq < X.Entries[i].FreqBegin) or
|
if (X.Entries[i].LastFreq < X.Entries[i].FreqBegin) or
|
||||||
(X.Entries[i].LastFreq > X.Entries[i].FreqEnd) then
|
(X.Entries[i].LastFreq > X.Entries[i].FreqEnd) then
|
||||||
@@ -1285,6 +1291,8 @@ begin
|
|||||||
JW(EObj, 'last_agc_top', X.Entries[i].LastAGCTop);
|
JW(EObj, 'last_agc_top', X.Entries[i].LastAGCTop);
|
||||||
JW(EObj, 'last_fmrpt_dir', X.Entries[i].LastFMRptDir);
|
JW(EObj, 'last_fmrpt_dir', X.Entries[i].LastFMRptDir);
|
||||||
JW(EObj, 'last_fmrpt_offset', X.Entries[i].LastFMRptOffsetHz);
|
JW(EObj, 'last_fmrpt_offset', X.Entries[i].LastFMRptOffsetHz);
|
||||||
|
JW(EObj, 'last_ctcss_auto', X.Entries[i].LastCTCSSAutoActive);
|
||||||
|
JW(EObj, 'last_fmrpt_auto', X.Entries[i].LastFMRptAutoActive);
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
|||||||
@@ -150,6 +150,14 @@
|
|||||||
<Filename Value="FMRepeater.pas"/>
|
<Filename Value="FMRepeater.pas"/>
|
||||||
<IsPartOfProject Value="True"/>
|
<IsPartOfProject Value="True"/>
|
||||||
</Unit>
|
</Unit>
|
||||||
|
<Unit>
|
||||||
|
<Filename Value="ChannelStore.pas"/>
|
||||||
|
<IsPartOfProject Value="True"/>
|
||||||
|
</Unit>
|
||||||
|
<Unit>
|
||||||
|
<Filename Value="ChannelsForm.pas"/>
|
||||||
|
<IsPartOfProject Value="True"/>
|
||||||
|
</Unit>
|
||||||
<Unit>
|
<Unit>
|
||||||
<Filename Value="FlatSpinEdit.pas"/>
|
<Filename Value="FlatSpinEdit.pas"/>
|
||||||
<IsPartOfProject Value="True"/>
|
<IsPartOfProject Value="True"/>
|
||||||
|
|||||||
Reference in New Issue
Block a user