Files
ewsdr/PanafallPanel.pas
T
ew8bakandClaude Fable 5 95b4f749be 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>
2026-07-11 22:56:49 +03:00

1158 lines
47 KiB
ObjectPascal
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
unit PanafallPanel;
{
TPanafallPanel — панадаптер (спектр + линейка частот + сплиттер + водопад +
полоса пана/зума с кнопками) как единый composite-объект.
Этап 3.0 плана мультислайсов (doc/SLICES_PLAN.md): вынос панадаптера из
MainForm без изменения поведения. Панель владеет:
• конструированием контролов (Build) и SpectrumView (CPU или OpenGL);
• геометрией стека спектр/линейка/сплиттер/водопад/пан-зум (Layout);
• механикой сплиттера (drag с призраком, пересчёт по MouseUp);
• прокидкой мыши пан/зум-полосы в TPanZoomBar.
Семантика мыши спектра/водопада (тюн, слайсы, маркер, оверлеи) остаётся у
хозяина — он подвешивает свои обработчики через публичные поля Spectrum*/
Waterfall*/Zoom*Click ДО вызова Build.
Хозяин по-прежнему сам синхронизирует состояние view (частоты, палитры,
wf-настройки) и решает, когда звать Layout (resize, show/hide, сплиттер).
Фаза B1: панель владеет коллекцией флагов слайсов (главный флаг A создаёт
хозяин и отдаёт через AttachMainFlag): раскладка LayoutFlags, диспетчер мыши
флагов, создание/закрытие слайс-флагов, заливка их состояния из контроллера,
тик S-метров (TickFlags). Семантические обработчики событий флага (MODE/DSP/
AUD/TX…) остаются у хозяина — он проводит их через OnWireFlag.
}
{$IFDEF FPC}
{$MODE Delphi}
{$ENDIF}
interface
uses
Classes, SysUtils, Math, Controls, ExtCtrls, StdCtrls, Graphics, Forms,
LCLIntf, LCLType,
OpenGLContextEx,
AppTheme, WDSPEngine, RadioController, VfoOverlay,
SpectrumView, SpectrumViewOpengl, PanZoomBar, FlatButton;
type
TWireFlagEvent = procedure(O: TVfoOverlay) of object;
TSliceClosedEvent = procedure(SliceId: Integer) of object;
TPanafallPanel = class(TComponent)
private
FParent: TWinControl;
FUseOpenGL: Boolean;
FView: TSpectrumView;
FPbSpectrum: TControl;
FPbRuler: TPaintBox;
FSplitter: TPanel;
FPbWaterfall: TControl;
FPbPanZoom: TPaintBox;
FZoomBar: TPanZoomBar;
FBtnZoomIn: TFlatButton;
FBtnZoomDef: TFlatButton;
FBtnZoomOut: TFlatButton;
// Актуальные размеры областей после Layout (для DSP-движка и
// resize-детектора хозяина).
FSpectrumWidth: Integer;
FSpectrumHeight: Integer;
FWaterfallHeight: Integer;
FShowSpectrum: Boolean;
FShowWaterfall: Boolean;
FSplitterRatio: Double;
// Сплиттер-драг: обновление размеров только при отпускании мыши (MouseUp),
// чтобы исключить фризы; во время перетаскивания двигаем контролы визуально.
FSplitterDrag: Boolean;
FSplitterDragY0: Integer;
FSplitterSH0: Integer;
// Геометрия последнего Layout для drag-математики: верх области спектра и
// высота спектр+водопад ВНУТРИ региона пана (не всего родителя!).
FSplitTopOff: Integer;
FSplitAvailH: Integer; // 0 = сплиттер сейчас нелегитимен
FOnSplitterMoved: TNotifyEvent;
// ---- Флаги слайсов (фаза B1) ----
FController: TRadioController; // не владеем
FMainFlag: TVfoOverlay; // флаг A (главный) — создаёт хозяин
FSliceFlags: array of TVfoOverlay; // флаги B+ (Owner=Self)
FActiveFlag: TVfoOverlay; // чьё событие сейчас обрабатывается
FPendingSliceClose: Integer; // SliceId к удалению после мыши (0=нет)
FMainFlagPinned: Boolean; // True = левая панель хозяина скрыта
FLastSliceMeterMs: QWord; // троттлинг S-метра слайсов (~10 Гц)
FOnWireFlag: TWireFlagEvent;
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;
X, Y: Integer);
procedure SplitterMouseUp(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
procedure PanZoomMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
procedure PanZoomMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
procedure PanZoomMouseUp(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
procedure PanZoomDblClick(Sender: TObject);
procedure PositionPanZoomBar(BottomY, RW, H: Integer; AVisible: Boolean);
public
// Обработчики мыши спектра/водопада и кликов зум-кнопок хозяина —
// задать ДО вызова Build (Build подвешивает их на создаваемые контролы).
SpectrumMouseDown: TMouseEvent;
SpectrumMouseMove: TMouseMoveEvent;
SpectrumMouseUp: TMouseEvent;
SpectrumMouseLeave: TNotifyEvent;
SpectrumDblClick: TNotifyEvent;
WaterfallMouseDown: TMouseEvent;
WaterfallMouseMove: TMouseMoveEvent;
WaterfallMouseUp: TMouseEvent;
ZoomInClick: TNotifyEvent;
ZoomDefClick: TNotifyEvent;
ZoomOutClick: TNotifyEvent;
constructor Create(AOwner: TComponent; AUseOpenGL: Boolean); reintroduce;
destructor Destroy; override;
// Создаёт контролы в AParent и подвешивает обработчики (см. поля выше).
procedure Build(AParent: TWinControl; AMSAA: Integer);
// Полная раскладка стека. ATopOffset — высота wideband-блока хозяина
// над спектром. Show-флаги приходят от контроллера (FShowSpectrum/
// FShowWaterfall).
procedure Layout(ATopOffset: Integer; AShowSpectrum, AShowWaterfall: Boolean);
property View: TSpectrumView read FView;
property PbSpectrum: TControl read FPbSpectrum;
property PbRuler: TPaintBox read FPbRuler;
property Splitter: TPanel read FSplitter;
property PbWaterfall: TControl read FPbWaterfall;
property PbPanZoom: TPaintBox read FPbPanZoom;
property ZoomBar: TPanZoomBar read FZoomBar;
property BtnZoomIn: TFlatButton read FBtnZoomIn;
property BtnZoomDef: TFlatButton read FBtnZoomDef;
property BtnZoomOut: TFlatButton read FBtnZoomOut;
property SpectrumWidth: Integer read FSpectrumWidth;
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;
// ---- Флаги слайсов ----
// Главный флаг (слайс A) создаёт хозяин (проводка событий у него) и отдаёт
// сюда; панель подключает его к view и включает в раскладку/диспетчер.
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);
// «×» на флаге: нельзя free внутри его же обработчика мыши — пометить...
procedure RequestSliceClose(SliceId: Integer);
// …и снять после возврата из диспетчера.
procedure ProcessPendingSliceClose;
// SliceId, чья несущая под пикселем X спектра (0 = нет).
function SliceAtPixel(X: Integer): Integer;
// SliceId флага под курсором мыши (прямоугольник оверлея; 0 = нет).
function SliceUnderCursor: Integer;
// Единый packing всех флагов (A+B+) в один ряд, без наездов.
procedure LayoutFlags;
// Флаг A виден: панель хозяина скрыта (MainFlagPinned) ИЛИ есть слайсы B+.
procedure UpdateMainFlagVisibility;
procedure PushSliceFlagState(SliceId: Integer);
procedure PushMainFlagExtState;
procedure PushMainFlagAudioDevices;
// Тик: S-метр/частота главного флага + позиции + троттленный S-метр слайсов.
procedure TickFlags;
procedure SetFlagsTheme(const T: TAppTheme);
function DispatchFlagsMouseDown(Button: TMouseButton; X, Y: Integer): Boolean;
procedure DispatchFlagsMouseMove(X, Y: Integer);
function DispatchFlagsMouseUp(X, Y: Integer): Boolean;
procedure DispatchFlagsMouseLeave;
property Controller: TRadioController read FController write FController;
property MainFlag: TVfoOverlay read FMainFlag;
property MainFlagPinned: Boolean read FMainFlagPinned write FMainFlagPinned;
// Проводка семантических событий флага (MODE/DSP/AUD/…) — хозяин.
property OnWireFlag: TWireFlagEvent read FOnWireFlag write FOnWireFlag;
// Слайс закрыт «×» — хозяину сбросить активный слайс и т.п.
property OnSliceClosed: TSliceClosedEvent read FOnSliceClosed write FOnSliceClosed;
// Флаги сдвинулись/пересозданы — хозяину пометить кадр грязным
// (эквивалент прежнего OnVfoOverlayInvalidate(nil)).
property OnFlagsInvalidate: TNotifyEvent read FOnFlagsInvalidate write FOnFlagsInvalidate;
end;
implementation
constructor TPanafallPanel.Create(AOwner: TComponent; AUseOpenGL: Boolean);
begin
inherited Create(AOwner);
FUseOpenGL := AUseOpenGL;
if FUseOpenGL then
FView := TSpectrumViewOpenGL.Create
else
FView := TSpectrumView.Create;
FZoomBar := TPanZoomBar.Create;
FSplitterRatio := 0.40;
FSplitterDrag := False;
FShowZoomRow := True; // паны N>0 выключают (их зум — этап 3.3+)
FHeaderVisible := False; // шапка появляется при панах >= 2
end;
destructor TPanafallPanel.Destroy;
begin
// Порядок как в прежнем FormDestroy: view/зум-бар первыми, контролы
// (Owner=Self) освободит inherited.
FreeAndNil(FZoomBar);
FreeAndNil(FView);
inherited Destroy;
end;
procedure TPanafallPanel.Build(AParent: TWinControl; AMSAA: Integer);
begin
FParent := AParent;
if FUseOpenGL then
begin
FPbSpectrum := TOpenGLControl.Create(Self);
TOpenGLControl(FPbSpectrum).AutoResizeViewport := False;
TOpenGLControl(FPbSpectrum).DoubleBuffered := True;
TOpenGLControl(FPbSpectrum).MultiSampling := Max(1, AMSAA);
TOpenGLControl(FPbSpectrum).OnPaint := FView.PaintSpectrum;
TOpenGLControl(FPbSpectrum).OnMouseDown := SpectrumMouseDown;
TOpenGLControl(FPbSpectrum).OnDblClick := SpectrumDblClick;
TOpenGLControl(FPbSpectrum).OnMouseMove := SpectrumMouseMove;
TOpenGLControl(FPbSpectrum).OnMouseUp := SpectrumMouseUp;
TOpenGLControl(FPbSpectrum).OnMouseLeave := SpectrumMouseLeave;
TSpectrumViewOpenGL(FView).AttachControl(TOpenGLControl(FPbSpectrum));
end
else
begin
FPbSpectrum := TPaintBox.Create(Self);
TPaintBox(FPbSpectrum).OnPaint := FView.PaintSpectrum;
TPaintBox(FPbSpectrum).OnMouseDown := SpectrumMouseDown;
TPaintBox(FPbSpectrum).OnDblClick := SpectrumDblClick;
TPaintBox(FPbSpectrum).OnMouseMove := SpectrumMouseMove;
TPaintBox(FPbSpectrum).OnMouseUp := SpectrumMouseUp;
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;
FPbRuler.OnPaint := FView.PaintRuler;
FPbRuler.Cursor := crDefault;
FView.PbRuler := FPbRuler;
// ---- Сплиттер между спектром+линейкой и водопадом ----
FSplitter := TPanel.Create(Self);
FSplitter.Parent := AParent;
FSplitter.BevelOuter := bvNone;
FSplitter.Color := TColor($00303030);
FSplitter.Cursor := crVSplit;
FSplitter.Height := 5;
FSplitter.OnMouseDown := SplitterMouseDown;
FSplitter.OnMouseMove := SplitterMouseMove;
FSplitter.OnMouseUp := SplitterMouseUp;
if FUseOpenGL then
begin
FPbWaterfall := TOpenGLControl.Create(Self);
TOpenGLControl(FPbWaterfall).AutoResizeViewport := False;
TOpenGLControl(FPbWaterfall).DoubleBuffered := True;
TOpenGLControl(FPbWaterfall).OnPaint := FView.PaintWaterfall;
TOpenGLControl(FPbWaterfall).OnMouseDown := WaterfallMouseDown;
TOpenGLControl(FPbWaterfall).OnMouseMove := WaterfallMouseMove;
TOpenGLControl(FPbWaterfall).OnMouseUp := WaterfallMouseUp;
TSpectrumViewOpenGL(FView).AttachWaterfallControl(TOpenGLControl(FPbWaterfall));
end
else
begin
FPbWaterfall := TPaintBox.Create(Self);
TPaintBox(FPbWaterfall).OnPaint := FView.PaintWaterfall;
TPaintBox(FPbWaterfall).OnMouseDown := WaterfallMouseDown;
TPaintBox(FPbWaterfall).OnMouseMove := WaterfallMouseMove;
TPaintBox(FPbWaterfall).OnMouseUp := WaterfallMouseUp;
end;
FPbWaterfall.Parent := AParent;
FPbWaterfall.Cursor := crArrow; // см. комментарий у FPbSpectrum.Cursor
// ---- Панель пана/зума под водопадом ----
// Кнопки создаём с дефолтными цветами; финальная тема — у хозяина
// (ApplyDarkTheme → StyleButton/SetTheme через алиасы).
FPbPanZoom := TPaintBox.Create(Self);
FPbPanZoom.Parent := AParent;
FPbPanZoom.OnPaint := FZoomBar.Paint;
FPbPanZoom.OnMouseDown := PanZoomMouseDown;
FPbPanZoom.OnMouseMove := PanZoomMouseMove;
FPbPanZoom.OnMouseUp := PanZoomMouseUp;
FPbPanZoom.OnDblClick := PanZoomDblClick;
FZoomBar.PaintBox := FPbPanZoom;
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, 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; // квадратные кнопки в высоту строки
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;
procedure TPanafallPanel.Layout(ATopOffset: Integer;
AShowSpectrum, AShowWaterfall: Boolean);
const
SPLITTER_H = 5;
MIN_SH = 60; // минимальная высота спектра
MIN_WH = 40; // минимальная высота водопада
var
RW, RH, SH, WH, TopOff: Integer;
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;
// Регион пана в стеке: [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) and FShowZoomRow;
PositionPanZoomBar(RH - PANZOOM_H, RW, PANZOOM_H, ShowPZ);
if ShowPZ then RH := RH - PANZOOM_H;
TopOff := ATopOffset + FStackTop;
if FHeaderVisible then Inc(TopOff, HEADER_H);
// ---- Оба скрыты ----
if (not AShowSpectrum) and (not AShowWaterfall) then
begin
FPbSpectrum.Visible := False;
FPbRuler.Visible := False;
FSplitter.Visible := False;
FPbWaterfall.Visible := False;
FSpectrumWidth := RW;
FSpectrumHeight := 0;
FWaterfallHeight := 0;
Exit;
end;
// ---- Только спектр (водопад скрыт) ----
if AShowSpectrum and (not AShowWaterfall) then
begin
AvailH := RH - TopOff - RULER_H;
if AvailH < MIN_SH then AvailH := MIN_SH;
SH := AvailH;
FPbSpectrum.SetBounds(0, TopOff, RW, SH);
FPbSpectrum.Visible := True;
FPbRuler.SetBounds(0, TopOff + SH, RW, RULER_H);
FPbRuler.Visible := True;
FSplitter.Visible := False;
FPbWaterfall.Visible := False;
FSpectrumWidth := RW;
FSpectrumHeight := SH;
FWaterfallHeight := 0;
if RW > 0 then
begin
FView.SetSpectrumBitmapSize(RW, SH);
FView.SetWaterfallBitmapSize(1, 1);
FView.SetRulerSize(RW, RULER_H);
FView.DrawSpectrum;
FPbSpectrum.Invalidate;
FPbRuler.Invalidate;
end;
Exit;
end;
// ---- Только водопад (спектр скрыт): линейка под водопадом ----
if (not AShowSpectrum) and AShowWaterfall then
begin
AvailH := RH - TopOff - RULER_H;
if AvailH < MIN_WH then AvailH := MIN_WH;
WH := AvailH;
FPbSpectrum.Visible := False;
FSplitter.Visible := False;
FPbWaterfall.SetBounds(0, TopOff, RW, WH);
FPbWaterfall.Visible := True;
FPbRuler.SetBounds(0, TopOff + WH, RW, RULER_H);
FPbRuler.Visible := True;
FSpectrumWidth := RW;
FSpectrumHeight := 0;
FWaterfallHeight := WH;
if RW > 0 then
begin
FView.SetSpectrumBitmapSize(1, 1);
FView.SetWaterfallBitmapSize(RW, WH);
FView.SetRulerSize(RW, RULER_H);
FView.DrawWaterfall;
FPbWaterfall.Invalidate;
FPbRuler.Invalidate;
end;
Exit;
end;
// ---- Оба видимы: стандартный режим со сплиттером ----
FPbSpectrum.Visible := True;
FPbRuler.Visible := True;
FSplitter.Visible := True;
FPbWaterfall.Visible := True;
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
FSplitterRatio := MIN_SH / AvailH;
if FSplitterRatio > 1.0 - MIN_WH / AvailH then
FSplitterRatio := 1.0 - MIN_WH / AvailH;
SH := Round(AvailH * FSplitterRatio);
WH := AvailH - SH;
if WH < MIN_WH then WH := MIN_WH;
// Спектр
FPbSpectrum.SetBounds(0, TopOff, RW, SH);
// Линейка частот — всегда прижата к низу спектра
FPbRuler.SetBounds(0, TopOff + SH, RW, RULER_H);
// Сплиттер — между линейкой и водопадом
FSplitter.SetBounds(0, TopOff + SH + RULER_H, RW, SPLITTER_H);
// Водопад — под сплиттером
FPbWaterfall.SetBounds(0, TopOff + SH + RULER_H + SPLITTER_H, RW, WH);
// Обновляем переменные размеров
FSpectrumWidth := RW;
FSpectrumHeight := SH;
FWaterfallHeight := WH;
// Пересоздаём bitmap точно под новый размер
if RW > 0 then
begin
if SH > 0 then FView.SetSpectrumBitmapSize(RW, SH);
if WH > 0 then FView.SetWaterfallBitmapSize(RW, WH);
FView.SetRulerSize(RW, RULER_H);
FView.DrawSpectrum;
FPbSpectrum.Invalidate;
if FPbRuler <> nil then FPbRuler.Invalidate;
FPbWaterfall.Invalidate;
end;
end;
// ===========================================================================
// Splitter — перетаскивание границы спектр/водопад
// Обновление размеров происходит только при отпускании мыши (MouseUp),
// чтобы исключить фризы во время перетаскивания.
// Во время перетаскивания рисуем только призрак-линию на сплиттере.
// ===========================================================================
procedure TPanafallPanel.SplitterMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
var
P: TPoint;
begin
if Button <> mbLeft then Exit;
FSplitterDrag := True;
FSplitterSH0 := FSpectrumHeight;
// Y в координатах родителя (области панадаптера)
P := FParent.ScreenToClient(FSplitter.ClientToScreen(Point(X, Y)));
FSplitterDragY0 := P.Y;
FSplitter.Color := TColor($005050A0); // подсветка при захвате
{$IFDEF WINDOWS}
SetCapture(FSplitter.Handle);
{$ENDIF}
end;
procedure TPanafallPanel.SplitterMouseMove(Sender: TObject; Shift: TShiftState;
X, Y: Integer);
var
P: TPoint;
DeltaY: Integer;
NewSH: Integer;
TopOff, AvailH: Integer;
RULER_H: Integer;
const
SPLITTER_H = 5;
MIN_SH = 60;
MIN_WH = 40;
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;
NewSH := FSplitterSH0 + DeltaY;
if NewSH < MIN_SH then NewSH := MIN_SH;
if NewSH > AvailH - MIN_WH then NewSH := AvailH - MIN_WH;
// Только двигаем сплиттер визуально — без пересчёта битмапов
FSplitter.Top := TopOff + NewSH + RULER_H;
FPbWaterfall.Top := TopOff + NewSH + RULER_H + SPLITTER_H;
FPbWaterfall.Height := AvailH - NewSH;
end;
procedure TPanafallPanel.SplitterMouseUp(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
var
P: TPoint;
DeltaY: Integer;
NewSH: Integer;
AvailH: Integer;
const
MIN_SH = 60;
MIN_WH = 40;
begin
if not FSplitterDrag then Exit;
FSplitterDrag := False;
{$IFDEF WINDOWS}
ReleaseCapture;
{$ENDIF}
FSplitter.Color := TColor($00303030); // обычный цвет
P := FParent.ScreenToClient(FSplitter.ClientToScreen(Point(X, Y)));
DeltaY := P.Y - FSplitterDragY0;
// Высота спектр+водопад региона ЭТОГО пана из последнего Layout (стек панов!).
AvailH := FSplitAvailH;
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;
// Сохраняем новое соотношение; полный пересчёт с перерисовкой — у хозяина
// (ему виднее: wideband, S-метр, оверлеи).
FSplitterRatio := NewSH / AvailH;
if Assigned(FOnSplitterMoved) then FOnSplitterMoved(Self);
end;
// ===========================================================================
// Пан/зум-полоса — прокидка мыши в TPanZoomBar
// ===========================================================================
procedure TPanafallPanel.PanZoomMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
if FZoomBar <> nil then FZoomBar.HandleMouseDown(Button, X, Y);
end;
procedure TPanafallPanel.PanZoomMouseMove(Sender: TObject; Shift: TShiftState;
X, Y: Integer);
begin
if FZoomBar <> nil then FZoomBar.HandleMouseMove(X, Y);
end;
procedure TPanafallPanel.PanZoomMouseUp(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
if FZoomBar <> nil then FZoomBar.HandleMouseUp;
end;
procedure TPanafallPanel.PanZoomDblClick(Sender: TObject);
begin
if FZoomBar <> nil then FZoomBar.HandleDblClick;
end;
// ===========================================================================
// Флаги слайсов: коллекция, раскладка, диспетчер мыши, тик
// ===========================================================================
procedure TPanafallPanel.AttachMainFlag(O: TVfoOverlay);
begin
FMainFlag := O;
FView.VfoOverlay := O;
end;
function TPanafallPanel.CurSliceId: Integer;
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
Result := nil;
for i := 0 to High(FSliceFlags) do
if (FSliceFlags[i] <> nil) and (FSliceFlags[i].SliceId = SliceId) then
Exit(FSliceFlags[i]);
end;
procedure TPanafallPanel.CreateSliceFlag(SliceId: Integer);
var
O: TVfoOverlay;
S: TCtrlSlice;
begin
if FindSliceFlag(SliceId) <> nil then Exit;
if (FController = nil) or not FController.GetSlice(SliceId, S) then Exit;
O := TVfoOverlay.Create(Self);
if Assigned(FOnWireFlag) then FOnWireFlag(O);
O.SetSliceInfo(SliceId, S.Letter, False);
O.SMeterThreshold := 1.5; // слайсы: грубее метр → меньше дорогих ре-рендеров флага
O.Width := OVL_W;
O.Height := O.BaseHeight;
O.Visible := True; // флаги слайсов видны всегда (не зависят от скрытия панели)
SetLength(FSliceFlags, Length(FSliceFlags) + 1);
FSliceFlags[High(FSliceFlags)] := O;
FView.AddSliceOverlay(O);
PushSliceFlagState(SliceId);
UpdateMainFlagVisibility; // появился слайс B+ → показать и флаг A (если панель открыта)
LayoutFlags;
end;
procedure TPanafallPanel.DestroySliceFlag(SliceId: Integer);
var
i, j: Integer;
O: TVfoOverlay;
begin
O := FindSliceFlag(SliceId);
if O = nil then Exit;
if FController <> nil then FController.RemoveSlice(SliceId);
FView.RemoveSliceOverlay(O);
// Убираем из массива со сжатием.
j := 0;
for i := 0 to High(FSliceFlags) do
if FSliceFlags[i] <> O then
begin
FSliceFlags[j] := FSliceFlags[i];
Inc(j);
end;
SetLength(FSliceFlags, j);
O.Free;
if Assigned(FOnSliceClosed) then FOnSliceClosed(SliceId);
UpdateMainFlagVisibility; // закрыт последний слайс при открытой панели → скрыть флаг A
if Assigned(FOnFlagsInvalidate) then FOnFlagsInvalidate(Self); // Self = панель (роутинг по пану)
end;
procedure TPanafallPanel.RequestSliceClose(SliceId: Integer);
begin
FPendingSliceClose := SliceId;
end;
procedure TPanafallPanel.ProcessPendingSliceClose;
var Id: Integer;
begin
if FPendingSliceClose = 0 then Exit;
Id := FPendingSliceClose;
FPendingSliceClose := 0;
DestroySliceFlag(Id);
end;
// SliceId, чья несущая под пикселем X (в пределах допуска), 0 если нет.
function TPanafallPanel.SliceAtPixel(X: Integer): Integer;
const TOL = 6;
var
i, CarrierX: Integer;
VC, VS: Double;
S: TCtrlSlice;
O: TVfoOverlay;
begin
Result := 0;
if (FPbSpectrum = nil) or (FPbSpectrum.Width <= 0) or (FController = nil) then Exit;
ViewWindow(VC, VS);
if VS <= 0 then Exit;
for i := 0 to High(FSliceFlags) do
begin
O := FSliceFlags[i];
if (O = nil) or (not O.Visible) then Continue;
if not FController.GetSlice(O.SliceId, S) then Continue;
CarrierX := Round((S.TargetHz - VC + VS / 2) / VS * FPbSpectrum.Width);
if Abs(X - CarrierX) <= TOL then Exit(O.SliceId);
end;
end;
// Флаг слайса под курсором мыши (в клиентских координатах спектра)?
function TPanafallPanel.SliceUnderCursor: Integer;
var P: TPoint; i: Integer; O: TVfoOverlay;
begin
Result := 0;
if (FPbSpectrum = nil) or (not FPbSpectrum.Visible) then Exit;
P := FPbSpectrum.ScreenToClient(Mouse.CursorPos);
for i := 0 to High(FSliceFlags) do
begin
O := FSliceFlags[i];
if (O = nil) or (not O.Visible) then Continue;
if (P.X >= O.Left) and (P.X < O.Left + O.Width) and
(P.Y >= O.Top) and (P.Y < O.Top + O.Height) then
Exit(O.SliceId);
end;
end;
// Позиционирование всех флагов (главный A + слайсы B+) единым проходом.
// Идеал каждого флага — рядом со СВОЕЙ полосой фильтра (справа, флип влево
// если не влезает). Флаги не наезжают друг на друга: сосед слева «толкает»
// правого; если ряд вылез за правый край — обратный проход поджимает влево.
procedure TPanafallPanel.LayoutFlags;
const
FLAG_TOP = 35; // все флаги на одном уровне
GAPX = 4; // минимальный зазор между соседними флагами
var
Ovls: array of TVfoOverlay;
Ideal, OWs, Lefts: array of Integer;
n, i, j, W: Integer;
VC, VS: Double;
S: TCtrlSlice;
LoHz, HiHz: Double;
TmpO: TVfoOverlay;
TmpI: Integer;
Moved: Boolean;
// Идеальный Left рядом с полосой фильтра [loHz..hiHz] вокруг несущей Carrier.
function IdealLeftFor(Carrier, loHz, hiHz: Double; OW: Integer): Integer;
var vfoX, sx1, sx2, nl: Integer;
begin
vfoX := Round((Carrier - VC + VS / 2) / VS * W);
sx1 := Max(0, Min(W - 1, vfoX + Round(loHz / VS * W)));
sx2 := Max(0, Min(W - 1, vfoX + Round(hiHz / VS * W)));
nl := sx2 + 6; // по умолчанию справа от полосы
if nl + OW > W - 4 then nl := sx1 - 6 - OW; // не влезает → слева
Result := nl;
end;
begin
if (FPbSpectrum = nil) or (FController = nil) then Exit;
W := FPbSpectrum.Width;
ViewWindow(VC, VS);
if (W <= 0) or (VS <= 0) then Exit;
// 1. Собрать видимые флаги + их идеальный Left рядом со своей полосой.
n := 0;
SetLength(Ovls, 1 + Length(FSliceFlags));
SetLength(Ideal, 1 + Length(FSliceFlags));
SetLength(OWs, 1 + Length(FSliceFlags));
if Assigned(FMainFlag) and FMainFlag.Visible then
begin
case FController.FMode of
MODE_LSB: begin LoHz := -FController.FFilterBW; HiHz := -100; end;
MODE_USB: begin LoHz := 100; HiHz := FController.FFilterBW; end;
MODE_DIGL: begin LoHz := -FController.FFilterBW; HiHz := 0; end;
MODE_DIGU: begin LoHz := 0; HiHz := FController.FFilterBW; end;
else begin LoHz := -FController.FFilterBW / 2; HiHz := FController.FFilterBW / 2; end;
end;
Ovls[n] := FMainFlag;
OWs[n] := FMainFlag.Width;
Ideal[n] := IdealLeftFor(FController.FVfoA, LoHz, HiHz, OWs[n]);
Inc(n);
end;
for i := 0 to High(FSliceFlags) do
begin
if (FSliceFlags[i] = nil) or (not FSliceFlags[i].Visible) then Continue;
if not FController.GetSlice(FSliceFlags[i].SliceId, S) then Continue;
Ovls[n] := FSliceFlags[i];
OWs[n] := FSliceFlags[i].Width;
Ideal[n] := IdealLeftFor(S.TargetHz, S.FilterLo, S.FilterHi, OWs[n]);
Inc(n);
end;
if n = 0 then Exit;
// 2. Сортировка по идеальному Left (≈ по частоте) — пузырьком, n мал.
for i := 0 to n - 2 do
for j := 0 to n - 2 - i do
if Ideal[j] > Ideal[j + 1] then
begin
TmpI := Ideal[j]; Ideal[j] := Ideal[j + 1]; Ideal[j + 1] := TmpI;
TmpI := OWs[j]; OWs[j] := OWs[j + 1]; OWs[j + 1] := TmpI;
TmpO := Ovls[j]; Ovls[j] := Ovls[j + 1]; Ovls[j + 1] := TmpO;
end;
// 3. Вперёд-проход: сосед слева толкает правого (тот не пересекает).
SetLength(Lefts, n);
for i := 0 to n - 1 do
begin
Lefts[i] := Ideal[i];
if (i > 0) and (Lefts[i] < Lefts[i - 1] + OWs[i - 1] + GAPX) then
Lefts[i] := Lefts[i - 1] + OWs[i - 1] + GAPX;
end;
// 4. Если ряд вылез за правый край — обратный проход поджимает влево.
if Lefts[n - 1] + OWs[n - 1] > W - 4 then
for i := n - 1 downto 0 do
if i = n - 1 then
Lefts[i] := Min(Lefts[i], W - 4 - OWs[i])
else
Lefts[i] := Min(Lefts[i], Lefts[i + 1] - GAPX - OWs[i]);
// 5. Применить (зажать в поле), инвалидировать при реальном сдвиге.
Moved := False;
for i := 0 to n - 1 do
begin
Lefts[i] := Max(4, Lefts[i]);
if (Ovls[i].Left <> Lefts[i]) or (Ovls[i].Top <> FLAG_TOP) then
begin
Ovls[i].Left := Lefts[i];
Ovls[i].Top := FLAG_TOP;
Moved := True;
end;
end;
if Moved and Assigned(FOnFlagsInvalidate) then FOnFlagsInvalidate(Self);
end;
// Флаг главного приёмника (слайс A) виден, когда СКРЫТА левая панель хозяина
// (MainFlagPinned) ЛИБО есть доп. слайсы (B+): тогда A всегда представлен
// флагом рядом с B/C…, а сворачивание/раскрытие панели при живых слайсах не
// дёргает его появление.
procedure TPanafallPanel.UpdateMainFlagVisibility;
var
ShouldShow: Boolean;
begin
if (FMainFlag = nil) or (FController = nil) then Exit;
ShouldShow := FMainFlagPinned or (Length(FSliceFlags) > 0);
if ShouldShow = FMainFlag.Visible then Exit;
if ShouldShow then
begin
FMainFlag.Width := OVL_W;
FMainFlag.Height := FMainFlag.BaseHeight; // растёт сам при открытии fly-out
FMainFlag.SetState(FController.FMode, FController.FFilterBW, FController.FVfoA, FController.FLastSMeter);
FMainFlag.SetDSPState(FController.FNRMode, FController.FNBMode, FController.FSNB, FController.FANF, FController.FAGCMode);
FMainFlag.SetSquelchState(FController.FFMSQOn, FController.FFMSQLevel);
FMainFlag.Visible := True;
PushMainFlagExtState;
PushMainFlagAudioDevices;
LayoutFlags;
end
else
begin
FMainFlag.HandleMouseLeave;
FMainFlag.Visible := False;
end;
end;
// Заливает во флаг слайса его текущее состояние из контроллера.
procedure TPanafallPanel.PushSliceFlagState(SliceId: Integer);
var
O: TVfoOverlay;
S: TCtrlSlice;
BW, Cnt, I: Integer;
Names: TStringList;
Indices: array[0..63] of Integer;
Idx: array of Integer;
begin
O := FindSliceFlag(SliceId);
if (O = nil) or (FController = nil) or not FController.GetSlice(SliceId, S) then Exit;
case S.Mode of
MODE_LSB, MODE_DIGL: BW := -S.FilterLo;
MODE_USB, MODE_DIGU: BW := S.FilterHi;
else BW := S.FilterHi - S.FilterLo;
end;
O.SetState(S.Mode, BW, S.TargetHz, FController.SliceSMeter(SliceId));
O.SetDSPState(0, 0, False, False, FController.FAGCMode);
O.SetSquelchState(S.FMSQOn, S.FMSQLevel);
O.SetExtState(Round(S.Volume * 100), S.Muted, False, False, False);
// Список аудио-устройств (общий PA-список) + текущее устройство слайса.
if Assigned(FController.FAudioOut) then
begin
Names := TStringList.Create;
try
Cnt := 0;
FController.FAudioOut.EnumOutputDevices(Names, Indices, Cnt);
SetLength(Idx, Cnt);
for I := 0 to Cnt - 1 do Idx[I] := Indices[I];
O.SetAudioDevices(Names, Idx, S.DevName);
finally
Names.Free;
end;
end;
end;
procedure TPanafallPanel.PushMainFlagExtState;
begin
if not (Assigned(FMainFlag) and FMainFlag.Visible) or (FController = nil) then Exit;
// TxSel: этот слайс выбран для передачи. Пока TX только главный → всегда True.
FMainFlag.SetExtState(FController.ActiveVolume, FController.FMuted,
FController.FSplitTxB, FController.FTransmitting, True);
end;
procedure TPanafallPanel.PushMainFlagAudioDevices;
var
Names: TStringList;
Indices: array[0..63] of Integer;
Cnt, I: Integer;
Idx: array of Integer;
begin
if (FMainFlag = nil) or (FController = nil) or
not Assigned(FController.FAudioOut) then Exit;
Names := TStringList.Create;
try
Cnt := 0;
FController.FAudioOut.EnumOutputDevices(Names, Indices, Cnt);
SetLength(Idx, Cnt);
for I := 0 to Cnt - 1 do Idx[I] := Indices[I];
FMainFlag.SetAudioDevices(Names, Idx, FController.FAudioOutDevName);
finally
Names.Free;
end;
end;
// Тик (~частота FSpectrumTimer хозяина): метр/частота главного флага, позиции
// всех флагов, троттленный S-метр слайсов.
procedure TPanafallPanel.TickFlags;
var I: Integer;
begin
if FController = nil then Exit;
if Assigned(FMainFlag) and FMainFlag.Visible then
begin
FMainFlag.UpdateSMeter(FController.FLastSMeter);
FMainFlag.UpdateVfo(FController.FVfoA);
PushMainFlagExtState;
LayoutFlags;
end;
if Length(FSliceFlags) > 0 then
begin
LayoutFlags; // держим слайс-флаги на частоте
// S-метр слайсов троттлим до ~10 Гц: каждое изменение метра = софтовый
// ре-рендер флага (дорого), 60 Гц там не нужно.
if GetTickCount64 - FLastSliceMeterMs >= 100 then
begin
FLastSliceMeterMs := GetTickCount64;
for I := 0 to High(FSliceFlags) do // S-метр каждого слайса из его канала
if FSliceFlags[I] <> nil then
FSliceFlags[I].UpdateSMeter(FController.SliceSMeter(FSliceFlags[I].SliceId));
end;
end;
end;
procedure TPanafallPanel.SetFlagsTheme(const T: TAppTheme);
var i: Integer;
begin
if Assigned(FMainFlag) then FMainFlag.SetTheme(T);
for i := 0 to High(FSliceFlags) do
if Assigned(FSliceFlags[i]) then FSliceFlags[i].SetTheme(T);
end;
// ---- Диспетчер мыши флагов: FActiveFlag выставляется на время Handle*, чтобы
// события флага (синхронные) роутились по CurSliceId. ----
function TPanafallPanel.DispatchFlagsMouseDown(Button: TMouseButton; X, Y: Integer): Boolean;
var i: Integer;
begin
Result := False;
if Assigned(FMainFlag) and FMainFlag.Visible then
begin
FActiveFlag := FMainFlag;
if FMainFlag.HandleMouseDown(Button, X, Y) then Exit(True);
end;
for i := 0 to High(FSliceFlags) do
if (FSliceFlags[i] <> nil) and FSliceFlags[i].Visible then
begin
FActiveFlag := FSliceFlags[i];
if FSliceFlags[i].HandleMouseDown(Button, X, Y) then Exit(True);
end;
FActiveFlag := nil;
end;
procedure TPanafallPanel.DispatchFlagsMouseMove(X, Y: Integer);
var i: Integer;
begin
if Assigned(FMainFlag) then
begin
FActiveFlag := FMainFlag;
FMainFlag.HandleMouseMove(X, Y);
end;
for i := 0 to High(FSliceFlags) do
if FSliceFlags[i] <> nil then
begin
FActiveFlag := FSliceFlags[i];
FSliceFlags[i].HandleMouseMove(X, Y);
end;
FActiveFlag := nil;
end;
function TPanafallPanel.DispatchFlagsMouseUp(X, Y: Integer): Boolean;
var i: Integer;
begin
Result := False;
if Assigned(FMainFlag) then
begin
FActiveFlag := FMainFlag;
if FMainFlag.HandleMouseUp(X, Y) then Result := True;
end;
for i := 0 to High(FSliceFlags) do
if FSliceFlags[i] <> nil then
begin
FActiveFlag := FSliceFlags[i];
if FSliceFlags[i].HandleMouseUp(X, Y) then Result := True;
end;
FActiveFlag := nil;
end;
procedure TPanafallPanel.DispatchFlagsMouseLeave;
var i: Integer;
begin
if Assigned(FMainFlag) then FMainFlag.HandleMouseLeave;
for i := 0 to High(FSliceFlags) do
if FSliceFlags[i] <> nil then FSliceFlags[i].HandleMouseLeave;
end;
end.