mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 19:45:09 +00:00
feat(dxcluster): споты DX-кластера на панадаптере
Telnet-клиент DX-кластера, база спотов и подписи позывных прямо на спектре на своих частотах. Разложено на три слоя, как бэндплан и лупа маяка. DXClusterClient.pas — один рабочий поток: резолв, неблокирующий connect с select квантами по 200 мс (Stop не ждёт таймаут соединения), логин позывным, чтение строк, реконнект с backoff. Приглашения логина И пароля ловятся в незавершённом хвосте буфера — типичный telnet-prompt приходит без CR/LF. Ошибки recv отличаются от таймаута кванта (EAGAIN/EINTR/WSAETIMEDOUT), иначе на ECONNRESET поток крутился бы в пустом цикле вместо реконнекта. Отправка дописывает частичный send. LCL-free. DXSpotStore.pas — потокобезопасная база: дедуп по позывному, TTL, потолок записей, монотонный Version. Единственная точка обмена потока с UI: никакого Synchronize, UI сам замечает правки по Version, как оверлеи — по ключам кэша. DXSpotOverlay.pas — рендер по модели BandPlanOverlay/VfoOverlay: кэшируется только полоса подписей (W × BandH) в key-color битмап, пересборка строго по dirty-ключу, на кадр — один keyed-композит. Штрихи от полосы до низа спектра рисует вызывающая сторона теми же примитивами, что и прочие маркеры: CPU — RawVLine внутри RawBegin/RawEnd, GL — DrawLine по готовому списку X/цвет. GL берёт тот же битмап текстурой и заливает её только при смене RenderVersion. Пересекающиеся подписи раскладываются лесенкой, цвет гаснет с возрастом, свой позывной выделен; палитра парная под тёмную и светлую тему. DXClusterForm.pas — окно списка: споты, лог соединения, строка команды кластеру (диалект set/filter у всех свой — не угадываем). Двойной клик или Enter = QSY. Данные тянутся поллингом по Version/LogVersion. Интеграция: кнопка DX в тулбаре (ЛКМ — подписи на спектре, ПКМ — окно), клик по подписи спота = QSY с автовыбором моды по комментарию кластера, страница SETUP → DX Cluster с персистом в секции "dxcluster". Правки SETUP прилетают посимвольно, поэтому запись конфига, TTL стора и переподключение откладываются до паузы в наборе — иначе набор позывного стоил бы шесть реконнектов, а промежуточный TTL «3» необратимо выбросил бы споты. Частота спота кладётся как есть и сравнивается с GetViewWindow: на QO-100 кластеры постят downlink 10489.xxx, что совпадает со шкалой пана само собой. Побочно в общих юнитах: WebUtils.SockSetRcvTimeout, FlatMemo.OnChange, FlatListBox.OnKeyDown, BlendBitmapKey вынесен в interface VfoOverlay (одна копия дворд-блендера на проект). Проверено на локальном фейковом кластере: логин и пароль по prompt без CR/LF, разбор спотов (включая QO-100), уход в RETRY по RST, Stop за 200 мс на висящем connect. На железе рендер не проверялся. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+254
-3
@@ -22,8 +22,9 @@ unit MainForm;
|
||||
interface
|
||||
|
||||
uses
|
||||
Classes, SysUtils, StrUtils, FreqDisplay, DeviceForm, FilterPopup,
|
||||
Classes, SysUtils, StrUtils, DateUtils, FreqDisplay, DeviceForm, FilterPopup,
|
||||
VfoOverlay, SampleRateOverlay, BandPlanOverlay,
|
||||
DXSpotStore, DXClusterClient, DXSpotOverlay, DXClusterForm,
|
||||
FlatButton, FlatSlider, FlatDropDown, OverlayScrollBar, AppTheme,
|
||||
Forms, Controls, Graphics, Dialogs,
|
||||
StdCtrls, ExtCtrls, Buttons, Menus, Math, Types, SyncObjs,
|
||||
@@ -139,7 +140,18 @@ type
|
||||
FSliceDrag: Boolean; // тащим маркер несущей слайса по спектру
|
||||
FSliceDragId: Integer; // какой слайс тащим
|
||||
FSampleRateOverlay: TSampleRateOverlay; // оверлей span/hide слева вверху спектра
|
||||
FBandPlanOverlay: TBandPlanOverlay; // полоска бэндплана QO-100 внизу спектра
|
||||
FBandPlanOverlay: TBandPlanOverlay; // полоска бэндплана QO-100
|
||||
// ── DX-кластер ────────────────────────────────────────────────────────
|
||||
FDXStore: TDXSpotStore; // общая база спотов (поток кластера пишет)
|
||||
FDXClient: TDXClusterClient; // telnet-соединение (свой поток)
|
||||
FDXSpotOverlay: TDXSpotOverlay; // подписи спотов на спектре
|
||||
FDXClusterForm: TDXClusterForm; // окно списка (ПКМ по кнопке DX)
|
||||
FDXCfg: TDXClusterSettings;
|
||||
FDXConnCfg: TDXClusterSettings; // параметры, с которыми поднят клиент
|
||||
FDXPendingCfg: TDXClusterSettings; // правки SETUP, ждущие тишины
|
||||
FDXPending: Boolean;
|
||||
FDXPendingAt: TDateTime;
|
||||
FDXAgeTickAt: TDateTime; // когда последний раз гасили старые споты внизу спектра
|
||||
FPanelHidden: Boolean; // True = левая панель скрыта
|
||||
FLeftPanelW: Integer; // ширина панели до скрытия (DPI-safe restore)
|
||||
FPendingBoardType: Integer; // BoardType устройства для подключения
|
||||
@@ -263,6 +275,7 @@ type
|
||||
BtnDiscover: TFlatButton;
|
||||
BtnStartStop: TFlatButton;
|
||||
BtnSettings: TFlatButton;
|
||||
BtnDXCluster: TFlatButton; // ЛКМ — споты на спектре, ПКМ — окно кластера
|
||||
|
||||
// ---- Left panel ----
|
||||
PanelLeft: TPanel;
|
||||
@@ -497,6 +510,19 @@ type
|
||||
procedure BtnRxMuteClick(Sender: TObject);
|
||||
procedure UpdateRxMuteButton; // видимость (Pluto+full-duplex) + стиль кнопки RX MUTE
|
||||
procedure UpdateBandPlanOverlay; // вкл/выкл полоски бэндплана QO-100 по InQO100
|
||||
// ── DX-кластер ────────────────────────────────────────────────────────
|
||||
procedure InitDXCluster; // стор+клиент+оверлей, чтение настроек
|
||||
procedure ServiceDXCluster; // тик затухания спотов (редкий)
|
||||
procedure ApplyDXClusterSettings(const D: TDXClusterSettings);
|
||||
procedure UpdateDXClusterButton;
|
||||
procedure BtnDXClusterClick(Sender: TObject);
|
||||
procedure BtnDXClusterMouseDown(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
procedure ShowDXClusterForm;
|
||||
procedure ApplyDXConnection(const D: TDXClusterSettings);
|
||||
procedure OnDXClusterSettingsChange(const D: TDXClusterSettings);
|
||||
procedure CommitDXClusterSettings;
|
||||
procedure OnDXTuneSpot(FreqHz: Double; Mode: TDXMode);
|
||||
procedure BtnTUNClick(Sender: TObject);
|
||||
procedure ApplyTUN(Active: Boolean);
|
||||
// PureSignal: ЛКМ по PS = вкл/выкл, ПКМ = поповер настроек.
|
||||
@@ -1373,6 +1399,17 @@ begin
|
||||
PanelSplitter := nil;
|
||||
PbWaterfall := nil;
|
||||
PbPanZoom := nil;
|
||||
// DX-кластер: незакоммиченную правку SETUP дописываем в конфиг (соединение
|
||||
// при этом не трогаем — программа закрывается), гасим сетевой поток (его
|
||||
// Stop делает shutdown сокета и ждёт поток), потом базу спотов — оверлей
|
||||
// к этому моменту уже мёртв вместе с view пана.
|
||||
if FDXPending then
|
||||
begin
|
||||
FDXPending := False;
|
||||
FController.FSettings.SaveDXClusterSettings(FDXPendingCfg);
|
||||
end;
|
||||
FreeAndNil(FDXClient);
|
||||
FreeAndNil(FDXStore);
|
||||
DestroyAllExtraPans; // доп. паны (DDC/движок/панели) — до пана 0
|
||||
FreeAndNil(FPan);
|
||||
FPans[0] := nil;
|
||||
@@ -1976,6 +2013,10 @@ begin
|
||||
BtnDiscover := MakeBtn(PanelToolbar, 'DISCOVER', X, 3, 80, BTN_H, BtnDiscoverClick);
|
||||
BtnStartStop := MakeBtn(PanelToolbar, 'START', X+84, 3, 76, BTN_H, BtnStartStopClick);
|
||||
BtnSettings := MakeBtn(PanelToolbar, 'SETUP', X+164, 3, 66, BTN_H, BtnSettingsClick);
|
||||
// DX — споты кластера: ЛКМ включает/выключает подписи на спектре, ПКМ
|
||||
// открывает окно списка (та же семантика, что у BEACON → окно констелляции).
|
||||
BtnDXCluster := MakeBtn(PanelToolbar, 'DX', X+234, 3, 46, BTN_H, BtnDXClusterClick);
|
||||
BtnDXCluster.OnMouseDown := BtnDXClusterMouseDown;
|
||||
|
||||
// DISCOVER — синеватый акцент
|
||||
BtnDiscover.ClrNorm := TColor($00101828);
|
||||
@@ -2641,6 +2682,9 @@ begin
|
||||
FBandPlanOverlay.SetView(FController.FCenterFreq, FController.FSpanHz);
|
||||
FSpecView.BandPlanOverlay := FBandPlanOverlay;
|
||||
|
||||
// DX-кластер: база спотов + сетевой поток + оверлей подписей.
|
||||
InitDXCluster;
|
||||
|
||||
// S-метр — внутри PanelToolbar, справа
|
||||
PanelSMeterRight := TPanel.Create(Self);
|
||||
PanelSMeterRight.Parent := PanelToolbar;
|
||||
@@ -2763,6 +2807,9 @@ begin
|
||||
SafeLeft := MARGIN;
|
||||
if BtnSettings <> nil then
|
||||
SafeLeft := BtnSettings.Left + BtnSettings.Width + SETTINGS_GAP;
|
||||
// Кнопка DX стоит правее SETUP — группа VFO начинается уже за ней.
|
||||
if (BtnDXCluster <> nil) and BtnDXCluster.Visible then
|
||||
SafeLeft := Max(SafeLeft, BtnDXCluster.Left + BtnDXCluster.Width + SETTINGS_GAP);
|
||||
SafeRight := ClientWidth - MARGIN;
|
||||
if PanelLeft <> nil then
|
||||
begin
|
||||
@@ -3301,6 +3348,9 @@ begin
|
||||
BtnStartStop.ClrTextAct:= T.TbStartText;
|
||||
BtnStartStop.Invalidate;
|
||||
StyleButton(BtnSettings, False);
|
||||
// DX — тумблер подписей спотов: состояние берём из самой кнопки, как у
|
||||
// прочих тумблеров ниже (BtnCTun/BtnBeacon).
|
||||
if BtnDXCluster <> nil then StyleButton(BtnDXCluster, BtnDXCluster.Active);
|
||||
|
||||
// Обычные кнопки — все массивы
|
||||
for i := 0 to BAND_COUNT-1 do StyleButton(BtnBand[i], BtnBand[i].Active);
|
||||
@@ -3451,6 +3501,8 @@ begin
|
||||
if FCWMsgForm <> nil then TCWMessagesForm(FCWMsgForm).ApplyTheme(T);
|
||||
if FCWTermForm <> nil then TCWTerminalForm(FCWTermForm).ApplyTheme(T);
|
||||
if FBeaconScopeForm <> nil then FBeaconScopeForm.ApplyTheme(T);
|
||||
if FDXClusterForm <> nil then FDXClusterForm.ApplyTheme(T);
|
||||
if FDXSpotOverlay <> nil then FDXSpotOverlay.SetLightTheme(FLightTheme);
|
||||
FController.FSettings.SaveTheme(V);
|
||||
FController.FSettings.Save;
|
||||
if (FSettingsForm <> nil) and TSettingsForm(FSettingsForm).Visible then
|
||||
@@ -3690,6 +3742,9 @@ begin
|
||||
// отсекает неизменное → пересборки кэша на каждый вызов нет.
|
||||
if Assigned(FBandPlanOverlay) then
|
||||
FBandPlanOverlay.SetView(ViewCenter, ViewSpan);
|
||||
// Подписи спотов — тот же видимый домен, что у бэндплана.
|
||||
if Assigned(FDXSpotOverlay) then
|
||||
FDXSpotOverlay.SetView(ViewCenter, ViewSpan);
|
||||
if UpdateWidebandFrequencyView and FController.FShowWideband and (PbWideband <> nil) then
|
||||
PbWideband.Invalidate;
|
||||
UpdatePanZoomBar; // линейка пана следует за частотой/зумом (no-op если без изменений)
|
||||
@@ -3714,6 +3769,9 @@ var
|
||||
PeakDiff, MinDiff: Double;
|
||||
PeakAlpha, MinAlpha: Double;
|
||||
begin
|
||||
// Затухание спотов не зависит от того, крутится ли приём.
|
||||
ServiceDXCluster;
|
||||
|
||||
if not FController.FRunning then
|
||||
begin
|
||||
if PbSMeterRight <> nil then PbSMeterRight.Invalidate;
|
||||
@@ -4947,6 +5005,186 @@ begin
|
||||
StyleButton(BtnRxMute, FController.FRxMuteOnTx);
|
||||
end;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// DX-кластер: база спотов + telnet-клиент + оверлей подписей на спектре
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function DXModeSetFromMask(Mask: LongWord): TDXModeSet;
|
||||
// Маска настроек → множество мод. 0 = без фильтра (показываем всё).
|
||||
var M: TDXMode;
|
||||
begin
|
||||
Result := [];
|
||||
if Mask = 0 then Exit;
|
||||
for M := Low(TDXMode) to High(TDXMode) do
|
||||
if (Mask and (LongWord(1) shl Ord(M))) <> 0 then Include(Result, M);
|
||||
end;
|
||||
|
||||
procedure TMainForm.InitDXCluster;
|
||||
begin
|
||||
FDXStore := TDXSpotStore.Create;
|
||||
FDXClient := TDXClusterClient.Create(FDXStore);
|
||||
|
||||
FDXSpotOverlay := TDXSpotOverlay.Create(Self);
|
||||
FDXSpotOverlay.Attach(FDXStore);
|
||||
FDXSpotOverlay.SetLightTheme(FLightTheme);
|
||||
FDXSpotOverlay.SetView(FController.FCenterFreq, FController.FSpanHz);
|
||||
FSpecView.DXSpotOverlay := FDXSpotOverlay;
|
||||
|
||||
FDXAgeTickAt := Now;
|
||||
FController.FSettings.LoadDXClusterSettings(FDXCfg);
|
||||
FDXStore.TTLMinutes := FDXCfg.TTLMinutes;
|
||||
ApplyDXClusterSettings(FDXCfg);
|
||||
// FDXConnCfg пуст → ApplyDXConnection увидит смену параметров и поднимет
|
||||
// соединение, если оно включено и позывной задан.
|
||||
ApplyDXConnection(FDXCfg);
|
||||
end;
|
||||
|
||||
procedure TMainForm.ApplyDXClusterSettings(const D: TDXClusterSettings);
|
||||
// Вид: оверлей + кнопка. Применяется сразу на каждую правку в SETUP — это
|
||||
// дёшево, обратимо и даёт живой отклик. Всё, что МЕНЯЕТ ДАННЫЕ (TTL стора) или
|
||||
// трогает соединение, откладывается до CommitDXClusterSettings: набирая «30» в
|
||||
// поле TTL, пользователь на миг проходит через «3», а это выбросило бы из базы
|
||||
// все споты старше трёх минут — необратимо.
|
||||
begin
|
||||
FDXCfg := D;
|
||||
if FDXSpotOverlay <> nil then
|
||||
begin
|
||||
FDXSpotOverlay.MaxRows := D.MaxRows;
|
||||
FDXSpotOverlay.SetOwnCall(D.Login);
|
||||
FDXSpotOverlay.SetFilters(DXModeSetFromMask(D.ModeMask), D.TTLMinutes);
|
||||
FDXSpotOverlay.SetEnabled(D.ShowSpots);
|
||||
FDXSpotOverlay.Invalidate;
|
||||
end;
|
||||
UpdateDXClusterButton;
|
||||
FSpectrumDirty := True;
|
||||
end;
|
||||
|
||||
procedure TMainForm.ApplyDXConnection(const D: TDXClusterSettings);
|
||||
// Соединение: конфиг в клиент + поднять/положить/переподнять. Поток снимает
|
||||
// конфиг один раз на сессию, поэтому смена адреса/учётки требует рестарта —
|
||||
// как у веб-сервера при смене порта.
|
||||
var Reconnect: Boolean;
|
||||
begin
|
||||
if FDXClient = nil then Exit;
|
||||
Reconnect := (D.Host <> FDXConnCfg.Host) or (D.Port <> FDXConnCfg.Port) or
|
||||
(D.Login <> FDXConnCfg.Login) or
|
||||
(D.Password <> FDXConnCfg.Password) or
|
||||
(D.PostLogin <> FDXConnCfg.PostLogin);
|
||||
FDXConnCfg := D;
|
||||
FDXClient.Configure(D.Host, D.Port, D.Login, D.Password, D.PostLogin);
|
||||
|
||||
// Без позывного логиниться нечем — не поднимаем соединение вообще.
|
||||
if (not D.Enabled) or (Trim(D.Login) = '') then
|
||||
begin
|
||||
if FDXClient.Running then FDXClient.Stop;
|
||||
Exit;
|
||||
end;
|
||||
if FDXClient.Running then
|
||||
begin
|
||||
if not Reconnect then Exit;
|
||||
FDXClient.Stop;
|
||||
end;
|
||||
FDXClient.Start;
|
||||
end;
|
||||
|
||||
procedure TMainForm.OnDXClusterSettingsChange(const D: TDXClusterSettings);
|
||||
// Каждая правка в SETUP прилетает сюда ПОСИМВОЛЬНО (OnChange поля). Вид
|
||||
// применяем сразу, а запись в конфиг и переподключение откладываем: иначе
|
||||
// набор позывного «UA3XYZ» дёргал бы шесть реконнектов и шесть записей JSON.
|
||||
begin
|
||||
ApplyDXClusterSettings(D);
|
||||
FDXPendingCfg := D;
|
||||
FDXPendingAt := Now;
|
||||
FDXPending := True;
|
||||
end;
|
||||
|
||||
procedure TMainForm.CommitDXClusterSettings;
|
||||
// Отложенный коммит правок SETUP: пользователь перестал печатать.
|
||||
begin
|
||||
FDXPending := False;
|
||||
FController.FSettings.SaveDXClusterSettings(FDXPendingCfg);
|
||||
if FDXStore <> nil then FDXStore.TTLMinutes := FDXPendingCfg.TTLMinutes;
|
||||
ApplyDXConnection(FDXPendingCfg);
|
||||
end;
|
||||
|
||||
procedure TMainForm.ServiceDXCluster;
|
||||
// Раз в DX_AGE_TICK_SEC подталкиваем оверлей пересобраться: споты тускнеют с
|
||||
// возрастом и уходят по TTL, а без нового спота версия стора не меняется и
|
||||
// повода для пересборки бы не было. Реже — незаметно, чаще — впустую.
|
||||
const
|
||||
DX_AGE_TICK_SEC = 30;
|
||||
DX_SETTINGS_QUIET_MS = 1200; // тишина в SETUP, после которой коммитим
|
||||
begin
|
||||
// Отложенный коммит настроек: пользователь перестал печатать в SETUP.
|
||||
if FDXPending and (MilliSecondsBetween(Now, FDXPendingAt) >= DX_SETTINGS_QUIET_MS) then
|
||||
CommitDXClusterSettings;
|
||||
|
||||
if FDXSpotOverlay = nil then Exit;
|
||||
if SecondsBetween(Now, FDXAgeTickAt) < DX_AGE_TICK_SEC then Exit;
|
||||
FDXAgeTickAt := Now;
|
||||
if not FDXSpotOverlay.Active then Exit;
|
||||
FDXSpotOverlay.TickAge;
|
||||
FSpectrumDirty := True;
|
||||
end;
|
||||
|
||||
procedure TMainForm.UpdateDXClusterButton;
|
||||
begin
|
||||
if BtnDXCluster = nil then Exit;
|
||||
StyleButton(BtnDXCluster,
|
||||
(FDXSpotOverlay <> nil) and FDXSpotOverlay.Active);
|
||||
end;
|
||||
|
||||
procedure TMainForm.BtnDXClusterClick(Sender: TObject);
|
||||
// ЛКМ — показывать/не показывать подписи спотов на спектре (настройка живёт
|
||||
// в конфиге, чтобы состояние пережило перезапуск).
|
||||
begin
|
||||
if FDXSpotOverlay = nil then Exit;
|
||||
FDXCfg.ShowSpots := not FDXCfg.ShowSpots;
|
||||
FDXSpotOverlay.SetEnabled(FDXCfg.ShowSpots);
|
||||
FController.FSettings.SaveDXClusterSettings(FDXCfg);
|
||||
UpdateDXClusterButton;
|
||||
FSpectrumDirty := True;
|
||||
end;
|
||||
|
||||
procedure TMainForm.BtnDXClusterMouseDown(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
begin
|
||||
if Button = mbRight then ShowDXClusterForm;
|
||||
end;
|
||||
|
||||
procedure TMainForm.ShowDXClusterForm;
|
||||
begin
|
||||
if FDXClusterForm = nil then
|
||||
begin
|
||||
FDXClusterForm := TDXClusterForm.CreateWith(Self, FDXStore, FDXClient);
|
||||
FDXClusterForm.OnTuneSpot := OnDXTuneSpot;
|
||||
end;
|
||||
FDXClusterForm.ApplyTheme(CurrentAppTheme);
|
||||
if FDXClusterForm.Visible then FDXClusterForm.Hide
|
||||
else FDXClusterForm.Show;
|
||||
end;
|
||||
|
||||
procedure TMainForm.OnDXTuneSpot(FreqHz: Double; Mode: TDXMode);
|
||||
// QSY по споту: клик по подписи на спектре или двойной клик в окне списка.
|
||||
// Мода ставится только когда она однозначна из комментария кластера; при
|
||||
// dxmUnknown текущую не трогаем — гадать хуже, чем не менять.
|
||||
var NewMode: Integer;
|
||||
begin
|
||||
if FreqHz <= 0 then Exit;
|
||||
ApplyVfoA(Round(FreqHz));
|
||||
NewMode := -1;
|
||||
case Mode of
|
||||
dxmCW: NewMode := MODE_CWU;
|
||||
// Боковая по общепринятому правилу: ниже 10 МГц — LSB, выше — USB.
|
||||
dxmSSB: if FreqHz < 10000000.0 then NewMode := MODE_LSB else NewMode := MODE_USB;
|
||||
dxmFT8, dxmFT4, dxmDigi, dxmRTTY, dxmPSK: NewMode := MODE_DIGU;
|
||||
dxmSSTV: NewMode := MODE_USB;
|
||||
dxmFM: NewMode := MODE_FM;
|
||||
end;
|
||||
if (NewMode >= 0) and (NewMode <> FController.FMode) then
|
||||
FController.SetMode(NewMode);
|
||||
end;
|
||||
|
||||
procedure TMainForm.UpdateBandPlanOverlay;
|
||||
// Полоска бэндплана видна только в QO-100 (Pluto + full-duplex транспондер).
|
||||
begin
|
||||
@@ -5251,7 +5489,9 @@ end;
|
||||
// Спектр
|
||||
procedure TMainForm.PbSpectrumMouseDown(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
var VC, VS: Double;
|
||||
var
|
||||
VC, VS: Double;
|
||||
DXSpot: TDXSpot;
|
||||
begin
|
||||
if FPan.DispatchFlagsMouseDown(Button, X, Y) then
|
||||
begin
|
||||
@@ -5261,6 +5501,15 @@ begin
|
||||
if Assigned(FSampleRateOverlay) and
|
||||
FSampleRateOverlay.HandleMouseDown(Button, X, Y) then Exit;
|
||||
|
||||
// ЛКМ по подписи DX-спота — QSY на него. Проверяется до тюнинга/драга, но
|
||||
// только в полосе подписей вверху: ниже неё поведение спектра прежнее.
|
||||
if (Button = mbLeft) and Assigned(FDXSpotOverlay) and
|
||||
FDXSpotOverlay.SpotAtPixel(X, Y, DXSpot) then
|
||||
begin
|
||||
OnDXTuneSpot(DXSpot.FreqHz, DXSpot.Mode);
|
||||
Exit;
|
||||
end;
|
||||
|
||||
// Ctrl+ЛКМ по спектру — добавить софт-слайс на этой частоте (в пределах захвата).
|
||||
if (Button = mbLeft) and (ssCtrl in Shift) and (PbSpectrum.Width > 0) then
|
||||
begin
|
||||
@@ -9286,6 +9535,7 @@ begin
|
||||
SF.OnWfAGCNFChange := ApplyWfAGCNF;
|
||||
SF.OnADCChange := ApplyADCSettings;
|
||||
SF.OnWebSettingsChange := ApplyWebSettings;
|
||||
SF.OnDXClusterChange := OnDXClusterSettingsChange;
|
||||
end;
|
||||
SF := TSettingsForm(FSettingsForm);
|
||||
PushTXProfilesToSettings;
|
||||
@@ -9340,6 +9590,7 @@ begin
|
||||
SF.LoadADCSettings(FController.FDitherEnabled, FController.FRandomEnabled);
|
||||
SF.LoadWebSettings(FWebEnabled, FWebPort, FWebBindAddr, FWebUser, FWebPass,
|
||||
FWebSpecPixels);
|
||||
SF.LoadDXClusterSettings(FDXCfg);
|
||||
SF.LoadFPS(FDisplayFPS);
|
||||
SF.LoadLightTheme(FLightTheme);
|
||||
SF.LoadFreqMhzDigits(FFreqMhzDigits);
|
||||
|
||||
Reference in New Issue
Block a user