mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 17:27:32 +00:00
feat(slices): этап 3.3 — UI стек панадаптеров
Несколько панадаптеров в главном окне (кнопка «⊞», до MAX_PANS): - TPanafallPanel: регион в стеке (StackTop/Height), шапка с «×», pan-aware ViewWindow, FirstSliceId; явный crArrow GL-канвам (WA_NativeWindow-поверхность на Wayland навсегда наследует курсор момента создания — прилипала «рука» кнопки «⊞») - MainForm: FPans[]/LayoutPanStack (доли + межпанные сплиттеры с живым призраком), создание/закрытие панов (DDC+слайс+вьюха), пиксели панов из display-потока под FPanCbLock, рендер по dirty в тике - Мышь панов N: Ctrl+ЛКМ = слайс, drag маркера = ретюн слайса, drag фона = live-ретюн DDC (гистерезис 4px), клик = тюн слайса пана (PanClickTune, снап 100 Гц), флаг-диспетчер только на спектре - Сплиттер спектр/водопад: drag-математика от региона пана (FSplitTopOff/FSplitAvailH из Layout), OnSplitterMoved у панов N - Паны живут в Run-сессии, умирают на STOP Проверено юзером на экране (3 раунда фидбека) + e2e против hpsdrsim. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+784
-18
@@ -24,7 +24,7 @@ interface
|
||||
uses
|
||||
Classes, SysUtils, StrUtils, FreqDisplay, DeviceForm, VfoOverlay, SampleRateOverlay, BandPlanOverlay,
|
||||
FlatButton, FlatSlider, FlatDropDown, AppTheme, Forms, Controls, Graphics, Dialogs,
|
||||
StdCtrls, ExtCtrls, Buttons, Menus, Math, Types,
|
||||
StdCtrls, ExtCtrls, Buttons, Menus, Math, Types, SyncObjs,
|
||||
OpenGLContextEx,
|
||||
LCLIntf, LCLType, GraphType,
|
||||
HPSDRProtocol, HPSDRNetwork,
|
||||
@@ -159,6 +159,31 @@ type
|
||||
// (исторические имена; постепенная миграция на FPan.*). Размеры областей —
|
||||
// FPan.SpectrumWidth/SpectrumHeight/WaterfallHeight.
|
||||
FPan: TPanafallPanel;
|
||||
// ---- Стек панов (этап 3.3): FPans[0] = FPan, FPans[N>0] — доп. паны на
|
||||
// своих DDC (nil = не создан). Сплиттер FPanSplitters[i] стоит НАД паном i.
|
||||
FPans: array[0..MAX_PANS-1] of TPanafallPanel;
|
||||
FPanSplitters: array[1..MAX_PANS-1] of TPanel;
|
||||
FPanShare: array[0..MAX_PANS-1] of Double; // доля высоты стека
|
||||
FPanSpecDirty: array[0..MAX_PANS-1] of Boolean; // кадр пана готов к рендеру
|
||||
FPanWfDirty: array[0..MAX_PANS-1] of Boolean;
|
||||
FPanCbLock: SyncObjs.TCriticalSection; // жизнь FPans[i] vs пиксельные колбэки
|
||||
// Мышь на пане N>0: drag слайса или drag центра пана (ретюн DDC).
|
||||
FPanSliceDrag: Boolean;
|
||||
FPanSliceDragId: Integer;
|
||||
FPanCenterDrag: Boolean;
|
||||
FPanCenterMoved: Boolean; // был реальный сдвиг (иначе up = клик-тюн)
|
||||
FPanCenterDragX0: Integer;
|
||||
FPanCenterDragF0: Double;
|
||||
FPanMousePan: TPanafallPanel; // пан, в котором идёт drag
|
||||
// Сплиттер между панами (индекс пана, чей верхний сплиттер тащим; 0 = нет).
|
||||
// Во время drag сплиттер двигается «призраком» (клампы в FPanSplitMin/MaxTop),
|
||||
// пересчёт долей — по отпусканию.
|
||||
FPanSplitDrag: Integer;
|
||||
FPanSplitY0: Integer;
|
||||
FPanSplitTop0: Integer;
|
||||
FPanSplitMinTop: Integer;
|
||||
FPanSplitMaxTop: Integer;
|
||||
FPendingPanClose: Integer; // «×» шапки: PanId к закрытию в тике (0 = нет)
|
||||
FSpecView: TSpectrumView;
|
||||
FWidebandView: TWidebandView;
|
||||
FUseOpenGLSpectrum: Boolean;
|
||||
@@ -533,6 +558,44 @@ type
|
||||
function CurSliceId: Integer; // SliceId обрабатываемого сейчас флага
|
||||
procedure WireOverlayEvents(O: TVfoOverlay);
|
||||
procedure AddSliceAtFreq(Hz: Double);
|
||||
// --- Стек панов (этап 3.3) ---
|
||||
function PanCount: Integer; // активных панов (>=1)
|
||||
function PanFromSender(Sender: TObject): TPanafallPanel;
|
||||
function PanOfOverlay(Sender: TObject): TPanafallPanel;
|
||||
procedure PushFlagStateAllPans(SliceId: Integer);
|
||||
procedure LayoutFlagsAllPans;
|
||||
procedure OnAddPanClick(Sender: TObject); // «⊞» пана 0
|
||||
procedure OnPanHeaderClose(Sender: TObject); // «×» шапки пана
|
||||
procedure ClosePanadapter(PanId: Integer);
|
||||
procedure DestroyAllExtraPans; // stop/выход
|
||||
procedure UpdatePanHeaders; // текст+видимость шапок
|
||||
procedure UpdateAddPanButton;
|
||||
procedure AddSliceAtFreqPan(P: TPanafallPanel; Hz: Double);
|
||||
procedure PanClickTune(P: TPanafallPanel; X, W: Integer); // клик по фону пана N
|
||||
procedure PanViewWindow(P: TPanafallPanel; out VC, VS: Double);
|
||||
procedure SyncPanViews; // центр/span вьюх панов
|
||||
procedure MarkPanDirty(P: TPanafallPanel); // репейнт пана N
|
||||
// Мышь панов N>0 (у пана 0 семантика ниже, PbSpectrum*).
|
||||
procedure PanNSpectrumMouseDown(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
procedure PanNSpectrumMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
|
||||
procedure PanNSpectrumMouseUp(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
procedure PanNSpectrumMouseLeave(Sender: TObject);
|
||||
procedure PanNSpectrumDblClick(Sender: TObject);
|
||||
// Пиксели панов из display-потока движка.
|
||||
procedure OnPanSpectrumReady(PanId: Integer; const Pixels: array of Single;
|
||||
Count: Integer);
|
||||
procedure OnPanWaterfallReady(PanId: Integer; const Pixels: array of Single;
|
||||
Count: Integer);
|
||||
// Сплиттеры между панами.
|
||||
procedure PanSplitMouseDown(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
procedure PanSplitMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
|
||||
procedure PanSplitMouseUp(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
procedure LayoutPanStack(AWideH: Integer); // регионы панов + Layout каждого
|
||||
function Pan0HeaderH: Integer; // высота шапки пана 0 (0 без стека)
|
||||
procedure PbSpectrumDblClick(Sender: TObject);
|
||||
procedure TrkDriveChange(Sender: TObject);
|
||||
procedure UpdateDriveLabel; // % (openHPSDR) / дБ (Pluto) рядом с ползунком drive
|
||||
@@ -990,6 +1053,12 @@ begin
|
||||
FPan.OnSliceClosed := OnPanSliceClosed;
|
||||
FPan.OnFlagsInvalidate := OnVfoOverlayInvalidate;
|
||||
FPan.AttachMainFlag(FVfoOverlay); // внутри: FSpecView.VfoOverlay := флаг A
|
||||
// Стек панов (этап 3.3): пан 0 — в общем массиве; «⊞» добавляет пан.
|
||||
FPan.PanId := 0;
|
||||
FPan.OnAddPan := OnAddPanClick;
|
||||
FPans[0] := FPan;
|
||||
FPanShare[0] := 1.0;
|
||||
FPanCbLock := SyncObjs.TCriticalSection.Create;
|
||||
FSpecView.VfoA := FController.FVfoA;
|
||||
FSpecView.VfoB := FController.FVfoB;
|
||||
FSpecView.ActiveVfo := FController.FActiveVfo;
|
||||
@@ -1040,6 +1109,9 @@ begin
|
||||
FController.FDSPEngine.OnAudio := FController.OnAudioReady;
|
||||
FController.FDSPEngine.OnSpectrum := FController.OnSpectrumReady;
|
||||
FController.FDSPEngine.OnWaterfall := FController.OnWaterfallReady;
|
||||
// Пиксели доп. панов (этап 3.3) — прямо в UI (display-поток; см. FPanCbLock).
|
||||
FController.FDSPEngine.OnPanSpectrum := OnPanSpectrumReady;
|
||||
FController.FDSPEngine.OnPanWaterfall := OnPanWaterfallReady;
|
||||
FController.FDSPEngine.OnTXIQ := FController.OnTXIQ;
|
||||
// Web-клиент перехватывает RX-аудио (моно-микс) у контроллера, если подключён.
|
||||
FController.OnAudioConsume := FWebAdapter.AudioConsume;
|
||||
@@ -1093,7 +1165,10 @@ begin
|
||||
PanelSplitter := nil;
|
||||
PbWaterfall := nil;
|
||||
PbPanZoom := nil;
|
||||
DestroyAllExtraPans; // доп. паны (DDC/движок/панели) — до пана 0
|
||||
FreeAndNil(FPan);
|
||||
FPans[0] := nil;
|
||||
FreeAndNil(FPanCbLock);
|
||||
FreeAndNil(FWidebandView);
|
||||
|
||||
// Сохраняем настройки при закрытии
|
||||
@@ -1216,6 +1291,8 @@ begin
|
||||
BtnStartStop.Caption := 'STOP';
|
||||
StyleButton(BtnStartStop, True);
|
||||
BtnMOX.Enabled := True;
|
||||
UpdateAddPanButton; // «⊞» доступна только в Run
|
||||
ResizeSpectrumPanels;
|
||||
end
|
||||
else
|
||||
begin
|
||||
@@ -1223,6 +1300,11 @@ begin
|
||||
BtnStartStop.Caption := 'START';
|
||||
StyleButton(BtnStartStop, False);
|
||||
BtnMOX.Enabled := False;
|
||||
// Доп. паны живут только в Run-сессии (их DDC уже погашены
|
||||
// контроллером в StopRunning) — убираем и UI.
|
||||
DestroyAllExtraPans;
|
||||
UpdateAddPanButton;
|
||||
ResizeSpectrumPanels;
|
||||
end;
|
||||
rfADCOverload:
|
||||
if FSpecView <> nil then
|
||||
@@ -2435,7 +2517,8 @@ begin
|
||||
WideSpecH := Max(1, WideH - RULER_H);
|
||||
if PbWideband <> nil then
|
||||
begin
|
||||
PbWideband.SetBounds(0, 0, RW, WideSpecH);
|
||||
// При стеке панов над wideband стоит шапка пана 0.
|
||||
PbWideband.SetBounds(0, Pan0HeaderH, RW, WideSpecH);
|
||||
PbWideband.Visible := FController.FShowWideband and (WideSpecH > 0);
|
||||
if FController.FShowWideband and (RW > 0) and (WideSpecH > 0) then
|
||||
begin
|
||||
@@ -2445,7 +2528,7 @@ begin
|
||||
end;
|
||||
if PbWidebandRuler <> nil then
|
||||
begin
|
||||
PbWidebandRuler.SetBounds(0, WideSpecH, RW, IfThen(FController.FShowWideband and (WideH > 0), RULER_H, 0));
|
||||
PbWidebandRuler.SetBounds(0, Pan0HeaderH + WideSpecH, RW, IfThen(FController.FShowWideband and (WideH > 0), RULER_H, 0));
|
||||
PbWidebandRuler.Visible := FController.FShowWideband and (WideH > 0);
|
||||
if FController.FShowWideband and (RW > 0) and (WideH > 0) then
|
||||
begin
|
||||
@@ -2456,11 +2539,61 @@ begin
|
||||
// S-метр позиционируется отдельно в ResizeSMeter
|
||||
ResizeSMeter;
|
||||
|
||||
// Стек спектр/линейка/сплиттер/водопад/пан-зум раскладывает панадаптер.
|
||||
FPan.Layout(WideH, FController.FShowSpectrum, FController.FShowWaterfall);
|
||||
// Стек панов: регионы по долям, каждый пан раскладывает свой стек
|
||||
// спектр/линейка/сплиттер/водопад/пан-зум внутри своего региона.
|
||||
LayoutPanStack(WideH);
|
||||
PositionSampleRateOverlay;
|
||||
end;
|
||||
|
||||
function TMainForm.Pan0HeaderH: Integer;
|
||||
begin
|
||||
if PanCount > 1 then
|
||||
Result := MulDiv(20, Screen.PixelsPerInch, 96) // = HEADER_H панели
|
||||
else
|
||||
Result := 0;
|
||||
end;
|
||||
|
||||
procedure TMainForm.LayoutPanStack(AWideH: Integer);
|
||||
const
|
||||
PAN_SPLIT_H = 5;
|
||||
var
|
||||
ActiveIds: array[0..MAX_PANS-1] of Integer;
|
||||
N, i, k, TotalH, UsableH, Y, Hi: Integer;
|
||||
ShareSum: Double;
|
||||
begin
|
||||
N := 0;
|
||||
for i := 0 to MAX_PANS - 1 do
|
||||
if FPans[i] <> nil then begin ActiveIds[N] := i; Inc(N); end;
|
||||
if N = 0 then Exit;
|
||||
TotalH := PanelRight.ClientHeight;
|
||||
UsableH := TotalH - (N - 1) * PAN_SPLIT_H;
|
||||
ShareSum := 0;
|
||||
for k := 0 to N - 1 do ShareSum := ShareSum + FPanShare[ActiveIds[k]];
|
||||
if ShareSum <= 0 then ShareSum := 1;
|
||||
Y := 0;
|
||||
for k := 0 to N - 1 do
|
||||
begin
|
||||
i := ActiveIds[k];
|
||||
if k > 0 then
|
||||
begin
|
||||
FPanSplitters[i].SetBounds(0, Y, PanelRight.ClientWidth, PAN_SPLIT_H);
|
||||
Inc(Y, PAN_SPLIT_H);
|
||||
end;
|
||||
if k = N - 1 then
|
||||
Hi := TotalH - Y // последний пан добирает остаток (округления долей)
|
||||
else
|
||||
Hi := Round(UsableH * FPanShare[i] / ShareSum);
|
||||
FPans[i].StackTop := Y;
|
||||
FPans[i].StackHeight := Hi;
|
||||
FPans[i].HeaderVisible := N > 1;
|
||||
if i = 0 then
|
||||
FPans[i].Layout(AWideH, FController.FShowSpectrum, FController.FShowWaterfall)
|
||||
else
|
||||
FPans[i].Layout(0, FController.FShowSpectrum, FController.FShowWaterfall);
|
||||
Inc(Y, Hi);
|
||||
end;
|
||||
end;
|
||||
|
||||
// ===========================================================================
|
||||
// Dark Theme
|
||||
// ===========================================================================
|
||||
@@ -2536,6 +2669,23 @@ begin
|
||||
if BtnZoomIn <> nil then StyleButton(BtnZoomIn, False);
|
||||
if BtnZoomDef <> nil then StyleButton(BtnZoomDef, False);
|
||||
if BtnZoomOut <> nil then StyleButton(BtnZoomOut, False);
|
||||
// Стек панов: шапки/кнопки всех панелей (пан 0 — «⊞», паны N — «×»).
|
||||
for i := 0 to MAX_PANS - 1 do
|
||||
if FPans[i] <> nil then
|
||||
begin
|
||||
if FPans[i].BtnAddPan <> nil then StyleButton(FPans[i].BtnAddPan, False);
|
||||
if FPans[i].BtnClosePan <> nil then StyleButton(FPans[i].BtnClosePan, False);
|
||||
if FPans[i].HeaderPanel <> nil then
|
||||
begin
|
||||
FPans[i].HeaderPanel.Color := T.Panel;
|
||||
FPans[i].HeaderLabel.Font.Color := T.Text;
|
||||
end;
|
||||
if i > 0 then
|
||||
begin
|
||||
FPans[i].SetFlagsTheme(T);
|
||||
if FPanSplitters[i] <> nil then FPanSplitters[i].Color := T.Border;
|
||||
end;
|
||||
end;
|
||||
if FPanZoomBar <> nil then
|
||||
begin
|
||||
FPanZoomBar.SetTheme(T);
|
||||
@@ -2938,6 +3088,13 @@ var
|
||||
ElapsedTick: QWord;
|
||||
I: Integer;
|
||||
begin
|
||||
// «×» шапки пана: закрытие отложено сюда (нельзя free панель из её кнопки).
|
||||
if FPendingPanClose <> 0 then
|
||||
begin
|
||||
I := FPendingPanClose;
|
||||
FPendingPanClose := 0;
|
||||
ClosePanadapter(I);
|
||||
end;
|
||||
// Синхронизируем размеры если размер панели изменился (после ресайза).
|
||||
// Важно: проверяем только видимые панели — когда спектр скрыт,
|
||||
// PbSpectrum.Height может быть ненулевым при FPan.SpectrumHeight=0,
|
||||
@@ -3009,6 +3166,27 @@ begin
|
||||
FSpecView.WDSPReady := FController.FWDSPReady;
|
||||
// Флаги: метр/частота главного, позиции, троттленный S-метр слайсов.
|
||||
FPan.TickFlags;
|
||||
// Доп. паны: синк вьюх + тик флагов + рендер готовых кадров.
|
||||
SyncPanViews;
|
||||
for I := 1 to MAX_PANS - 1 do
|
||||
if FPans[I] <> nil then
|
||||
begin
|
||||
FPans[I].TickFlags;
|
||||
if FPanSpecDirty[I] then
|
||||
begin
|
||||
FPans[I].View.DrawSpectrum;
|
||||
FPans[I].PbSpectrum.Invalidate;
|
||||
if FPans[I].View.NeedsRulerRedraw then
|
||||
FPans[I].PbRuler.Invalidate;
|
||||
FPanSpecDirty[I] := False;
|
||||
end;
|
||||
if FPanWfDirty[I] then
|
||||
begin
|
||||
FPans[I].View.DrawWaterfall;
|
||||
FPans[I].PbWaterfall.Invalidate;
|
||||
FPanWfDirty[I] := False;
|
||||
end;
|
||||
end;
|
||||
// Панорама и водопад теперь рисуются независимо по факту новых кадров.
|
||||
// Это лучше совпадает с Thetis: маленький FFT даёт новые строки чаще.
|
||||
if FSpectrumDirty then
|
||||
@@ -3266,6 +3444,9 @@ begin
|
||||
FController.FDSPEngine.OnAudio := FController.OnAudioReady;
|
||||
FController.FDSPEngine.OnSpectrum := FController.OnSpectrumReady;
|
||||
FController.FDSPEngine.OnWaterfall := FController.OnWaterfallReady;
|
||||
// Пиксели доп. панов (этап 3.3) — прямо в UI (display-поток; см. FPanCbLock).
|
||||
FController.FDSPEngine.OnPanSpectrum := OnPanSpectrumReady;
|
||||
FController.FDSPEngine.OnPanWaterfall := OnPanWaterfallReady;
|
||||
FController.FDSPEngine.OnTXIQ := FController.OnTXIQ;
|
||||
// Иначе sound-card mic путь теряется после смены sample rate (колбэк
|
||||
// навешивается в FormCreate, но новый движок про него не знал).
|
||||
@@ -4823,8 +5004,8 @@ begin
|
||||
if CurSliceId <> 0 then
|
||||
begin
|
||||
FController.SetSliceModeBW(CurSliceId, Mode, FilterBW);
|
||||
FPan.PushSliceFlagState(CurSliceId);
|
||||
FPan.LayoutFlags;
|
||||
PushFlagStateAllPans(CurSliceId);
|
||||
LayoutFlagsAllPans;
|
||||
Exit;
|
||||
end;
|
||||
FController.FMode := Mode;
|
||||
@@ -4944,7 +5125,7 @@ begin
|
||||
if CurSliceId <> 0 then
|
||||
begin
|
||||
FController.SetSliceDevice(CurSliceId, DevIndex, DevName);
|
||||
FPan.PushSliceFlagState(CurSliceId); // обновить подсветку текущего устройства слайса
|
||||
PushFlagStateAllPans(CurSliceId); // обновить подсветку текущего устройства слайса
|
||||
Exit;
|
||||
end;
|
||||
ApplyAudioDevice(DevIndex, DevName);
|
||||
@@ -4952,7 +5133,15 @@ begin
|
||||
end;
|
||||
|
||||
procedure TMainForm.OnVfoOverlayInvalidate(Sender: TObject);
|
||||
var P: TPanafallPanel;
|
||||
begin
|
||||
// Флаг/панель доп. пана → грязним и репейнтим ЕГО канву, пан 0 не трогаем.
|
||||
P := PanOfOverlay(Sender);
|
||||
if (P <> nil) and (P <> FPan) then
|
||||
begin
|
||||
MarkPanDirty(P);
|
||||
Exit;
|
||||
end;
|
||||
FSpectrumDirty := True;
|
||||
if Assigned(FSpecView) then
|
||||
begin
|
||||
@@ -4981,8 +5170,14 @@ end;
|
||||
|
||||
// SliceId флага, чьё событие сейчас обрабатывается (0 = главный приёмник / слайс A).
|
||||
function TMainForm.CurSliceId: Integer;
|
||||
// Диспетчер флагов синхронный — ненулевой CurSliceId сейчас максимум у одной
|
||||
// панели (той, чей флаг обрабатывает событие).
|
||||
var i: Integer;
|
||||
begin
|
||||
Result := FPan.CurSliceId;
|
||||
Result := 0;
|
||||
for i := 0 to MAX_PANS - 1 do
|
||||
if (FPans[i] <> nil) and (FPans[i].CurSliceId <> 0) then
|
||||
Exit(FPans[i].CurSliceId);
|
||||
end;
|
||||
|
||||
// Единая проводка событий флага (главного и слайсов) на общие обработчики.
|
||||
@@ -5018,6 +5213,568 @@ begin
|
||||
FActiveSliceId := Id;
|
||||
end;
|
||||
|
||||
// ===========================================================================
|
||||
// Стек панадаптеров (этап 3.3): доп. паны на своих DDC
|
||||
// ===========================================================================
|
||||
|
||||
function TMainForm.PanCount: Integer;
|
||||
var i: Integer;
|
||||
begin
|
||||
Result := 0;
|
||||
for i := 0 to MAX_PANS - 1 do
|
||||
if FPans[i] <> nil then Inc(Result);
|
||||
end;
|
||||
|
||||
function TMainForm.PanFromSender(Sender: TObject): TPanafallPanel;
|
||||
// Контролы панели создаются с Owner=панель (Build) — восстановление пана
|
||||
// из Sender мышиного события.
|
||||
begin
|
||||
Result := nil;
|
||||
if (Sender is TComponent) and (TComponent(Sender).Owner is TPanafallPanel) then
|
||||
Result := TPanafallPanel(TComponent(Sender).Owner);
|
||||
end;
|
||||
|
||||
function TMainForm.PanOfOverlay(Sender: TObject): TPanafallPanel;
|
||||
// Панель, которой принадлежит источник события: сама панель (Sender=панель),
|
||||
// её флаг (Owner=панель) или главный флаг/nil → пан 0.
|
||||
begin
|
||||
Result := FPan;
|
||||
if Sender is TPanafallPanel then Exit(TPanafallPanel(Sender));
|
||||
if (Sender is TComponent) and (TComponent(Sender).Owner is TPanafallPanel) then
|
||||
Result := TPanafallPanel(TComponent(Sender).Owner);
|
||||
end;
|
||||
|
||||
procedure TMainForm.PushFlagStateAllPans(SliceId: Integer);
|
||||
// Флаг слайса живёт ровно на одной панели; у остальных PushSliceFlagState
|
||||
// не найдёт флаг и выйдет.
|
||||
var i: Integer;
|
||||
begin
|
||||
for i := 0 to MAX_PANS - 1 do
|
||||
if FPans[i] <> nil then FPans[i].PushSliceFlagState(SliceId);
|
||||
end;
|
||||
|
||||
procedure TMainForm.LayoutFlagsAllPans;
|
||||
var i: Integer;
|
||||
begin
|
||||
for i := 0 to MAX_PANS - 1 do
|
||||
if FPans[i] <> nil then FPans[i].LayoutFlags;
|
||||
end;
|
||||
|
||||
procedure TMainForm.MarkPanDirty(P: TPanafallPanel);
|
||||
begin
|
||||
if P = nil then Exit;
|
||||
if P = FPan then
|
||||
begin
|
||||
FSpectrumDirty := True;
|
||||
if Assigned(FSpecView) then FSpecView.SpectrumDirty := True;
|
||||
if Assigned(PbSpectrum) then PbSpectrum.Invalidate;
|
||||
Exit;
|
||||
end;
|
||||
FPanSpecDirty[P.PanId] := True;
|
||||
P.View.SpectrumDirty := True;
|
||||
if P.PbSpectrum <> nil then P.PbSpectrum.Invalidate;
|
||||
end;
|
||||
|
||||
procedure TMainForm.PanViewWindow(P: TPanafallPanel; out VC, VS: Double);
|
||||
// Видимое окно доп. пана: центр DDC ± rate/2 (пан-зума пока нет).
|
||||
begin
|
||||
VC := FController.PanDDCFreq(P.PanId);
|
||||
VS := FController.PanDDCRateKHz(P.PanId) * 1000.0;
|
||||
if VS <= 0 then VS := 384000;
|
||||
end;
|
||||
|
||||
procedure TMainForm.AddSliceAtFreqPan(P: TPanafallPanel; Hz: Double);
|
||||
// Слайс в доп. пане (режим/полоса копируются с главного, звуковуха default).
|
||||
var Lo, Hi, Id: Integer;
|
||||
begin
|
||||
if not FController.SliceFitsCapture(Hz, P.PanId) then Exit;
|
||||
TRadioController.FilterEdgesFor(FController.FMode, FController.FFilterBW, Lo, Hi);
|
||||
Id := FController.AddSlice(Hz, FController.FMode, Lo, Hi,
|
||||
TRadioController.AGCModeFromUI(FController.FAGCMode), 0.7, -1, '',
|
||||
P.PanId);
|
||||
if Id < 0 then Exit;
|
||||
P.CreateSliceFlag(Id);
|
||||
FActiveSliceId := Id;
|
||||
end;
|
||||
|
||||
procedure TMainForm.PanClickTune(P: TPanafallPanel; X, W: Integer);
|
||||
// Клик по фону доп. пана = перестроить его слайс на частоту клика (аналог
|
||||
// DoSpectrumClick главного: снап к 100 Гц). Цель — активный слайс, если он
|
||||
// живёт в этом пане, иначе первый слайс пана; слайсов нет — создать в точке.
|
||||
const
|
||||
StepHz = 100;
|
||||
var
|
||||
VC, VS, LoHz, HiHz: Double;
|
||||
Hz: Int64;
|
||||
Id: Integer;
|
||||
S: TCtrlSlice;
|
||||
begin
|
||||
PanViewWindow(P, VC, VS);
|
||||
if (W <= 0) or (VS <= 0) then Exit;
|
||||
Hz := Round(VC + (X / W - 0.5) * VS);
|
||||
Hz := ((Hz + StepHz div 2) div StepHz) * StepHz;
|
||||
FController.CapturedSpan(LoHz, HiHz, P.PanId);
|
||||
if Hz < LoHz then Hz := Round(LoHz);
|
||||
if Hz > HiHz then Hz := Round(HiHz);
|
||||
if (FActiveSliceId <> 0) and FController.GetSlice(FActiveSliceId, S) and
|
||||
(S.PanId = P.PanId) then
|
||||
Id := FActiveSliceId
|
||||
else
|
||||
Id := P.FirstSliceId;
|
||||
if Id = 0 then
|
||||
begin
|
||||
AddSliceAtFreqPan(P, Hz);
|
||||
Exit;
|
||||
end;
|
||||
FController.SetSliceTarget(Id, Hz);
|
||||
FActiveSliceId := Id;
|
||||
P.PushSliceFlagState(Id);
|
||||
P.LayoutFlags;
|
||||
MarkPanDirty(P);
|
||||
end;
|
||||
|
||||
procedure TMainForm.UpdatePanHeaders;
|
||||
// Текст/видимость шапок: у одного пана шапки нет, у стека — у всех.
|
||||
var
|
||||
i, N: Integer;
|
||||
P: TPanafallPanel;
|
||||
begin
|
||||
N := PanCount;
|
||||
for i := 0 to MAX_PANS - 1 do
|
||||
begin
|
||||
P := FPans[i];
|
||||
if P = nil then Continue;
|
||||
P.HeaderVisible := N > 1;
|
||||
if P.BtnClosePan <> nil then P.BtnClosePan.Visible := (N > 1) and (i > 0);
|
||||
if i = 0 then
|
||||
P.SetHeaderText(Format('PAN 0 · %.6f MHz · %d kHz',
|
||||
[FController.FCenterFreq / 1e6, FController.FSampleRate div 1000]))
|
||||
else
|
||||
P.SetHeaderText(Format('PAN %d · %.6f MHz · %d kHz',
|
||||
[i, FController.PanDDCFreq(i) / 1e6, FController.PanDDCRateKHz(i)]));
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TMainForm.UpdateAddPanButton;
|
||||
var HasSlot: Boolean; i: Integer;
|
||||
begin
|
||||
HasSlot := False;
|
||||
for i := 1 to MAX_PANS - 1 do
|
||||
if FPans[i] = nil then begin HasSlot := True; Break; end;
|
||||
FPan.AddPanEnabled := (FController.BackendCaps.MaxPans > 1) and
|
||||
FController.FRunning and HasSlot;
|
||||
end;
|
||||
|
||||
procedure TMainForm.OnAddPanClick(Sender: TObject);
|
||||
// «⊞»: новый пан на своём DDC. Центр = частота активного VFO, rate 384к,
|
||||
// ADC 0; в центре автоматически создаётся первый слайс.
|
||||
var
|
||||
PanId, i: Integer;
|
||||
Freq: Double;
|
||||
P: TPanafallPanel;
|
||||
Split: TPanel;
|
||||
begin
|
||||
if not FController.FRunning then Exit;
|
||||
PanId := -1;
|
||||
for i := 1 to MAX_PANS - 1 do
|
||||
if FPans[i] = nil then begin PanId := i; Break; end;
|
||||
if PanId < 0 then Exit;
|
||||
|
||||
Freq := ActiveVfoFreq;
|
||||
if not FController.AddPanDDC(PanId, Freq, 384, 0) then Exit;
|
||||
|
||||
P := TPanafallPanel.Create(nil, FUseOpenGLSpectrum);
|
||||
P.PanId := PanId;
|
||||
P.Controller := FController;
|
||||
P.ShowZoomRow := False; // пан-зум доп. панов — этап 3.3+
|
||||
P.OnWireFlag := WireOverlayEvents;
|
||||
P.OnSliceClosed := OnPanSliceClosed;
|
||||
P.OnFlagsInvalidate := OnVfoOverlayInvalidate;
|
||||
P.OnPanClose := OnPanHeaderClose;
|
||||
P.OnSplitterMoved := RightPanelResize; // ratio обновлён → полный re-layout
|
||||
P.SpectrumMouseDown := PanNSpectrumMouseDown;
|
||||
P.SpectrumMouseMove := PanNSpectrumMouseMove;
|
||||
P.SpectrumMouseUp := PanNSpectrumMouseUp;
|
||||
P.SpectrumMouseLeave := PanNSpectrumMouseLeave;
|
||||
P.SpectrumDblClick := PanNSpectrumDblClick;
|
||||
P.WaterfallMouseDown := PanNSpectrumMouseDown; // X→частота одинаково
|
||||
P.WaterfallMouseMove := PanNSpectrumMouseMove;
|
||||
P.WaterfallMouseUp := PanNSpectrumMouseUp;
|
||||
P.Build(PanelRight, Max(1, FController.FSpecMSAA));
|
||||
|
||||
// Параметры отображения — с главного view (пан-персональные — позже).
|
||||
P.View.WfAGCEnabled := FController.FWfAGCEnabled;
|
||||
P.View.WfNFEnabled := FController.FWfNFEnabled;
|
||||
P.View.WfManualHigh := FController.FWfManualHigh;
|
||||
P.View.WfManualLow := FController.FWfManualLow;
|
||||
P.View.WfAGCOffset := FController.FWfAGCOffset;
|
||||
P.View.WfPalette := FController.FWfPalette;
|
||||
P.View.WfGamma := FController.FWfGamma;
|
||||
P.View.SpecRefLevel := FController.FSpecRefLevel;
|
||||
P.View.SpecRange := FController.FSpecRange;
|
||||
P.View.SpecGridStep := FController.FSpecGridStep;
|
||||
P.View.WfFrameInterval := EffectiveWfInterval;
|
||||
P.View.PAMaxPower := FController.FPAMaxPower;
|
||||
P.View.CenterFreq := Freq;
|
||||
P.View.SpanHz := 384000;
|
||||
// VFO/TX-маркеры главного на доп. пане не рисуем — уводим за пределы окна.
|
||||
P.View.VfoA := -1e15;
|
||||
P.View.VfoB := -1e15;
|
||||
P.View.TXFreq := -1e15;
|
||||
|
||||
// Сплиттер над паном.
|
||||
Split := TPanel.Create(Self);
|
||||
Split.Parent := PanelRight;
|
||||
Split.BevelOuter := bvNone;
|
||||
Split.Color := TColor($00303030);
|
||||
Split.Cursor := crVSplit;
|
||||
Split.Tag := PanId;
|
||||
Split.OnMouseDown := PanSplitMouseDown;
|
||||
Split.OnMouseMove := PanSplitMouseMove;
|
||||
Split.OnMouseUp := PanSplitMouseUp;
|
||||
FPanSplitters[PanId] := Split;
|
||||
|
||||
// Публикуем пан для пиксельных колбэков (display-поток) и делим высоту поровну.
|
||||
FPanCbLock.Enter;
|
||||
try
|
||||
FPans[PanId] := P;
|
||||
finally
|
||||
FPanCbLock.Leave;
|
||||
end;
|
||||
for i := 0 to MAX_PANS - 1 do
|
||||
if FPans[i] <> nil then FPanShare[i] := 1.0 / PanCount;
|
||||
|
||||
AddSliceAtFreqPan(P, Freq);
|
||||
ApplyDarkTheme; // темизация шапки/кнопок нового пана
|
||||
UpdatePanHeaders;
|
||||
UpdateAddPanButton;
|
||||
ResizeSpectrumPanels;
|
||||
end;
|
||||
|
||||
procedure TMainForm.OnPanHeaderClose(Sender: TObject);
|
||||
// «×» шапки: нельзя free панель из обработчика её же кнопки — откладываем
|
||||
// в тик (FPendingPanClose).
|
||||
begin
|
||||
if Sender is TPanafallPanel then
|
||||
FPendingPanClose := TPanafallPanel(Sender).PanId;
|
||||
end;
|
||||
|
||||
procedure TMainForm.ClosePanadapter(PanId: Integer);
|
||||
var
|
||||
P: TPanafallPanel;
|
||||
S: TCtrlSlice;
|
||||
i: Integer;
|
||||
begin
|
||||
if (PanId < 1) or (PanId >= MAX_PANS) then Exit;
|
||||
P := FPans[PanId];
|
||||
if P = nil then Exit;
|
||||
// Отписываем от пиксельных колбэков ДО удаления DDC/панели.
|
||||
FPanCbLock.Enter;
|
||||
try
|
||||
FPans[PanId] := nil;
|
||||
finally
|
||||
FPanCbLock.Leave;
|
||||
end;
|
||||
// Слайсы пана (со звуковухами) + движок + DDC.
|
||||
FController.RemovePanDDC(PanId);
|
||||
if (FActiveSliceId <> 0) and not FController.GetSlice(FActiveSliceId, S) then
|
||||
FActiveSliceId := 0;
|
||||
FreeAndNil(FPanSplitters[PanId]);
|
||||
P.Free;
|
||||
for i := 0 to MAX_PANS - 1 do
|
||||
if FPans[i] <> nil then FPanShare[i] := 1.0 / PanCount;
|
||||
UpdatePanHeaders;
|
||||
UpdateAddPanButton;
|
||||
ResizeSpectrumPanels;
|
||||
end;
|
||||
|
||||
procedure TMainForm.DestroyAllExtraPans;
|
||||
var i: Integer;
|
||||
begin
|
||||
for i := 1 to MAX_PANS - 1 do
|
||||
if FPans[i] <> nil then ClosePanadapter(i);
|
||||
end;
|
||||
|
||||
procedure TMainForm.SyncPanViews;
|
||||
// Центр/span вьюх доп. панов (аналог SyncSpecViewFreq главного, но без
|
||||
// VFO/TX-маркеров — они уведены за окно при создании).
|
||||
var i: Integer; P: TPanafallPanel;
|
||||
begin
|
||||
for i := 1 to MAX_PANS - 1 do
|
||||
begin
|
||||
P := FPans[i];
|
||||
if P = nil then Continue;
|
||||
P.View.CenterFreq := FController.PanDDCFreq(i);
|
||||
P.View.SpanHz := FController.PanDDCRateKHz(i) * 1000.0;
|
||||
P.View.WDSPReady := FController.FWDSPReady;
|
||||
end;
|
||||
// Шапки следуют за частотами (Caption-сеттер — no-op при том же тексте).
|
||||
if PanCount > 1 then UpdatePanHeaders;
|
||||
end;
|
||||
|
||||
procedure TMainForm.OnPanSpectrumReady(PanId: Integer;
|
||||
const Pixels: array of Single; Count: Integer);
|
||||
// Display-поток движка. Лок закрывает гонку с Close/AddPanadapter (UI).
|
||||
begin
|
||||
FPanCbLock.Enter;
|
||||
try
|
||||
if (PanId >= 1) and (PanId < MAX_PANS) and (FPans[PanId] <> nil) then
|
||||
begin
|
||||
FPans[PanId].View.SetSpectrumData(Pixels, Count);
|
||||
FPanSpecDirty[PanId] := True;
|
||||
end;
|
||||
finally
|
||||
FPanCbLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TMainForm.OnPanWaterfallReady(PanId: Integer;
|
||||
const Pixels: array of Single; Count: Integer);
|
||||
begin
|
||||
FPanCbLock.Enter;
|
||||
try
|
||||
if (PanId >= 1) and (PanId < MAX_PANS) and (FPans[PanId] <> nil) then
|
||||
begin
|
||||
FPans[PanId].View.SetWaterfallData(Pixels, Count);
|
||||
if FPans[PanId].View.WaterfallDirty then FPanWfDirty[PanId] := True;
|
||||
end;
|
||||
finally
|
||||
FPanCbLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
// ---- Мышь доп. панов: тюн слайсов + drag центра (ретюн DDC). Семантика
|
||||
// главного пана (VFO/CTUN/маяк/оверлеи) сюда не переносится. ----
|
||||
|
||||
procedure TMainForm.PanNSpectrumMouseDown(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
var
|
||||
P: TPanafallPanel;
|
||||
VC, VS: Double;
|
||||
Id, W: Integer;
|
||||
IsSpec: Boolean;
|
||||
begin
|
||||
P := PanFromSender(Sender);
|
||||
if P = nil then Exit;
|
||||
// Флаги и маркеры несущих живут ТОЛЬКО на спектре: координаты водопада в
|
||||
// диспетчер флагов отдавать нельзя (его прямоугольники — в системе спектра).
|
||||
IsSpec := TControl(Sender) = P.PbSpectrum;
|
||||
if IsSpec and P.DispatchFlagsMouseDown(Button, X, Y) then
|
||||
begin
|
||||
P.ProcessPendingSliceClose;
|
||||
Exit;
|
||||
end;
|
||||
W := TControl(Sender).Width;
|
||||
if W <= 0 then Exit;
|
||||
if (Button = mbLeft) and (ssCtrl in Shift) then
|
||||
begin
|
||||
PanViewWindow(P, VC, VS);
|
||||
AddSliceAtFreqPan(P, PixelToFreq(X, W, VC, VS));
|
||||
Exit;
|
||||
end;
|
||||
if Button = mbLeft then
|
||||
begin
|
||||
if IsSpec then
|
||||
begin
|
||||
Id := P.SliceAtPixel(X);
|
||||
if Id <> 0 then
|
||||
begin
|
||||
FPanSliceDrag := True;
|
||||
FPanSliceDragId := Id;
|
||||
FPanMousePan := P;
|
||||
FActiveSliceId := Id;
|
||||
TControl(Sender).Cursor := crSizeWE;
|
||||
Exit;
|
||||
end;
|
||||
end;
|
||||
// Пустое место: армируем drag ЦЕНТРА пана (ретюн его DDC; слайсы стоят на
|
||||
// своих абсолютных частотах). Клик без движения = тюн слайса пана (MouseUp).
|
||||
// Курсор не трогаем до реального сдвига.
|
||||
FPanCenterDrag := True;
|
||||
FPanCenterMoved := False;
|
||||
FPanMousePan := P;
|
||||
FPanCenterDragX0 := X;
|
||||
FPanCenterDragF0 := FController.PanDDCFreq(P.PanId);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TMainForm.PanNSpectrumMouseMove(Sender: TObject; Shift: TShiftState;
|
||||
X, Y: Integer);
|
||||
var
|
||||
P: TPanafallPanel;
|
||||
VC, VS, NewHz, LoHz, HiHz: Double;
|
||||
W: Integer;
|
||||
begin
|
||||
P := PanFromSender(Sender);
|
||||
if P = nil then Exit;
|
||||
W := TControl(Sender).Width;
|
||||
|
||||
if FPanSliceDrag and (FPanMousePan = P) then
|
||||
begin
|
||||
if (ssLeft in Shift) and (W > 0) then
|
||||
begin
|
||||
PanViewWindow(P, VC, VS);
|
||||
NewHz := PixelToFreq(X, W, VC, VS);
|
||||
FController.CapturedSpan(LoHz, HiHz, P.PanId);
|
||||
if NewHz < LoHz then NewHz := LoHz;
|
||||
if NewHz > HiHz then NewHz := HiHz;
|
||||
FController.SetSliceTarget(FPanSliceDragId, NewHz);
|
||||
P.PushSliceFlagState(FPanSliceDragId);
|
||||
P.LayoutFlags;
|
||||
MarkPanDirty(P);
|
||||
end;
|
||||
Exit;
|
||||
end;
|
||||
|
||||
if FPanCenterDrag and (FPanMousePan = P) then
|
||||
begin
|
||||
if (ssLeft in Shift) and (W > 0) then
|
||||
begin
|
||||
// Гистерезис 4px: до него это ещё «клик» (тюн по MouseUp), не drag.
|
||||
if (not FPanCenterMoved) and (Abs(X - FPanCenterDragX0) < 4) then Exit;
|
||||
if not FPanCenterMoved then
|
||||
begin
|
||||
FPanCenterMoved := True;
|
||||
TControl(Sender).Cursor := crSizeWE;
|
||||
end;
|
||||
PanViewWindow(P, VC, VS);
|
||||
NewHz := FPanCenterDragF0 - (X - FPanCenterDragX0) * VS / W;
|
||||
NewHz := Max(FController.BackendCaps.MinFreqHz,
|
||||
Min(FController.BackendCaps.MaxFreqHz, NewHz));
|
||||
FController.SetPanDDCFreq(P.PanId, NewHz);
|
||||
P.View.CenterFreq := NewHz;
|
||||
P.LayoutFlags;
|
||||
UpdatePanHeaders;
|
||||
MarkPanDirty(P);
|
||||
end;
|
||||
Exit;
|
||||
end;
|
||||
|
||||
// Hover-логика (флаги, маркеры несущих) — только на спектре; на водопаде
|
||||
// курсор остаётся обычным, как у главного пана.
|
||||
if TControl(Sender) = P.PbSpectrum then
|
||||
begin
|
||||
P.DispatchFlagsMouseMove(X, Y);
|
||||
if P.SliceAtPixel(X) <> 0 then
|
||||
TControl(Sender).Cursor := crSizeWE
|
||||
else
|
||||
TControl(Sender).Cursor := crArrow; // crDefault «снимает» курсор — см. Build
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TMainForm.PanNSpectrumMouseUp(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
var P: TPanafallPanel; W: Integer;
|
||||
begin
|
||||
P := PanFromSender(Sender);
|
||||
if P = nil then Exit;
|
||||
if FPanSliceDrag and (FPanMousePan = P) then
|
||||
begin
|
||||
FPanSliceDrag := False;
|
||||
FPanMousePan := nil;
|
||||
TControl(Sender).Cursor := crArrow;
|
||||
Exit;
|
||||
end;
|
||||
if FPanCenterDrag and (FPanMousePan = P) then
|
||||
begin
|
||||
FPanCenterDrag := False;
|
||||
FPanMousePan := nil;
|
||||
TControl(Sender).Cursor := crArrow;
|
||||
// Пресс без движения = клик: перестроить слайс пана на частоту клика.
|
||||
W := TControl(Sender).Width;
|
||||
if (Button = mbLeft) and (not FPanCenterMoved) and (W > 0) then
|
||||
PanClickTune(P, X, W);
|
||||
Exit;
|
||||
end;
|
||||
if (TControl(Sender) = P.PbSpectrum) and P.DispatchFlagsMouseUp(X, Y) then
|
||||
P.ProcessPendingSliceClose;
|
||||
end;
|
||||
|
||||
procedure TMainForm.PanNSpectrumMouseLeave(Sender: TObject);
|
||||
var P: TPanafallPanel;
|
||||
begin
|
||||
P := PanFromSender(Sender);
|
||||
if P <> nil then P.DispatchFlagsMouseLeave;
|
||||
end;
|
||||
|
||||
procedure TMainForm.PanNSpectrumDblClick(Sender: TObject);
|
||||
begin
|
||||
// Дабл-клик на доп. пане пока без действия.
|
||||
end;
|
||||
|
||||
// ---- Сплиттеры между панами: пересчёт долей по отпусканию мыши. ----
|
||||
|
||||
procedure TMainForm.PanSplitMouseDown(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
const
|
||||
PAN_SPLIT_H = 5;
|
||||
var
|
||||
BelowId, AboveId, i, TotalH, MinPx: Integer;
|
||||
Split: TPanel;
|
||||
begin
|
||||
if Button <> mbLeft then Exit;
|
||||
Split := TPanel(Sender);
|
||||
BelowId := Split.Tag;
|
||||
// Пан над сплиттером — ближайший активный с меньшим PanId.
|
||||
AboveId := -1;
|
||||
for i := BelowId - 1 downto 0 do
|
||||
if FPans[i] <> nil then begin AboveId := i; Break; end;
|
||||
if (AboveId < 0) or (FPans[BelowId] = nil) then Exit;
|
||||
TotalH := PanelRight.ClientHeight;
|
||||
if TotalH <= 0 then Exit;
|
||||
// Границы призрака: каждому пану — минимум 15% стека (= кламп долей в MouseUp).
|
||||
MinPx := Round(0.15 * TotalH);
|
||||
FPanSplitMinTop := FPans[AboveId].StackTop + MinPx;
|
||||
FPanSplitMaxTop := FPans[BelowId].StackTop + FPans[BelowId].StackHeight
|
||||
- MinPx - PAN_SPLIT_H;
|
||||
FPanSplitDrag := BelowId;
|
||||
FPanSplitY0 := Mouse.CursorPos.Y;
|
||||
FPanSplitTop0 := Split.Top;
|
||||
Split.Color := TColor($005050A0); // подсветка при захвате (как у внутрипанного)
|
||||
end;
|
||||
|
||||
procedure TMainForm.PanSplitMouseMove(Sender: TObject; Shift: TShiftState;
|
||||
X, Y: Integer);
|
||||
var NewTop: Integer;
|
||||
begin
|
||||
// Живой призрак: двигаем сам сплиттер, пересчёт регионов — по MouseUp.
|
||||
if (FPanSplitDrag = 0) or (TPanel(Sender).Tag <> FPanSplitDrag) then Exit;
|
||||
NewTop := FPanSplitTop0 + (Mouse.CursorPos.Y - FPanSplitY0);
|
||||
if NewTop < FPanSplitMinTop then NewTop := FPanSplitMinTop;
|
||||
if NewTop > FPanSplitMaxTop then NewTop := FPanSplitMaxTop;
|
||||
TPanel(Sender).Top := NewTop;
|
||||
end;
|
||||
|
||||
procedure TMainForm.PanSplitMouseUp(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
var
|
||||
BelowId, AboveId, i, DeltaY, TotalH: Integer;
|
||||
DShare, MinShare: Double;
|
||||
begin
|
||||
if FPanSplitDrag = 0 then Exit;
|
||||
BelowId := FPanSplitDrag;
|
||||
FPanSplitDrag := 0;
|
||||
TPanel(Sender).Color := TColor($00303030);
|
||||
// Пан над сплиттером — ближайший активный с меньшим PanId.
|
||||
AboveId := -1;
|
||||
for i := BelowId - 1 downto 0 do
|
||||
if FPans[i] <> nil then begin AboveId := i; Break; end;
|
||||
if (AboveId < 0) or (FPans[BelowId] = nil) then Exit;
|
||||
TotalH := PanelRight.ClientHeight;
|
||||
if TotalH <= 0 then Exit;
|
||||
// Дельта — по финальному положению призрака (он уже клампился в Move).
|
||||
DeltaY := TPanel(Sender).Top - FPanSplitTop0;
|
||||
DShare := DeltaY / TotalH;
|
||||
MinShare := 0.15;
|
||||
if FPanShare[AboveId] + DShare < MinShare then
|
||||
DShare := MinShare - FPanShare[AboveId];
|
||||
if FPanShare[BelowId] - DShare < MinShare then
|
||||
DShare := FPanShare[BelowId] - MinShare;
|
||||
FPanShare[AboveId] := FPanShare[AboveId] + DShare;
|
||||
FPanShare[BelowId] := FPanShare[BelowId] - DShare;
|
||||
ResizeSpectrumPanels;
|
||||
end;
|
||||
|
||||
procedure TMainForm.OnVfoOverlaySliceSelect(Sender: TObject);
|
||||
begin
|
||||
if Sender is TVfoOverlay then
|
||||
@@ -5025,12 +5782,14 @@ begin
|
||||
end;
|
||||
|
||||
procedure TMainForm.OnVfoOverlayClose(Sender: TObject);
|
||||
var P: TPanafallPanel;
|
||||
begin
|
||||
if not (Sender is TVfoOverlay) then Exit;
|
||||
if TVfoOverlay(Sender).IsMain then Exit;
|
||||
// Нельзя освобождать флаг внутри его же обработчика мыши — панель пометит
|
||||
// и снимет его после возврата из диспетчера (ProcessPendingSliceClose).
|
||||
FPan.RequestSliceClose(TVfoOverlay(Sender).SliceId);
|
||||
P := PanOfOverlay(Sender);
|
||||
if P <> nil then P.RequestSliceClose(TVfoOverlay(Sender).SliceId);
|
||||
end;
|
||||
|
||||
// FPan закрыл слайс («×») — сбрасываем активный, если это был он.
|
||||
@@ -5598,17 +6357,25 @@ var
|
||||
Delta: Int64;
|
||||
Base: Int64;
|
||||
NewFreq: Int64;
|
||||
SliceId: Integer;
|
||||
SliceId, i: Integer;
|
||||
SLo, SHi, SNew: Double;
|
||||
Sl: TCtrlSlice;
|
||||
SlicePan: TPanafallPanel;
|
||||
begin
|
||||
// Над дисплеем VFO (A или B) колесо обрабатывает сам TFreqDisplay поразрядно
|
||||
// (включая неактивный VFO). Не дублируем здесь, иначе менялись бы обе частоты:
|
||||
// виджет крутил бы B, а этот хендлер — активный A.
|
||||
if OverFreqDisp(FreqDispA) or OverFreqDisp(FreqDispB) then Exit;
|
||||
|
||||
// Колесо над маркером слайса — двигаем частоту ЭТОГО слайса, а не главного VFO.
|
||||
SliceId := FPan.SliceUnderCursor;
|
||||
// Колесо над маркером слайса (любого пана) — двигаем частоту ЭТОГО слайса.
|
||||
SliceId := 0;
|
||||
SlicePan := nil;
|
||||
for i := 0 to MAX_PANS - 1 do
|
||||
if FPans[i] <> nil then
|
||||
begin
|
||||
SliceId := FPans[i].SliceUnderCursor;
|
||||
if SliceId <> 0 then begin SlicePan := FPans[i]; Break; end;
|
||||
end;
|
||||
if (SliceId <> 0) and FController.GetSlice(SliceId, Sl) then
|
||||
begin
|
||||
if (ssCtrl in Shift) and (ssShift in Shift) then Step := 1000000
|
||||
@@ -5622,14 +6389,13 @@ begin
|
||||
if Delta > 0 then SNew := Base + Step
|
||||
else if Round(Sl.TargetHz) = Base then SNew := Base - Step
|
||||
else SNew := Base;
|
||||
FController.CapturedSpan(SLo, SHi);
|
||||
FController.CapturedSpan(SLo, SHi, Sl.PanId); // полоса ЕГО пана
|
||||
if SNew < SLo then SNew := SLo;
|
||||
if SNew > SHi then SNew := SHi;
|
||||
FController.SetSliceTarget(SliceId, SNew);
|
||||
FPan.PushSliceFlagState(SliceId);
|
||||
FPan.LayoutFlags;
|
||||
FSpectrumDirty := True;
|
||||
PbSpectrum.Invalidate;
|
||||
SlicePan.PushSliceFlagState(SliceId);
|
||||
SlicePan.LayoutFlags;
|
||||
MarkPanDirty(SlicePan);
|
||||
FActiveSliceId := SliceId;
|
||||
Handled := True;
|
||||
Exit;
|
||||
|
||||
+172
-26
@@ -31,7 +31,7 @@ unit PanafallPanel;
|
||||
interface
|
||||
|
||||
uses
|
||||
Classes, SysUtils, Math, Controls, ExtCtrls, Graphics, Forms,
|
||||
Classes, SysUtils, Math, Controls, ExtCtrls, StdCtrls, Graphics, Forms,
|
||||
LCLIntf, LCLType,
|
||||
OpenGLContextEx,
|
||||
AppTheme, WDSPEngine, RadioController, VfoOverlay,
|
||||
@@ -70,6 +70,10 @@ type
|
||||
FSplitterDrag: Boolean;
|
||||
FSplitterDragY0: Integer;
|
||||
FSplitterSH0: Integer;
|
||||
// Геометрия последнего Layout для drag-математики: верх области спектра и
|
||||
// высота спектр+водопад ВНУТРИ региона пана (не всего родителя!).
|
||||
FSplitTopOff: Integer;
|
||||
FSplitAvailH: Integer; // 0 = сплиттер сейчас нелегитимен
|
||||
|
||||
FOnSplitterMoved: TNotifyEvent;
|
||||
|
||||
@@ -85,6 +89,27 @@ type
|
||||
FOnSliceClosed: TSliceClosedEvent;
|
||||
FOnFlagsInvalidate: TNotifyEvent;
|
||||
|
||||
// ---- Стек панов (этап 3.3) ----
|
||||
FPanId: Integer; // 0 = главный пан
|
||||
FStackTop: Integer; // Y-начало региона пана внутри родителя
|
||||
FStackHeight: Integer; // высота региона (0 = до низа родителя)
|
||||
FHeaderPanel: TPanel; // шапка пана (видна при панах >= 2)
|
||||
FHeaderLabel: TLabel;
|
||||
FBtnClosePan: TFlatButton; // «×» в шапке (только паны N>0)
|
||||
FBtnAddPan: TFlatButton; // «⊞» в ряду пан/зума (только пан 0)
|
||||
FHeaderVisible: Boolean;
|
||||
FShowZoomRow: Boolean; // False у панов N>0 (пан-зум — этап 3.3+)
|
||||
FOnPanClose: TNotifyEvent;
|
||||
FOnAddPan: TNotifyEvent;
|
||||
FAddPanWanted: Boolean; // хозяин: показывать «⊞» в ряду пан/зума
|
||||
|
||||
procedure BtnClosePanClick(Sender: TObject);
|
||||
procedure BtnAddPanClick(Sender: TObject);
|
||||
procedure PositionHeaderChildren(RW, H: Integer);
|
||||
// Видимое частотное окно ЭТОГО пана: пан 0 — окно главного (зум/пан),
|
||||
// паны N — центр их DDC ± rate/2 (пан-зум — этап 3.3+).
|
||||
procedure ViewWindow(out VC, VS: Double);
|
||||
|
||||
procedure SplitterMouseDown(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
procedure SplitterMouseMove(Sender: TObject; Shift: TShiftState;
|
||||
@@ -138,6 +163,25 @@ type
|
||||
property SpectrumHeight: Integer read FSpectrumHeight;
|
||||
property WaterfallHeight: Integer read FWaterfallHeight;
|
||||
property SplitterRatio: Double read FSplitterRatio write FSplitterRatio;
|
||||
|
||||
// ---- Стек панов (этап 3.3) ----
|
||||
// Регион пана внутри родителя задаёт хозяин ПЕРЕД Layout (стек панов).
|
||||
property PanId: Integer read FPanId write FPanId;
|
||||
property StackTop: Integer read FStackTop write FStackTop;
|
||||
property StackHeight: Integer read FStackHeight write FStackHeight;
|
||||
// Шапка пана: текст ставит хозяин (частота/rate), «×» → OnPanClose.
|
||||
property HeaderVisible: Boolean read FHeaderVisible write FHeaderVisible;
|
||||
procedure SetHeaderText(const S: string);
|
||||
property HeaderPanel: TPanel read FHeaderPanel;
|
||||
property HeaderLabel: TLabel read FHeaderLabel;
|
||||
property BtnClosePan: TFlatButton read FBtnClosePan;
|
||||
property BtnAddPan: TFlatButton read FBtnAddPan;
|
||||
// Ряд пан/зума: у панов N>0 скрыт (их зум — этап 3.3+).
|
||||
property ShowZoomRow: Boolean read FShowZoomRow write FShowZoomRow;
|
||||
// «⊞» в ряду пан/зума (пан 0): показать/спрятать решает хозяин.
|
||||
property AddPanEnabled: Boolean read FAddPanWanted write FAddPanWanted;
|
||||
property OnPanClose: TNotifyEvent read FOnPanClose write FOnPanClose;
|
||||
property OnAddPan: TNotifyEvent read FOnAddPan write FOnAddPan;
|
||||
// Пользователь отпустил сплиттер (ratio уже обновлён) — хозяин делает
|
||||
// полный пересчёт (у него wideband/S-метр/оверлеи).
|
||||
property OnSplitterMoved: TNotifyEvent read FOnSplitterMoved write FOnSplitterMoved;
|
||||
@@ -148,6 +192,8 @@ type
|
||||
procedure AttachMainFlag(O: TVfoOverlay);
|
||||
// SliceId флага, чьё событие сейчас обрабатывается (0 = главный).
|
||||
function CurSliceId: Integer;
|
||||
// Первый слайс панели (0 = слайсов B+ нет) — цель клика-тюна пана N.
|
||||
function FirstSliceId: Integer;
|
||||
function FindSliceFlag(SliceId: Integer): TVfoOverlay;
|
||||
procedure CreateSliceFlag(SliceId: Integer);
|
||||
procedure DestroySliceFlag(SliceId: Integer);
|
||||
@@ -199,6 +245,8 @@ begin
|
||||
FZoomBar := TPanZoomBar.Create;
|
||||
FSplitterRatio := 0.40;
|
||||
FSplitterDrag := False;
|
||||
FShowZoomRow := True; // паны N>0 выключают (их зум — этап 3.3+)
|
||||
FHeaderVisible := False; // шапка появляется при панах >= 2
|
||||
end;
|
||||
|
||||
destructor TPanafallPanel.Destroy;
|
||||
@@ -239,6 +287,11 @@ begin
|
||||
TPaintBox(FPbSpectrum).OnMouseLeave := SpectrumMouseLeave;
|
||||
end;
|
||||
FPbSpectrum.Parent := AParent;
|
||||
// ЯВНЫЙ crArrow (не crDefault!): GL-виджет с WA_NativeWindow — отдельная
|
||||
// Wayland-поверхность; без явного курсора она навсегда наследует курсор,
|
||||
// бывший под мышью в момент создания (пан создаётся кнопкой «⊞» с
|
||||
// crHandPoint → «рука» прилипала ко всему пану).
|
||||
FPbSpectrum.Cursor := crArrow;
|
||||
|
||||
FPbRuler := TPaintBox.Create(Self);
|
||||
FPbRuler.Parent := AParent;
|
||||
@@ -277,6 +330,7 @@ begin
|
||||
TPaintBox(FPbWaterfall).OnMouseUp := WaterfallMouseUp;
|
||||
end;
|
||||
FPbWaterfall.Parent := AParent;
|
||||
FPbWaterfall.Cursor := crArrow; // см. комментарий у FPbSpectrum.Cursor
|
||||
|
||||
// ---- Панель пана/зума под водопадом ----
|
||||
// Кнопки создаём с дефолтными цветами; финальная тема — у хозяина
|
||||
@@ -292,27 +346,96 @@ begin
|
||||
FBtnZoomOut := MakeFlatBtn(AParent, '−', 0, 0, 10, 10, ZoomOutClick);
|
||||
FBtnZoomDef := MakeFlatBtn(AParent, '⌂', 0, 0, 10, 10, ZoomDefClick);
|
||||
FBtnZoomIn := MakeFlatBtn(AParent, '+', 0, 0, 10, 10, ZoomInClick);
|
||||
|
||||
// ---- Кнопка «добавить пан» в ряду пан/зума (хозяин включает на пане 0) ----
|
||||
FBtnAddPan := MakeFlatBtn(AParent, '⊞', 0, 0, 10, 10, BtnAddPanClick);
|
||||
FBtnAddPan.Hint := 'Add panadapter';
|
||||
FBtnAddPan.ShowHint := True;
|
||||
FBtnAddPan.Visible := False;
|
||||
|
||||
// ---- Шапка пана (тонкая полоска над спектром; видна при панах >= 2) ----
|
||||
FHeaderPanel := TPanel.Create(Self);
|
||||
FHeaderPanel.Parent := AParent;
|
||||
FHeaderPanel.BevelOuter := bvNone;
|
||||
FHeaderPanel.Visible := False;
|
||||
FHeaderLabel := TLabel.Create(Self);
|
||||
FHeaderLabel.Parent := FHeaderPanel;
|
||||
FHeaderLabel.AutoSize := True;
|
||||
FHeaderLabel.Left := 8;
|
||||
FBtnClosePan := MakeFlatBtn(FHeaderPanel, '×', 0, 0, 10, 10, BtnClosePanClick);
|
||||
FBtnClosePan.Visible := False; // хозяин включает у панов N>0
|
||||
end;
|
||||
|
||||
procedure TPanafallPanel.BtnClosePanClick(Sender: TObject);
|
||||
begin
|
||||
if Assigned(FOnPanClose) then FOnPanClose(Self);
|
||||
end;
|
||||
|
||||
procedure TPanafallPanel.BtnAddPanClick(Sender: TObject);
|
||||
begin
|
||||
if Assigned(FOnAddPan) then FOnAddPan(Self);
|
||||
end;
|
||||
|
||||
procedure TPanafallPanel.SetHeaderText(const S: string);
|
||||
begin
|
||||
if FHeaderLabel <> nil then FHeaderLabel.Caption := S;
|
||||
end;
|
||||
|
||||
procedure TPanafallPanel.ViewWindow(out VC, VS: Double);
|
||||
begin
|
||||
VC := 0; VS := 0;
|
||||
if FController = nil then Exit;
|
||||
if FPanId = 0 then
|
||||
FController.GetViewWindow(VC, VS)
|
||||
else
|
||||
begin
|
||||
VC := FController.PanDDCFreq(FPanId);
|
||||
VS := FController.PanDDCRateKHz(FPanId) * 1000.0;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TPanafallPanel.PositionHeaderChildren(RW, H: Integer);
|
||||
var BtnS: Integer;
|
||||
begin
|
||||
if FHeaderLabel <> nil then
|
||||
FHeaderLabel.Top := (H - FHeaderLabel.Height) div 2;
|
||||
if FBtnClosePan <> nil then
|
||||
begin
|
||||
BtnS := H - 4;
|
||||
FBtnClosePan.SetBounds(RW - BtnS - 2, 2, BtnS, BtnS);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TPanafallPanel.PositionPanZoomBar(BottomY, RW, H: Integer;
|
||||
AVisible: Boolean);
|
||||
var BtnW, Gap, StripW, X: Integer;
|
||||
var BtnW, Gap, StripW, X, NBtns: Integer; WantAdd: Boolean;
|
||||
begin
|
||||
if (FPbPanZoom = nil) or (FBtnZoomIn = nil) then Exit;
|
||||
// «⊞» добавления пана живёт в этом же ряду; хозяин включает его флагом
|
||||
// AddPanEnabled (Visible кнопки управляется здесь вместе с рядом).
|
||||
WantAdd := FAddPanWanted and (FBtnAddPan <> nil);
|
||||
FPbPanZoom.Visible := AVisible;
|
||||
FBtnZoomIn.Visible := AVisible;
|
||||
FBtnZoomDef.Visible := AVisible;
|
||||
FBtnZoomOut.Visible := AVisible;
|
||||
if FBtnAddPan <> nil then FBtnAddPan.Visible := AVisible and WantAdd;
|
||||
if not AVisible then Exit;
|
||||
Gap := MulDiv(2, Screen.PixelsPerInch, 96);
|
||||
BtnW := H; // квадратные кнопки в высоту строки
|
||||
StripW := RW - 3 * BtnW - 4 * Gap;
|
||||
NBtns := 3;
|
||||
if WantAdd then NBtns := 4;
|
||||
StripW := RW - NBtns * BtnW - (NBtns + 1) * Gap;
|
||||
if StripW < 20 then StripW := 20;
|
||||
FPbPanZoom.SetBounds(0, BottomY, StripW, H);
|
||||
X := StripW + Gap;
|
||||
FBtnZoomOut.SetBounds(X, BottomY, BtnW, H); Inc(X, BtnW + Gap);
|
||||
FBtnZoomDef.SetBounds(X, BottomY, BtnW, H); Inc(X, BtnW + Gap);
|
||||
FBtnZoomIn.SetBounds(X, BottomY, BtnW, H);
|
||||
if WantAdd then
|
||||
begin
|
||||
Inc(X, BtnW + Gap);
|
||||
FBtnAddPan.SetBounds(X, BottomY, BtnW, H);
|
||||
end;
|
||||
FPbPanZoom.Invalidate;
|
||||
end;
|
||||
|
||||
@@ -327,20 +450,38 @@ var
|
||||
AvailH: Integer;
|
||||
RULER_H: Integer;
|
||||
PANZOOM_H: Integer;
|
||||
HEADER_H: Integer;
|
||||
ShowPZ: Boolean;
|
||||
begin
|
||||
if (FParent = nil) or (FPbSpectrum = nil) then Exit;
|
||||
FShowSpectrum := AShowSpectrum;
|
||||
FShowWaterfall := AShowWaterfall;
|
||||
FSplitAvailH := 0; // валидно только в режиме «спектр+водопад» (ниже)
|
||||
RULER_H := MulDiv(18, Screen.PixelsPerInch, 96);
|
||||
RW := FParent.ClientWidth;
|
||||
RH := FParent.ClientHeight;
|
||||
// Регион пана в стеке: [FStackTop .. RH). 0/0 = весь родитель (один пан).
|
||||
if FStackHeight > 0 then
|
||||
RH := FStackTop + FStackHeight
|
||||
else
|
||||
RH := FParent.ClientHeight;
|
||||
// Шапка пана — первой строкой региона (видна при панах >= 2).
|
||||
HEADER_H := MulDiv(20, Screen.PixelsPerInch, 96);
|
||||
if FHeaderPanel <> nil then
|
||||
begin
|
||||
FHeaderPanel.Visible := FHeaderVisible;
|
||||
if FHeaderVisible then
|
||||
begin
|
||||
FHeaderPanel.SetBounds(0, FStackTop, RW, HEADER_H);
|
||||
PositionHeaderChildren(RW, HEADER_H);
|
||||
end;
|
||||
end;
|
||||
// Резервируем низ под панель пана/зума (видна, если виден спектр или водопад).
|
||||
PANZOOM_H := MulDiv(22, Screen.PixelsPerInch, 96);
|
||||
ShowPZ := AShowSpectrum or AShowWaterfall;
|
||||
ShowPZ := (AShowSpectrum or AShowWaterfall) and FShowZoomRow;
|
||||
PositionPanZoomBar(RH - PANZOOM_H, RW, PANZOOM_H, ShowPZ);
|
||||
if ShowPZ then RH := RH - PANZOOM_H;
|
||||
TopOff := ATopOffset;
|
||||
TopOff := ATopOffset + FStackTop;
|
||||
if FHeaderVisible then Inc(TopOff, HEADER_H);
|
||||
|
||||
// ---- Оба скрыты ----
|
||||
if (not AShowSpectrum) and (not AShowWaterfall) then
|
||||
@@ -417,6 +558,9 @@ begin
|
||||
|
||||
AvailH := RH - TopOff - RULER_H - SPLITTER_H;
|
||||
if AvailH < (MIN_SH + MIN_WH) then Exit;
|
||||
// Запоминаем геометрию для drag-математики сплиттера (регион пана в стеке).
|
||||
FSplitTopOff := TopOff;
|
||||
FSplitAvailH := AvailH;
|
||||
|
||||
// Ограничиваем соотношение, чтобы каждая зона имела минимальный размер
|
||||
if FSplitterRatio < MIN_SH / AvailH then
|
||||
@@ -494,15 +638,14 @@ const
|
||||
begin
|
||||
RULER_H := MulDiv(18, Screen.PixelsPerInch, 96);
|
||||
if not FSplitterDrag then Exit;
|
||||
// Геометрия региона ЭТОГО пана из последнего Layout (стек панов!),
|
||||
// а не от всего родителя.
|
||||
TopOff := FSplitTopOff;
|
||||
AvailH := FSplitAvailH;
|
||||
if AvailH <= 0 then Exit;
|
||||
P := FParent.ScreenToClient(FSplitter.ClientToScreen(Point(X, Y)));
|
||||
DeltaY := P.Y - FSplitterDragY0;
|
||||
|
||||
TopOff := 0;
|
||||
// Низ зарезервирован под панель пана/зума (она видна, пока виден водопад).
|
||||
AvailH := FParent.ClientHeight - TopOff - RULER_H - SPLITTER_H
|
||||
- MulDiv(22, Screen.PixelsPerInch, 96);
|
||||
if AvailH <= 0 then Exit;
|
||||
|
||||
NewSH := FSplitterSH0 + DeltaY;
|
||||
if NewSH < MIN_SH then NewSH := MIN_SH;
|
||||
if NewSH > AvailH - MIN_WH then NewSH := AvailH - MIN_WH;
|
||||
@@ -519,14 +662,11 @@ var
|
||||
P: TPoint;
|
||||
DeltaY: Integer;
|
||||
NewSH: Integer;
|
||||
TopOff, AvailH: Integer;
|
||||
RULER_H: Integer;
|
||||
AvailH: Integer;
|
||||
const
|
||||
SPLITTER_H = 5;
|
||||
MIN_SH = 60;
|
||||
MIN_WH = 40;
|
||||
MIN_SH = 60;
|
||||
MIN_WH = 40;
|
||||
begin
|
||||
RULER_H := MulDiv(18, Screen.PixelsPerInch, 96);
|
||||
if not FSplitterDrag then Exit;
|
||||
FSplitterDrag := False;
|
||||
{$IFDEF WINDOWS}
|
||||
@@ -537,10 +677,8 @@ begin
|
||||
P := FParent.ScreenToClient(FSplitter.ClientToScreen(Point(X, Y)));
|
||||
DeltaY := P.Y - FSplitterDragY0;
|
||||
|
||||
TopOff := 0;
|
||||
// Низ зарезервирован под панель пана/зума (она видна, пока виден водопад).
|
||||
AvailH := FParent.ClientHeight - TopOff - RULER_H - SPLITTER_H
|
||||
- MulDiv(22, Screen.PixelsPerInch, 96);
|
||||
// Высота спектр+водопад региона ЭТОГО пана из последнего Layout (стек панов!).
|
||||
AvailH := FSplitAvailH;
|
||||
if AvailH <= 0 then Exit;
|
||||
|
||||
NewSH := FSplitterSH0 + DeltaY;
|
||||
@@ -595,6 +733,14 @@ begin
|
||||
if Assigned(FActiveFlag) then Result := FActiveFlag.SliceId else Result := 0;
|
||||
end;
|
||||
|
||||
function TPanafallPanel.FirstSliceId: Integer;
|
||||
var i: Integer;
|
||||
begin
|
||||
Result := 0;
|
||||
for i := 0 to High(FSliceFlags) do
|
||||
if FSliceFlags[i] <> nil then Exit(FSliceFlags[i].SliceId);
|
||||
end;
|
||||
|
||||
function TPanafallPanel.FindSliceFlag(SliceId: Integer): TVfoOverlay;
|
||||
var i: Integer;
|
||||
begin
|
||||
@@ -647,7 +793,7 @@ begin
|
||||
O.Free;
|
||||
if Assigned(FOnSliceClosed) then FOnSliceClosed(SliceId);
|
||||
UpdateMainFlagVisibility; // закрыт последний слайс при открытой панели → скрыть флаг A
|
||||
if Assigned(FOnFlagsInvalidate) then FOnFlagsInvalidate(nil);
|
||||
if Assigned(FOnFlagsInvalidate) then FOnFlagsInvalidate(Self); // Self = панель (роутинг по пану)
|
||||
end;
|
||||
|
||||
procedure TPanafallPanel.RequestSliceClose(SliceId: Integer);
|
||||
@@ -675,7 +821,7 @@ var
|
||||
begin
|
||||
Result := 0;
|
||||
if (FPbSpectrum = nil) or (FPbSpectrum.Width <= 0) or (FController = nil) then Exit;
|
||||
FController.GetViewWindow(VC, VS);
|
||||
ViewWindow(VC, VS);
|
||||
if VS <= 0 then Exit;
|
||||
for i := 0 to High(FSliceFlags) do
|
||||
begin
|
||||
@@ -738,7 +884,7 @@ var
|
||||
begin
|
||||
if (FPbSpectrum = nil) or (FController = nil) then Exit;
|
||||
W := FPbSpectrum.Width;
|
||||
FController.GetViewWindow(VC, VS);
|
||||
ViewWindow(VC, VS);
|
||||
if (W <= 0) or (VS <= 0) then Exit;
|
||||
|
||||
// 1. Собрать видимые флаги + их идеальный Left рядом со своей полосой.
|
||||
@@ -809,7 +955,7 @@ begin
|
||||
Moved := True;
|
||||
end;
|
||||
end;
|
||||
if Moved and Assigned(FOnFlagsInvalidate) then FOnFlagsInvalidate(nil);
|
||||
if Moved and Assigned(FOnFlagsInvalidate) then FOnFlagsInvalidate(Self);
|
||||
end;
|
||||
|
||||
// Флаг главного приёмника (слайс A) виден, когда СКРЫТА левая панель хозяина
|
||||
|
||||
+99
-8
@@ -89,7 +89,8 @@ SmartSDR НЕ умеет — наш pop-out будет улучшением по
|
||||
один слайс в его центре (поведение Flex).
|
||||
3. ADC-источник — свойство **панадаптера**, не слайса (бейдж `A1`/`A2` в шапке).
|
||||
В отличие от Flex показываем именно АЦП, а не антенну: на HPSDR-платах
|
||||
маппинг «вход→АЦП» жёсткий, антенная абстракция не нужна.
|
||||
маппинг «вход→АЦП» жёсткий. Антенны ADC0 (Alex ANT1/2/3) — отдельная
|
||||
ГЛОБАЛЬНАЯ ручка, общая для всех панов этого АЦП (детали в 3.4).
|
||||
4. Буквы слайсов — глобальный пул A..G через все паны (как во Flex A-H):
|
||||
буква = личность слайса, пан = место жительства.
|
||||
5. Возможности бэкенда описывает запись capabilities (см. таблицу) — UI по ней
|
||||
@@ -342,14 +343,99 @@ ChangeSampleRate).
|
||||
независим от DDC, TX-частота = частота слайса. (Полный мультислайс-TX —
|
||||
отдельная старая задача, тут ничего нового не ломаем.)
|
||||
|
||||
### 3.4 — ADC-селектор панадаптера
|
||||
**СТАТУС 3.3 (2026-07-11): БАЗОВЫЙ UI РЕАЛИЗОВАН, собирается (GUI+демон),
|
||||
бэкенд-тракт проверен END-TO-END против эмулятора hpsdrsim -hermes -P2
|
||||
(scratchpad pantest2: DDC1 включается, IQ течёт, спектр/водопад пана рисуются,
|
||||
слайс живой S-метр −95дБм, ретюн/снятие чистые, seq-ошибок нет). НА ЭКРАНЕ
|
||||
НЕ ПРОВЕРЕНО.** Что сделано:
|
||||
|
||||
- `TPanafallPanel`: `PanId`, регион в стеке (`StackTop/StackHeight`, Layout
|
||||
считает внутри региона), шапка (TPanel+TLabel+«×»→OnPanClose; видна при
|
||||
панах ≥2), кнопка «⊞» в ряду пан/зума (`AddPanEnabled`, только пан 0),
|
||||
`ShowZoomRow=False` у панов N (их зум позже), pan-aware `ViewWindow`
|
||||
(SliceAtPixel/LayoutFlags от окна СВОЕГО пана), `FOnFlagsInvalidate(Self)`.
|
||||
- MainForm: `FPans[0..MAX_PANS-1]` (пан 0 = FPan), `LayoutPanStack` (доли
|
||||
`FPanShare` + межпанные сплиттеры, пересчёт по MouseUp), «⊞» →
|
||||
`OnAddPanClick` (AddPanDDC на частоте активного VFO, rate 384к, автослайс
|
||||
в центре, копия display-параметров view, ApplyDarkTheme), «×» шапки →
|
||||
отложенное `ClosePanadapter` в тике (нельзя free из своей кнопки).
|
||||
- Пиксели: `engine.OnPanSpectrum/OnPanWaterfall` → MainForm напрямую
|
||||
(display-поток, `FPanCbLock` закрывает гонку с созданием/закрытием пана);
|
||||
рендер в SpectrumTimerTick по dirty-флагам, `SyncPanViews` держит
|
||||
центр/span вьюх и текст шапок.
|
||||
- Мышь панов N (НОВЫЕ простые обработчики, VFO/CTUN/маяк/оверлеи главного
|
||||
сюда не переносились — решение по B2 выполнено): Ctrl+ЛКМ = слайс в этом
|
||||
пане, ЛКМ по маркеру = drag слайса (кламп полосой пана), ЛКМ по фону =
|
||||
drag ЦЕНТРА пана (live-ретюн DDC), колесо над флагом любого пана = тюн
|
||||
того слайса; флаги/CurSliceId/инвалидции обобщены на все панели
|
||||
(PanOfOverlay/PushFlagStateAllPans/LayoutFlagsAllPans).
|
||||
- Паны живут только в Run-сессии: на STOP UI убирает их вместе с контроллером.
|
||||
- Отложено: пан-зум панов N, per-pan детектор/усреднение, персист (после 3.3),
|
||||
wideband при маленьком регионе пана 0 может вылезти за регион (редкий кейс),
|
||||
колесо над фоном пана N крутит главный VFO (как везде вне слайсов).
|
||||
|
||||
**ФИКСЫ ПО ПЕРВОМУ ЭКРАННОМУ ТЕСТУ (2026-07-11, фидбек юзера):**
|
||||
|
||||
1. **Сплиттер спектр/водопад был сломан при ≥2 панах**: drag-математика
|
||||
(`SplitterMouseMove/Up`) считала доступную высоту от ВСЕГО родителя
|
||||
(`FParent.ClientHeight`, `TopOff=0`), игнорируя регион пана в стеке,
|
||||
шапку и wideband. Симптомы: у пана 0 ratio занижался (спектр сжимался
|
||||
при тяге вниз), у пана 1 сплиттер прыгал в регион пана 0. Фикс: Layout
|
||||
запоминает `FSplitTopOff/FSplitAvailH` (геометрия «спектр+водопад»
|
||||
СВОЕГО региона), drag использует только их.
|
||||
2. **Межпанный сплиттер** не давал обратной связи до отпускания — теперь
|
||||
живой «призрак» (сам сплиттер едет за мышью, клампится 15%-минимумом
|
||||
обоих панов, подсветка при захвате), доли пересчитываются по MouseUp
|
||||
от финального положения призрака.
|
||||
3. **Клик по фону пана N не перестраивал частоту** (только drag центра).
|
||||
Теперь пресс без движения (<4px) = клик-тюн `PanClickTune`: слайс пана
|
||||
(активный, если он этого пана, иначе первый; нет — создаётся) на частоту
|
||||
клика со снапом 100 Гц и клампом полосой пана. Drag центра включается
|
||||
после гистерезиса 4px, курсор `crSizeWE` — только с этого момента
|
||||
(раньше менялся на голый пресс).
|
||||
4. **Диспетчер флагов/маркеров на панах N** получал координаты ВОДОПАДА
|
||||
(прямоугольники флагов — в системе спектра): клики в верхней части
|
||||
водопада могли молча съедаться невидимым флагом. Теперь флаги/hover —
|
||||
только для событий спектра, курсор на водопаде не трогаем.
|
||||
5. **(2-й раунд фидбека)** У панов N сплиттер спектр/водопад «залипал
|
||||
призраком» (спектр не пересчитывался, при следующем drag прыжок к
|
||||
старому ratio): в `OnAddPanClick` не был подвешен `OnSplitterMoved`
|
||||
(у пана 0 = `RightPanelResize` → полный re-layout). Подвешен тот же
|
||||
обработчик. Пан 0 и межпанные сплиттеры юзер подтвердил рабочими.
|
||||
6. **Курсор-«рука» на всей площади панов N (разгадан, 3-й раунд)**: юзер
|
||||
заметил, что рука = курсор кнопки «⊞». Механизм: GL-канвы — QOpenGLWidget
|
||||
с `WA_NativeWindow` = отдельная Wayland-поверхность; `crDefault` в LCL
|
||||
означает «курсор не задан», и поверхность НАВСЕГДА наследует курсор,
|
||||
бывший под мышью в момент создания виджета. Пан N создаётся кликом по
|
||||
«⊞» (`crHandPoint` у TFlatButton) → рука прилипает; пан 0 создаётся на
|
||||
старте (мышь нигде) → норм. Фикс: `Build` ставит спектру/водопаду ЯВНЫЙ
|
||||
`crArrow` (реальный push), обработчики панов N восстанавливают `crArrow`,
|
||||
не `crDefault`. ⚠Урок на 3.5 (pop-out) и вообще: любой динамически
|
||||
создаваемый нативный GL-виджет должен получать явный курсор при создании.
|
||||
|
||||
Статус: собирается (GUI+демон), НА ЭКРАНЕ НЕ ПРОВЕРЕНО (юзер проверяет).
|
||||
Открыто из фидбека: юзер видел курсор-«палец» на водопаде пана N — в коде
|
||||
панов N `crHandPoint` нигде не ставится (подозрение: артефакт сломанной
|
||||
раскладки, перекрытие чужим контролом); перепроверить после фиксов.
|
||||
|
||||
### 3.4 — ADC-селектор панадаптера (+ антенны Alex, уточнено 2026-07-11)
|
||||
|
||||
Источник сигнала пана — ДВЕ независимые ручки, не одна:
|
||||
|
||||
1. **АЦП (per-pan)**: `TPanafall.ADCSource: 0/1`; в шапке пана селектор/бейдж
|
||||
`A1`/`A2`. Показывается только при `NumADCs=2` (детект платы уже есть —
|
||||
`NumADCs` в ConfigureDDCs-пути). Переключение = `ConfigureDDCs` с новым
|
||||
ADCSource для DDC этого пана. Пан 0 тоже можно перекинуть на ADC1 — это
|
||||
закрывает сценарий старой ветки `feature/rx2-support` как частный случай.
|
||||
2. **Антенна первого АЦП (глобальная, НЕ per-pan)**: у плат с Alex на ADC0
|
||||
уже есть аппаратный селектор RX-антенны **ANT1/ANT2/ANT3** (+ EXT-входы) —
|
||||
реализован per-band в Antenna/Alex-табе (`Alex.RxAnt[band]`, XVTR-override).
|
||||
Селектор один на весь ADC0 ⇒ антенну нельзя сделать свойством пана: смена
|
||||
антенны меняет её для ВСЕХ панов, сидящих на ADC0. В UI пана максимум —
|
||||
бейдж текущей антенны (например `A1·ANT2`) и/или шорткат к глобальному
|
||||
переключателю с явным пониманием, что это общая ручка. ADC1 (RX2-вход) —
|
||||
отдельный разъём без Alex-коммутации.
|
||||
|
||||
- `TPanafall.ADCSource: 0/1`; в шапке пана селектор/бейдж `A1`/`A2`.
|
||||
Показывается только при `NumADCs=2` (детект платы уже есть — `NumADCs`
|
||||
в ConfigureDDCs-пути).
|
||||
- Переключение = `ConfigureDDCs` с новым ADCSource для DDC этого пана.
|
||||
- Пан 0 тоже можно перекинуть на ADC1 — это закрывает сценарий старой ветки
|
||||
`feature/rx2-support` (вторая антенна) бесплатно, как частный случай.
|
||||
- Diversity (sync-DDC, оба АЦП когерентно в один канал) — НЕ здесь; протокол
|
||||
умеет, дизайну панов не мешает, отдельная будущая фича.
|
||||
|
||||
@@ -437,6 +523,11 @@ ChangeSampleRate).
|
||||
осознанно: только явное создание пана (решение «сначала статика» остаётся).
|
||||
- Настройки водопада (палитра/AGC/avg) per-pan или глобально — стартуем с
|
||||
наследования глобальных, разделим по запросу.
|
||||
- **Компоновка панов сеткой** (запрос юзера 2026-07-11): не только вертикальный
|
||||
стек — например, пан 0 сверху во всю ширину, паны B и C ниже бок о бок
|
||||
(пол-ширины каждый), и т.п. Идея на после 3.3/3.5: обобщить `LayoutPanStack`
|
||||
до грид-раскладки (регион пана = прямоугольник, а не полоса; `StackTop/Height`
|
||||
→ `Rect`), сплиттеры по обеим осям. Не блокер текущего этапа.
|
||||
|
||||
## Отвергнутые альтернативы (для истории)
|
||||
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
<UseVersionInfo Value="True"/>
|
||||
<AutoIncrementBuild Value="True"/>
|
||||
<MinorVersionNr Value="9"/>
|
||||
<BuildNr Value="256"/>
|
||||
<BuildNr Value="257"/>
|
||||
</VersionInfo>
|
||||
<MacroValues Count="74">
|
||||
<MacroValues Count="76">
|
||||
<Macro1 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro2 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro3 Name="LCLWidgetType" Value="qt6"/>
|
||||
@@ -94,6 +94,8 @@
|
||||
<Macro72 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro73 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro74 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro75 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro76 Name="LCLWidgetType" Value="qt6"/>
|
||||
</MacroValues>
|
||||
<BuildModes>
|
||||
<Item Name="Debug" Default="True"/>
|
||||
|
||||
Reference in New Issue
Block a user