mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +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
|
uses
|
||||||
Classes, SysUtils, StrUtils, FreqDisplay, DeviceForm, VfoOverlay, SampleRateOverlay, BandPlanOverlay,
|
Classes, SysUtils, StrUtils, FreqDisplay, DeviceForm, VfoOverlay, SampleRateOverlay, BandPlanOverlay,
|
||||||
FlatButton, FlatSlider, FlatDropDown, AppTheme, Forms, Controls, Graphics, Dialogs,
|
FlatButton, FlatSlider, FlatDropDown, AppTheme, Forms, Controls, Graphics, Dialogs,
|
||||||
StdCtrls, ExtCtrls, Buttons, Menus, Math, Types,
|
StdCtrls, ExtCtrls, Buttons, Menus, Math, Types, SyncObjs,
|
||||||
OpenGLContextEx,
|
OpenGLContextEx,
|
||||||
LCLIntf, LCLType, GraphType,
|
LCLIntf, LCLType, GraphType,
|
||||||
HPSDRProtocol, HPSDRNetwork,
|
HPSDRProtocol, HPSDRNetwork,
|
||||||
@@ -159,6 +159,31 @@ type
|
|||||||
// (исторические имена; постепенная миграция на FPan.*). Размеры областей —
|
// (исторические имена; постепенная миграция на FPan.*). Размеры областей —
|
||||||
// FPan.SpectrumWidth/SpectrumHeight/WaterfallHeight.
|
// FPan.SpectrumWidth/SpectrumHeight/WaterfallHeight.
|
||||||
FPan: TPanafallPanel;
|
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;
|
FSpecView: TSpectrumView;
|
||||||
FWidebandView: TWidebandView;
|
FWidebandView: TWidebandView;
|
||||||
FUseOpenGLSpectrum: Boolean;
|
FUseOpenGLSpectrum: Boolean;
|
||||||
@@ -533,6 +558,44 @@ type
|
|||||||
function CurSliceId: Integer; // SliceId обрабатываемого сейчас флага
|
function CurSliceId: Integer; // SliceId обрабатываемого сейчас флага
|
||||||
procedure WireOverlayEvents(O: TVfoOverlay);
|
procedure WireOverlayEvents(O: TVfoOverlay);
|
||||||
procedure AddSliceAtFreq(Hz: Double);
|
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 PbSpectrumDblClick(Sender: TObject);
|
||||||
procedure TrkDriveChange(Sender: TObject);
|
procedure TrkDriveChange(Sender: TObject);
|
||||||
procedure UpdateDriveLabel; // % (openHPSDR) / дБ (Pluto) рядом с ползунком drive
|
procedure UpdateDriveLabel; // % (openHPSDR) / дБ (Pluto) рядом с ползунком drive
|
||||||
@@ -990,6 +1053,12 @@ begin
|
|||||||
FPan.OnSliceClosed := OnPanSliceClosed;
|
FPan.OnSliceClosed := OnPanSliceClosed;
|
||||||
FPan.OnFlagsInvalidate := OnVfoOverlayInvalidate;
|
FPan.OnFlagsInvalidate := OnVfoOverlayInvalidate;
|
||||||
FPan.AttachMainFlag(FVfoOverlay); // внутри: FSpecView.VfoOverlay := флаг A
|
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.VfoA := FController.FVfoA;
|
||||||
FSpecView.VfoB := FController.FVfoB;
|
FSpecView.VfoB := FController.FVfoB;
|
||||||
FSpecView.ActiveVfo := FController.FActiveVfo;
|
FSpecView.ActiveVfo := FController.FActiveVfo;
|
||||||
@@ -1040,6 +1109,9 @@ begin
|
|||||||
FController.FDSPEngine.OnAudio := FController.OnAudioReady;
|
FController.FDSPEngine.OnAudio := FController.OnAudioReady;
|
||||||
FController.FDSPEngine.OnSpectrum := FController.OnSpectrumReady;
|
FController.FDSPEngine.OnSpectrum := FController.OnSpectrumReady;
|
||||||
FController.FDSPEngine.OnWaterfall := FController.OnWaterfallReady;
|
FController.FDSPEngine.OnWaterfall := FController.OnWaterfallReady;
|
||||||
|
// Пиксели доп. панов (этап 3.3) — прямо в UI (display-поток; см. FPanCbLock).
|
||||||
|
FController.FDSPEngine.OnPanSpectrum := OnPanSpectrumReady;
|
||||||
|
FController.FDSPEngine.OnPanWaterfall := OnPanWaterfallReady;
|
||||||
FController.FDSPEngine.OnTXIQ := FController.OnTXIQ;
|
FController.FDSPEngine.OnTXIQ := FController.OnTXIQ;
|
||||||
// Web-клиент перехватывает RX-аудио (моно-микс) у контроллера, если подключён.
|
// Web-клиент перехватывает RX-аудио (моно-микс) у контроллера, если подключён.
|
||||||
FController.OnAudioConsume := FWebAdapter.AudioConsume;
|
FController.OnAudioConsume := FWebAdapter.AudioConsume;
|
||||||
@@ -1093,7 +1165,10 @@ begin
|
|||||||
PanelSplitter := nil;
|
PanelSplitter := nil;
|
||||||
PbWaterfall := nil;
|
PbWaterfall := nil;
|
||||||
PbPanZoom := nil;
|
PbPanZoom := nil;
|
||||||
|
DestroyAllExtraPans; // доп. паны (DDC/движок/панели) — до пана 0
|
||||||
FreeAndNil(FPan);
|
FreeAndNil(FPan);
|
||||||
|
FPans[0] := nil;
|
||||||
|
FreeAndNil(FPanCbLock);
|
||||||
FreeAndNil(FWidebandView);
|
FreeAndNil(FWidebandView);
|
||||||
|
|
||||||
// Сохраняем настройки при закрытии
|
// Сохраняем настройки при закрытии
|
||||||
@@ -1216,6 +1291,8 @@ begin
|
|||||||
BtnStartStop.Caption := 'STOP';
|
BtnStartStop.Caption := 'STOP';
|
||||||
StyleButton(BtnStartStop, True);
|
StyleButton(BtnStartStop, True);
|
||||||
BtnMOX.Enabled := True;
|
BtnMOX.Enabled := True;
|
||||||
|
UpdateAddPanButton; // «⊞» доступна только в Run
|
||||||
|
ResizeSpectrumPanels;
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
begin
|
begin
|
||||||
@@ -1223,6 +1300,11 @@ begin
|
|||||||
BtnStartStop.Caption := 'START';
|
BtnStartStop.Caption := 'START';
|
||||||
StyleButton(BtnStartStop, False);
|
StyleButton(BtnStartStop, False);
|
||||||
BtnMOX.Enabled := False;
|
BtnMOX.Enabled := False;
|
||||||
|
// Доп. паны живут только в Run-сессии (их DDC уже погашены
|
||||||
|
// контроллером в StopRunning) — убираем и UI.
|
||||||
|
DestroyAllExtraPans;
|
||||||
|
UpdateAddPanButton;
|
||||||
|
ResizeSpectrumPanels;
|
||||||
end;
|
end;
|
||||||
rfADCOverload:
|
rfADCOverload:
|
||||||
if FSpecView <> nil then
|
if FSpecView <> nil then
|
||||||
@@ -2435,7 +2517,8 @@ begin
|
|||||||
WideSpecH := Max(1, WideH - RULER_H);
|
WideSpecH := Max(1, WideH - RULER_H);
|
||||||
if PbWideband <> nil then
|
if PbWideband <> nil then
|
||||||
begin
|
begin
|
||||||
PbWideband.SetBounds(0, 0, RW, WideSpecH);
|
// При стеке панов над wideband стоит шапка пана 0.
|
||||||
|
PbWideband.SetBounds(0, Pan0HeaderH, RW, WideSpecH);
|
||||||
PbWideband.Visible := FController.FShowWideband and (WideSpecH > 0);
|
PbWideband.Visible := FController.FShowWideband and (WideSpecH > 0);
|
||||||
if FController.FShowWideband and (RW > 0) and (WideSpecH > 0) then
|
if FController.FShowWideband and (RW > 0) and (WideSpecH > 0) then
|
||||||
begin
|
begin
|
||||||
@@ -2445,7 +2528,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
if PbWidebandRuler <> nil then
|
if PbWidebandRuler <> nil then
|
||||||
begin
|
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);
|
PbWidebandRuler.Visible := FController.FShowWideband and (WideH > 0);
|
||||||
if FController.FShowWideband and (RW > 0) and (WideH > 0) then
|
if FController.FShowWideband and (RW > 0) and (WideH > 0) then
|
||||||
begin
|
begin
|
||||||
@@ -2456,11 +2539,61 @@ begin
|
|||||||
// S-метр позиционируется отдельно в ResizeSMeter
|
// S-метр позиционируется отдельно в ResizeSMeter
|
||||||
ResizeSMeter;
|
ResizeSMeter;
|
||||||
|
|
||||||
// Стек спектр/линейка/сплиттер/водопад/пан-зум раскладывает панадаптер.
|
// Стек панов: регионы по долям, каждый пан раскладывает свой стек
|
||||||
FPan.Layout(WideH, FController.FShowSpectrum, FController.FShowWaterfall);
|
// спектр/линейка/сплиттер/водопад/пан-зум внутри своего региона.
|
||||||
|
LayoutPanStack(WideH);
|
||||||
PositionSampleRateOverlay;
|
PositionSampleRateOverlay;
|
||||||
end;
|
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
|
// Dark Theme
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
@@ -2536,6 +2669,23 @@ begin
|
|||||||
if BtnZoomIn <> nil then StyleButton(BtnZoomIn, False);
|
if BtnZoomIn <> nil then StyleButton(BtnZoomIn, False);
|
||||||
if BtnZoomDef <> nil then StyleButton(BtnZoomDef, False);
|
if BtnZoomDef <> nil then StyleButton(BtnZoomDef, False);
|
||||||
if BtnZoomOut <> nil then StyleButton(BtnZoomOut, 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
|
if FPanZoomBar <> nil then
|
||||||
begin
|
begin
|
||||||
FPanZoomBar.SetTheme(T);
|
FPanZoomBar.SetTheme(T);
|
||||||
@@ -2938,6 +3088,13 @@ var
|
|||||||
ElapsedTick: QWord;
|
ElapsedTick: QWord;
|
||||||
I: Integer;
|
I: Integer;
|
||||||
begin
|
begin
|
||||||
|
// «×» шапки пана: закрытие отложено сюда (нельзя free панель из её кнопки).
|
||||||
|
if FPendingPanClose <> 0 then
|
||||||
|
begin
|
||||||
|
I := FPendingPanClose;
|
||||||
|
FPendingPanClose := 0;
|
||||||
|
ClosePanadapter(I);
|
||||||
|
end;
|
||||||
// Синхронизируем размеры если размер панели изменился (после ресайза).
|
// Синхронизируем размеры если размер панели изменился (после ресайза).
|
||||||
// Важно: проверяем только видимые панели — когда спектр скрыт,
|
// Важно: проверяем только видимые панели — когда спектр скрыт,
|
||||||
// PbSpectrum.Height может быть ненулевым при FPan.SpectrumHeight=0,
|
// PbSpectrum.Height может быть ненулевым при FPan.SpectrumHeight=0,
|
||||||
@@ -3009,6 +3166,27 @@ begin
|
|||||||
FSpecView.WDSPReady := FController.FWDSPReady;
|
FSpecView.WDSPReady := FController.FWDSPReady;
|
||||||
// Флаги: метр/частота главного, позиции, троттленный S-метр слайсов.
|
// Флаги: метр/частота главного, позиции, троттленный S-метр слайсов.
|
||||||
FPan.TickFlags;
|
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 даёт новые строки чаще.
|
// Это лучше совпадает с Thetis: маленький FFT даёт новые строки чаще.
|
||||||
if FSpectrumDirty then
|
if FSpectrumDirty then
|
||||||
@@ -3266,6 +3444,9 @@ begin
|
|||||||
FController.FDSPEngine.OnAudio := FController.OnAudioReady;
|
FController.FDSPEngine.OnAudio := FController.OnAudioReady;
|
||||||
FController.FDSPEngine.OnSpectrum := FController.OnSpectrumReady;
|
FController.FDSPEngine.OnSpectrum := FController.OnSpectrumReady;
|
||||||
FController.FDSPEngine.OnWaterfall := FController.OnWaterfallReady;
|
FController.FDSPEngine.OnWaterfall := FController.OnWaterfallReady;
|
||||||
|
// Пиксели доп. панов (этап 3.3) — прямо в UI (display-поток; см. FPanCbLock).
|
||||||
|
FController.FDSPEngine.OnPanSpectrum := OnPanSpectrumReady;
|
||||||
|
FController.FDSPEngine.OnPanWaterfall := OnPanWaterfallReady;
|
||||||
FController.FDSPEngine.OnTXIQ := FController.OnTXIQ;
|
FController.FDSPEngine.OnTXIQ := FController.OnTXIQ;
|
||||||
// Иначе sound-card mic путь теряется после смены sample rate (колбэк
|
// Иначе sound-card mic путь теряется после смены sample rate (колбэк
|
||||||
// навешивается в FormCreate, но новый движок про него не знал).
|
// навешивается в FormCreate, но новый движок про него не знал).
|
||||||
@@ -4823,8 +5004,8 @@ begin
|
|||||||
if CurSliceId <> 0 then
|
if CurSliceId <> 0 then
|
||||||
begin
|
begin
|
||||||
FController.SetSliceModeBW(CurSliceId, Mode, FilterBW);
|
FController.SetSliceModeBW(CurSliceId, Mode, FilterBW);
|
||||||
FPan.PushSliceFlagState(CurSliceId);
|
PushFlagStateAllPans(CurSliceId);
|
||||||
FPan.LayoutFlags;
|
LayoutFlagsAllPans;
|
||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
FController.FMode := Mode;
|
FController.FMode := Mode;
|
||||||
@@ -4944,7 +5125,7 @@ begin
|
|||||||
if CurSliceId <> 0 then
|
if CurSliceId <> 0 then
|
||||||
begin
|
begin
|
||||||
FController.SetSliceDevice(CurSliceId, DevIndex, DevName);
|
FController.SetSliceDevice(CurSliceId, DevIndex, DevName);
|
||||||
FPan.PushSliceFlagState(CurSliceId); // обновить подсветку текущего устройства слайса
|
PushFlagStateAllPans(CurSliceId); // обновить подсветку текущего устройства слайса
|
||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
ApplyAudioDevice(DevIndex, DevName);
|
ApplyAudioDevice(DevIndex, DevName);
|
||||||
@@ -4952,7 +5133,15 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TMainForm.OnVfoOverlayInvalidate(Sender: TObject);
|
procedure TMainForm.OnVfoOverlayInvalidate(Sender: TObject);
|
||||||
|
var P: TPanafallPanel;
|
||||||
begin
|
begin
|
||||||
|
// Флаг/панель доп. пана → грязним и репейнтим ЕГО канву, пан 0 не трогаем.
|
||||||
|
P := PanOfOverlay(Sender);
|
||||||
|
if (P <> nil) and (P <> FPan) then
|
||||||
|
begin
|
||||||
|
MarkPanDirty(P);
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
FSpectrumDirty := True;
|
FSpectrumDirty := True;
|
||||||
if Assigned(FSpecView) then
|
if Assigned(FSpecView) then
|
||||||
begin
|
begin
|
||||||
@@ -4981,8 +5170,14 @@ end;
|
|||||||
|
|
||||||
// SliceId флага, чьё событие сейчас обрабатывается (0 = главный приёмник / слайс A).
|
// SliceId флага, чьё событие сейчас обрабатывается (0 = главный приёмник / слайс A).
|
||||||
function TMainForm.CurSliceId: Integer;
|
function TMainForm.CurSliceId: Integer;
|
||||||
|
// Диспетчер флагов синхронный — ненулевой CurSliceId сейчас максимум у одной
|
||||||
|
// панели (той, чей флаг обрабатывает событие).
|
||||||
|
var i: Integer;
|
||||||
begin
|
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;
|
end;
|
||||||
|
|
||||||
// Единая проводка событий флага (главного и слайсов) на общие обработчики.
|
// Единая проводка событий флага (главного и слайсов) на общие обработчики.
|
||||||
@@ -5018,6 +5213,568 @@ begin
|
|||||||
FActiveSliceId := Id;
|
FActiveSliceId := Id;
|
||||||
end;
|
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);
|
procedure TMainForm.OnVfoOverlaySliceSelect(Sender: TObject);
|
||||||
begin
|
begin
|
||||||
if Sender is TVfoOverlay then
|
if Sender is TVfoOverlay then
|
||||||
@@ -5025,12 +5782,14 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TMainForm.OnVfoOverlayClose(Sender: TObject);
|
procedure TMainForm.OnVfoOverlayClose(Sender: TObject);
|
||||||
|
var P: TPanafallPanel;
|
||||||
begin
|
begin
|
||||||
if not (Sender is TVfoOverlay) then Exit;
|
if not (Sender is TVfoOverlay) then Exit;
|
||||||
if TVfoOverlay(Sender).IsMain then Exit;
|
if TVfoOverlay(Sender).IsMain then Exit;
|
||||||
// Нельзя освобождать флаг внутри его же обработчика мыши — панель пометит
|
// Нельзя освобождать флаг внутри его же обработчика мыши — панель пометит
|
||||||
// и снимет его после возврата из диспетчера (ProcessPendingSliceClose).
|
// и снимет его после возврата из диспетчера (ProcessPendingSliceClose).
|
||||||
FPan.RequestSliceClose(TVfoOverlay(Sender).SliceId);
|
P := PanOfOverlay(Sender);
|
||||||
|
if P <> nil then P.RequestSliceClose(TVfoOverlay(Sender).SliceId);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
// FPan закрыл слайс («×») — сбрасываем активный, если это был он.
|
// FPan закрыл слайс («×») — сбрасываем активный, если это был он.
|
||||||
@@ -5598,17 +6357,25 @@ var
|
|||||||
Delta: Int64;
|
Delta: Int64;
|
||||||
Base: Int64;
|
Base: Int64;
|
||||||
NewFreq: Int64;
|
NewFreq: Int64;
|
||||||
SliceId: Integer;
|
SliceId, i: Integer;
|
||||||
SLo, SHi, SNew: Double;
|
SLo, SHi, SNew: Double;
|
||||||
Sl: TCtrlSlice;
|
Sl: TCtrlSlice;
|
||||||
|
SlicePan: TPanafallPanel;
|
||||||
begin
|
begin
|
||||||
// Над дисплеем VFO (A или B) колесо обрабатывает сам TFreqDisplay поразрядно
|
// Над дисплеем VFO (A или B) колесо обрабатывает сам TFreqDisplay поразрядно
|
||||||
// (включая неактивный VFO). Не дублируем здесь, иначе менялись бы обе частоты:
|
// (включая неактивный VFO). Не дублируем здесь, иначе менялись бы обе частоты:
|
||||||
// виджет крутил бы B, а этот хендлер — активный A.
|
// виджет крутил бы B, а этот хендлер — активный A.
|
||||||
if OverFreqDisp(FreqDispA) or OverFreqDisp(FreqDispB) then Exit;
|
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
|
if (SliceId <> 0) and FController.GetSlice(SliceId, Sl) then
|
||||||
begin
|
begin
|
||||||
if (ssCtrl in Shift) and (ssShift in Shift) then Step := 1000000
|
if (ssCtrl in Shift) and (ssShift in Shift) then Step := 1000000
|
||||||
@@ -5622,14 +6389,13 @@ begin
|
|||||||
if Delta > 0 then SNew := Base + Step
|
if Delta > 0 then SNew := Base + Step
|
||||||
else if Round(Sl.TargetHz) = Base then SNew := Base - Step
|
else if Round(Sl.TargetHz) = Base then SNew := Base - Step
|
||||||
else SNew := Base;
|
else SNew := Base;
|
||||||
FController.CapturedSpan(SLo, SHi);
|
FController.CapturedSpan(SLo, SHi, Sl.PanId); // полоса ЕГО пана
|
||||||
if SNew < SLo then SNew := SLo;
|
if SNew < SLo then SNew := SLo;
|
||||||
if SNew > SHi then SNew := SHi;
|
if SNew > SHi then SNew := SHi;
|
||||||
FController.SetSliceTarget(SliceId, SNew);
|
FController.SetSliceTarget(SliceId, SNew);
|
||||||
FPan.PushSliceFlagState(SliceId);
|
SlicePan.PushSliceFlagState(SliceId);
|
||||||
FPan.LayoutFlags;
|
SlicePan.LayoutFlags;
|
||||||
FSpectrumDirty := True;
|
MarkPanDirty(SlicePan);
|
||||||
PbSpectrum.Invalidate;
|
|
||||||
FActiveSliceId := SliceId;
|
FActiveSliceId := SliceId;
|
||||||
Handled := True;
|
Handled := True;
|
||||||
Exit;
|
Exit;
|
||||||
|
|||||||
+172
-26
@@ -31,7 +31,7 @@ unit PanafallPanel;
|
|||||||
interface
|
interface
|
||||||
|
|
||||||
uses
|
uses
|
||||||
Classes, SysUtils, Math, Controls, ExtCtrls, Graphics, Forms,
|
Classes, SysUtils, Math, Controls, ExtCtrls, StdCtrls, Graphics, Forms,
|
||||||
LCLIntf, LCLType,
|
LCLIntf, LCLType,
|
||||||
OpenGLContextEx,
|
OpenGLContextEx,
|
||||||
AppTheme, WDSPEngine, RadioController, VfoOverlay,
|
AppTheme, WDSPEngine, RadioController, VfoOverlay,
|
||||||
@@ -70,6 +70,10 @@ type
|
|||||||
FSplitterDrag: Boolean;
|
FSplitterDrag: Boolean;
|
||||||
FSplitterDragY0: Integer;
|
FSplitterDragY0: Integer;
|
||||||
FSplitterSH0: Integer;
|
FSplitterSH0: Integer;
|
||||||
|
// Геометрия последнего Layout для drag-математики: верх области спектра и
|
||||||
|
// высота спектр+водопад ВНУТРИ региона пана (не всего родителя!).
|
||||||
|
FSplitTopOff: Integer;
|
||||||
|
FSplitAvailH: Integer; // 0 = сплиттер сейчас нелегитимен
|
||||||
|
|
||||||
FOnSplitterMoved: TNotifyEvent;
|
FOnSplitterMoved: TNotifyEvent;
|
||||||
|
|
||||||
@@ -85,6 +89,27 @@ type
|
|||||||
FOnSliceClosed: TSliceClosedEvent;
|
FOnSliceClosed: TSliceClosedEvent;
|
||||||
FOnFlagsInvalidate: TNotifyEvent;
|
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;
|
procedure SplitterMouseDown(Sender: TObject; Button: TMouseButton;
|
||||||
Shift: TShiftState; X, Y: Integer);
|
Shift: TShiftState; X, Y: Integer);
|
||||||
procedure SplitterMouseMove(Sender: TObject; Shift: TShiftState;
|
procedure SplitterMouseMove(Sender: TObject; Shift: TShiftState;
|
||||||
@@ -138,6 +163,25 @@ type
|
|||||||
property SpectrumHeight: Integer read FSpectrumHeight;
|
property SpectrumHeight: Integer read FSpectrumHeight;
|
||||||
property WaterfallHeight: Integer read FWaterfallHeight;
|
property WaterfallHeight: Integer read FWaterfallHeight;
|
||||||
property SplitterRatio: Double read FSplitterRatio write FSplitterRatio;
|
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 уже обновлён) — хозяин делает
|
// Пользователь отпустил сплиттер (ratio уже обновлён) — хозяин делает
|
||||||
// полный пересчёт (у него wideband/S-метр/оверлеи).
|
// полный пересчёт (у него wideband/S-метр/оверлеи).
|
||||||
property OnSplitterMoved: TNotifyEvent read FOnSplitterMoved write FOnSplitterMoved;
|
property OnSplitterMoved: TNotifyEvent read FOnSplitterMoved write FOnSplitterMoved;
|
||||||
@@ -148,6 +192,8 @@ type
|
|||||||
procedure AttachMainFlag(O: TVfoOverlay);
|
procedure AttachMainFlag(O: TVfoOverlay);
|
||||||
// SliceId флага, чьё событие сейчас обрабатывается (0 = главный).
|
// SliceId флага, чьё событие сейчас обрабатывается (0 = главный).
|
||||||
function CurSliceId: Integer;
|
function CurSliceId: Integer;
|
||||||
|
// Первый слайс панели (0 = слайсов B+ нет) — цель клика-тюна пана N.
|
||||||
|
function FirstSliceId: Integer;
|
||||||
function FindSliceFlag(SliceId: Integer): TVfoOverlay;
|
function FindSliceFlag(SliceId: Integer): TVfoOverlay;
|
||||||
procedure CreateSliceFlag(SliceId: Integer);
|
procedure CreateSliceFlag(SliceId: Integer);
|
||||||
procedure DestroySliceFlag(SliceId: Integer);
|
procedure DestroySliceFlag(SliceId: Integer);
|
||||||
@@ -199,6 +245,8 @@ begin
|
|||||||
FZoomBar := TPanZoomBar.Create;
|
FZoomBar := TPanZoomBar.Create;
|
||||||
FSplitterRatio := 0.40;
|
FSplitterRatio := 0.40;
|
||||||
FSplitterDrag := False;
|
FSplitterDrag := False;
|
||||||
|
FShowZoomRow := True; // паны N>0 выключают (их зум — этап 3.3+)
|
||||||
|
FHeaderVisible := False; // шапка появляется при панах >= 2
|
||||||
end;
|
end;
|
||||||
|
|
||||||
destructor TPanafallPanel.Destroy;
|
destructor TPanafallPanel.Destroy;
|
||||||
@@ -239,6 +287,11 @@ begin
|
|||||||
TPaintBox(FPbSpectrum).OnMouseLeave := SpectrumMouseLeave;
|
TPaintBox(FPbSpectrum).OnMouseLeave := SpectrumMouseLeave;
|
||||||
end;
|
end;
|
||||||
FPbSpectrum.Parent := AParent;
|
FPbSpectrum.Parent := AParent;
|
||||||
|
// ЯВНЫЙ crArrow (не crDefault!): GL-виджет с WA_NativeWindow — отдельная
|
||||||
|
// Wayland-поверхность; без явного курсора она навсегда наследует курсор,
|
||||||
|
// бывший под мышью в момент создания (пан создаётся кнопкой «⊞» с
|
||||||
|
// crHandPoint → «рука» прилипала ко всему пану).
|
||||||
|
FPbSpectrum.Cursor := crArrow;
|
||||||
|
|
||||||
FPbRuler := TPaintBox.Create(Self);
|
FPbRuler := TPaintBox.Create(Self);
|
||||||
FPbRuler.Parent := AParent;
|
FPbRuler.Parent := AParent;
|
||||||
@@ -277,6 +330,7 @@ begin
|
|||||||
TPaintBox(FPbWaterfall).OnMouseUp := WaterfallMouseUp;
|
TPaintBox(FPbWaterfall).OnMouseUp := WaterfallMouseUp;
|
||||||
end;
|
end;
|
||||||
FPbWaterfall.Parent := AParent;
|
FPbWaterfall.Parent := AParent;
|
||||||
|
FPbWaterfall.Cursor := crArrow; // см. комментарий у FPbSpectrum.Cursor
|
||||||
|
|
||||||
// ---- Панель пана/зума под водопадом ----
|
// ---- Панель пана/зума под водопадом ----
|
||||||
// Кнопки создаём с дефолтными цветами; финальная тема — у хозяина
|
// Кнопки создаём с дефолтными цветами; финальная тема — у хозяина
|
||||||
@@ -292,27 +346,96 @@ begin
|
|||||||
FBtnZoomOut := MakeFlatBtn(AParent, '−', 0, 0, 10, 10, ZoomOutClick);
|
FBtnZoomOut := MakeFlatBtn(AParent, '−', 0, 0, 10, 10, ZoomOutClick);
|
||||||
FBtnZoomDef := MakeFlatBtn(AParent, '⌂', 0, 0, 10, 10, ZoomDefClick);
|
FBtnZoomDef := MakeFlatBtn(AParent, '⌂', 0, 0, 10, 10, ZoomDefClick);
|
||||||
FBtnZoomIn := MakeFlatBtn(AParent, '+', 0, 0, 10, 10, ZoomInClick);
|
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;
|
end;
|
||||||
|
|
||||||
procedure TPanafallPanel.PositionPanZoomBar(BottomY, RW, H: Integer;
|
procedure TPanafallPanel.PositionPanZoomBar(BottomY, RW, H: Integer;
|
||||||
AVisible: Boolean);
|
AVisible: Boolean);
|
||||||
var BtnW, Gap, StripW, X: Integer;
|
var BtnW, Gap, StripW, X, NBtns: Integer; WantAdd: Boolean;
|
||||||
begin
|
begin
|
||||||
if (FPbPanZoom = nil) or (FBtnZoomIn = nil) then Exit;
|
if (FPbPanZoom = nil) or (FBtnZoomIn = nil) then Exit;
|
||||||
|
// «⊞» добавления пана живёт в этом же ряду; хозяин включает его флагом
|
||||||
|
// AddPanEnabled (Visible кнопки управляется здесь вместе с рядом).
|
||||||
|
WantAdd := FAddPanWanted and (FBtnAddPan <> nil);
|
||||||
FPbPanZoom.Visible := AVisible;
|
FPbPanZoom.Visible := AVisible;
|
||||||
FBtnZoomIn.Visible := AVisible;
|
FBtnZoomIn.Visible := AVisible;
|
||||||
FBtnZoomDef.Visible := AVisible;
|
FBtnZoomDef.Visible := AVisible;
|
||||||
FBtnZoomOut.Visible := AVisible;
|
FBtnZoomOut.Visible := AVisible;
|
||||||
|
if FBtnAddPan <> nil then FBtnAddPan.Visible := AVisible and WantAdd;
|
||||||
if not AVisible then Exit;
|
if not AVisible then Exit;
|
||||||
Gap := MulDiv(2, Screen.PixelsPerInch, 96);
|
Gap := MulDiv(2, Screen.PixelsPerInch, 96);
|
||||||
BtnW := H; // квадратные кнопки в высоту строки
|
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;
|
if StripW < 20 then StripW := 20;
|
||||||
FPbPanZoom.SetBounds(0, BottomY, StripW, H);
|
FPbPanZoom.SetBounds(0, BottomY, StripW, H);
|
||||||
X := StripW + Gap;
|
X := StripW + Gap;
|
||||||
FBtnZoomOut.SetBounds(X, BottomY, BtnW, H); Inc(X, BtnW + Gap);
|
FBtnZoomOut.SetBounds(X, BottomY, BtnW, H); Inc(X, BtnW + Gap);
|
||||||
FBtnZoomDef.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);
|
FBtnZoomIn.SetBounds(X, BottomY, BtnW, H);
|
||||||
|
if WantAdd then
|
||||||
|
begin
|
||||||
|
Inc(X, BtnW + Gap);
|
||||||
|
FBtnAddPan.SetBounds(X, BottomY, BtnW, H);
|
||||||
|
end;
|
||||||
FPbPanZoom.Invalidate;
|
FPbPanZoom.Invalidate;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@@ -327,20 +450,38 @@ var
|
|||||||
AvailH: Integer;
|
AvailH: Integer;
|
||||||
RULER_H: Integer;
|
RULER_H: Integer;
|
||||||
PANZOOM_H: Integer;
|
PANZOOM_H: Integer;
|
||||||
|
HEADER_H: Integer;
|
||||||
ShowPZ: Boolean;
|
ShowPZ: Boolean;
|
||||||
begin
|
begin
|
||||||
if (FParent = nil) or (FPbSpectrum = nil) then Exit;
|
if (FParent = nil) or (FPbSpectrum = nil) then Exit;
|
||||||
FShowSpectrum := AShowSpectrum;
|
FShowSpectrum := AShowSpectrum;
|
||||||
FShowWaterfall := AShowWaterfall;
|
FShowWaterfall := AShowWaterfall;
|
||||||
|
FSplitAvailH := 0; // валидно только в режиме «спектр+водопад» (ниже)
|
||||||
RULER_H := MulDiv(18, Screen.PixelsPerInch, 96);
|
RULER_H := MulDiv(18, Screen.PixelsPerInch, 96);
|
||||||
RW := FParent.ClientWidth;
|
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);
|
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);
|
PositionPanZoomBar(RH - PANZOOM_H, RW, PANZOOM_H, ShowPZ);
|
||||||
if ShowPZ then RH := RH - PANZOOM_H;
|
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
|
if (not AShowSpectrum) and (not AShowWaterfall) then
|
||||||
@@ -417,6 +558,9 @@ begin
|
|||||||
|
|
||||||
AvailH := RH - TopOff - RULER_H - SPLITTER_H;
|
AvailH := RH - TopOff - RULER_H - SPLITTER_H;
|
||||||
if AvailH < (MIN_SH + MIN_WH) then Exit;
|
if AvailH < (MIN_SH + MIN_WH) then Exit;
|
||||||
|
// Запоминаем геометрию для drag-математики сплиттера (регион пана в стеке).
|
||||||
|
FSplitTopOff := TopOff;
|
||||||
|
FSplitAvailH := AvailH;
|
||||||
|
|
||||||
// Ограничиваем соотношение, чтобы каждая зона имела минимальный размер
|
// Ограничиваем соотношение, чтобы каждая зона имела минимальный размер
|
||||||
if FSplitterRatio < MIN_SH / AvailH then
|
if FSplitterRatio < MIN_SH / AvailH then
|
||||||
@@ -494,15 +638,14 @@ const
|
|||||||
begin
|
begin
|
||||||
RULER_H := MulDiv(18, Screen.PixelsPerInch, 96);
|
RULER_H := MulDiv(18, Screen.PixelsPerInch, 96);
|
||||||
if not FSplitterDrag then Exit;
|
if not FSplitterDrag then Exit;
|
||||||
|
// Геометрия региона ЭТОГО пана из последнего Layout (стек панов!),
|
||||||
|
// а не от всего родителя.
|
||||||
|
TopOff := FSplitTopOff;
|
||||||
|
AvailH := FSplitAvailH;
|
||||||
|
if AvailH <= 0 then Exit;
|
||||||
P := FParent.ScreenToClient(FSplitter.ClientToScreen(Point(X, Y)));
|
P := FParent.ScreenToClient(FSplitter.ClientToScreen(Point(X, Y)));
|
||||||
DeltaY := P.Y - FSplitterDragY0;
|
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;
|
NewSH := FSplitterSH0 + DeltaY;
|
||||||
if NewSH < MIN_SH then NewSH := MIN_SH;
|
if NewSH < MIN_SH then NewSH := MIN_SH;
|
||||||
if NewSH > AvailH - MIN_WH then NewSH := AvailH - MIN_WH;
|
if NewSH > AvailH - MIN_WH then NewSH := AvailH - MIN_WH;
|
||||||
@@ -519,14 +662,11 @@ var
|
|||||||
P: TPoint;
|
P: TPoint;
|
||||||
DeltaY: Integer;
|
DeltaY: Integer;
|
||||||
NewSH: Integer;
|
NewSH: Integer;
|
||||||
TopOff, AvailH: Integer;
|
AvailH: Integer;
|
||||||
RULER_H: Integer;
|
|
||||||
const
|
const
|
||||||
SPLITTER_H = 5;
|
MIN_SH = 60;
|
||||||
MIN_SH = 60;
|
MIN_WH = 40;
|
||||||
MIN_WH = 40;
|
|
||||||
begin
|
begin
|
||||||
RULER_H := MulDiv(18, Screen.PixelsPerInch, 96);
|
|
||||||
if not FSplitterDrag then Exit;
|
if not FSplitterDrag then Exit;
|
||||||
FSplitterDrag := False;
|
FSplitterDrag := False;
|
||||||
{$IFDEF WINDOWS}
|
{$IFDEF WINDOWS}
|
||||||
@@ -537,10 +677,8 @@ begin
|
|||||||
P := FParent.ScreenToClient(FSplitter.ClientToScreen(Point(X, Y)));
|
P := FParent.ScreenToClient(FSplitter.ClientToScreen(Point(X, Y)));
|
||||||
DeltaY := P.Y - FSplitterDragY0;
|
DeltaY := P.Y - FSplitterDragY0;
|
||||||
|
|
||||||
TopOff := 0;
|
// Высота спектр+водопад региона ЭТОГО пана из последнего Layout (стек панов!).
|
||||||
// Низ зарезервирован под панель пана/зума (она видна, пока виден водопад).
|
AvailH := FSplitAvailH;
|
||||||
AvailH := FParent.ClientHeight - TopOff - RULER_H - SPLITTER_H
|
|
||||||
- MulDiv(22, Screen.PixelsPerInch, 96);
|
|
||||||
if AvailH <= 0 then Exit;
|
if AvailH <= 0 then Exit;
|
||||||
|
|
||||||
NewSH := FSplitterSH0 + DeltaY;
|
NewSH := FSplitterSH0 + DeltaY;
|
||||||
@@ -595,6 +733,14 @@ begin
|
|||||||
if Assigned(FActiveFlag) then Result := FActiveFlag.SliceId else Result := 0;
|
if Assigned(FActiveFlag) then Result := FActiveFlag.SliceId else Result := 0;
|
||||||
end;
|
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;
|
function TPanafallPanel.FindSliceFlag(SliceId: Integer): TVfoOverlay;
|
||||||
var i: Integer;
|
var i: Integer;
|
||||||
begin
|
begin
|
||||||
@@ -647,7 +793,7 @@ begin
|
|||||||
O.Free;
|
O.Free;
|
||||||
if Assigned(FOnSliceClosed) then FOnSliceClosed(SliceId);
|
if Assigned(FOnSliceClosed) then FOnSliceClosed(SliceId);
|
||||||
UpdateMainFlagVisibility; // закрыт последний слайс при открытой панели → скрыть флаг A
|
UpdateMainFlagVisibility; // закрыт последний слайс при открытой панели → скрыть флаг A
|
||||||
if Assigned(FOnFlagsInvalidate) then FOnFlagsInvalidate(nil);
|
if Assigned(FOnFlagsInvalidate) then FOnFlagsInvalidate(Self); // Self = панель (роутинг по пану)
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TPanafallPanel.RequestSliceClose(SliceId: Integer);
|
procedure TPanafallPanel.RequestSliceClose(SliceId: Integer);
|
||||||
@@ -675,7 +821,7 @@ var
|
|||||||
begin
|
begin
|
||||||
Result := 0;
|
Result := 0;
|
||||||
if (FPbSpectrum = nil) or (FPbSpectrum.Width <= 0) or (FController = nil) then Exit;
|
if (FPbSpectrum = nil) or (FPbSpectrum.Width <= 0) or (FController = nil) then Exit;
|
||||||
FController.GetViewWindow(VC, VS);
|
ViewWindow(VC, VS);
|
||||||
if VS <= 0 then Exit;
|
if VS <= 0 then Exit;
|
||||||
for i := 0 to High(FSliceFlags) do
|
for i := 0 to High(FSliceFlags) do
|
||||||
begin
|
begin
|
||||||
@@ -738,7 +884,7 @@ var
|
|||||||
begin
|
begin
|
||||||
if (FPbSpectrum = nil) or (FController = nil) then Exit;
|
if (FPbSpectrum = nil) or (FController = nil) then Exit;
|
||||||
W := FPbSpectrum.Width;
|
W := FPbSpectrum.Width;
|
||||||
FController.GetViewWindow(VC, VS);
|
ViewWindow(VC, VS);
|
||||||
if (W <= 0) or (VS <= 0) then Exit;
|
if (W <= 0) or (VS <= 0) then Exit;
|
||||||
|
|
||||||
// 1. Собрать видимые флаги + их идеальный Left рядом со своей полосой.
|
// 1. Собрать видимые флаги + их идеальный Left рядом со своей полосой.
|
||||||
@@ -809,7 +955,7 @@ begin
|
|||||||
Moved := True;
|
Moved := True;
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
if Moved and Assigned(FOnFlagsInvalidate) then FOnFlagsInvalidate(nil);
|
if Moved and Assigned(FOnFlagsInvalidate) then FOnFlagsInvalidate(Self);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
// Флаг главного приёмника (слайс A) виден, когда СКРЫТА левая панель хозяина
|
// Флаг главного приёмника (слайс A) виден, когда СКРЫТА левая панель хозяина
|
||||||
|
|||||||
+99
-8
@@ -89,7 +89,8 @@ SmartSDR НЕ умеет — наш pop-out будет улучшением по
|
|||||||
один слайс в его центре (поведение Flex).
|
один слайс в его центре (поведение Flex).
|
||||||
3. ADC-источник — свойство **панадаптера**, не слайса (бейдж `A1`/`A2` в шапке).
|
3. ADC-источник — свойство **панадаптера**, не слайса (бейдж `A1`/`A2` в шапке).
|
||||||
В отличие от Flex показываем именно АЦП, а не антенну: на HPSDR-платах
|
В отличие от Flex показываем именно АЦП, а не антенну: на HPSDR-платах
|
||||||
маппинг «вход→АЦП» жёсткий, антенная абстракция не нужна.
|
маппинг «вход→АЦП» жёсткий. Антенны ADC0 (Alex ANT1/2/3) — отдельная
|
||||||
|
ГЛОБАЛЬНАЯ ручка, общая для всех панов этого АЦП (детали в 3.4).
|
||||||
4. Буквы слайсов — глобальный пул A..G через все паны (как во Flex A-H):
|
4. Буквы слайсов — глобальный пул A..G через все паны (как во Flex A-H):
|
||||||
буква = личность слайса, пан = место жительства.
|
буква = личность слайса, пан = место жительства.
|
||||||
5. Возможности бэкенда описывает запись capabilities (см. таблицу) — UI по ней
|
5. Возможности бэкенда описывает запись capabilities (см. таблицу) — UI по ней
|
||||||
@@ -342,14 +343,99 @@ ChangeSampleRate).
|
|||||||
независим от DDC, TX-частота = частота слайса. (Полный мультислайс-TX —
|
независим от 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, оба АЦП когерентно в один канал) — НЕ здесь; протокол
|
- Diversity (sync-DDC, оба АЦП когерентно в один канал) — НЕ здесь; протокол
|
||||||
умеет, дизайну панов не мешает, отдельная будущая фича.
|
умеет, дизайну панов не мешает, отдельная будущая фича.
|
||||||
|
|
||||||
@@ -437,6 +523,11 @@ ChangeSampleRate).
|
|||||||
осознанно: только явное создание пана (решение «сначала статика» остаётся).
|
осознанно: только явное создание пана (решение «сначала статика» остаётся).
|
||||||
- Настройки водопада (палитра/AGC/avg) per-pan или глобально — стартуем с
|
- Настройки водопада (палитра/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"/>
|
<UseVersionInfo Value="True"/>
|
||||||
<AutoIncrementBuild Value="True"/>
|
<AutoIncrementBuild Value="True"/>
|
||||||
<MinorVersionNr Value="9"/>
|
<MinorVersionNr Value="9"/>
|
||||||
<BuildNr Value="256"/>
|
<BuildNr Value="257"/>
|
||||||
</VersionInfo>
|
</VersionInfo>
|
||||||
<MacroValues Count="74">
|
<MacroValues Count="76">
|
||||||
<Macro1 Name="LCLWidgetType" Value="qt6"/>
|
<Macro1 Name="LCLWidgetType" Value="qt6"/>
|
||||||
<Macro2 Name="LCLWidgetType" Value="qt6"/>
|
<Macro2 Name="LCLWidgetType" Value="qt6"/>
|
||||||
<Macro3 Name="LCLWidgetType" Value="qt6"/>
|
<Macro3 Name="LCLWidgetType" Value="qt6"/>
|
||||||
@@ -94,6 +94,8 @@
|
|||||||
<Macro72 Name="LCLWidgetType" Value="qt6"/>
|
<Macro72 Name="LCLWidgetType" Value="qt6"/>
|
||||||
<Macro73 Name="LCLWidgetType" Value="qt6"/>
|
<Macro73 Name="LCLWidgetType" Value="qt6"/>
|
||||||
<Macro74 Name="LCLWidgetType" Value="qt6"/>
|
<Macro74 Name="LCLWidgetType" Value="qt6"/>
|
||||||
|
<Macro75 Name="LCLWidgetType" Value="qt6"/>
|
||||||
|
<Macro76 Name="LCLWidgetType" Value="qt6"/>
|
||||||
</MacroValues>
|
</MacroValues>
|
||||||
<BuildModes>
|
<BuildModes>
|
||||||
<Item Name="Debug" Default="True"/>
|
<Item Name="Debug" Default="True"/>
|
||||||
|
|||||||
Reference in New Issue
Block a user