mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
refactor(slices): этап 3.0B1 — флаги слайсов переехали в TPanafallPanel
Панель владеет коллекцией флагов B+ (создание/закрытие/заливка состояния из контроллера), раскладкой LayoutFlags, диспетчером мыши флагов, тиком S-метров (TickFlags) и видимостью флага A (UpdateMainFlagVisibility). Главный флаг A создаёт MainForm (проводка семантических событий у него) и отдаёт через AttachMainFlag; новые флаги проводятся через OnWireFlag. Роутинг по слайсу — FPan.CurSliceId; закрытие «×» — RequestSliceClose/OnSliceClosed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+46
-449
@@ -128,14 +128,12 @@ type
|
|||||||
// Список устройств (saved+discovered) → FController.FDeviceStore (общий с web).
|
// Список устройств (saved+discovered) → FController.FDeviceStore (общий с web).
|
||||||
FDeviceDialog: TDeviceDialog;
|
FDeviceDialog: TDeviceDialog;
|
||||||
FPendingIP: string; // IP устройства для подключения
|
FPendingIP: string; // IP устройства для подключения
|
||||||
FVfoOverlay: TVfoOverlay; // накладка SmartSDR-стиль на спектре (слайс A / главный)
|
// Флаг главного (слайс A): создаётся здесь (проводка событий), живёт в
|
||||||
FSliceFlags: array of TVfoOverlay; // флаги доп. слайсов (B+)
|
// раскладке/диспетчере FPan (AttachMainFlag). Флаги B+ — целиком в FPan.
|
||||||
FActiveFlag: TVfoOverlay; // флаг, чьё событие сейчас обрабатывается (для роутинга по SliceId)
|
FVfoOverlay: TVfoOverlay;
|
||||||
FActiveSliceId: Integer; // выбранный активный слайс (0 = главный)
|
FActiveSliceId: Integer; // выбранный активный слайс (0 = главный)
|
||||||
FPendingSliceClose: Integer; // SliceId к удалению после возврата из мыши (0 = нет)
|
|
||||||
FSliceDrag: Boolean; // тащим маркер несущей слайса по спектру
|
FSliceDrag: Boolean; // тащим маркер несущей слайса по спектру
|
||||||
FSliceDragId: Integer; // какой слайс тащим
|
FSliceDragId: Integer; // какой слайс тащим
|
||||||
FLastSliceMeterMs: QWord; // троттлинг S-метра слайсов (~10 Гц)
|
|
||||||
FSampleRateOverlay: TSampleRateOverlay; // оверлей span/hide слева вверху спектра
|
FSampleRateOverlay: TSampleRateOverlay; // оверлей span/hide слева вверху спектра
|
||||||
FBandPlanOverlay: TBandPlanOverlay; // полоска бэндплана QO-100 внизу спектра
|
FBandPlanOverlay: TBandPlanOverlay; // полоска бэндплана QO-100 внизу спектра
|
||||||
FPanelHidden: Boolean; // True = левая панель скрыта
|
FPanelHidden: Boolean; // True = левая панель скрыта
|
||||||
@@ -516,9 +514,8 @@ type
|
|||||||
procedure DeactivateXvtr;
|
procedure DeactivateXvtr;
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
procedure BtnSettingsClick(Sender: TObject);
|
procedure BtnSettingsClick(Sender: TObject);
|
||||||
procedure PositionVfoOverlay;
|
// Семантические обработчики событий флагов (роутинг по FPan.CurSliceId).
|
||||||
procedure LayoutFlags; // единый packing всех флагов (A+B+): не наезжают друг на друга
|
// Коллекция/раскладка/диспетчер флагов — в TPanafallPanel.
|
||||||
procedure UpdateVfoOverlayVisibility; // флаг A виден: панель скрыта ИЛИ есть слайсы B+
|
|
||||||
procedure OnModeFilterSelect(Mode: Integer; FilterBW: Integer);
|
procedure OnModeFilterSelect(Mode: Integer; FilterBW: Integer);
|
||||||
procedure OnVfoOverlayDSPChange(NRMode, NBMode: Integer; SNBOn, ANFOn: Boolean);
|
procedure OnVfoOverlayDSPChange(NRMode, NBMode: Integer; SNBOn, ANFOn: Boolean);
|
||||||
procedure OnVfoOverlaySquelchChange(SQLOn: Boolean; SQLLevel: Integer);
|
procedure OnVfoOverlaySquelchChange(SQLOn: Boolean; SQLLevel: Integer);
|
||||||
@@ -530,24 +527,12 @@ type
|
|||||||
procedure OnVfoOverlayTxSelect(Sender: TObject);
|
procedure OnVfoOverlayTxSelect(Sender: TObject);
|
||||||
procedure OnVfoOverlaySliceSelect(Sender: TObject); // выбрать слайс активным
|
procedure OnVfoOverlaySliceSelect(Sender: TObject); // выбрать слайс активным
|
||||||
procedure OnVfoOverlayClose(Sender: TObject); // × — удалить слайс
|
procedure OnVfoOverlayClose(Sender: TObject); // × — удалить слайс
|
||||||
procedure ProcessPendingSliceClose; // отложенное удаление флага
|
|
||||||
procedure PushVfoOverlayExtState;
|
|
||||||
procedure PushVfoOverlayAudioDevices;
|
|
||||||
procedure OnVfoOverlayInvalidate(Sender: TObject);
|
procedure OnVfoOverlayInvalidate(Sender: TObject);
|
||||||
|
procedure OnPanSliceClosed(SliceId: Integer); // FPan закрыл слайс
|
||||||
// --- Мультислайсы (UI) ---
|
// --- Мультислайсы (UI) ---
|
||||||
function CurSliceId: Integer; // SliceId обрабатываемого сейчас флага
|
function CurSliceId: Integer; // SliceId обрабатываемого сейчас флага
|
||||||
procedure WireOverlayEvents(O: TVfoOverlay);
|
procedure WireOverlayEvents(O: TVfoOverlay);
|
||||||
procedure CreateSliceFlag(SliceId: Integer);
|
|
||||||
procedure DestroySliceFlag(SliceId: Integer);
|
|
||||||
function FindSliceFlag(SliceId: Integer): TVfoOverlay;
|
|
||||||
procedure AddSliceAtFreq(Hz: Double);
|
procedure AddSliceAtFreq(Hz: Double);
|
||||||
procedure PositionSliceFlags;
|
|
||||||
procedure PushSliceFlagState(SliceId: Integer);
|
|
||||||
function DispatchFlagsMouseDown(Button: TMouseButton; X, Y: Integer): Boolean;
|
|
||||||
procedure DispatchFlagsMouseMove(X, Y: Integer);
|
|
||||||
function DispatchFlagsMouseUp(X, Y: Integer): Boolean;
|
|
||||||
procedure DispatchFlagsMouseLeave;
|
|
||||||
function SliceAtPixel(X: Integer): Integer; // SliceId, чья несущая под X (0 = нет)
|
|
||||||
procedure PbSpectrumDblClick(Sender: TObject);
|
procedure PbSpectrumDblClick(Sender: TObject);
|
||||||
procedure TrkDriveChange(Sender: TObject);
|
procedure TrkDriveChange(Sender: TObject);
|
||||||
procedure UpdateDriveLabel; // % (openHPSDR) / дБ (Pluto) рядом с ползунком drive
|
procedure UpdateDriveLabel; // % (openHPSDR) / дБ (Pluto) рядом с ползунком drive
|
||||||
@@ -980,8 +965,6 @@ begin
|
|||||||
FVfoOverlay.SetSliceInfo(0, 'A', True); // главный приёмник = слайс A
|
FVfoOverlay.SetSliceInfo(0, 'A', True); // главный приёмник = слайс A
|
||||||
FVfoOverlay.Visible := False;
|
FVfoOverlay.Visible := False;
|
||||||
FActiveSliceId := 0;
|
FActiveSliceId := 0;
|
||||||
FActiveFlag := nil;
|
|
||||||
FPendingSliceClose := 0;
|
|
||||||
FSliceDrag := False;
|
FSliceDrag := False;
|
||||||
FSliceDragId := 0;
|
FSliceDragId := 0;
|
||||||
FPanelHidden := False;
|
FPanelHidden := False;
|
||||||
@@ -1001,7 +984,12 @@ begin
|
|||||||
FPan := TPanafallPanel.Create(nil, FUseOpenGLSpectrum);
|
FPan := TPanafallPanel.Create(nil, FUseOpenGLSpectrum);
|
||||||
FSpecView := FPan.View; // алиас (историческое имя)
|
FSpecView := FPan.View; // алиас (историческое имя)
|
||||||
FPanZoomBar := FPan.ZoomBar; // алиас
|
FPanZoomBar := FPan.ZoomBar; // алиас
|
||||||
FSpecView.VfoOverlay := FVfoOverlay;
|
// Флаги: контроллер, проводка событий новых флагов, обратные вызовы.
|
||||||
|
FPan.Controller := FController;
|
||||||
|
FPan.OnWireFlag := WireOverlayEvents;
|
||||||
|
FPan.OnSliceClosed := OnPanSliceClosed;
|
||||||
|
FPan.OnFlagsInvalidate := OnVfoOverlayInvalidate;
|
||||||
|
FPan.AttachMainFlag(FVfoOverlay); // внутри: FSpecView.VfoOverlay := флаг A
|
||||||
FSpecView.VfoA := FController.FVfoA;
|
FSpecView.VfoA := FController.FVfoA;
|
||||||
FSpecView.VfoB := FController.FVfoB;
|
FSpecView.VfoB := FController.FVfoB;
|
||||||
FSpecView.ActiveVfo := FController.FActiveVfo;
|
FSpecView.ActiveVfo := FController.FActiveVfo;
|
||||||
@@ -1416,8 +1404,7 @@ begin
|
|||||||
FSpecView.InvalidateRulerCache;
|
FSpecView.InvalidateRulerCache;
|
||||||
SyncSpecViewFreq;
|
SyncSpecViewFreq;
|
||||||
UpdatePanZoomBar;
|
UpdatePanZoomBar;
|
||||||
PositionVfoOverlay;
|
FPan.LayoutFlags;
|
||||||
PositionSliceFlags;
|
|
||||||
FSpecView.DrawSpectrum;
|
FSpecView.DrawSpectrum;
|
||||||
PbSpectrum.Invalidate;
|
PbSpectrum.Invalidate;
|
||||||
FSpecView.DrawWaterfall;
|
FSpecView.DrawWaterfall;
|
||||||
@@ -2577,9 +2564,7 @@ begin
|
|||||||
if Assigned(FWidebandView) then FWidebandView.SetTheme(T);
|
if Assigned(FWidebandView) then FWidebandView.SetTheme(T);
|
||||||
|
|
||||||
// Флаги слайсов (оверлеи) — цвета кнопок из темы, как левая панель.
|
// Флаги слайсов (оверлеи) — цвета кнопок из темы, как левая панель.
|
||||||
if Assigned(FVfoOverlay) then FVfoOverlay.SetTheme(T);
|
if FPan <> nil then FPan.SetFlagsTheme(T);
|
||||||
for i := 0 to High(FSliceFlags) do
|
|
||||||
if Assigned(FSliceFlags[i]) then FSliceFlags[i].SetTheme(T);
|
|
||||||
|
|
||||||
// VFO display — вызывает UpdateVfoDisplay, который использует тему
|
// VFO display — вызывает UpdateVfoDisplay, который использует тему
|
||||||
UpdateVfoDisplay;
|
UpdateVfoDisplay;
|
||||||
@@ -3022,27 +3007,8 @@ begin
|
|||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
FSpecView.WDSPReady := FController.FWDSPReady;
|
FSpecView.WDSPReady := FController.FWDSPReady;
|
||||||
// Обновляем оверлей если видим
|
// Флаги: метр/частота главного, позиции, троттленный S-метр слайсов.
|
||||||
if Assigned(FVfoOverlay) and FVfoOverlay.Visible then
|
FPan.TickFlags;
|
||||||
begin
|
|
||||||
FVfoOverlay.UpdateSMeter(FController.FLastSMeter);
|
|
||||||
FVfoOverlay.UpdateVfo(FController.FVfoA);
|
|
||||||
PushVfoOverlayExtState;
|
|
||||||
PositionVfoOverlay;
|
|
||||||
end;
|
|
||||||
if Length(FSliceFlags) > 0 then
|
|
||||||
begin
|
|
||||||
PositionSliceFlags; // держим слайс-флаги на частоте
|
|
||||||
// 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;
|
|
||||||
// Панорама и водопад теперь рисуются независимо по факту новых кадров.
|
// Панорама и водопад теперь рисуются независимо по факту новых кадров.
|
||||||
// Это лучше совпадает с Thetis: маленький FFT даёт новые строки чаще.
|
// Это лучше совпадает с Thetis: маленький FFT даёт новые строки чаще.
|
||||||
if FSpectrumDirty then
|
if FSpectrumDirty then
|
||||||
@@ -4195,9 +4161,9 @@ procedure TMainForm.PbSpectrumMouseDown(Sender: TObject; Button: TMouseButton;
|
|||||||
Shift: TShiftState; X, Y: Integer);
|
Shift: TShiftState; X, Y: Integer);
|
||||||
var VC, VS: Double;
|
var VC, VS: Double;
|
||||||
begin
|
begin
|
||||||
if DispatchFlagsMouseDown(Button, X, Y) then
|
if FPan.DispatchFlagsMouseDown(Button, X, Y) then
|
||||||
begin
|
begin
|
||||||
ProcessPendingSliceClose; // × мог пометить слайс к удалению
|
FPan.ProcessPendingSliceClose; // × мог пометить слайс к удалению
|
||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
if Assigned(FSampleRateOverlay) and
|
if Assigned(FSampleRateOverlay) and
|
||||||
@@ -4229,7 +4195,7 @@ begin
|
|||||||
// ЛКМ по маркеру несущей слайса — начинаем его перетаскивание (перетюн).
|
// ЛКМ по маркеру несущей слайса — начинаем его перетаскивание (перетюн).
|
||||||
if (PbSpectrum.Width > 0) then
|
if (PbSpectrum.Width > 0) then
|
||||||
begin
|
begin
|
||||||
FSliceDragId := SliceAtPixel(X);
|
FSliceDragId := FPan.SliceAtPixel(X);
|
||||||
if FSliceDragId <> 0 then
|
if FSliceDragId <> 0 then
|
||||||
begin
|
begin
|
||||||
FSliceDrag := True;
|
FSliceDrag := True;
|
||||||
@@ -4296,8 +4262,8 @@ begin
|
|||||||
if NewHz < LoHz then NewHz := LoHz;
|
if NewHz < LoHz then NewHz := LoHz;
|
||||||
if NewHz > HiHz then NewHz := HiHz;
|
if NewHz > HiHz then NewHz := HiHz;
|
||||||
FController.SetSliceTarget(FSliceDragId, NewHz);
|
FController.SetSliceTarget(FSliceDragId, NewHz);
|
||||||
PushSliceFlagState(FSliceDragId); // обновить частоту во флаге
|
FPan.PushSliceFlagState(FSliceDragId); // обновить частоту во флаге
|
||||||
PositionSliceFlags;
|
FPan.LayoutFlags;
|
||||||
FSpectrumDirty := True; // кадр — по таймеру (FPS-гейт, как при drag VFO)
|
FSpectrumDirty := True; // кадр — по таймеру (FPS-гейт, как при drag VFO)
|
||||||
end;
|
end;
|
||||||
Exit;
|
Exit;
|
||||||
@@ -4307,13 +4273,13 @@ begin
|
|||||||
begin
|
begin
|
||||||
FSampleRateOverlay.HandleMouseMove(X, Y);
|
FSampleRateOverlay.HandleMouseMove(X, Y);
|
||||||
end;
|
end;
|
||||||
DispatchFlagsMouseMove(X, Y);
|
FPan.DispatchFlagsMouseMove(X, Y);
|
||||||
if not FSpecDrag then
|
if not FSpecDrag then
|
||||||
begin
|
begin
|
||||||
if (Assigned(FVfoOverlay) and (FVfoOverlay.HotIdx >= 0)) or
|
if (Assigned(FVfoOverlay) and (FVfoOverlay.HotIdx >= 0)) or
|
||||||
(Assigned(FSampleRateOverlay) and (FSampleRateOverlay.HotIdx >= 0)) then
|
(Assigned(FSampleRateOverlay) and (FSampleRateOverlay.HotIdx >= 0)) then
|
||||||
PbSpectrum.Cursor := crHandPoint
|
PbSpectrum.Cursor := crHandPoint
|
||||||
else if SliceAtPixel(X) <> 0 then
|
else if FPan.SliceAtPixel(X) <> 0 then
|
||||||
PbSpectrum.Cursor := crSizeWE // над маркером слайса — его можно тащить
|
PbSpectrum.Cursor := crSizeWE // над маркером слайса — его можно тащить
|
||||||
else
|
else
|
||||||
PbSpectrum.Cursor := crDefault;
|
PbSpectrum.Cursor := crDefault;
|
||||||
@@ -4339,7 +4305,7 @@ begin
|
|||||||
PbSpectrum.Cursor := crDefault;
|
PbSpectrum.Cursor := crDefault;
|
||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
if DispatchFlagsMouseUp(X, Y) then
|
if FPan.DispatchFlagsMouseUp(X, Y) then
|
||||||
begin
|
begin
|
||||||
FSpecDrag := False;
|
FSpecDrag := False;
|
||||||
PbSpectrum.Cursor := crDefault;
|
PbSpectrum.Cursor := crDefault;
|
||||||
@@ -4358,7 +4324,7 @@ procedure TMainForm.PbSpectrumMouseLeave(Sender: TObject);
|
|||||||
begin
|
begin
|
||||||
if Assigned(FSampleRateOverlay) then
|
if Assigned(FSampleRateOverlay) then
|
||||||
FSampleRateOverlay.HandleMouseLeave;
|
FSampleRateOverlay.HandleMouseLeave;
|
||||||
DispatchFlagsMouseLeave;
|
FPan.DispatchFlagsMouseLeave;
|
||||||
if not FSpecDrag then PbSpectrum.Cursor := crDefault;
|
if not FSpecDrag then PbSpectrum.Cursor := crDefault;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@@ -4835,156 +4801,15 @@ begin
|
|||||||
end
|
end
|
||||||
else
|
else
|
||||||
PanelLeft.Width := FLeftPanelW; // восстанавливаем сохранённую ширину
|
PanelLeft.Width := FLeftPanelW; // восстанавливаем сохранённую ширину
|
||||||
UpdateVfoOverlayVisibility; // видимость флага A решает общий инвариант
|
// Видимость флага A решает общий инвариант (панель скрыта ИЛИ есть слайсы).
|
||||||
|
FPan.MainFlagPinned := FPanelHidden;
|
||||||
|
FPan.UpdateMainFlagVisibility;
|
||||||
if Assigned(FSampleRateOverlay) then
|
if Assigned(FSampleRateOverlay) then
|
||||||
FSampleRateOverlay.SetPanelHidden(FPanelHidden);
|
FSampleRateOverlay.SetPanelHidden(FPanelHidden);
|
||||||
ResizeSpectrumPanels;
|
ResizeSpectrumPanels;
|
||||||
ResizeSMeter;
|
ResizeSMeter;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
// Флаг главного приёмника (слайс A) виден, когда СКРЫТА левая панель ЛИБО есть
|
|
||||||
// доп. слайсы (B+): тогда A всегда представлен флагом на спектре рядом с B/C…,
|
|
||||||
// а сворачивание/раскрытие панели при живых слайсах не дёргает его появление.
|
|
||||||
procedure TMainForm.UpdateVfoOverlayVisibility;
|
|
||||||
var
|
|
||||||
ShouldShow: Boolean;
|
|
||||||
begin
|
|
||||||
if not Assigned(FVfoOverlay) then Exit;
|
|
||||||
ShouldShow := FPanelHidden or (Length(FSliceFlags) > 0);
|
|
||||||
if ShouldShow = FVfoOverlay.Visible then Exit;
|
|
||||||
if ShouldShow then
|
|
||||||
begin
|
|
||||||
FVfoOverlay.Width := OVL_W;
|
|
||||||
FVfoOverlay.Height := FVfoOverlay.BaseHeight; // растёт сам при открытии fly-out
|
|
||||||
FVfoOverlay.SetState(FController.FMode, FController.FFilterBW, FController.FVfoA, FController.FLastSMeter);
|
|
||||||
FVfoOverlay.SetDSPState(FController.FNRMode, FController.FNBMode, FController.FSNB, FController.FANF, FController.FAGCMode);
|
|
||||||
FVfoOverlay.SetSquelchState(FController.FFMSQOn, FController.FFMSQLevel);
|
|
||||||
FVfoOverlay.Visible := True;
|
|
||||||
PushVfoOverlayExtState;
|
|
||||||
PushVfoOverlayAudioDevices;
|
|
||||||
PositionVfoOverlay;
|
|
||||||
end
|
|
||||||
else
|
|
||||||
begin
|
|
||||||
FVfoOverlay.HandleMouseLeave;
|
|
||||||
FVfoOverlay.Visible := False;
|
|
||||||
end;
|
|
||||||
end;
|
|
||||||
|
|
||||||
// Позиционирование всех флагов (главный A + слайсы B+) единым проходом.
|
|
||||||
// Идеал каждого флага — рядом со СВОЕЙ полосой фильтра (справа, флип влево
|
|
||||||
// если не влезает), как раньше. Но флаги не наезжают друг на друга: сосед
|
|
||||||
// слева «толкает» правого, тот останавливается и не пересекает. Какому флагу
|
|
||||||
// принадлежит уехавшая полоса — видно по букве на самой полосе (рендерер).
|
|
||||||
procedure TMainForm.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 PbSpectrum = nil then Exit;
|
|
||||||
W := PbSpectrum.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(FVfoOverlay) and FVfoOverlay.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] := FVfoOverlay;
|
|
||||||
OWs[n] := FVfoOverlay.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 then OnVfoOverlayInvalidate(nil);
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TMainForm.PositionVfoOverlay;
|
|
||||||
begin
|
|
||||||
LayoutFlags;
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TMainForm.PbSpectrumDblClick(Sender: TObject);
|
procedure TMainForm.PbSpectrumDblClick(Sender: TObject);
|
||||||
begin
|
begin
|
||||||
// Двойной клик — ничего не делаем (оверлей всегда виден при скрытой панели)
|
// Двойной клик — ничего не делаем (оверлей всегда виден при скрытой панели)
|
||||||
@@ -4998,8 +4823,8 @@ begin
|
|||||||
if CurSliceId <> 0 then
|
if CurSliceId <> 0 then
|
||||||
begin
|
begin
|
||||||
FController.SetSliceModeBW(CurSliceId, Mode, FilterBW);
|
FController.SetSliceModeBW(CurSliceId, Mode, FilterBW);
|
||||||
PushSliceFlagState(CurSliceId);
|
FPan.PushSliceFlagState(CurSliceId);
|
||||||
PositionSliceFlags;
|
FPan.LayoutFlags;
|
||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
FController.FMode := Mode;
|
FController.FMode := Mode;
|
||||||
@@ -5038,7 +4863,7 @@ begin
|
|||||||
FVfoOverlay.SetState(FController.FMode, FController.FFilterBW, FController.FVfoA, FController.FLastSMeter);
|
FVfoOverlay.SetState(FController.FMode, FController.FFilterBW, FController.FVfoA, FController.FLastSMeter);
|
||||||
FVfoOverlay.SetDSPState(FController.FNRMode, FController.FNBMode, FController.FSNB, FController.FANF, FController.FAGCMode);
|
FVfoOverlay.SetDSPState(FController.FNRMode, FController.FNBMode, FController.FSNB, FController.FANF, FController.FAGCMode);
|
||||||
FVfoOverlay.SetSquelchState(FController.FFMSQOn, FController.FFMSQLevel);
|
FVfoOverlay.SetSquelchState(FController.FFMSQOn, FController.FFMSQLevel);
|
||||||
PositionVfoOverlay; // перепозиционируем при смене LSB↔USB
|
FPan.LayoutFlags; // перепозиционируем при смене LSB↔USB
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@@ -5103,7 +4928,7 @@ procedure TMainForm.OnVfoOverlaySplit(Sender: TObject);
|
|||||||
begin
|
begin
|
||||||
if CurSliceId <> 0 then Exit; // SPLIT — только главный (слайсы не передают)
|
if CurSliceId <> 0 then Exit; // SPLIT — только главный (слайсы не передают)
|
||||||
FController.SetSplit(not FController.FSplitTxB);
|
FController.SetSplit(not FController.FSplitTxB);
|
||||||
PushVfoOverlayExtState;
|
FPan.PushMainFlagExtState;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TMainForm.OnVfoOverlayTxSelect(Sender: TObject);
|
procedure TMainForm.OnVfoOverlayTxSelect(Sender: TObject);
|
||||||
@@ -5111,7 +4936,7 @@ begin
|
|||||||
// TODO(слайсы): выбор TX-слайса при полноценной мультислайс-передаче.
|
// TODO(слайсы): выбор TX-слайса при полноценной мультислайс-передаче.
|
||||||
// Пока передаёт только главный канал — клик по TX неглавного игнорируем.
|
// Пока передаёт только главный канал — клик по TX неглавного игнорируем.
|
||||||
if CurSliceId <> 0 then Exit;
|
if CurSliceId <> 0 then Exit;
|
||||||
PushVfoOverlayExtState;
|
FPan.PushMainFlagExtState;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TMainForm.OnVfoOverlayAudioDevice(DevIndex: Integer; const DevName: string);
|
procedure TMainForm.OnVfoOverlayAudioDevice(DevIndex: Integer; const DevName: string);
|
||||||
@@ -5119,39 +4944,11 @@ begin
|
|||||||
if CurSliceId <> 0 then
|
if CurSliceId <> 0 then
|
||||||
begin
|
begin
|
||||||
FController.SetSliceDevice(CurSliceId, DevIndex, DevName);
|
FController.SetSliceDevice(CurSliceId, DevIndex, DevName);
|
||||||
PushSliceFlagState(CurSliceId); // обновить подсветку текущего устройства слайса
|
FPan.PushSliceFlagState(CurSliceId); // обновить подсветку текущего устройства слайса
|
||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
ApplyAudioDevice(DevIndex, DevName);
|
ApplyAudioDevice(DevIndex, DevName);
|
||||||
PushVfoOverlayAudioDevices; // обновить подсветку текущего устройства
|
FPan.PushMainFlagAudioDevices; // обновить подсветку текущего устройства
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TMainForm.PushVfoOverlayExtState;
|
|
||||||
begin
|
|
||||||
if not (Assigned(FVfoOverlay) and FVfoOverlay.Visible) then Exit;
|
|
||||||
// TxSel: этот слайс выбран для передачи. Пока слайс один → всегда True.
|
|
||||||
FVfoOverlay.SetExtState(FController.ActiveVolume, FController.FMuted,
|
|
||||||
FController.FSplitTxB, FController.FTransmitting, True);
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TMainForm.PushVfoOverlayAudioDevices;
|
|
||||||
var
|
|
||||||
Names: TStringList;
|
|
||||||
Indices: array[0..63] of Integer;
|
|
||||||
Cnt, I: Integer;
|
|
||||||
Idx: array of Integer;
|
|
||||||
begin
|
|
||||||
if not (Assigned(FVfoOverlay) and 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];
|
|
||||||
FVfoOverlay.SetAudioDevices(Names, Idx, FController.FAudioOutDevName);
|
|
||||||
finally
|
|
||||||
Names.Free;
|
|
||||||
end;
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TMainForm.OnVfoOverlayInvalidate(Sender: TObject);
|
procedure TMainForm.OnVfoOverlayInvalidate(Sender: TObject);
|
||||||
@@ -5185,7 +4982,7 @@ end;
|
|||||||
// SliceId флага, чьё событие сейчас обрабатывается (0 = главный приёмник / слайс A).
|
// SliceId флага, чьё событие сейчас обрабатывается (0 = главный приёмник / слайс A).
|
||||||
function TMainForm.CurSliceId: Integer;
|
function TMainForm.CurSliceId: Integer;
|
||||||
begin
|
begin
|
||||||
if Assigned(FActiveFlag) then Result := FActiveFlag.SliceId else Result := 0;
|
Result := FPan.CurSliceId;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
// Единая проводка событий флага (главного и слайсов) на общие обработчики.
|
// Единая проводка событий флага (главного и слайсов) на общие обработчики.
|
||||||
@@ -5207,61 +5004,6 @@ begin
|
|||||||
if FLightTheme then O.SetTheme(LightTheme) else O.SetTheme(DarkTheme);
|
if FLightTheme then O.SetTheme(LightTheme) else O.SetTheme(DarkTheme);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TMainForm.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 TMainForm.CreateSliceFlag(SliceId: Integer);
|
|
||||||
var
|
|
||||||
O: TVfoOverlay;
|
|
||||||
S: TCtrlSlice;
|
|
||||||
begin
|
|
||||||
if FindSliceFlag(SliceId) <> nil then Exit;
|
|
||||||
if not FController.GetSlice(SliceId, S) then Exit;
|
|
||||||
O := TVfoOverlay.Create(Self);
|
|
||||||
WireOverlayEvents(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;
|
|
||||||
FSpecView.AddSliceOverlay(O);
|
|
||||||
PushSliceFlagState(SliceId);
|
|
||||||
UpdateVfoOverlayVisibility; // появился слайс B+ → показать и флаг A (если панель открыта)
|
|
||||||
PositionSliceFlags;
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TMainForm.DestroySliceFlag(SliceId: Integer);
|
|
||||||
var
|
|
||||||
i, j: Integer;
|
|
||||||
O: TVfoOverlay;
|
|
||||||
begin
|
|
||||||
O := FindSliceFlag(SliceId);
|
|
||||||
if O = nil then Exit;
|
|
||||||
FController.RemoveSlice(SliceId);
|
|
||||||
FSpecView.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 FActiveSliceId = SliceId then FActiveSliceId := 0;
|
|
||||||
UpdateVfoOverlayVisibility; // закрыт последний слайс при открытой панели → скрыть флаг A
|
|
||||||
OnVfoOverlayInvalidate(nil);
|
|
||||||
end;
|
|
||||||
|
|
||||||
// Добавить софт-слайс на частоте Hz (режим/полоса копируются с главного).
|
// Добавить софт-слайс на частоте Hz (режим/полоса копируются с главного).
|
||||||
procedure TMainForm.AddSliceAtFreq(Hz: Double);
|
procedure TMainForm.AddSliceAtFreq(Hz: Double);
|
||||||
var
|
var
|
||||||
@@ -5272,75 +5014,10 @@ begin
|
|||||||
Id := FController.AddSlice(Hz, FController.FMode, Lo, Hi,
|
Id := FController.AddSlice(Hz, FController.FMode, Lo, Hi,
|
||||||
TRadioController.AGCModeFromUI(FController.FAGCMode), 0.7, -1, '');
|
TRadioController.AGCModeFromUI(FController.FAGCMode), 0.7, -1, '');
|
||||||
if Id < 0 then Exit;
|
if Id < 0 then Exit;
|
||||||
CreateSliceFlag(Id);
|
FPan.CreateSliceFlag(Id);
|
||||||
FActiveSliceId := Id;
|
FActiveSliceId := Id;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
// SliceId, чья несущая под пикселем X (в пределах допуска), 0 если нет.
|
|
||||||
function TMainForm.SliceAtPixel(X: Integer): Integer;
|
|
||||||
const TOL = 6;
|
|
||||||
var
|
|
||||||
i, CarrierX: Integer;
|
|
||||||
VC, VS: Double;
|
|
||||||
S: TCtrlSlice;
|
|
||||||
O: TVfoOverlay;
|
|
||||||
begin
|
|
||||||
Result := 0;
|
|
||||||
if (PbSpectrum = nil) or (PbSpectrum.Width <= 0) 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 * PbSpectrum.Width);
|
|
||||||
if Abs(X - CarrierX) <= TOL then Exit(O.SliceId);
|
|
||||||
end;
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TMainForm.PositionSliceFlags;
|
|
||||||
begin
|
|
||||||
LayoutFlags;
|
|
||||||
end;
|
|
||||||
|
|
||||||
// Заливает во флаг слайса его текущее состояние из контроллера.
|
|
||||||
procedure TMainForm.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 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 TMainForm.OnVfoOverlaySliceSelect(Sender: TObject);
|
procedure TMainForm.OnVfoOverlaySliceSelect(Sender: TObject);
|
||||||
begin
|
begin
|
||||||
if Sender is TVfoOverlay then
|
if Sender is TVfoOverlay then
|
||||||
@@ -5351,79 +5028,15 @@ procedure TMainForm.OnVfoOverlayClose(Sender: TObject);
|
|||||||
begin
|
begin
|
||||||
if not (Sender is TVfoOverlay) then Exit;
|
if not (Sender is TVfoOverlay) then Exit;
|
||||||
if TVfoOverlay(Sender).IsMain then Exit;
|
if TVfoOverlay(Sender).IsMain then Exit;
|
||||||
// Нельзя освобождать флаг внутри его же обработчика мыши — помечаем к удалению,
|
// Нельзя освобождать флаг внутри его же обработчика мыши — панель пометит
|
||||||
// ProcessPendingSliceClose снимет его после возврата из диспетчера.
|
// и снимет его после возврата из диспетчера (ProcessPendingSliceClose).
|
||||||
FPendingSliceClose := TVfoOverlay(Sender).SliceId;
|
FPan.RequestSliceClose(TVfoOverlay(Sender).SliceId);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TMainForm.ProcessPendingSliceClose;
|
// FPan закрыл слайс («×») — сбрасываем активный, если это был он.
|
||||||
var Id: Integer;
|
procedure TMainForm.OnPanSliceClosed(SliceId: Integer);
|
||||||
begin
|
begin
|
||||||
if FPendingSliceClose = 0 then Exit;
|
if FActiveSliceId = SliceId then FActiveSliceId := 0;
|
||||||
Id := FPendingSliceClose;
|
|
||||||
FPendingSliceClose := 0;
|
|
||||||
DestroySliceFlag(Id);
|
|
||||||
end;
|
|
||||||
|
|
||||||
function TMainForm.DispatchFlagsMouseDown(Button: TMouseButton; X, Y: Integer): Boolean;
|
|
||||||
var i: Integer;
|
|
||||||
begin
|
|
||||||
Result := False;
|
|
||||||
if Assigned(FVfoOverlay) and FVfoOverlay.Visible then
|
|
||||||
begin
|
|
||||||
FActiveFlag := FVfoOverlay;
|
|
||||||
if FVfoOverlay.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 TMainForm.DispatchFlagsMouseMove(X, Y: Integer);
|
|
||||||
var i: Integer;
|
|
||||||
begin
|
|
||||||
if Assigned(FVfoOverlay) then
|
|
||||||
begin
|
|
||||||
FActiveFlag := FVfoOverlay;
|
|
||||||
FVfoOverlay.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 TMainForm.DispatchFlagsMouseUp(X, Y: Integer): Boolean;
|
|
||||||
var i: Integer;
|
|
||||||
begin
|
|
||||||
Result := False;
|
|
||||||
if Assigned(FVfoOverlay) then
|
|
||||||
begin
|
|
||||||
FActiveFlag := FVfoOverlay;
|
|
||||||
if FVfoOverlay.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 TMainForm.DispatchFlagsMouseLeave;
|
|
||||||
var i: Integer;
|
|
||||||
begin
|
|
||||||
if Assigned(FVfoOverlay) then FVfoOverlay.HandleMouseLeave;
|
|
||||||
for i := 0 to High(FSliceFlags) do
|
|
||||||
if FSliceFlags[i] <> nil then FSliceFlags[i].HandleMouseLeave;
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TMainForm.OnSampleRateSelect(SampleRate: Integer);
|
procedure TMainForm.OnSampleRateSelect(SampleRate: Integer);
|
||||||
@@ -5980,22 +5593,6 @@ procedure TMainForm.FormMouseWheel(Sender: TObject; Shift: TShiftState;
|
|||||||
P := C.ScreenToClient(Mouse.CursorPos);
|
P := C.ScreenToClient(Mouse.CursorPos);
|
||||||
Result := (P.X >= 0) and (P.Y >= 0) and (P.X < C.Width) and (P.Y < C.Height);
|
Result := (P.X >= 0) and (P.Y >= 0) and (P.X < C.Width) and (P.Y < C.Height);
|
||||||
end;
|
end;
|
||||||
// Курсор над флагом-оверлеем слайса (в клиентских координатах спектра)?
|
|
||||||
function SliceUnderCursor: Integer;
|
|
||||||
var P: TPoint; i: Integer; O: TVfoOverlay;
|
|
||||||
begin
|
|
||||||
Result := 0;
|
|
||||||
if (PbSpectrum = nil) or (not PbSpectrum.Visible) then Exit;
|
|
||||||
P := PbSpectrum.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;
|
|
||||||
var
|
var
|
||||||
Step: Int64;
|
Step: Int64;
|
||||||
Delta: Int64;
|
Delta: Int64;
|
||||||
@@ -6011,7 +5608,7 @@ begin
|
|||||||
if OverFreqDisp(FreqDispA) or OverFreqDisp(FreqDispB) then Exit;
|
if OverFreqDisp(FreqDispA) or OverFreqDisp(FreqDispB) then Exit;
|
||||||
|
|
||||||
// Колесо над маркером слайса — двигаем частоту ЭТОГО слайса, а не главного VFO.
|
// Колесо над маркером слайса — двигаем частоту ЭТОГО слайса, а не главного VFO.
|
||||||
SliceId := SliceUnderCursor;
|
SliceId := FPan.SliceUnderCursor;
|
||||||
if (SliceId <> 0) and FController.GetSlice(SliceId, Sl) then
|
if (SliceId <> 0) and FController.GetSlice(SliceId, Sl) then
|
||||||
begin
|
begin
|
||||||
if (ssCtrl in Shift) and (ssShift in Shift) then Step := 1000000
|
if (ssCtrl in Shift) and (ssShift in Shift) then Step := 1000000
|
||||||
@@ -6029,8 +5626,8 @@ begin
|
|||||||
if SNew < SLo then SNew := SLo;
|
if SNew < SLo then SNew := SLo;
|
||||||
if SNew > SHi then SNew := SHi;
|
if SNew > SHi then SNew := SHi;
|
||||||
FController.SetSliceTarget(SliceId, SNew);
|
FController.SetSliceTarget(SliceId, SNew);
|
||||||
PushSliceFlagState(SliceId);
|
FPan.PushSliceFlagState(SliceId);
|
||||||
PositionSliceFlags;
|
FPan.LayoutFlags;
|
||||||
FSpectrumDirty := True;
|
FSpectrumDirty := True;
|
||||||
PbSpectrum.Invalidate;
|
PbSpectrum.Invalidate;
|
||||||
FActiveSliceId := SliceId;
|
FActiveSliceId := SliceId;
|
||||||
|
|||||||
@@ -16,6 +16,12 @@ unit PanafallPanel;
|
|||||||
|
|
||||||
Хозяин по-прежнему сам синхронизирует состояние view (частоты, палитры,
|
Хозяин по-прежнему сам синхронизирует состояние view (частоты, палитры,
|
||||||
wf-настройки) и решает, когда звать Layout (resize, show/hide, сплиттер).
|
wf-настройки) и решает, когда звать Layout (resize, show/hide, сплиттер).
|
||||||
|
|
||||||
|
Фаза B1: панель владеет коллекцией флагов слайсов (главный флаг A создаёт
|
||||||
|
хозяин и отдаёт через AttachMainFlag): раскладка LayoutFlags, диспетчер мыши
|
||||||
|
флагов, создание/закрытие слайс-флагов, заливка их состояния из контроллера,
|
||||||
|
тик S-метров (TickFlags). Семантические обработчики событий флага (MODE/DSP/
|
||||||
|
AUD/TX…) остаются у хозяина — он проводит их через OnWireFlag.
|
||||||
}
|
}
|
||||||
|
|
||||||
{$IFDEF FPC}
|
{$IFDEF FPC}
|
||||||
@@ -28,9 +34,13 @@ uses
|
|||||||
Classes, SysUtils, Math, Controls, ExtCtrls, Graphics, Forms,
|
Classes, SysUtils, Math, Controls, ExtCtrls, Graphics, Forms,
|
||||||
LCLIntf, LCLType,
|
LCLIntf, LCLType,
|
||||||
OpenGLContextEx,
|
OpenGLContextEx,
|
||||||
|
AppTheme, WDSPEngine, RadioController, VfoOverlay,
|
||||||
SpectrumView, SpectrumViewOpengl, PanZoomBar, FlatButton;
|
SpectrumView, SpectrumViewOpengl, PanZoomBar, FlatButton;
|
||||||
|
|
||||||
type
|
type
|
||||||
|
TWireFlagEvent = procedure(O: TVfoOverlay) of object;
|
||||||
|
TSliceClosedEvent = procedure(SliceId: Integer) of object;
|
||||||
|
|
||||||
TPanafallPanel = class(TComponent)
|
TPanafallPanel = class(TComponent)
|
||||||
private
|
private
|
||||||
FParent: TWinControl;
|
FParent: TWinControl;
|
||||||
@@ -63,6 +73,18 @@ type
|
|||||||
|
|
||||||
FOnSplitterMoved: TNotifyEvent;
|
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;
|
procedure SplitterMouseDown(Sender: TObject; Button: TMouseButton;
|
||||||
Shift: TShiftState; X, Y: Integer);
|
Shift: TShiftState; X, Y: Integer);
|
||||||
procedure SplitterMouseMove(Sender: TObject; Shift: TShiftState;
|
procedure SplitterMouseMove(Sender: TObject; Shift: TShiftState;
|
||||||
@@ -119,6 +141,49 @@ type
|
|||||||
// Пользователь отпустил сплиттер (ratio уже обновлён) — хозяин делает
|
// Пользователь отпустил сплиттер (ratio уже обновлён) — хозяин делает
|
||||||
// полный пересчёт (у него wideband/S-метр/оверлеи).
|
// полный пересчёт (у него wideband/S-метр/оверлеи).
|
||||||
property OnSplitterMoved: TNotifyEvent read FOnSplitterMoved write FOnSplitterMoved;
|
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;
|
end;
|
||||||
|
|
||||||
implementation
|
implementation
|
||||||
@@ -515,4 +580,432 @@ begin
|
|||||||
if FZoomBar <> nil then FZoomBar.HandleDblClick;
|
if FZoomBar <> nil then FZoomBar.HandleDblClick;
|
||||||
end;
|
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.
|
end.
|
||||||
|
|||||||
Reference in New Issue
Block a user