mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
feat(slices): этап 3.3 — UI стек панадаптеров
Несколько панадаптеров в главном окне (кнопка «⊞», до MAX_PANS): - TPanafallPanel: регион в стеке (StackTop/Height), шапка с «×», pan-aware ViewWindow, FirstSliceId; явный crArrow GL-канвам (WA_NativeWindow-поверхность на Wayland навсегда наследует курсор момента создания — прилипала «рука» кнопки «⊞») - MainForm: FPans[]/LayoutPanStack (доли + межпанные сплиттеры с живым призраком), создание/закрытие панов (DDC+слайс+вьюха), пиксели панов из display-потока под FPanCbLock, рендер по dirty в тике - Мышь панов N: Ctrl+ЛКМ = слайс, drag маркера = ретюн слайса, drag фона = live-ретюн DDC (гистерезис 4px), клик = тюн слайса пана (PanClickTune, снап 100 Гц), флаг-диспетчер только на спектре - Сплиттер спектр/водопад: drag-математика от региона пана (FSplitTopOff/FSplitAvailH из Layout), OnSplitterMoved у панов N - Паны живут в Run-сессии, умирают на STOP Проверено юзером на экране (3 раунда фидбека) + e2e против hpsdrsim. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+784
-18
@@ -24,7 +24,7 @@ interface
|
||||
uses
|
||||
Classes, SysUtils, StrUtils, FreqDisplay, DeviceForm, VfoOverlay, SampleRateOverlay, BandPlanOverlay,
|
||||
FlatButton, FlatSlider, FlatDropDown, AppTheme, Forms, Controls, Graphics, Dialogs,
|
||||
StdCtrls, ExtCtrls, Buttons, Menus, Math, Types,
|
||||
StdCtrls, ExtCtrls, Buttons, Menus, Math, Types, SyncObjs,
|
||||
OpenGLContextEx,
|
||||
LCLIntf, LCLType, GraphType,
|
||||
HPSDRProtocol, HPSDRNetwork,
|
||||
@@ -159,6 +159,31 @@ type
|
||||
// (исторические имена; постепенная миграция на FPan.*). Размеры областей —
|
||||
// FPan.SpectrumWidth/SpectrumHeight/WaterfallHeight.
|
||||
FPan: TPanafallPanel;
|
||||
// ---- Стек панов (этап 3.3): FPans[0] = FPan, FPans[N>0] — доп. паны на
|
||||
// своих DDC (nil = не создан). Сплиттер FPanSplitters[i] стоит НАД паном i.
|
||||
FPans: array[0..MAX_PANS-1] of TPanafallPanel;
|
||||
FPanSplitters: array[1..MAX_PANS-1] of TPanel;
|
||||
FPanShare: array[0..MAX_PANS-1] of Double; // доля высоты стека
|
||||
FPanSpecDirty: array[0..MAX_PANS-1] of Boolean; // кадр пана готов к рендеру
|
||||
FPanWfDirty: array[0..MAX_PANS-1] of Boolean;
|
||||
FPanCbLock: SyncObjs.TCriticalSection; // жизнь FPans[i] vs пиксельные колбэки
|
||||
// Мышь на пане N>0: drag слайса или drag центра пана (ретюн DDC).
|
||||
FPanSliceDrag: Boolean;
|
||||
FPanSliceDragId: Integer;
|
||||
FPanCenterDrag: Boolean;
|
||||
FPanCenterMoved: Boolean; // был реальный сдвиг (иначе up = клик-тюн)
|
||||
FPanCenterDragX0: Integer;
|
||||
FPanCenterDragF0: Double;
|
||||
FPanMousePan: TPanafallPanel; // пан, в котором идёт drag
|
||||
// Сплиттер между панами (индекс пана, чей верхний сплиттер тащим; 0 = нет).
|
||||
// Во время drag сплиттер двигается «призраком» (клампы в FPanSplitMin/MaxTop),
|
||||
// пересчёт долей — по отпусканию.
|
||||
FPanSplitDrag: Integer;
|
||||
FPanSplitY0: Integer;
|
||||
FPanSplitTop0: Integer;
|
||||
FPanSplitMinTop: Integer;
|
||||
FPanSplitMaxTop: Integer;
|
||||
FPendingPanClose: Integer; // «×» шапки: PanId к закрытию в тике (0 = нет)
|
||||
FSpecView: TSpectrumView;
|
||||
FWidebandView: TWidebandView;
|
||||
FUseOpenGLSpectrum: Boolean;
|
||||
@@ -533,6 +558,44 @@ type
|
||||
function CurSliceId: Integer; // SliceId обрабатываемого сейчас флага
|
||||
procedure WireOverlayEvents(O: TVfoOverlay);
|
||||
procedure AddSliceAtFreq(Hz: Double);
|
||||
// --- Стек панов (этап 3.3) ---
|
||||
function PanCount: Integer; // активных панов (>=1)
|
||||
function PanFromSender(Sender: TObject): TPanafallPanel;
|
||||
function PanOfOverlay(Sender: TObject): TPanafallPanel;
|
||||
procedure PushFlagStateAllPans(SliceId: Integer);
|
||||
procedure LayoutFlagsAllPans;
|
||||
procedure OnAddPanClick(Sender: TObject); // «⊞» пана 0
|
||||
procedure OnPanHeaderClose(Sender: TObject); // «×» шапки пана
|
||||
procedure ClosePanadapter(PanId: Integer);
|
||||
procedure DestroyAllExtraPans; // stop/выход
|
||||
procedure UpdatePanHeaders; // текст+видимость шапок
|
||||
procedure UpdateAddPanButton;
|
||||
procedure AddSliceAtFreqPan(P: TPanafallPanel; Hz: Double);
|
||||
procedure PanClickTune(P: TPanafallPanel; X, W: Integer); // клик по фону пана N
|
||||
procedure PanViewWindow(P: TPanafallPanel; out VC, VS: Double);
|
||||
procedure SyncPanViews; // центр/span вьюх панов
|
||||
procedure MarkPanDirty(P: TPanafallPanel); // репейнт пана N
|
||||
// Мышь панов N>0 (у пана 0 семантика ниже, PbSpectrum*).
|
||||
procedure PanNSpectrumMouseDown(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
procedure PanNSpectrumMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
|
||||
procedure PanNSpectrumMouseUp(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
procedure PanNSpectrumMouseLeave(Sender: TObject);
|
||||
procedure PanNSpectrumDblClick(Sender: TObject);
|
||||
// Пиксели панов из display-потока движка.
|
||||
procedure OnPanSpectrumReady(PanId: Integer; const Pixels: array of Single;
|
||||
Count: Integer);
|
||||
procedure OnPanWaterfallReady(PanId: Integer; const Pixels: array of Single;
|
||||
Count: Integer);
|
||||
// Сплиттеры между панами.
|
||||
procedure PanSplitMouseDown(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
procedure PanSplitMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
|
||||
procedure PanSplitMouseUp(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
procedure LayoutPanStack(AWideH: Integer); // регионы панов + Layout каждого
|
||||
function Pan0HeaderH: Integer; // высота шапки пана 0 (0 без стека)
|
||||
procedure PbSpectrumDblClick(Sender: TObject);
|
||||
procedure TrkDriveChange(Sender: TObject);
|
||||
procedure UpdateDriveLabel; // % (openHPSDR) / дБ (Pluto) рядом с ползунком drive
|
||||
@@ -990,6 +1053,12 @@ begin
|
||||
FPan.OnSliceClosed := OnPanSliceClosed;
|
||||
FPan.OnFlagsInvalidate := OnVfoOverlayInvalidate;
|
||||
FPan.AttachMainFlag(FVfoOverlay); // внутри: FSpecView.VfoOverlay := флаг A
|
||||
// Стек панов (этап 3.3): пан 0 — в общем массиве; «⊞» добавляет пан.
|
||||
FPan.PanId := 0;
|
||||
FPan.OnAddPan := OnAddPanClick;
|
||||
FPans[0] := FPan;
|
||||
FPanShare[0] := 1.0;
|
||||
FPanCbLock := SyncObjs.TCriticalSection.Create;
|
||||
FSpecView.VfoA := FController.FVfoA;
|
||||
FSpecView.VfoB := FController.FVfoB;
|
||||
FSpecView.ActiveVfo := FController.FActiveVfo;
|
||||
@@ -1040,6 +1109,9 @@ begin
|
||||
FController.FDSPEngine.OnAudio := FController.OnAudioReady;
|
||||
FController.FDSPEngine.OnSpectrum := FController.OnSpectrumReady;
|
||||
FController.FDSPEngine.OnWaterfall := FController.OnWaterfallReady;
|
||||
// Пиксели доп. панов (этап 3.3) — прямо в UI (display-поток; см. FPanCbLock).
|
||||
FController.FDSPEngine.OnPanSpectrum := OnPanSpectrumReady;
|
||||
FController.FDSPEngine.OnPanWaterfall := OnPanWaterfallReady;
|
||||
FController.FDSPEngine.OnTXIQ := FController.OnTXIQ;
|
||||
// Web-клиент перехватывает RX-аудио (моно-микс) у контроллера, если подключён.
|
||||
FController.OnAudioConsume := FWebAdapter.AudioConsume;
|
||||
@@ -1093,7 +1165,10 @@ begin
|
||||
PanelSplitter := nil;
|
||||
PbWaterfall := nil;
|
||||
PbPanZoom := nil;
|
||||
DestroyAllExtraPans; // доп. паны (DDC/движок/панели) — до пана 0
|
||||
FreeAndNil(FPan);
|
||||
FPans[0] := nil;
|
||||
FreeAndNil(FPanCbLock);
|
||||
FreeAndNil(FWidebandView);
|
||||
|
||||
// Сохраняем настройки при закрытии
|
||||
@@ -1216,6 +1291,8 @@ begin
|
||||
BtnStartStop.Caption := 'STOP';
|
||||
StyleButton(BtnStartStop, True);
|
||||
BtnMOX.Enabled := True;
|
||||
UpdateAddPanButton; // «⊞» доступна только в Run
|
||||
ResizeSpectrumPanels;
|
||||
end
|
||||
else
|
||||
begin
|
||||
@@ -1223,6 +1300,11 @@ begin
|
||||
BtnStartStop.Caption := 'START';
|
||||
StyleButton(BtnStartStop, False);
|
||||
BtnMOX.Enabled := False;
|
||||
// Доп. паны живут только в Run-сессии (их DDC уже погашены
|
||||
// контроллером в StopRunning) — убираем и UI.
|
||||
DestroyAllExtraPans;
|
||||
UpdateAddPanButton;
|
||||
ResizeSpectrumPanels;
|
||||
end;
|
||||
rfADCOverload:
|
||||
if FSpecView <> nil then
|
||||
@@ -2435,7 +2517,8 @@ begin
|
||||
WideSpecH := Max(1, WideH - RULER_H);
|
||||
if PbWideband <> nil then
|
||||
begin
|
||||
PbWideband.SetBounds(0, 0, RW, WideSpecH);
|
||||
// При стеке панов над wideband стоит шапка пана 0.
|
||||
PbWideband.SetBounds(0, Pan0HeaderH, RW, WideSpecH);
|
||||
PbWideband.Visible := FController.FShowWideband and (WideSpecH > 0);
|
||||
if FController.FShowWideband and (RW > 0) and (WideSpecH > 0) then
|
||||
begin
|
||||
@@ -2445,7 +2528,7 @@ begin
|
||||
end;
|
||||
if PbWidebandRuler <> nil then
|
||||
begin
|
||||
PbWidebandRuler.SetBounds(0, WideSpecH, RW, IfThen(FController.FShowWideband and (WideH > 0), RULER_H, 0));
|
||||
PbWidebandRuler.SetBounds(0, Pan0HeaderH + WideSpecH, RW, IfThen(FController.FShowWideband and (WideH > 0), RULER_H, 0));
|
||||
PbWidebandRuler.Visible := FController.FShowWideband and (WideH > 0);
|
||||
if FController.FShowWideband and (RW > 0) and (WideH > 0) then
|
||||
begin
|
||||
@@ -2456,11 +2539,61 @@ begin
|
||||
// S-метр позиционируется отдельно в ResizeSMeter
|
||||
ResizeSMeter;
|
||||
|
||||
// Стек спектр/линейка/сплиттер/водопад/пан-зум раскладывает панадаптер.
|
||||
FPan.Layout(WideH, FController.FShowSpectrum, FController.FShowWaterfall);
|
||||
// Стек панов: регионы по долям, каждый пан раскладывает свой стек
|
||||
// спектр/линейка/сплиттер/водопад/пан-зум внутри своего региона.
|
||||
LayoutPanStack(WideH);
|
||||
PositionSampleRateOverlay;
|
||||
end;
|
||||
|
||||
function TMainForm.Pan0HeaderH: Integer;
|
||||
begin
|
||||
if PanCount > 1 then
|
||||
Result := MulDiv(20, Screen.PixelsPerInch, 96) // = HEADER_H панели
|
||||
else
|
||||
Result := 0;
|
||||
end;
|
||||
|
||||
procedure TMainForm.LayoutPanStack(AWideH: Integer);
|
||||
const
|
||||
PAN_SPLIT_H = 5;
|
||||
var
|
||||
ActiveIds: array[0..MAX_PANS-1] of Integer;
|
||||
N, i, k, TotalH, UsableH, Y, Hi: Integer;
|
||||
ShareSum: Double;
|
||||
begin
|
||||
N := 0;
|
||||
for i := 0 to MAX_PANS - 1 do
|
||||
if FPans[i] <> nil then begin ActiveIds[N] := i; Inc(N); end;
|
||||
if N = 0 then Exit;
|
||||
TotalH := PanelRight.ClientHeight;
|
||||
UsableH := TotalH - (N - 1) * PAN_SPLIT_H;
|
||||
ShareSum := 0;
|
||||
for k := 0 to N - 1 do ShareSum := ShareSum + FPanShare[ActiveIds[k]];
|
||||
if ShareSum <= 0 then ShareSum := 1;
|
||||
Y := 0;
|
||||
for k := 0 to N - 1 do
|
||||
begin
|
||||
i := ActiveIds[k];
|
||||
if k > 0 then
|
||||
begin
|
||||
FPanSplitters[i].SetBounds(0, Y, PanelRight.ClientWidth, PAN_SPLIT_H);
|
||||
Inc(Y, PAN_SPLIT_H);
|
||||
end;
|
||||
if k = N - 1 then
|
||||
Hi := TotalH - Y // последний пан добирает остаток (округления долей)
|
||||
else
|
||||
Hi := Round(UsableH * FPanShare[i] / ShareSum);
|
||||
FPans[i].StackTop := Y;
|
||||
FPans[i].StackHeight := Hi;
|
||||
FPans[i].HeaderVisible := N > 1;
|
||||
if i = 0 then
|
||||
FPans[i].Layout(AWideH, FController.FShowSpectrum, FController.FShowWaterfall)
|
||||
else
|
||||
FPans[i].Layout(0, FController.FShowSpectrum, FController.FShowWaterfall);
|
||||
Inc(Y, Hi);
|
||||
end;
|
||||
end;
|
||||
|
||||
// ===========================================================================
|
||||
// Dark Theme
|
||||
// ===========================================================================
|
||||
@@ -2536,6 +2669,23 @@ begin
|
||||
if BtnZoomIn <> nil then StyleButton(BtnZoomIn, False);
|
||||
if BtnZoomDef <> nil then StyleButton(BtnZoomDef, False);
|
||||
if BtnZoomOut <> nil then StyleButton(BtnZoomOut, False);
|
||||
// Стек панов: шапки/кнопки всех панелей (пан 0 — «⊞», паны N — «×»).
|
||||
for i := 0 to MAX_PANS - 1 do
|
||||
if FPans[i] <> nil then
|
||||
begin
|
||||
if FPans[i].BtnAddPan <> nil then StyleButton(FPans[i].BtnAddPan, False);
|
||||
if FPans[i].BtnClosePan <> nil then StyleButton(FPans[i].BtnClosePan, False);
|
||||
if FPans[i].HeaderPanel <> nil then
|
||||
begin
|
||||
FPans[i].HeaderPanel.Color := T.Panel;
|
||||
FPans[i].HeaderLabel.Font.Color := T.Text;
|
||||
end;
|
||||
if i > 0 then
|
||||
begin
|
||||
FPans[i].SetFlagsTheme(T);
|
||||
if FPanSplitters[i] <> nil then FPanSplitters[i].Color := T.Border;
|
||||
end;
|
||||
end;
|
||||
if FPanZoomBar <> nil then
|
||||
begin
|
||||
FPanZoomBar.SetTheme(T);
|
||||
@@ -2938,6 +3088,13 @@ var
|
||||
ElapsedTick: QWord;
|
||||
I: Integer;
|
||||
begin
|
||||
// «×» шапки пана: закрытие отложено сюда (нельзя free панель из её кнопки).
|
||||
if FPendingPanClose <> 0 then
|
||||
begin
|
||||
I := FPendingPanClose;
|
||||
FPendingPanClose := 0;
|
||||
ClosePanadapter(I);
|
||||
end;
|
||||
// Синхронизируем размеры если размер панели изменился (после ресайза).
|
||||
// Важно: проверяем только видимые панели — когда спектр скрыт,
|
||||
// PbSpectrum.Height может быть ненулевым при FPan.SpectrumHeight=0,
|
||||
@@ -3009,6 +3166,27 @@ begin
|
||||
FSpecView.WDSPReady := FController.FWDSPReady;
|
||||
// Флаги: метр/частота главного, позиции, троттленный S-метр слайсов.
|
||||
FPan.TickFlags;
|
||||
// Доп. паны: синк вьюх + тик флагов + рендер готовых кадров.
|
||||
SyncPanViews;
|
||||
for I := 1 to MAX_PANS - 1 do
|
||||
if FPans[I] <> nil then
|
||||
begin
|
||||
FPans[I].TickFlags;
|
||||
if FPanSpecDirty[I] then
|
||||
begin
|
||||
FPans[I].View.DrawSpectrum;
|
||||
FPans[I].PbSpectrum.Invalidate;
|
||||
if FPans[I].View.NeedsRulerRedraw then
|
||||
FPans[I].PbRuler.Invalidate;
|
||||
FPanSpecDirty[I] := False;
|
||||
end;
|
||||
if FPanWfDirty[I] then
|
||||
begin
|
||||
FPans[I].View.DrawWaterfall;
|
||||
FPans[I].PbWaterfall.Invalidate;
|
||||
FPanWfDirty[I] := False;
|
||||
end;
|
||||
end;
|
||||
// Панорама и водопад теперь рисуются независимо по факту новых кадров.
|
||||
// Это лучше совпадает с Thetis: маленький FFT даёт новые строки чаще.
|
||||
if FSpectrumDirty then
|
||||
@@ -3266,6 +3444,9 @@ begin
|
||||
FController.FDSPEngine.OnAudio := FController.OnAudioReady;
|
||||
FController.FDSPEngine.OnSpectrum := FController.OnSpectrumReady;
|
||||
FController.FDSPEngine.OnWaterfall := FController.OnWaterfallReady;
|
||||
// Пиксели доп. панов (этап 3.3) — прямо в UI (display-поток; см. FPanCbLock).
|
||||
FController.FDSPEngine.OnPanSpectrum := OnPanSpectrumReady;
|
||||
FController.FDSPEngine.OnPanWaterfall := OnPanWaterfallReady;
|
||||
FController.FDSPEngine.OnTXIQ := FController.OnTXIQ;
|
||||
// Иначе sound-card mic путь теряется после смены sample rate (колбэк
|
||||
// навешивается в FormCreate, но новый движок про него не знал).
|
||||
@@ -4823,8 +5004,8 @@ begin
|
||||
if CurSliceId <> 0 then
|
||||
begin
|
||||
FController.SetSliceModeBW(CurSliceId, Mode, FilterBW);
|
||||
FPan.PushSliceFlagState(CurSliceId);
|
||||
FPan.LayoutFlags;
|
||||
PushFlagStateAllPans(CurSliceId);
|
||||
LayoutFlagsAllPans;
|
||||
Exit;
|
||||
end;
|
||||
FController.FMode := Mode;
|
||||
@@ -4944,7 +5125,7 @@ begin
|
||||
if CurSliceId <> 0 then
|
||||
begin
|
||||
FController.SetSliceDevice(CurSliceId, DevIndex, DevName);
|
||||
FPan.PushSliceFlagState(CurSliceId); // обновить подсветку текущего устройства слайса
|
||||
PushFlagStateAllPans(CurSliceId); // обновить подсветку текущего устройства слайса
|
||||
Exit;
|
||||
end;
|
||||
ApplyAudioDevice(DevIndex, DevName);
|
||||
@@ -4952,7 +5133,15 @@ begin
|
||||
end;
|
||||
|
||||
procedure TMainForm.OnVfoOverlayInvalidate(Sender: TObject);
|
||||
var P: TPanafallPanel;
|
||||
begin
|
||||
// Флаг/панель доп. пана → грязним и репейнтим ЕГО канву, пан 0 не трогаем.
|
||||
P := PanOfOverlay(Sender);
|
||||
if (P <> nil) and (P <> FPan) then
|
||||
begin
|
||||
MarkPanDirty(P);
|
||||
Exit;
|
||||
end;
|
||||
FSpectrumDirty := True;
|
||||
if Assigned(FSpecView) then
|
||||
begin
|
||||
@@ -4981,8 +5170,14 @@ end;
|
||||
|
||||
// SliceId флага, чьё событие сейчас обрабатывается (0 = главный приёмник / слайс A).
|
||||
function TMainForm.CurSliceId: Integer;
|
||||
// Диспетчер флагов синхронный — ненулевой CurSliceId сейчас максимум у одной
|
||||
// панели (той, чей флаг обрабатывает событие).
|
||||
var i: Integer;
|
||||
begin
|
||||
Result := FPan.CurSliceId;
|
||||
Result := 0;
|
||||
for i := 0 to MAX_PANS - 1 do
|
||||
if (FPans[i] <> nil) and (FPans[i].CurSliceId <> 0) then
|
||||
Exit(FPans[i].CurSliceId);
|
||||
end;
|
||||
|
||||
// Единая проводка событий флага (главного и слайсов) на общие обработчики.
|
||||
@@ -5018,6 +5213,568 @@ begin
|
||||
FActiveSliceId := Id;
|
||||
end;
|
||||
|
||||
// ===========================================================================
|
||||
// Стек панадаптеров (этап 3.3): доп. паны на своих DDC
|
||||
// ===========================================================================
|
||||
|
||||
function TMainForm.PanCount: Integer;
|
||||
var i: Integer;
|
||||
begin
|
||||
Result := 0;
|
||||
for i := 0 to MAX_PANS - 1 do
|
||||
if FPans[i] <> nil then Inc(Result);
|
||||
end;
|
||||
|
||||
function TMainForm.PanFromSender(Sender: TObject): TPanafallPanel;
|
||||
// Контролы панели создаются с Owner=панель (Build) — восстановление пана
|
||||
// из Sender мышиного события.
|
||||
begin
|
||||
Result := nil;
|
||||
if (Sender is TComponent) and (TComponent(Sender).Owner is TPanafallPanel) then
|
||||
Result := TPanafallPanel(TComponent(Sender).Owner);
|
||||
end;
|
||||
|
||||
function TMainForm.PanOfOverlay(Sender: TObject): TPanafallPanel;
|
||||
// Панель, которой принадлежит источник события: сама панель (Sender=панель),
|
||||
// её флаг (Owner=панель) или главный флаг/nil → пан 0.
|
||||
begin
|
||||
Result := FPan;
|
||||
if Sender is TPanafallPanel then Exit(TPanafallPanel(Sender));
|
||||
if (Sender is TComponent) and (TComponent(Sender).Owner is TPanafallPanel) then
|
||||
Result := TPanafallPanel(TComponent(Sender).Owner);
|
||||
end;
|
||||
|
||||
procedure TMainForm.PushFlagStateAllPans(SliceId: Integer);
|
||||
// Флаг слайса живёт ровно на одной панели; у остальных PushSliceFlagState
|
||||
// не найдёт флаг и выйдет.
|
||||
var i: Integer;
|
||||
begin
|
||||
for i := 0 to MAX_PANS - 1 do
|
||||
if FPans[i] <> nil then FPans[i].PushSliceFlagState(SliceId);
|
||||
end;
|
||||
|
||||
procedure TMainForm.LayoutFlagsAllPans;
|
||||
var i: Integer;
|
||||
begin
|
||||
for i := 0 to MAX_PANS - 1 do
|
||||
if FPans[i] <> nil then FPans[i].LayoutFlags;
|
||||
end;
|
||||
|
||||
procedure TMainForm.MarkPanDirty(P: TPanafallPanel);
|
||||
begin
|
||||
if P = nil then Exit;
|
||||
if P = FPan then
|
||||
begin
|
||||
FSpectrumDirty := True;
|
||||
if Assigned(FSpecView) then FSpecView.SpectrumDirty := True;
|
||||
if Assigned(PbSpectrum) then PbSpectrum.Invalidate;
|
||||
Exit;
|
||||
end;
|
||||
FPanSpecDirty[P.PanId] := True;
|
||||
P.View.SpectrumDirty := True;
|
||||
if P.PbSpectrum <> nil then P.PbSpectrum.Invalidate;
|
||||
end;
|
||||
|
||||
procedure TMainForm.PanViewWindow(P: TPanafallPanel; out VC, VS: Double);
|
||||
// Видимое окно доп. пана: центр DDC ± rate/2 (пан-зума пока нет).
|
||||
begin
|
||||
VC := FController.PanDDCFreq(P.PanId);
|
||||
VS := FController.PanDDCRateKHz(P.PanId) * 1000.0;
|
||||
if VS <= 0 then VS := 384000;
|
||||
end;
|
||||
|
||||
procedure TMainForm.AddSliceAtFreqPan(P: TPanafallPanel; Hz: Double);
|
||||
// Слайс в доп. пане (режим/полоса копируются с главного, звуковуха default).
|
||||
var Lo, Hi, Id: Integer;
|
||||
begin
|
||||
if not FController.SliceFitsCapture(Hz, P.PanId) then Exit;
|
||||
TRadioController.FilterEdgesFor(FController.FMode, FController.FFilterBW, Lo, Hi);
|
||||
Id := FController.AddSlice(Hz, FController.FMode, Lo, Hi,
|
||||
TRadioController.AGCModeFromUI(FController.FAGCMode), 0.7, -1, '',
|
||||
P.PanId);
|
||||
if Id < 0 then Exit;
|
||||
P.CreateSliceFlag(Id);
|
||||
FActiveSliceId := Id;
|
||||
end;
|
||||
|
||||
procedure TMainForm.PanClickTune(P: TPanafallPanel; X, W: Integer);
|
||||
// Клик по фону доп. пана = перестроить его слайс на частоту клика (аналог
|
||||
// DoSpectrumClick главного: снап к 100 Гц). Цель — активный слайс, если он
|
||||
// живёт в этом пане, иначе первый слайс пана; слайсов нет — создать в точке.
|
||||
const
|
||||
StepHz = 100;
|
||||
var
|
||||
VC, VS, LoHz, HiHz: Double;
|
||||
Hz: Int64;
|
||||
Id: Integer;
|
||||
S: TCtrlSlice;
|
||||
begin
|
||||
PanViewWindow(P, VC, VS);
|
||||
if (W <= 0) or (VS <= 0) then Exit;
|
||||
Hz := Round(VC + (X / W - 0.5) * VS);
|
||||
Hz := ((Hz + StepHz div 2) div StepHz) * StepHz;
|
||||
FController.CapturedSpan(LoHz, HiHz, P.PanId);
|
||||
if Hz < LoHz then Hz := Round(LoHz);
|
||||
if Hz > HiHz then Hz := Round(HiHz);
|
||||
if (FActiveSliceId <> 0) and FController.GetSlice(FActiveSliceId, S) and
|
||||
(S.PanId = P.PanId) then
|
||||
Id := FActiveSliceId
|
||||
else
|
||||
Id := P.FirstSliceId;
|
||||
if Id = 0 then
|
||||
begin
|
||||
AddSliceAtFreqPan(P, Hz);
|
||||
Exit;
|
||||
end;
|
||||
FController.SetSliceTarget(Id, Hz);
|
||||
FActiveSliceId := Id;
|
||||
P.PushSliceFlagState(Id);
|
||||
P.LayoutFlags;
|
||||
MarkPanDirty(P);
|
||||
end;
|
||||
|
||||
procedure TMainForm.UpdatePanHeaders;
|
||||
// Текст/видимость шапок: у одного пана шапки нет, у стека — у всех.
|
||||
var
|
||||
i, N: Integer;
|
||||
P: TPanafallPanel;
|
||||
begin
|
||||
N := PanCount;
|
||||
for i := 0 to MAX_PANS - 1 do
|
||||
begin
|
||||
P := FPans[i];
|
||||
if P = nil then Continue;
|
||||
P.HeaderVisible := N > 1;
|
||||
if P.BtnClosePan <> nil then P.BtnClosePan.Visible := (N > 1) and (i > 0);
|
||||
if i = 0 then
|
||||
P.SetHeaderText(Format('PAN 0 · %.6f MHz · %d kHz',
|
||||
[FController.FCenterFreq / 1e6, FController.FSampleRate div 1000]))
|
||||
else
|
||||
P.SetHeaderText(Format('PAN %d · %.6f MHz · %d kHz',
|
||||
[i, FController.PanDDCFreq(i) / 1e6, FController.PanDDCRateKHz(i)]));
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TMainForm.UpdateAddPanButton;
|
||||
var HasSlot: Boolean; i: Integer;
|
||||
begin
|
||||
HasSlot := False;
|
||||
for i := 1 to MAX_PANS - 1 do
|
||||
if FPans[i] = nil then begin HasSlot := True; Break; end;
|
||||
FPan.AddPanEnabled := (FController.BackendCaps.MaxPans > 1) and
|
||||
FController.FRunning and HasSlot;
|
||||
end;
|
||||
|
||||
procedure TMainForm.OnAddPanClick(Sender: TObject);
|
||||
// «⊞»: новый пан на своём DDC. Центр = частота активного VFO, rate 384к,
|
||||
// ADC 0; в центре автоматически создаётся первый слайс.
|
||||
var
|
||||
PanId, i: Integer;
|
||||
Freq: Double;
|
||||
P: TPanafallPanel;
|
||||
Split: TPanel;
|
||||
begin
|
||||
if not FController.FRunning then Exit;
|
||||
PanId := -1;
|
||||
for i := 1 to MAX_PANS - 1 do
|
||||
if FPans[i] = nil then begin PanId := i; Break; end;
|
||||
if PanId < 0 then Exit;
|
||||
|
||||
Freq := ActiveVfoFreq;
|
||||
if not FController.AddPanDDC(PanId, Freq, 384, 0) then Exit;
|
||||
|
||||
P := TPanafallPanel.Create(nil, FUseOpenGLSpectrum);
|
||||
P.PanId := PanId;
|
||||
P.Controller := FController;
|
||||
P.ShowZoomRow := False; // пан-зум доп. панов — этап 3.3+
|
||||
P.OnWireFlag := WireOverlayEvents;
|
||||
P.OnSliceClosed := OnPanSliceClosed;
|
||||
P.OnFlagsInvalidate := OnVfoOverlayInvalidate;
|
||||
P.OnPanClose := OnPanHeaderClose;
|
||||
P.OnSplitterMoved := RightPanelResize; // ratio обновлён → полный re-layout
|
||||
P.SpectrumMouseDown := PanNSpectrumMouseDown;
|
||||
P.SpectrumMouseMove := PanNSpectrumMouseMove;
|
||||
P.SpectrumMouseUp := PanNSpectrumMouseUp;
|
||||
P.SpectrumMouseLeave := PanNSpectrumMouseLeave;
|
||||
P.SpectrumDblClick := PanNSpectrumDblClick;
|
||||
P.WaterfallMouseDown := PanNSpectrumMouseDown; // X→частота одинаково
|
||||
P.WaterfallMouseMove := PanNSpectrumMouseMove;
|
||||
P.WaterfallMouseUp := PanNSpectrumMouseUp;
|
||||
P.Build(PanelRight, Max(1, FController.FSpecMSAA));
|
||||
|
||||
// Параметры отображения — с главного view (пан-персональные — позже).
|
||||
P.View.WfAGCEnabled := FController.FWfAGCEnabled;
|
||||
P.View.WfNFEnabled := FController.FWfNFEnabled;
|
||||
P.View.WfManualHigh := FController.FWfManualHigh;
|
||||
P.View.WfManualLow := FController.FWfManualLow;
|
||||
P.View.WfAGCOffset := FController.FWfAGCOffset;
|
||||
P.View.WfPalette := FController.FWfPalette;
|
||||
P.View.WfGamma := FController.FWfGamma;
|
||||
P.View.SpecRefLevel := FController.FSpecRefLevel;
|
||||
P.View.SpecRange := FController.FSpecRange;
|
||||
P.View.SpecGridStep := FController.FSpecGridStep;
|
||||
P.View.WfFrameInterval := EffectiveWfInterval;
|
||||
P.View.PAMaxPower := FController.FPAMaxPower;
|
||||
P.View.CenterFreq := Freq;
|
||||
P.View.SpanHz := 384000;
|
||||
// VFO/TX-маркеры главного на доп. пане не рисуем — уводим за пределы окна.
|
||||
P.View.VfoA := -1e15;
|
||||
P.View.VfoB := -1e15;
|
||||
P.View.TXFreq := -1e15;
|
||||
|
||||
// Сплиттер над паном.
|
||||
Split := TPanel.Create(Self);
|
||||
Split.Parent := PanelRight;
|
||||
Split.BevelOuter := bvNone;
|
||||
Split.Color := TColor($00303030);
|
||||
Split.Cursor := crVSplit;
|
||||
Split.Tag := PanId;
|
||||
Split.OnMouseDown := PanSplitMouseDown;
|
||||
Split.OnMouseMove := PanSplitMouseMove;
|
||||
Split.OnMouseUp := PanSplitMouseUp;
|
||||
FPanSplitters[PanId] := Split;
|
||||
|
||||
// Публикуем пан для пиксельных колбэков (display-поток) и делим высоту поровну.
|
||||
FPanCbLock.Enter;
|
||||
try
|
||||
FPans[PanId] := P;
|
||||
finally
|
||||
FPanCbLock.Leave;
|
||||
end;
|
||||
for i := 0 to MAX_PANS - 1 do
|
||||
if FPans[i] <> nil then FPanShare[i] := 1.0 / PanCount;
|
||||
|
||||
AddSliceAtFreqPan(P, Freq);
|
||||
ApplyDarkTheme; // темизация шапки/кнопок нового пана
|
||||
UpdatePanHeaders;
|
||||
UpdateAddPanButton;
|
||||
ResizeSpectrumPanels;
|
||||
end;
|
||||
|
||||
procedure TMainForm.OnPanHeaderClose(Sender: TObject);
|
||||
// «×» шапки: нельзя free панель из обработчика её же кнопки — откладываем
|
||||
// в тик (FPendingPanClose).
|
||||
begin
|
||||
if Sender is TPanafallPanel then
|
||||
FPendingPanClose := TPanafallPanel(Sender).PanId;
|
||||
end;
|
||||
|
||||
procedure TMainForm.ClosePanadapter(PanId: Integer);
|
||||
var
|
||||
P: TPanafallPanel;
|
||||
S: TCtrlSlice;
|
||||
i: Integer;
|
||||
begin
|
||||
if (PanId < 1) or (PanId >= MAX_PANS) then Exit;
|
||||
P := FPans[PanId];
|
||||
if P = nil then Exit;
|
||||
// Отписываем от пиксельных колбэков ДО удаления DDC/панели.
|
||||
FPanCbLock.Enter;
|
||||
try
|
||||
FPans[PanId] := nil;
|
||||
finally
|
||||
FPanCbLock.Leave;
|
||||
end;
|
||||
// Слайсы пана (со звуковухами) + движок + DDC.
|
||||
FController.RemovePanDDC(PanId);
|
||||
if (FActiveSliceId <> 0) and not FController.GetSlice(FActiveSliceId, S) then
|
||||
FActiveSliceId := 0;
|
||||
FreeAndNil(FPanSplitters[PanId]);
|
||||
P.Free;
|
||||
for i := 0 to MAX_PANS - 1 do
|
||||
if FPans[i] <> nil then FPanShare[i] := 1.0 / PanCount;
|
||||
UpdatePanHeaders;
|
||||
UpdateAddPanButton;
|
||||
ResizeSpectrumPanels;
|
||||
end;
|
||||
|
||||
procedure TMainForm.DestroyAllExtraPans;
|
||||
var i: Integer;
|
||||
begin
|
||||
for i := 1 to MAX_PANS - 1 do
|
||||
if FPans[i] <> nil then ClosePanadapter(i);
|
||||
end;
|
||||
|
||||
procedure TMainForm.SyncPanViews;
|
||||
// Центр/span вьюх доп. панов (аналог SyncSpecViewFreq главного, но без
|
||||
// VFO/TX-маркеров — они уведены за окно при создании).
|
||||
var i: Integer; P: TPanafallPanel;
|
||||
begin
|
||||
for i := 1 to MAX_PANS - 1 do
|
||||
begin
|
||||
P := FPans[i];
|
||||
if P = nil then Continue;
|
||||
P.View.CenterFreq := FController.PanDDCFreq(i);
|
||||
P.View.SpanHz := FController.PanDDCRateKHz(i) * 1000.0;
|
||||
P.View.WDSPReady := FController.FWDSPReady;
|
||||
end;
|
||||
// Шапки следуют за частотами (Caption-сеттер — no-op при том же тексте).
|
||||
if PanCount > 1 then UpdatePanHeaders;
|
||||
end;
|
||||
|
||||
procedure TMainForm.OnPanSpectrumReady(PanId: Integer;
|
||||
const Pixels: array of Single; Count: Integer);
|
||||
// Display-поток движка. Лок закрывает гонку с Close/AddPanadapter (UI).
|
||||
begin
|
||||
FPanCbLock.Enter;
|
||||
try
|
||||
if (PanId >= 1) and (PanId < MAX_PANS) and (FPans[PanId] <> nil) then
|
||||
begin
|
||||
FPans[PanId].View.SetSpectrumData(Pixels, Count);
|
||||
FPanSpecDirty[PanId] := True;
|
||||
end;
|
||||
finally
|
||||
FPanCbLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TMainForm.OnPanWaterfallReady(PanId: Integer;
|
||||
const Pixels: array of Single; Count: Integer);
|
||||
begin
|
||||
FPanCbLock.Enter;
|
||||
try
|
||||
if (PanId >= 1) and (PanId < MAX_PANS) and (FPans[PanId] <> nil) then
|
||||
begin
|
||||
FPans[PanId].View.SetWaterfallData(Pixels, Count);
|
||||
if FPans[PanId].View.WaterfallDirty then FPanWfDirty[PanId] := True;
|
||||
end;
|
||||
finally
|
||||
FPanCbLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
// ---- Мышь доп. панов: тюн слайсов + drag центра (ретюн DDC). Семантика
|
||||
// главного пана (VFO/CTUN/маяк/оверлеи) сюда не переносится. ----
|
||||
|
||||
procedure TMainForm.PanNSpectrumMouseDown(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
var
|
||||
P: TPanafallPanel;
|
||||
VC, VS: Double;
|
||||
Id, W: Integer;
|
||||
IsSpec: Boolean;
|
||||
begin
|
||||
P := PanFromSender(Sender);
|
||||
if P = nil then Exit;
|
||||
// Флаги и маркеры несущих живут ТОЛЬКО на спектре: координаты водопада в
|
||||
// диспетчер флагов отдавать нельзя (его прямоугольники — в системе спектра).
|
||||
IsSpec := TControl(Sender) = P.PbSpectrum;
|
||||
if IsSpec and P.DispatchFlagsMouseDown(Button, X, Y) then
|
||||
begin
|
||||
P.ProcessPendingSliceClose;
|
||||
Exit;
|
||||
end;
|
||||
W := TControl(Sender).Width;
|
||||
if W <= 0 then Exit;
|
||||
if (Button = mbLeft) and (ssCtrl in Shift) then
|
||||
begin
|
||||
PanViewWindow(P, VC, VS);
|
||||
AddSliceAtFreqPan(P, PixelToFreq(X, W, VC, VS));
|
||||
Exit;
|
||||
end;
|
||||
if Button = mbLeft then
|
||||
begin
|
||||
if IsSpec then
|
||||
begin
|
||||
Id := P.SliceAtPixel(X);
|
||||
if Id <> 0 then
|
||||
begin
|
||||
FPanSliceDrag := True;
|
||||
FPanSliceDragId := Id;
|
||||
FPanMousePan := P;
|
||||
FActiveSliceId := Id;
|
||||
TControl(Sender).Cursor := crSizeWE;
|
||||
Exit;
|
||||
end;
|
||||
end;
|
||||
// Пустое место: армируем drag ЦЕНТРА пана (ретюн его DDC; слайсы стоят на
|
||||
// своих абсолютных частотах). Клик без движения = тюн слайса пана (MouseUp).
|
||||
// Курсор не трогаем до реального сдвига.
|
||||
FPanCenterDrag := True;
|
||||
FPanCenterMoved := False;
|
||||
FPanMousePan := P;
|
||||
FPanCenterDragX0 := X;
|
||||
FPanCenterDragF0 := FController.PanDDCFreq(P.PanId);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TMainForm.PanNSpectrumMouseMove(Sender: TObject; Shift: TShiftState;
|
||||
X, Y: Integer);
|
||||
var
|
||||
P: TPanafallPanel;
|
||||
VC, VS, NewHz, LoHz, HiHz: Double;
|
||||
W: Integer;
|
||||
begin
|
||||
P := PanFromSender(Sender);
|
||||
if P = nil then Exit;
|
||||
W := TControl(Sender).Width;
|
||||
|
||||
if FPanSliceDrag and (FPanMousePan = P) then
|
||||
begin
|
||||
if (ssLeft in Shift) and (W > 0) then
|
||||
begin
|
||||
PanViewWindow(P, VC, VS);
|
||||
NewHz := PixelToFreq(X, W, VC, VS);
|
||||
FController.CapturedSpan(LoHz, HiHz, P.PanId);
|
||||
if NewHz < LoHz then NewHz := LoHz;
|
||||
if NewHz > HiHz then NewHz := HiHz;
|
||||
FController.SetSliceTarget(FPanSliceDragId, NewHz);
|
||||
P.PushSliceFlagState(FPanSliceDragId);
|
||||
P.LayoutFlags;
|
||||
MarkPanDirty(P);
|
||||
end;
|
||||
Exit;
|
||||
end;
|
||||
|
||||
if FPanCenterDrag and (FPanMousePan = P) then
|
||||
begin
|
||||
if (ssLeft in Shift) and (W > 0) then
|
||||
begin
|
||||
// Гистерезис 4px: до него это ещё «клик» (тюн по MouseUp), не drag.
|
||||
if (not FPanCenterMoved) and (Abs(X - FPanCenterDragX0) < 4) then Exit;
|
||||
if not FPanCenterMoved then
|
||||
begin
|
||||
FPanCenterMoved := True;
|
||||
TControl(Sender).Cursor := crSizeWE;
|
||||
end;
|
||||
PanViewWindow(P, VC, VS);
|
||||
NewHz := FPanCenterDragF0 - (X - FPanCenterDragX0) * VS / W;
|
||||
NewHz := Max(FController.BackendCaps.MinFreqHz,
|
||||
Min(FController.BackendCaps.MaxFreqHz, NewHz));
|
||||
FController.SetPanDDCFreq(P.PanId, NewHz);
|
||||
P.View.CenterFreq := NewHz;
|
||||
P.LayoutFlags;
|
||||
UpdatePanHeaders;
|
||||
MarkPanDirty(P);
|
||||
end;
|
||||
Exit;
|
||||
end;
|
||||
|
||||
// Hover-логика (флаги, маркеры несущих) — только на спектре; на водопаде
|
||||
// курсор остаётся обычным, как у главного пана.
|
||||
if TControl(Sender) = P.PbSpectrum then
|
||||
begin
|
||||
P.DispatchFlagsMouseMove(X, Y);
|
||||
if P.SliceAtPixel(X) <> 0 then
|
||||
TControl(Sender).Cursor := crSizeWE
|
||||
else
|
||||
TControl(Sender).Cursor := crArrow; // crDefault «снимает» курсор — см. Build
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TMainForm.PanNSpectrumMouseUp(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
var P: TPanafallPanel; W: Integer;
|
||||
begin
|
||||
P := PanFromSender(Sender);
|
||||
if P = nil then Exit;
|
||||
if FPanSliceDrag and (FPanMousePan = P) then
|
||||
begin
|
||||
FPanSliceDrag := False;
|
||||
FPanMousePan := nil;
|
||||
TControl(Sender).Cursor := crArrow;
|
||||
Exit;
|
||||
end;
|
||||
if FPanCenterDrag and (FPanMousePan = P) then
|
||||
begin
|
||||
FPanCenterDrag := False;
|
||||
FPanMousePan := nil;
|
||||
TControl(Sender).Cursor := crArrow;
|
||||
// Пресс без движения = клик: перестроить слайс пана на частоту клика.
|
||||
W := TControl(Sender).Width;
|
||||
if (Button = mbLeft) and (not FPanCenterMoved) and (W > 0) then
|
||||
PanClickTune(P, X, W);
|
||||
Exit;
|
||||
end;
|
||||
if (TControl(Sender) = P.PbSpectrum) and P.DispatchFlagsMouseUp(X, Y) then
|
||||
P.ProcessPendingSliceClose;
|
||||
end;
|
||||
|
||||
procedure TMainForm.PanNSpectrumMouseLeave(Sender: TObject);
|
||||
var P: TPanafallPanel;
|
||||
begin
|
||||
P := PanFromSender(Sender);
|
||||
if P <> nil then P.DispatchFlagsMouseLeave;
|
||||
end;
|
||||
|
||||
procedure TMainForm.PanNSpectrumDblClick(Sender: TObject);
|
||||
begin
|
||||
// Дабл-клик на доп. пане пока без действия.
|
||||
end;
|
||||
|
||||
// ---- Сплиттеры между панами: пересчёт долей по отпусканию мыши. ----
|
||||
|
||||
procedure TMainForm.PanSplitMouseDown(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
const
|
||||
PAN_SPLIT_H = 5;
|
||||
var
|
||||
BelowId, AboveId, i, TotalH, MinPx: Integer;
|
||||
Split: TPanel;
|
||||
begin
|
||||
if Button <> mbLeft then Exit;
|
||||
Split := TPanel(Sender);
|
||||
BelowId := Split.Tag;
|
||||
// Пан над сплиттером — ближайший активный с меньшим PanId.
|
||||
AboveId := -1;
|
||||
for i := BelowId - 1 downto 0 do
|
||||
if FPans[i] <> nil then begin AboveId := i; Break; end;
|
||||
if (AboveId < 0) or (FPans[BelowId] = nil) then Exit;
|
||||
TotalH := PanelRight.ClientHeight;
|
||||
if TotalH <= 0 then Exit;
|
||||
// Границы призрака: каждому пану — минимум 15% стека (= кламп долей в MouseUp).
|
||||
MinPx := Round(0.15 * TotalH);
|
||||
FPanSplitMinTop := FPans[AboveId].StackTop + MinPx;
|
||||
FPanSplitMaxTop := FPans[BelowId].StackTop + FPans[BelowId].StackHeight
|
||||
- MinPx - PAN_SPLIT_H;
|
||||
FPanSplitDrag := BelowId;
|
||||
FPanSplitY0 := Mouse.CursorPos.Y;
|
||||
FPanSplitTop0 := Split.Top;
|
||||
Split.Color := TColor($005050A0); // подсветка при захвате (как у внутрипанного)
|
||||
end;
|
||||
|
||||
procedure TMainForm.PanSplitMouseMove(Sender: TObject; Shift: TShiftState;
|
||||
X, Y: Integer);
|
||||
var NewTop: Integer;
|
||||
begin
|
||||
// Живой призрак: двигаем сам сплиттер, пересчёт регионов — по MouseUp.
|
||||
if (FPanSplitDrag = 0) or (TPanel(Sender).Tag <> FPanSplitDrag) then Exit;
|
||||
NewTop := FPanSplitTop0 + (Mouse.CursorPos.Y - FPanSplitY0);
|
||||
if NewTop < FPanSplitMinTop then NewTop := FPanSplitMinTop;
|
||||
if NewTop > FPanSplitMaxTop then NewTop := FPanSplitMaxTop;
|
||||
TPanel(Sender).Top := NewTop;
|
||||
end;
|
||||
|
||||
procedure TMainForm.PanSplitMouseUp(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
var
|
||||
BelowId, AboveId, i, DeltaY, TotalH: Integer;
|
||||
DShare, MinShare: Double;
|
||||
begin
|
||||
if FPanSplitDrag = 0 then Exit;
|
||||
BelowId := FPanSplitDrag;
|
||||
FPanSplitDrag := 0;
|
||||
TPanel(Sender).Color := TColor($00303030);
|
||||
// Пан над сплиттером — ближайший активный с меньшим PanId.
|
||||
AboveId := -1;
|
||||
for i := BelowId - 1 downto 0 do
|
||||
if FPans[i] <> nil then begin AboveId := i; Break; end;
|
||||
if (AboveId < 0) or (FPans[BelowId] = nil) then Exit;
|
||||
TotalH := PanelRight.ClientHeight;
|
||||
if TotalH <= 0 then Exit;
|
||||
// Дельта — по финальному положению призрака (он уже клампился в Move).
|
||||
DeltaY := TPanel(Sender).Top - FPanSplitTop0;
|
||||
DShare := DeltaY / TotalH;
|
||||
MinShare := 0.15;
|
||||
if FPanShare[AboveId] + DShare < MinShare then
|
||||
DShare := MinShare - FPanShare[AboveId];
|
||||
if FPanShare[BelowId] - DShare < MinShare then
|
||||
DShare := FPanShare[BelowId] - MinShare;
|
||||
FPanShare[AboveId] := FPanShare[AboveId] + DShare;
|
||||
FPanShare[BelowId] := FPanShare[BelowId] - DShare;
|
||||
ResizeSpectrumPanels;
|
||||
end;
|
||||
|
||||
procedure TMainForm.OnVfoOverlaySliceSelect(Sender: TObject);
|
||||
begin
|
||||
if Sender is TVfoOverlay then
|
||||
@@ -5025,12 +5782,14 @@ begin
|
||||
end;
|
||||
|
||||
procedure TMainForm.OnVfoOverlayClose(Sender: TObject);
|
||||
var P: TPanafallPanel;
|
||||
begin
|
||||
if not (Sender is TVfoOverlay) then Exit;
|
||||
if TVfoOverlay(Sender).IsMain then Exit;
|
||||
// Нельзя освобождать флаг внутри его же обработчика мыши — панель пометит
|
||||
// и снимет его после возврата из диспетчера (ProcessPendingSliceClose).
|
||||
FPan.RequestSliceClose(TVfoOverlay(Sender).SliceId);
|
||||
P := PanOfOverlay(Sender);
|
||||
if P <> nil then P.RequestSliceClose(TVfoOverlay(Sender).SliceId);
|
||||
end;
|
||||
|
||||
// FPan закрыл слайс («×») — сбрасываем активный, если это был он.
|
||||
@@ -5598,17 +6357,25 @@ var
|
||||
Delta: Int64;
|
||||
Base: Int64;
|
||||
NewFreq: Int64;
|
||||
SliceId: Integer;
|
||||
SliceId, i: Integer;
|
||||
SLo, SHi, SNew: Double;
|
||||
Sl: TCtrlSlice;
|
||||
SlicePan: TPanafallPanel;
|
||||
begin
|
||||
// Над дисплеем VFO (A или B) колесо обрабатывает сам TFreqDisplay поразрядно
|
||||
// (включая неактивный VFO). Не дублируем здесь, иначе менялись бы обе частоты:
|
||||
// виджет крутил бы B, а этот хендлер — активный A.
|
||||
if OverFreqDisp(FreqDispA) or OverFreqDisp(FreqDispB) then Exit;
|
||||
|
||||
// Колесо над маркером слайса — двигаем частоту ЭТОГО слайса, а не главного VFO.
|
||||
SliceId := FPan.SliceUnderCursor;
|
||||
// Колесо над маркером слайса (любого пана) — двигаем частоту ЭТОГО слайса.
|
||||
SliceId := 0;
|
||||
SlicePan := nil;
|
||||
for i := 0 to MAX_PANS - 1 do
|
||||
if FPans[i] <> nil then
|
||||
begin
|
||||
SliceId := FPans[i].SliceUnderCursor;
|
||||
if SliceId <> 0 then begin SlicePan := FPans[i]; Break; end;
|
||||
end;
|
||||
if (SliceId <> 0) and FController.GetSlice(SliceId, Sl) then
|
||||
begin
|
||||
if (ssCtrl in Shift) and (ssShift in Shift) then Step := 1000000
|
||||
@@ -5622,14 +6389,13 @@ begin
|
||||
if Delta > 0 then SNew := Base + Step
|
||||
else if Round(Sl.TargetHz) = Base then SNew := Base - Step
|
||||
else SNew := Base;
|
||||
FController.CapturedSpan(SLo, SHi);
|
||||
FController.CapturedSpan(SLo, SHi, Sl.PanId); // полоса ЕГО пана
|
||||
if SNew < SLo then SNew := SLo;
|
||||
if SNew > SHi then SNew := SHi;
|
||||
FController.SetSliceTarget(SliceId, SNew);
|
||||
FPan.PushSliceFlagState(SliceId);
|
||||
FPan.LayoutFlags;
|
||||
FSpectrumDirty := True;
|
||||
PbSpectrum.Invalidate;
|
||||
SlicePan.PushSliceFlagState(SliceId);
|
||||
SlicePan.LayoutFlags;
|
||||
MarkPanDirty(SlicePan);
|
||||
FActiveSliceId := SliceId;
|
||||
Handled := True;
|
||||
Exit;
|
||||
|
||||
Reference in New Issue
Block a user