mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 17:27:32 +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:
+51
-7
@@ -67,6 +67,13 @@ type
|
||||
|
||||
FStore: TDXSpotStore; // не владеет
|
||||
|
||||
// Сокет живого соединения — зеркало под FLock. Нужен ровно для того, чтобы
|
||||
// Stop() мог сделать SockShutdown и вывести поток из блокирующего recv:
|
||||
// иначе выход из программы ждал бы до конца кванта recv (и до конца
|
||||
// таймаута send, если кластер перестал забирать данные). Поток снимает
|
||||
// зеркало ДО close, поэтому под локом хэндл гарантированно ещё открыт.
|
||||
FActiveSock: TSocket;
|
||||
|
||||
FState: TDXClusterState;
|
||||
FStatusMsg: string;
|
||||
FSpotCount: Int64; // сколько спотов принято за сессию
|
||||
@@ -79,6 +86,7 @@ type
|
||||
FOutQueue: TStringList; // строки на отправку в кластер
|
||||
|
||||
procedure SetState(St: TDXClusterState; const Msg: string);
|
||||
procedure PublishSock(S: TSocket); // зеркало хэндла для Stop (см. FActiveSock)
|
||||
public
|
||||
constructor Create(AStore: TDXSpotStore);
|
||||
destructor Destroy; override;
|
||||
@@ -86,7 +94,11 @@ type
|
||||
procedure Configure(const AHost: string; APort: Integer;
|
||||
const ALogin, APassword, APostLogin: string);
|
||||
procedure Start;
|
||||
procedure Stop;
|
||||
// Неблокирующая половина Stop: разбудить поток и порвать сокет, не дожидаясь
|
||||
// его конца. Зовётся в начале выхода из программы, чтобы поток доживал
|
||||
// ПАРАЛЛЕЛЬНО с разборкой радио, а join в Stop уже никого не ждал.
|
||||
procedure RequestStop;
|
||||
procedure Stop; // RequestStop + join
|
||||
function Running: Boolean;
|
||||
|
||||
// Отправить сырую строку в кластер (окно списка: sh/dx, set/filter, …).
|
||||
@@ -274,6 +286,7 @@ begin
|
||||
FLogCount := 0;
|
||||
FLogVer := 0;
|
||||
FSpotCount := 0;
|
||||
FActiveSock := SOCK_INVALID;
|
||||
{$IFDEF MSWINDOWS}
|
||||
WSAStartup($0202, wsa);
|
||||
{$ENDIF}
|
||||
@@ -329,8 +342,7 @@ begin
|
||||
FThread := TDXClusterThread.Create(Self);
|
||||
end;
|
||||
|
||||
procedure TDXClusterClient.Stop;
|
||||
var T: TThread;
|
||||
procedure TDXClusterClient.RequestStop;
|
||||
begin
|
||||
// Всё, что не успели отправить, к следующему соединению уже неактуально.
|
||||
FLock.Enter;
|
||||
@@ -339,7 +351,28 @@ begin
|
||||
finally
|
||||
FLock.Leave;
|
||||
end;
|
||||
if FThread = nil then Exit;
|
||||
FThread.Terminate;
|
||||
FStopEvent.SetEvent;
|
||||
// Терминации мало: поток стоит в блокирующем recv и проснулся бы только по
|
||||
// SO_RCVTIMEO (до DX_RECV_MS), а на застрявшем send — до SO_SNDTIMEO. Всё это
|
||||
// время join держал бы главный поток, т.е. окно висело бы на выходе и на
|
||||
// переподключении после правки настроек. SockShutdown рвёт обе стороны
|
||||
// немедленно (на Linux одного close мало — см. WebUtils.SockShutdown).
|
||||
// Закрывает сокет по-прежнему сам поток: закрыть чужой хэндл значило бы
|
||||
// гонку с его же recv.
|
||||
FLock.Enter;
|
||||
try
|
||||
if FActiveSock <> SOCK_INVALID then SockShutdown(FActiveSock);
|
||||
finally
|
||||
FLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TDXClusterClient.Stop;
|
||||
var T: TThread;
|
||||
begin
|
||||
RequestStop;
|
||||
T := FThread;
|
||||
if T = nil then
|
||||
begin
|
||||
@@ -347,10 +380,6 @@ begin
|
||||
Exit;
|
||||
end;
|
||||
FThread := nil;
|
||||
T.Terminate;
|
||||
FStopEvent.SetEvent;
|
||||
// Сокет закрывает сам поток (TDXClusterThread.CloseSock из Execute);
|
||||
// FStopEvent + Terminate выводят его из ожиданий, а recv рвётся по таймауту.
|
||||
T.WaitFor;
|
||||
T.Free;
|
||||
SetState(dxsOff, '');
|
||||
@@ -361,6 +390,19 @@ begin
|
||||
Result := FThread <> nil;
|
||||
end;
|
||||
|
||||
procedure TDXClusterClient.PublishSock(S: TSocket);
|
||||
// Зеркало хэндла для Stop. Поток зовёт с валидным хэндлом сразу после connect
|
||||
// и с SOCK_INVALID ПЕРЕД close — так Stop под локом видит либо живой сокет,
|
||||
// либо ничего, но не закрытый.
|
||||
begin
|
||||
FLock.Enter;
|
||||
try
|
||||
FActiveSock := S;
|
||||
finally
|
||||
FLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TDXClusterClient.SetState(St: TDXClusterState; const Msg: string);
|
||||
begin
|
||||
FLock.Enter;
|
||||
@@ -606,6 +648,7 @@ begin
|
||||
{$ELSE}
|
||||
fpSetSockOpt(FSocket, SOL_SOCKET, SO_KEEPALIVE, @One, SizeOf(One));
|
||||
{$ENDIF}
|
||||
FOwner.PublishSock(FSocket); // с этого момента Stop может рвать нас shutdown'ом
|
||||
Result := True;
|
||||
end;
|
||||
|
||||
@@ -613,6 +656,7 @@ procedure TDXClusterThread.CloseSock;
|
||||
begin
|
||||
if FSocket <> SOCK_INVALID then
|
||||
begin
|
||||
FOwner.PublishSock(SOCK_INVALID); // до close: Stop не должен увидеть мёртвый хэндл
|
||||
SockShutdown(FSocket);
|
||||
SockClose(FSocket);
|
||||
FSocket := SOCK_INVALID;
|
||||
|
||||
+93
-25
@@ -12,6 +12,11 @@ unit DXSpotOverlay;
|
||||
W × FBandH с постоянной альфой (BlendBitmapKey, тот же дворд-блендинг).
|
||||
Полноэкранный кэш W × H на кадр стоил бы в разы дороже — поэтому штрихи
|
||||
ниже полосы в кэш НЕ входят.
|
||||
• Версия стора глобальна, а окно у каждого пана своё, поэтому на смену
|
||||
версии сначала берётся снимок окна и сверяется его ПОДПИСЬ: спот, севший
|
||||
на чужой диапазон, до полосы подписей этого пана не доходит. Без этого
|
||||
каждый пан пересобирал бы кэш (и перезаливал GL-текстуру) на любой спот
|
||||
откуда угодно — цена, которая множится на число панов.
|
||||
• Штрихи от полосы до низа спектра рисует вызывающая сторона теми же
|
||||
примитивами, что и все прочие маркеры: CPU — RawVLine внутри RawBegin/
|
||||
RawEnd (никакого Canvas на битмапе кадра), GL — DrawLine. Оверлей отдаёт
|
||||
@@ -77,20 +82,27 @@ type
|
||||
FKeyLight: Boolean;
|
||||
FKeyRows: Integer;
|
||||
FKeyAgeTick: Int64;
|
||||
FKeyStamp: Int64; // подпись набора, по которому собран кэш
|
||||
FAgeTick: Int64; // бампается таймером UI — обновить затухание
|
||||
|
||||
FRenderVer: Int64; // ++ на каждую пересборку кэша — ключ GL-текстуры
|
||||
FLayoutTTL: Integer; // горизонт затухания, снят один раз на раскладку
|
||||
FLayoutTTL: Integer; // горизонт затухания, снят один раз на раскладку
|
||||
// Снимок окна: берётся при смене вида/версии стора, живёт до пересборки —
|
||||
// подпись считается по нему же, второй раз стор не дёргаем.
|
||||
FArr: TDXSpotArray;
|
||||
FArrN: Integer;
|
||||
FArrStamp: Int64;
|
||||
FItems: array of TDXSpotItem;
|
||||
FItemCount: Integer;
|
||||
FHidden: Integer; // сколько спотов не влезло в лесенку
|
||||
|
||||
function FreqToX(FreqHz: Double; W: Integer): Integer;
|
||||
function AgeColor(const S: TDXSpot): TColor;
|
||||
procedure TakeSnapshot;
|
||||
procedure Layout(C: TCanvas; W: Integer);
|
||||
procedure DrawSelf(C: TCanvas; W: Integer);
|
||||
procedure RebuildCache(W: Integer);
|
||||
function NeedsRebuild(W: Integer): Boolean;
|
||||
procedure RebuildCache(W: Integer; Ver: Int64);
|
||||
function ViewKeysChanged(W: Integer): Boolean;
|
||||
procedure SetMaxRows(V: Integer);
|
||||
public
|
||||
constructor Create(AOwner: TComponent); override;
|
||||
@@ -157,6 +169,13 @@ const
|
||||
CHIP_PAD_X = 3;
|
||||
CHIP_GAP = 3; // минимальный зазор между чипами в одном ряду
|
||||
|
||||
// Подпись видимого набора (FNV-1a 64). Базис — канонический $CBF29CE4…,
|
||||
// сложенный из половин: цельным литералом он не влезает в Int64, а
|
||||
// сравниваем мы подпись только сама с собой, так что важна лишь стабильность.
|
||||
FNV_BASIS_HI = QWord($CBF29CE4);
|
||||
FNV_BASIS_LO = QWord($84222325);
|
||||
FNV_PRIME: QWord = $00000100000001B3;
|
||||
|
||||
function Mix(A, B: TColor; Num, Den: Integer): TColor;
|
||||
// A→B на Num/Den (0 = A, Den = B).
|
||||
var ra, ga, ba, rb, gb, bb: Integer;
|
||||
@@ -190,6 +209,9 @@ begin
|
||||
FCacheDirty := True;
|
||||
FKeyCenter := -1; FKeySpan := -1; FKeyW := -1; FKeyVersion := -1;
|
||||
FKeyEnabled := False; FKeyLight := False; FKeyRows := -1; FKeyAgeTick := -1;
|
||||
FKeyStamp := 0;
|
||||
FArrN := 0;
|
||||
FArrStamp := 0;
|
||||
FAgeTick := 0;
|
||||
FRenderVer := 0;
|
||||
end;
|
||||
@@ -293,28 +315,55 @@ end;
|
||||
|
||||
{ ── Раскладка лесенки ────────────────────────────────────────────────────── }
|
||||
|
||||
procedure TDXSpotOverlay.TakeSnapshot;
|
||||
// Снимок видимого окна + его подпись. Подпись складывает всё, от чего зависит
|
||||
// картинка полосы: позывной, частоту, моду и метку времени (она правит цвет
|
||||
// затухания и обновляется при пере-споте). Совпала с прошлой — пересобирать
|
||||
// нечего, какой бы ни стала версия стора.
|
||||
var
|
||||
i, k: Integer;
|
||||
H: QWord;
|
||||
LoHz, HiHz: Double;
|
||||
begin
|
||||
FArrN := 0;
|
||||
SetLength(FArr, 0);
|
||||
FArrStamp := 0;
|
||||
if (FStore = nil) or (FSpanHz <= 0) then Exit;
|
||||
|
||||
// Горизонт затухания: явный фильтр по возрасту, иначе TTL стора (чтение
|
||||
// под локом — поэтому один раз на снимок, а не на каждый спот).
|
||||
FLayoutTTL := FMaxAgeMin;
|
||||
if FLayoutTTL <= 0 then FLayoutTTL := FStore.TTLMinutes;
|
||||
|
||||
LoHz := FCenterFreq - FSpanHz / 2;
|
||||
HiHz := FCenterFreq + FSpanHz / 2;
|
||||
FArrN := FStore.SnapshotRange(LoHz, HiHz, FModes, FMaxAgeMin, FArr);
|
||||
|
||||
H := (FNV_BASIS_HI shl 32) or FNV_BASIS_LO; // FNV-1a 64
|
||||
for i := 0 to FArrN - 1 do
|
||||
begin
|
||||
for k := 1 to Length(FArr[i].Call) do
|
||||
H := (H xor QWord(Ord(FArr[i].Call[k]))) * FNV_PRIME;
|
||||
H := (H xor QWord(Round(FArr[i].FreqHz))) * FNV_PRIME;
|
||||
H := (H xor QWord(Ord(FArr[i].Mode))) * FNV_PRIME;
|
||||
H := (H xor QWord(Round(FArr[i].Stamp * 86400))) * FNV_PRIME;
|
||||
end;
|
||||
FArrStamp := Int64(H);
|
||||
end;
|
||||
|
||||
procedure TDXSpotOverlay.Layout(C: TCanvas; W: Integer);
|
||||
// Споты приходят из стора отсортированными по частоте; каждому даём ПЕРВЫЙ
|
||||
// ряд сверху, где подпись не наезжает на предыдущую. Не нашлось ряда —
|
||||
// спот скрыт (считаем в FHidden), в лесенке дырок не оставляем.
|
||||
// спот скрыт (считаем в FHidden), в лесенке дырок не оставляем. Работаем по
|
||||
// снимку FArr — его взял TakeSnapshot перед пересборкой.
|
||||
var
|
||||
Arr: TDXSpotArray;
|
||||
N, i, r, TW, ChipW, ChipX, Row: Integer;
|
||||
RowRight: array[0..DXSPOT_MAX_ROWS-1] of Integer;
|
||||
LoHz, HiHz: Double;
|
||||
begin
|
||||
FItemCount := 0;
|
||||
FHidden := 0;
|
||||
if (FStore = nil) or (FSpanHz <= 0) or (W <= 0) then Exit;
|
||||
|
||||
// Горизонт затухания: явный фильтр по возрасту, иначе TTL стора (чтение
|
||||
// под локом — поэтому один раз на раскладку, а не на каждый спот).
|
||||
FLayoutTTL := FMaxAgeMin;
|
||||
if FLayoutTTL <= 0 then FLayoutTTL := FStore.TTLMinutes;
|
||||
|
||||
LoHz := FCenterFreq - FSpanHz / 2;
|
||||
HiHz := FCenterFreq + FSpanHz / 2;
|
||||
N := FStore.SnapshotRange(LoHz, HiHz, FModes, FMaxAgeMin, Arr);
|
||||
N := FArrN;
|
||||
if N = 0 then Exit;
|
||||
|
||||
for r := 0 to FMaxRows - 1 do RowRight[r] := -CHIP_GAP - 1;
|
||||
@@ -322,9 +371,9 @@ begin
|
||||
|
||||
for i := 0 to N - 1 do
|
||||
begin
|
||||
TW := C.TextWidth(Arr[i].Call);
|
||||
TW := C.TextWidth(FArr[i].Call);
|
||||
ChipW := TW + CHIP_PAD_X * 2;
|
||||
ChipX := FreqToX(Arr[i].FreqHz, W) - ChipW div 2;
|
||||
ChipX := FreqToX(FArr[i].FreqHz, W) - ChipW div 2;
|
||||
if ChipX < 0 then ChipX := 0;
|
||||
if ChipX + ChipW > W then ChipX := W - ChipW;
|
||||
if ChipX < 0 then Continue; // подпись шире окна — не рисуем
|
||||
@@ -343,11 +392,11 @@ begin
|
||||
end;
|
||||
RowRight[Row] := ChipX + ChipW;
|
||||
|
||||
FItems[FItemCount].X := FreqToX(Arr[i].FreqHz, W);
|
||||
FItems[FItemCount].X := FreqToX(FArr[i].FreqHz, W);
|
||||
FItems[FItemCount].Chip := Rect(ChipX, Row * FRowH + 2,
|
||||
ChipX + ChipW, Row * FRowH + FRowH);
|
||||
FItems[FItemCount].Color := AgeColor(Arr[i]);
|
||||
FItems[FItemCount].Spot := Arr[i];
|
||||
FItems[FItemCount].Color := AgeColor(FArr[i]);
|
||||
FItems[FItemCount].Spot := FArr[i];
|
||||
Inc(FItemCount);
|
||||
end;
|
||||
SetLength(FItems, FItemCount);
|
||||
@@ -404,7 +453,9 @@ end;
|
||||
|
||||
{ ── Кэш ──────────────────────────────────────────────────────────────────── }
|
||||
|
||||
function TDXSpotOverlay.NeedsRebuild(W: Integer): Boolean;
|
||||
function TDXSpotOverlay.ViewKeysChanged(W: Integer): Boolean;
|
||||
// Всё, что делает картинку заведомо другой, КРОМЕ версии стора: её разбирает
|
||||
// EnsureRendered отдельно (версия глобальна, окно — наше).
|
||||
var HalfPxHz: Double;
|
||||
begin
|
||||
if FStore = nil then Exit(False);
|
||||
@@ -414,7 +465,6 @@ begin
|
||||
if HalfPxHz < 1.0 then HalfPxHz := 1.0;
|
||||
Result := FCacheDirty or (FCacheW <> W) or (FKeyW <> W)
|
||||
or (FKeyEnabled <> FEnabled)
|
||||
or (FKeyVersion <> FStore.Version)
|
||||
or (FKeyLight <> FLightTheme)
|
||||
or (FKeyRows <> FMaxRows)
|
||||
or (FKeyAgeTick <> FAgeTick)
|
||||
@@ -422,7 +472,9 @@ begin
|
||||
or (Abs(FKeySpan - FSpanHz) >= HalfPxHz);
|
||||
end;
|
||||
|
||||
procedure TDXSpotOverlay.RebuildCache(W: Integer);
|
||||
procedure TDXSpotOverlay.RebuildCache(W: Integer; Ver: Int64);
|
||||
// Снимок (TakeSnapshot) к этому моменту уже взят вызывающим — здесь только
|
||||
// рисование и запись ключей.
|
||||
begin
|
||||
if (W <= 0) or (FStore = nil) then
|
||||
begin
|
||||
@@ -439,7 +491,8 @@ begin
|
||||
FKeySpan := FSpanHz;
|
||||
FKeyW := W;
|
||||
FKeyEnabled := FEnabled;
|
||||
FKeyVersion := FStore.Version;
|
||||
FKeyVersion := Ver;
|
||||
FKeyStamp := FArrStamp;
|
||||
FKeyLight := FLightTheme;
|
||||
FKeyRows := FMaxRows;
|
||||
FKeyAgeTick := FAgeTick;
|
||||
@@ -449,9 +502,24 @@ end;
|
||||
{ ── Рисование ────────────────────────────────────────────────────────────── }
|
||||
|
||||
procedure TDXSpotOverlay.EnsureRendered(W: Integer);
|
||||
var Ver: Int64;
|
||||
begin
|
||||
if (not Active) or (W <= 0) then Exit;
|
||||
if NeedsRebuild(W) then RebuildCache(W);
|
||||
Ver := FStore.Version;
|
||||
if ViewKeysChanged(W) then
|
||||
begin
|
||||
TakeSnapshot;
|
||||
RebuildCache(W, Ver);
|
||||
Exit;
|
||||
end;
|
||||
if FKeyVersion = Ver then Exit; // в сторе с прошлого кадра ничего
|
||||
// Стор правили — но, может, не в нашем окне: снимок с подписью дешевле
|
||||
// пересборки полосы (и, в GL, перезаливки текстуры). Версию запоминаем в
|
||||
// любом случае: этот вопрос уже разобран.
|
||||
FKeyVersion := Ver;
|
||||
TakeSnapshot;
|
||||
if FArrStamp = FKeyStamp then Exit;
|
||||
RebuildCache(W, Ver);
|
||||
end;
|
||||
|
||||
procedure TDXSpotOverlay.DrawOverlay(Target: TBitmap; W, H: Integer);
|
||||
|
||||
+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
|
||||
|
||||
@@ -35,6 +35,7 @@ uses
|
||||
LCLIntf, LCLType,
|
||||
OpenGLContextEx,
|
||||
AppTheme, WDSPEngine, RadioController, DMRDecoder, VfoOverlay,
|
||||
DXSpotStore, DXSpotOverlay,
|
||||
SpectrumView, SpectrumViewOpengl, PanZoomBar, FlatButton, DpiUtils;
|
||||
|
||||
type
|
||||
@@ -83,6 +84,10 @@ type
|
||||
FSliceFlags: array of TVfoOverlay; // флаги B+ (Owner=Self)
|
||||
FActiveFlag: TVfoOverlay; // чьё событие сейчас обрабатывается
|
||||
FPendingSliceClose: Integer; // SliceId к удалению после мыши (0=нет)
|
||||
// Подписи DX-спотов на спектре ЭТОГО пана. Экземпляр свой на каждый пан:
|
||||
// кэш полосы подписей ключуется центром/спаном/шириной, а у панов они свои
|
||||
// — общий оверлей пересобирался бы на каждый пан каждый кадр.
|
||||
FDXOverlay: TDXSpotOverlay; // Owner=Self, база спотов общая
|
||||
FMainFlagPinned: Boolean; // True = левая панель хозяина скрыта
|
||||
FMainFlagForced: Boolean; // True = есть доп. панадаптеры
|
||||
FLastSliceMeterMs: QWord; // троттлинг S-метра слайсов (~10 Гц)
|
||||
@@ -268,6 +273,14 @@ type
|
||||
// полный пересчёт (у него wideband/S-метр/оверлеи).
|
||||
property OnSplitterMoved: TNotifyEvent read FOnSplitterMoved write FOnSplitterMoved;
|
||||
|
||||
// ---- Подписи DX-спотов ----
|
||||
// Своя копия оверлея на пан; база спотов (стор) — общая, её владелец хозяин.
|
||||
// Вид (центр/спан) пана заливает хозяин, как и прочие параметры вьюхи.
|
||||
procedure AttachDXSpots(AStore: TDXSpotStore);
|
||||
// Отвязать от базы (хозяин освобождает стор раньше панов).
|
||||
procedure DetachDXSpots;
|
||||
property DXOverlay: TDXSpotOverlay read FDXOverlay;
|
||||
|
||||
// ---- Флаги слайсов ----
|
||||
// Главный флаг (слайс A) создаёт хозяин (проводка событий у него) и отдаёт
|
||||
// сюда; панель подключает его к view и включает в раскладку/диспетчер.
|
||||
@@ -1108,6 +1121,21 @@ begin
|
||||
FView.VfoOverlay := O;
|
||||
end;
|
||||
|
||||
procedure TPanafallPanel.AttachDXSpots(AStore: TDXSpotStore);
|
||||
begin
|
||||
if FDXOverlay = nil then
|
||||
begin
|
||||
FDXOverlay := TDXSpotOverlay.Create(Self);
|
||||
FView.DXSpotOverlay := FDXOverlay;
|
||||
end;
|
||||
FDXOverlay.Attach(AStore);
|
||||
end;
|
||||
|
||||
procedure TPanafallPanel.DetachDXSpots;
|
||||
begin
|
||||
if FDXOverlay <> nil then FDXOverlay.Attach(nil);
|
||||
end;
|
||||
|
||||
function TPanafallPanel.CurSliceId: Integer;
|
||||
begin
|
||||
if Assigned(FActiveFlag) then Result := FActiveFlag.SliceId else Result := 0;
|
||||
|
||||
+2
-2
@@ -2898,7 +2898,7 @@ end;
|
||||
class procedure TSettingsManager.DefaultDXCluster(out D: TDXClusterSettings);
|
||||
begin
|
||||
D.Enabled := False; // без позывного подключаться всё равно нечем
|
||||
D.ShowSpots := True;
|
||||
D.ShowSpots := False; // подписи включает пользователь кнопкой DX
|
||||
D.Host := 'cluster.dxfun.com';
|
||||
D.Port := 8000;
|
||||
D.Login := '';
|
||||
@@ -2916,7 +2916,7 @@ begin
|
||||
if FRoot.Find('dxcluster') = nil then Exit;
|
||||
O := EnsureObj(FRoot, 'dxcluster');
|
||||
D.Enabled := JB(O, 'enabled', False);
|
||||
D.ShowSpots := JB(O, 'show_spots', True);
|
||||
D.ShowSpots := JB(O, 'show_spots', False);
|
||||
D.Host := JS(O, 'host', 'cluster.dxfun.com');
|
||||
D.Port := EnsureRange(JI(O, 'port', 8000), 1, 65535);
|
||||
D.Login := JS(O, 'login', '');
|
||||
|
||||
@@ -683,6 +683,9 @@ var
|
||||
Col: TColor;
|
||||
begin
|
||||
if (FDXSpotOverlay = nil) or (not FDXSpotOverlay.Active) then Exit;
|
||||
// Низкий пан (сетка панов): полоса подписей в него не влезла и не рисуется —
|
||||
// тогда и штрихам не от чего идти.
|
||||
if H <= FDXSpotOverlay.BandHeight + 2 then Exit;
|
||||
for i := 0 to FDXSpotOverlay.TickCount - 1 do
|
||||
begin
|
||||
FDXSpotOverlay.Tick(i, X, Col);
|
||||
|
||||
@@ -842,6 +842,9 @@ begin
|
||||
if (FDXSpotOverlay = nil) or (not FDXSpotOverlay.Active) then Exit;
|
||||
FDXSpotOverlay.EnsureRendered(W);
|
||||
Y0 := FDXSpotOverlay.BandHeight;
|
||||
// Низкий пан: полоса подписей не влезла (её текстуру DrawCachedOverlays тоже
|
||||
// пропускает) — штрихам не от чего идти.
|
||||
if H <= Y0 + 2 then Exit;
|
||||
for i := 0 to FDXSpotOverlay.TickCount - 1 do
|
||||
begin
|
||||
FDXSpotOverlay.Tick(i, X, Col);
|
||||
@@ -980,7 +983,10 @@ begin
|
||||
// Полоса подписей DX-спотов — вверху спектра. Тот же битмап, что в CPU-пути,
|
||||
// заливается текстурой ТОЛЬКО при dirty (новый спот меняет CacheDirty
|
||||
// оверлея, вид — центр/спан/ширину).
|
||||
if Assigned(FDXSpotOverlay) and FDXSpotOverlay.Active then
|
||||
// Низкому пану полоса подписей не по росту — пропускаем её целиком (тот же
|
||||
// гейт, что в CPU-пути DrawOverlay).
|
||||
if Assigned(FDXSpotOverlay) and FDXSpotOverlay.Active and
|
||||
(H > FDXSpotOverlay.BandHeight) then
|
||||
begin
|
||||
if FOverlayDirty or FDXOverlayDirty or FDXOverlayTex.Dirty or
|
||||
(FLastDXW <> W) or (FLastDXRender <> FDXSpotOverlay.RenderVersion) then
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
<UseVersionInfo Value="True"/>
|
||||
<AutoIncrementBuild Value="True"/>
|
||||
<MinorVersionNr Value="9"/>
|
||||
<BuildNr Value="294"/>
|
||||
<BuildNr Value="295"/>
|
||||
</VersionInfo>
|
||||
<MacroValues Count="127">
|
||||
<MacroValues Count="129">
|
||||
<Macro1 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro2 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro3 Name="LCLWidgetType" Value="qt6"/>
|
||||
@@ -147,6 +147,8 @@
|
||||
<Macro125 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro126 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro127 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro128 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro129 Name="LCLWidgetType" Value="qt6"/>
|
||||
</MacroValues>
|
||||
<BuildModes>
|
||||
<Item Name="Debug" Default="True"/>
|
||||
|
||||
Reference in New Issue
Block a user