mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:27:33 +00:00
Панель владеет коллекцией флагов B+ (создание/закрытие/заливка состояния из контроллера), раскладкой LayoutFlags, диспетчером мыши флагов, тиком S-метров (TickFlags) и видимостью флага A (UpdateMainFlagVisibility). Главный флаг A создаёт MainForm (проводка семантических событий у него) и отдаёт через AttachMainFlag; новые флаги проводятся через OnWireFlag. Роутинг по слайсу — FPan.CurSliceId; закрытие «×» — RequestSliceClose/OnSliceClosed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1012 lines
40 KiB
ObjectPascal
1012 lines
40 KiB
ObjectPascal
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, 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;
|
||
|
||
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;
|
||
|
||
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;
|
||
// Пользователь отпустил сплиттер (ratio уже обновлён) — хозяин делает
|
||
// полный пересчёт (у него wideband/S-метр/оверлеи).
|
||
property OnSplitterMoved: TNotifyEvent read FOnSplitterMoved write FOnSplitterMoved;
|
||
|
||
// ---- Флаги слайсов ----
|
||
// Главный флаг (слайс A) создаёт хозяин (проводка событий у него) и отдаёт
|
||
// сюда; панель подключает его к view и включает в раскладку/диспетчер.
|
||
procedure AttachMainFlag(O: TVfoOverlay);
|
||
// SliceId флага, чьё событие сейчас обрабатывается (0 = главный).
|
||
function CurSliceId: 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;
|
||
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;
|
||
|
||
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;
|
||
|
||
// ---- Панель пана/зума под водопадом ----
|
||
// Кнопки создаём с дефолтными цветами; финальная тема — у хозяина
|
||
// (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);
|
||
end;
|
||
|
||
procedure TPanafallPanel.PositionPanZoomBar(BottomY, RW, H: Integer;
|
||
AVisible: Boolean);
|
||
var BtnW, Gap, StripW, X: Integer;
|
||
begin
|
||
if (FPbPanZoom = nil) or (FBtnZoomIn = nil) then Exit;
|
||
FPbPanZoom.Visible := AVisible;
|
||
FBtnZoomIn.Visible := AVisible;
|
||
FBtnZoomDef.Visible := AVisible;
|
||
FBtnZoomOut.Visible := AVisible;
|
||
if not AVisible then Exit;
|
||
Gap := MulDiv(2, Screen.PixelsPerInch, 96);
|
||
BtnW := H; // квадратные кнопки в высоту строки
|
||
StripW := RW - 3 * BtnW - 4 * 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);
|
||
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;
|
||
ShowPZ: Boolean;
|
||
begin
|
||
if (FParent = nil) or (FPbSpectrum = nil) then Exit;
|
||
FShowSpectrum := AShowSpectrum;
|
||
FShowWaterfall := AShowWaterfall;
|
||
RULER_H := MulDiv(18, Screen.PixelsPerInch, 96);
|
||
RW := FParent.ClientWidth;
|
||
RH := FParent.ClientHeight;
|
||
// Резервируем низ под панель пана/зума (видна, если виден спектр или водопад).
|
||
PANZOOM_H := MulDiv(22, Screen.PixelsPerInch, 96);
|
||
ShowPZ := AShowSpectrum or AShowWaterfall;
|
||
PositionPanZoomBar(RH - PANZOOM_H, RW, PANZOOM_H, ShowPZ);
|
||
if ShowPZ then RH := RH - PANZOOM_H;
|
||
TopOff := ATopOffset;
|
||
|
||
// ---- Оба скрыты ----
|
||
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;
|
||
|
||
// Ограничиваем соотношение, чтобы каждая зона имела минимальный размер
|
||
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;
|
||
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;
|
||
|
||
// Только двигаем сплиттер визуально — без пересчёта битмапов
|
||
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;
|
||
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;
|
||
FSplitterDrag := False;
|
||
{$IFDEF WINDOWS}
|
||
ReleaseCapture;
|
||
{$ENDIF}
|
||
FSplitter.Color := TColor($00303030); // обычный цвет
|
||
|
||
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;
|
||
|
||
// Сохраняем новое соотношение; полный пересчёт с перерисовкой — у хозяина
|
||
// (ему виднее: 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.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(nil);
|
||
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;
|
||
FController.GetViewWindow(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;
|
||
FController.GetViewWindow(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(nil);
|
||
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.
|