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:
+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) виден, когда СКРЫТА левая панель хозяина
|
||||
|
||||
Reference in New Issue
Block a user