mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 18:43:51 +00:00
feat(dxcluster): споты на всех панадаптерах + кнопка DX в блоке RX
Подписи спотов рисовались только на главном пане. Теперь их видит каждый пан, в чьё окно попадает частота спота. Оверлей — ЭКЗЕМПЛЯР НА ПАН, а не один общий: кэш полосы подписей ключуется центром/спаном/шириной, а у панов они свои — общий оверлей пересобирался бы на каждый пан каждый кадр, то есть ровно то, ради чего кэш и заводился. Живёт в TPanafallPanel (AttachDXSpots/DetachDXSpots/DXOverlay, Owner=панель), база спотов по-прежнему одна на всех. FDXSpotOverlay в MainForm стал алиасом на оверлей пана 0 — как FSpecView для FPan.View: настройки, тема и тик затухания идут общим циклом по FPans, и главный пан перестал быть особым случаем (иначе тумблер DX гасил бы подписи только на нём). ★ Version стора глобальна, а окно у пана своё, поэтому в EnsureRendered на смену версии сначала берётся снимок окна и сверяется его подпись (FNV-1a по позывному, частоте, моде и метке времени). Совпала — ни пересборки битмапа, ни перезаливки GL-текстуры: спот, севший на чужой диапазон, до этого пана не доходит. Без этого цена пересборки множилась бы на число панов. Снимок берётся один раз и переиспользуется раскладкой — стор второй раз не дёргаем. Клик по подписи на пане N = QSY слайса ЭТОГО пана (активного, если он здесь, иначе первого; нет ни одного — создаём в точке) с модой по комментарию кластера и полосой пресета этой моды. Своего VFO у панов N нет, а ретюнить их DDC под спот нельзя — увезло бы весь пан. Мода спота → режим вынесена в общую DXSpotRadioMode для обоих путей. Низкий пан (грид): полоса подписей и штрихи пропускаются целиком — раньше в GL-пути полоса легла бы поверх спектра, а штрихи пошли бы снизу вверх. Кнопка DX переехала из тулбара в блок RX левой панели, сразу после CTUN (ряд стал четырёхколоночным: CTUN | DX | Channel | BEACON): споты — часть приёмного вида, а не глобальная команда уровня DISCOVER/START/SETUP. Заодно снят расчёт SafeLeft, резервировавший под неё место в шапке. Подписи по умолчанию ВЫКЛЮЧЕНЫ (show_spots: дефолт и фолбэк чтения были True), состояние переживает перезапуск. Клик по кнопке подливает состояние в открытый SETUP: там своя копия конфига, и первая же правка любого поля страницы вернула бы подписи обратно. fix: Stop клиента кластера больше не держит главный поток. Он делал Terminate + WaitFor, а поток в этот момент сидит в блокирующем recv и просыпается лишь по своему кванту (до 1 с), на застрявшем send — до SO_SNDTIMEO. Всё это время окно висело на выходе и на переподключении после правки настроек. Теперь Stop рвёт живой сокет SockShutdown (хэндл — зеркало под тем же локом, поток снимает его ДО close, поэтому shutdown по закрытому fd невозможен), а FormDestroy зовёт неблокирующий RequestStop первой строкой: поток доживает параллельно с разборкой радио, и join в конце уже никого не ждёт. Сборка ewsdr (--ws=qt6) и ewsdrd — ОК. На железе не проверено: доп. паны требуют подключённого радио. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+174
-50
@@ -275,7 +275,6 @@ type
|
||||
BtnDiscover: TFlatButton;
|
||||
BtnStartStop: TFlatButton;
|
||||
BtnSettings: TFlatButton;
|
||||
BtnDXCluster: TFlatButton; // ЛКМ — споты на спектре, ПКМ — окно кластера
|
||||
|
||||
// ---- Left panel ----
|
||||
PanelLeft: TPanel;
|
||||
@@ -373,6 +372,8 @@ type
|
||||
LblAtt: TLabel;
|
||||
LblAttVal: TLabel;
|
||||
TrkAtt: TFlatSlider;
|
||||
// Ряд CTUN | DX | Channel | BEACON.
|
||||
BtnDXCluster: TFlatButton; // ЛКМ — споты на спектре, ПКМ — окно кластера
|
||||
BtnChannels: TFlatButton;
|
||||
FChannelsDropDown: TFlatDropDown;
|
||||
// TX-профиль: кнопка с именем активного (блок TX, ряд DUP) + список.
|
||||
@@ -514,6 +515,9 @@ type
|
||||
procedure InitDXCluster; // стор+клиент+оверлей, чтение настроек
|
||||
procedure ServiceDXCluster; // тик затухания спотов (редкий)
|
||||
procedure ApplyDXClusterSettings(const D: TDXClusterSettings);
|
||||
procedure AttachPanDXSpots(P: TPanafallPanel); // свой оверлей спотов на пан
|
||||
procedure ApplyDXOverlayCfg(O: TDXSpotOverlay); // настройки вида → один оверлей
|
||||
procedure DXTuneSpotOnPan(P: TPanafallPanel; FreqHz: Double; Mode: TDXMode);
|
||||
procedure UpdateDXClusterButton;
|
||||
procedure BtnDXClusterClick(Sender: TObject);
|
||||
procedure BtnDXClusterMouseDown(Sender: TObject; Button: TMouseButton;
|
||||
@@ -1383,9 +1387,14 @@ begin
|
||||
end;
|
||||
|
||||
procedure TMainForm.FormDestroy(Sender: TObject);
|
||||
var i: Integer;
|
||||
begin
|
||||
FMeterTimer.Enabled := False;
|
||||
FSpectrumTimer.Enabled := False;
|
||||
// Поток кластера будим ПЕРВЫМ делом, не дожидаясь его: пока идёт (небыстрая)
|
||||
// разборка радио, он успевает выйти сам, и join в FreeAndNil(FDXClient) ниже
|
||||
// уже никого не ждёт. Иначе окно висело бы на выходе.
|
||||
if FDXClient <> nil then FDXClient.RequestStop;
|
||||
// Грациозный стоп радио (Run=0) до освобождения; сами движки закрывает и
|
||||
// освобождает TRadioController.Destroy (FreeEngines) ниже.
|
||||
if FController.FNetwork.Running then
|
||||
@@ -1401,13 +1410,18 @@ begin
|
||||
PbPanZoom := nil;
|
||||
// DX-кластер: незакоммиченную правку SETUP дописываем в конфиг (соединение
|
||||
// при этом не трогаем — программа закрывается), гасим сетевой поток (его
|
||||
// Stop делает shutdown сокета и ждёт поток), потом базу спотов — оверлей
|
||||
// к этому моменту уже мёртв вместе с view пана.
|
||||
// Stop делает shutdown сокета и ждёт поток), потом базу спотов. Оверлеи
|
||||
// панов живут до гибели своих панов (ниже) и на общую базу ссылаются —
|
||||
// поэтому сначала отвязываем их: после Detach оверлей неактивен и к стору
|
||||
// не ходит, даже если между этими строками прилетит перерисовка.
|
||||
if FDXPending then
|
||||
begin
|
||||
FDXPending := False;
|
||||
FController.FSettings.SaveDXClusterSettings(FDXPendingCfg);
|
||||
end;
|
||||
for i := 0 to MAX_PANS - 1 do
|
||||
if FPans[i] <> nil then FPans[i].DetachDXSpots;
|
||||
FDXSpotOverlay := nil;
|
||||
FreeAndNil(FDXClient);
|
||||
FreeAndNil(FDXStore);
|
||||
DestroyAllExtraPans; // доп. паны (DDC/движок/панели) — до пана 0
|
||||
@@ -2013,10 +2027,8 @@ 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;
|
||||
// DX живёт в блоке RX левой панели, рядом с CTUN (см. ниже): споты — часть
|
||||
// приёмного вида, а не глобальная команда уровня DISCOVER/START/SETUP.
|
||||
|
||||
// DISCOVER — синеватый акцент
|
||||
BtnDiscover.ClrNorm := TColor($00101828);
|
||||
@@ -2272,7 +2284,7 @@ begin
|
||||
|
||||
Inc(Y, 100);
|
||||
|
||||
// RX block: CTUN + VOL + NR/NB/SNB/ANF/MUTE
|
||||
// RX block: CTUN + DX + Channel + BEACON, VOL, NR/NB/SNB/ANF/MUTE
|
||||
PanelRXBlock := TPanel.Create(Self);
|
||||
PanelRXBlock.Parent := PanelLeft;
|
||||
PanelRXBlock.SetBounds(0, Y, LEFT_W, RX_BLOCK_H_ATT);
|
||||
@@ -2280,15 +2292,23 @@ begin
|
||||
MakeLbl(PanelRXBlock, 'RX', 4, 2);
|
||||
|
||||
BtnCTun := MakeBtn(PanelRXBlock, 'CTUN',
|
||||
LeftPanelButtonLeft(LEFT_W, 3, 0), 20,
|
||||
LeftPanelButtonWidth(LEFT_W, 3, 0),
|
||||
LeftPanelButtonLeft(LEFT_W, 4, 0), 20,
|
||||
LeftPanelButtonWidth(LEFT_W, 4, 0),
|
||||
BTN_H, BtnCTunClick);
|
||||
BtnCTun.Tag := 0;
|
||||
StyleButton(BtnCTun, FController.FCTun);
|
||||
|
||||
// DX — споты кластера: ЛКМ включает/выключает подписи на спектре, ПКМ
|
||||
// открывает окно списка (та же семантика, что у BEACON → окно констелляции).
|
||||
BtnDXCluster := MakeBtn(PanelRXBlock, 'DX',
|
||||
LeftPanelButtonLeft(LEFT_W, 4, 1), 20,
|
||||
LeftPanelButtonWidth(LEFT_W, 4, 1),
|
||||
BTN_H, BtnDXClusterClick);
|
||||
BtnDXCluster.OnMouseDown := BtnDXClusterMouseDown;
|
||||
|
||||
BtnChannels := MakeBtn(PanelRXBlock, 'Channel',
|
||||
LeftPanelButtonLeft(LEFT_W, 3, 1), 20,
|
||||
LeftPanelButtonWidth(LEFT_W, 3, 1),
|
||||
LeftPanelButtonLeft(LEFT_W, 4, 2), 20,
|
||||
LeftPanelButtonWidth(LEFT_W, 4, 2),
|
||||
BTN_H, BtnChannelsClick);
|
||||
BtnChannels.OnMouseDown := BtnChannelsMouseDown;
|
||||
StyleButton(BtnChannels, False);
|
||||
@@ -2298,11 +2318,11 @@ begin
|
||||
FChannelsDropDown.OnSelect := OnChannelsDropDownSelect;
|
||||
RefreshChannelsDropDown;
|
||||
|
||||
// QO-100 beacon lock — 3-я колонка ряда. Видима только в XVTR-режиме
|
||||
// QO-100 beacon lock — 4-я колонка ряда. Видима только в XVTR-режиме
|
||||
// (см. рендер rfXvtr ниже). Подпись/подсветка обновляются по rfBeaconLock.
|
||||
BtnBeacon := MakeBtn(PanelRXBlock, 'BEACON',
|
||||
LeftPanelButtonLeft(LEFT_W, 3, 2), 20,
|
||||
LeftPanelButtonWidth(LEFT_W, 3, 2),
|
||||
LeftPanelButtonLeft(LEFT_W, 4, 3), 20,
|
||||
LeftPanelButtonWidth(LEFT_W, 4, 3),
|
||||
BTN_H, BtnBeaconClick);
|
||||
BtnBeacon.Visible := False;
|
||||
BtnBeacon.OnMouseDown := BtnBeaconMouseDown; // ПКМ → декодер + окно констелляции
|
||||
@@ -2807,9 +2827,6 @@ 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
|
||||
@@ -3348,9 +3365,6 @@ 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);
|
||||
@@ -3366,6 +3380,8 @@ begin
|
||||
StyleButton(Btn2Ton, Btn2Ton.Active);
|
||||
StyleButton(BtnRxMute, BtnRxMute.Active);
|
||||
StyleButton(BtnBeacon, BtnBeacon.Active);
|
||||
// DX — тумблер подписей спотов, состояние берём из самой кнопки (сосед CTUN).
|
||||
if BtnDXCluster <> nil then StyleButton(BtnDXCluster, BtnDXCluster.Active);
|
||||
if BtnFMSQ <> nil then StyleButton(BtnFMSQ, BtnFMSQ.Active);
|
||||
if TrkFMSQ <> nil then DS(TrkFMSQ);
|
||||
if BtnFMCTCSS <> nil then StyleButton(BtnFMCTCSS, BtnFMCTCSS.Active);
|
||||
@@ -3387,6 +3403,9 @@ begin
|
||||
if FPans[i] <> nil then
|
||||
begin
|
||||
FPans[i].View.SetTheme(T);
|
||||
// Палитра подписей спотов парная под тему — у каждого пана свой оверлей.
|
||||
if FPans[i].DXOverlay <> nil then
|
||||
FPans[i].DXOverlay.SetLightTheme(FLightTheme);
|
||||
if FPans[i].PbSpectrum is TPaintBox then
|
||||
TPaintBox(FPans[i].PbSpectrum).Color := T.BG;
|
||||
if FPans[i].PbWaterfall is TPaintBox then
|
||||
@@ -3502,7 +3521,7 @@ begin
|
||||
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);
|
||||
// Оверлеи спотов перекрашены в ApplyDarkTheme — общим циклом по FPans.
|
||||
FController.FSettings.SaveTheme(V);
|
||||
FController.FSettings.Save;
|
||||
if (FSettingsForm <> nil) and TSettingsForm(FSettingsForm).Visible then
|
||||
@@ -5024,11 +5043,13 @@ begin
|
||||
FDXStore := TDXSpotStore.Create;
|
||||
FDXClient := TDXClusterClient.Create(FDXStore);
|
||||
|
||||
FDXSpotOverlay := TDXSpotOverlay.Create(Self);
|
||||
FDXSpotOverlay.Attach(FDXStore);
|
||||
// Оверлей пана 0 живёт в самом пане, как и у панов N; FDXSpotOverlay — только
|
||||
// алиас на него (как FSpecView для FPan.View), чтобы главный пан не был
|
||||
// особым случаем в настройках/теме/тике.
|
||||
FPan.AttachDXSpots(FDXStore);
|
||||
FDXSpotOverlay := FPan.DXOverlay;
|
||||
FDXSpotOverlay.SetLightTheme(FLightTheme);
|
||||
FDXSpotOverlay.SetView(FController.FCenterFreq, FController.FSpanHz);
|
||||
FSpecView.DXSpotOverlay := FDXSpotOverlay;
|
||||
|
||||
FDXAgeTickAt := Now;
|
||||
FController.FSettings.LoadDXClusterSettings(FDXCfg);
|
||||
@@ -5045,20 +5066,47 @@ procedure TMainForm.ApplyDXClusterSettings(const D: TDXClusterSettings);
|
||||
// трогает соединение, откладывается до CommitDXClusterSettings: набирая «30» в
|
||||
// поле TTL, пользователь на миг проходит через «3», а это выбросило бы из базы
|
||||
// все споты старше трёх минут — необратимо.
|
||||
var i: Integer;
|
||||
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;
|
||||
// Настройки общие для всех панов: споты рисуются на каждом, где частота
|
||||
// попадает в его окно.
|
||||
for i := 0 to MAX_PANS - 1 do
|
||||
if FPans[i] <> nil then
|
||||
begin
|
||||
ApplyDXOverlayCfg(FPans[i].DXOverlay);
|
||||
if i > 0 then MarkPanDirty(FPans[i]);
|
||||
end;
|
||||
UpdateDXClusterButton;
|
||||
FSpectrumDirty := True;
|
||||
end;
|
||||
|
||||
procedure TMainForm.ApplyDXOverlayCfg(O: TDXSpotOverlay);
|
||||
// Вид одного оверлея по текущему FDXCfg (сеттеры сами гасят «то же самое»).
|
||||
begin
|
||||
if O = nil then Exit;
|
||||
O.MaxRows := FDXCfg.MaxRows;
|
||||
O.SetOwnCall(FDXCfg.Login);
|
||||
O.SetFilters(DXModeSetFromMask(FDXCfg.ModeMask), FDXCfg.TTLMinutes);
|
||||
O.SetEnabled(FDXCfg.ShowSpots);
|
||||
O.Invalidate;
|
||||
end;
|
||||
|
||||
procedure TMainForm.AttachPanDXSpots(P: TPanafallPanel);
|
||||
// Доп. пан получает СВОЙ оверлей на общей базе спотов: кэш полосы подписей
|
||||
// ключуется центром/спаном/шириной пана, поэтому один экземпляр на всех
|
||||
// пересобирался бы по кругу на каждый пан каждый кадр — ровно то, ради чего
|
||||
// кэш и заводился.
|
||||
var VC, VS: Double;
|
||||
begin
|
||||
if (P = nil) or (FDXStore = nil) then Exit;
|
||||
P.AttachDXSpots(FDXStore);
|
||||
ApplyDXOverlayCfg(P.DXOverlay);
|
||||
P.DXOverlay.SetLightTheme(FLightTheme);
|
||||
PanViewWindow(P, VC, VS);
|
||||
P.DXOverlay.SetView(VC, VS);
|
||||
end;
|
||||
|
||||
procedure TMainForm.ApplyDXConnection(const D: TDXClusterSettings);
|
||||
// Соединение: конфиг в клиент + поднять/положить/переподнять. Поток снимает
|
||||
// конфиг один раз на сессию, поэтому смена адреса/учётки требует рестарта —
|
||||
@@ -5114,6 +5162,7 @@ procedure TMainForm.ServiceDXCluster;
|
||||
const
|
||||
DX_AGE_TICK_SEC = 30;
|
||||
DX_SETTINGS_QUIET_MS = 1200; // тишина в SETUP, после которой коммитим
|
||||
var i: Integer;
|
||||
begin
|
||||
// Отложенный коммит настроек: пользователь перестал печатать в SETUP.
|
||||
if FDXPending and (MilliSecondsBetween(Now, FDXPendingAt) >= DX_SETTINGS_QUIET_MS) then
|
||||
@@ -5123,7 +5172,13 @@ begin
|
||||
if SecondsBetween(Now, FDXAgeTickAt) < DX_AGE_TICK_SEC then Exit;
|
||||
FDXAgeTickAt := Now;
|
||||
if not FDXSpotOverlay.Active then Exit;
|
||||
FDXSpotOverlay.TickAge;
|
||||
// Тик получают оверлеи всех панов: возраст спота от пана не зависит.
|
||||
for i := 0 to MAX_PANS - 1 do
|
||||
if (FPans[i] <> nil) and (FPans[i].DXOverlay <> nil) then
|
||||
begin
|
||||
FPans[i].DXOverlay.TickAge;
|
||||
if i > 0 then MarkPanDirty(FPans[i]);
|
||||
end;
|
||||
FSpectrumDirty := True;
|
||||
end;
|
||||
|
||||
@@ -5136,14 +5191,18 @@ end;
|
||||
|
||||
procedure TMainForm.BtnDXClusterClick(Sender: TObject);
|
||||
// ЛКМ — показывать/не показывать подписи спотов на спектре (настройка живёт
|
||||
// в конфиге, чтобы состояние пережило перезапуск).
|
||||
// в конфиге, чтобы состояние пережило перезапуск). Кнопка одна на все паны:
|
||||
// применяем через общий Apply, иначе тумблер гасил бы только главный.
|
||||
begin
|
||||
if FDXSpotOverlay = nil then Exit;
|
||||
FDXCfg.ShowSpots := not FDXCfg.ShowSpots;
|
||||
FDXSpotOverlay.SetEnabled(FDXCfg.ShowSpots);
|
||||
ApplyDXClusterSettings(FDXCfg);
|
||||
FController.FSettings.SaveDXClusterSettings(FDXCfg);
|
||||
UpdateDXClusterButton;
|
||||
FSpectrumDirty := True;
|
||||
// Открытый SETUP держит СВОЮ копию конфига: без синхронизации галка «Show
|
||||
// spots» осталась бы старой, и первая же правка любого поля на этой странице
|
||||
// вернула бы подписи обратно.
|
||||
if (FSettingsForm <> nil) and TSettingsForm(FSettingsForm).Visible then
|
||||
TSettingsForm(FSettingsForm).LoadDXClusterSettings(FDXCfg);
|
||||
end;
|
||||
|
||||
procedure TMainForm.BtnDXClusterMouseDown(Sender: TObject; Button: TMouseButton;
|
||||
@@ -5164,27 +5223,78 @@ begin
|
||||
else FDXClusterForm.Show;
|
||||
end;
|
||||
|
||||
function DXSpotRadioMode(FreqHz: Double; Mode: TDXMode): Integer;
|
||||
// Мода спота → режим приёмника. −1 = однозначно не определяется (dxmUnknown):
|
||||
// текущую в этом случае не трогаем — гадать хуже, чем не менять.
|
||||
begin
|
||||
Result := -1;
|
||||
case Mode of
|
||||
dxmCW: Result := MODE_CWU;
|
||||
// Боковая по общепринятому правилу: ниже 10 МГц — LSB, выше — USB.
|
||||
dxmSSB: if FreqHz < 10000000.0 then Result := MODE_LSB else Result := MODE_USB;
|
||||
dxmFT8, dxmFT4, dxmDigi, dxmRTTY, dxmPSK: Result := MODE_DIGU;
|
||||
dxmSSTV: Result := MODE_USB;
|
||||
dxmFM: Result := MODE_FM;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TMainForm.OnDXTuneSpot(FreqHz: Double; Mode: TDXMode);
|
||||
// QSY по споту: клик по подписи на спектре или двойной клик в окне списка.
|
||||
// Мода ставится только когда она однозначна из комментария кластера; при
|
||||
// dxmUnknown текущую не трогаем — гадать хуже, чем не менять.
|
||||
// QSY по споту: клик по подписи на главном пане или двойной клик в окне списка.
|
||||
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;
|
||||
NewMode := DXSpotRadioMode(FreqHz, Mode);
|
||||
if (NewMode >= 0) and (NewMode <> FController.FMode) then
|
||||
FController.SetMode(NewMode);
|
||||
end;
|
||||
|
||||
procedure TMainForm.DXTuneSpotOnPan(P: TPanafallPanel; FreqHz: Double;
|
||||
Mode: TDXMode);
|
||||
// QSY по споту на доп. пане. Цель — слайс ЭТОГО пана (активный, если он здесь,
|
||||
// иначе первый; нет ни одного — создаём в точке), ровно как у клика по фону
|
||||
// пана: своего VFO у панов N нет, а ретюнить их DDC под спот нельзя — это
|
||||
// увезло бы весь пан.
|
||||
var
|
||||
Id, NewMode: Integer;
|
||||
Hz: Int64;
|
||||
S: TCtrlSlice;
|
||||
begin
|
||||
if (P = nil) or (FreqHz <= 0) then Exit;
|
||||
Hz := Round(FreqHz);
|
||||
// Спот видим в окне пана, но окно может быть шире полосы захвата (зум) —
|
||||
// слайс вне capture не поставить.
|
||||
if not FController.SliceFitsCapture(Hz, P.PanId) then Exit;
|
||||
|
||||
if (FActiveSliceId <> 0) and FController.GetSlice(FActiveSliceId, S) and
|
||||
(S.PanId = P.PanId) then
|
||||
Id := FActiveSliceId
|
||||
else
|
||||
Id := P.FirstSliceId;
|
||||
|
||||
NewMode := DXSpotRadioMode(FreqHz, Mode);
|
||||
if Id = 0 then
|
||||
begin
|
||||
AddSliceAtFreqPan(P, Hz); // ставит FActiveSliceId
|
||||
Id := FActiveSliceId;
|
||||
if Id = 0 then Exit;
|
||||
end
|
||||
else
|
||||
FController.SetSliceTarget(Id, Hz);
|
||||
|
||||
// Режим слайса — с полосой пресета ЭТОГО режима: тащить полосу с главного
|
||||
// приёмника (как делает AddSliceAtFreqPan для нового слайса) значило бы
|
||||
// слушать телеграф в 2.7 кГц.
|
||||
if NewMode >= 0 then
|
||||
FController.SetSliceModeBW(Id, NewMode,
|
||||
FController.FilterBWFor(NewMode, FilterDefIdx(NewMode)));
|
||||
|
||||
FActiveSliceId := Id;
|
||||
P.PushSliceFlagState(Id);
|
||||
P.LayoutFlags;
|
||||
MarkPanDirty(P);
|
||||
end;
|
||||
|
||||
procedure TMainForm.UpdateBandPlanOverlay;
|
||||
// Полоска бэндплана видна только в QO-100 (Pluto + full-duplex транспондер).
|
||||
begin
|
||||
@@ -6756,6 +6866,9 @@ begin
|
||||
FController.GetPanViewWindow(P.PanId, VC, VS);
|
||||
P.View.CenterFreq := VC;
|
||||
P.View.SpanHz := VS;
|
||||
// Подписи спотов пана следуют за его окном (SetView сам отсекает неизменное
|
||||
// → пересборки кэша на каждый кадр нет).
|
||||
if P.DXOverlay <> nil then P.DXOverlay.SetView(VC, VS);
|
||||
// Сетка каналов по шагу FM — как на главном пане, но по режиму слайсов
|
||||
// ЭТОГО пана (у доп. панов нет своего FMode).
|
||||
if FController.PanHasFMSlice(P.PanId) then
|
||||
@@ -7407,6 +7520,8 @@ begin
|
||||
P.View.VfoA := -1e15;
|
||||
P.View.VfoB := -1e15;
|
||||
P.View.TXFreq := -1e15;
|
||||
// Подписи DX-спотов: свой оверлей на общей базе, настройки/тема — общие.
|
||||
AttachPanDXSpots(P);
|
||||
|
||||
// Сплиттер над паном.
|
||||
Split := TPanel.Create(Self);
|
||||
@@ -7788,6 +7903,7 @@ var
|
||||
VC, VS: Double;
|
||||
Id, W: Integer;
|
||||
IsSpec: Boolean;
|
||||
DXSpot: TDXSpot;
|
||||
begin
|
||||
P := PanFromSender(Sender);
|
||||
if P = nil then Exit;
|
||||
@@ -7799,6 +7915,14 @@ begin
|
||||
P.ProcessPendingSliceClose;
|
||||
Exit;
|
||||
end;
|
||||
// ЛКМ по подписи DX-спота на пане — QSY слайса ЭТОГО пана (у панов N нет
|
||||
// VFO A). Как на главном: только в полосе подписей, ниже поведение прежнее.
|
||||
if IsSpec and (Button = mbLeft) and (P.DXOverlay <> nil) and
|
||||
P.DXOverlay.SpotAtPixel(X, Y, DXSpot) then
|
||||
begin
|
||||
DXTuneSpotOnPan(P, DXSpot.FreqHz, DXSpot.Mode);
|
||||
Exit;
|
||||
end;
|
||||
W := TControl(Sender).Width;
|
||||
if W <= 0 then Exit;
|
||||
if (Button = mbLeft) and (ssCtrl in Shift) then
|
||||
|
||||
Reference in New Issue
Block a user