From cc9919c35a6fea6beec636d68ec140f0099e8cc4 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Fri, 10 Jul 2026 22:51:47 +0300 Subject: [PATCH] =?UTF-8?q?refactor(slices):=20=D1=8D=D1=82=D0=B0=D0=BF=20?= =?UTF-8?q?3.0B1=20=E2=80=94=20=D1=84=D0=BB=D0=B0=D0=B3=D0=B8=20=D1=81?= =?UTF-8?q?=D0=BB=D0=B0=D0=B9=D1=81=D0=BE=D0=B2=20=D0=BF=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D0=B5=D1=85=D0=B0=D0=BB=D0=B8=20=D0=B2=20TPanafallPanel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Панель владеет коллекцией флагов B+ (создание/закрытие/заливка состояния из контроллера), раскладкой LayoutFlags, диспетчером мыши флагов, тиком S-метров (TickFlags) и видимостью флага A (UpdateMainFlagVisibility). Главный флаг A создаёт MainForm (проводка семантических событий у него) и отдаёт через AttachMainFlag; новые флаги проводятся через OnWireFlag. Роутинг по слайсу — FPan.CurSliceId; закрытие «×» — RequestSliceClose/OnSliceClosed. Co-Authored-By: Claude Fable 5 --- MainForm.pas | 495 +++++----------------------------------------- PanafallPanel.pas | 493 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 539 insertions(+), 449 deletions(-) diff --git a/MainForm.pas b/MainForm.pas index bb92cb4..b67f9fe 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -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; diff --git a/PanafallPanel.pas b/PanafallPanel.pas index 68f4166..8d37216 100644 --- a/PanafallPanel.pas +++ b/PanafallPanel.pas @@ -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.