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