feat(slices): кастомные тематизированные popup-меню панов + фиксы позиционирования

Селекторы диапазона/samplerate панов N переведены с нативного TPopupMenu на
своё меню в стиле приложения (одинаково на всех платформах, в цветовой теме).

- FlatPopupMenu.pas (new): TFlatPopupMenu — owner-draw меню как ДОЧЕРНИЙ контрол
  формы (не top-level окно: Wayland центрирует свои top-level окна). Hover,
  галка текущего, клавиатура (↑↓/Enter/Esc), закрытие по клику-вне (MouseCapture)
  и потере фокуса. Позиционируется под лейблом-якорем в клиентских координатах.
- PanDisplayPopup: поповер «◑» тоже переведён с top-level формы на дочерний
  контрол (центрировался на Wayland) — открывается под кнопкой, закрытие «×»
  или повторным «◑».
- MainForm: band/rate-селекторы на FlatPopupMenu; свёртка повторяющегося
  выбора темы (9 инлайнов) в CurrentAppTheme.
- PanafallPanel: курсор-«палец» кликабельных лейблов шапки переприменяется в
  конце Build (флаги *Clickable могли выставляться до создания лейблов —
  samplerate-лейбл не получал crHandPoint).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-13 15:02:08 +03:00
co-authored by Claude Opus 4.8
parent 3ee996154a
commit 9bb86b59b8
4 changed files with 415 additions and 79 deletions
+47 -49
View File
@@ -33,7 +33,7 @@ uses
WebServer, WebAdapter,
CATAdapter,
SpectrumView, SpectrumViewOpengl,
PanZoomBar, PanafallPanel, PanDisplayPopup,
PanZoomBar, PanafallPanel, PanDisplayPopup, FlatPopupMenu,
WidebandView,
StatusBar,
PlatformUtils,
@@ -200,9 +200,9 @@ type
FPanWideH: Integer; // wideband-высота последнего LayoutPanStack (для PanMinHeight)
FPendingPanClose: Integer; // «×» шапки: PanId к закрытию в тике (0 = нет)
// Меню rate пана («NNN kHz» в шапке панов N; создаётся лениво).
FPanRateMenu: TPopupMenu;
FPanRateMenu: TFlatPopupMenu; // тематизированное меню (не нативное)
FPanRateMenuPanId: Integer;
FPanBandMenu: TPopupMenu; // селектор диапазона пана N (3.6, КВ)
FPanBandMenu: TFlatPopupMenu; // селектор диапазона пана N (3.6, КВ)
FPanBandMenuPanId: Integer;
// Pop-out (3.5): пан N в отдельном OS-окне. nil = пан в стеке.
FPanFloatForm: array[1..MAX_PANS-1] of TForm;
@@ -605,9 +605,10 @@ type
procedure ApplyPanHeaderTheme(P: TPanafallPanel); // цвета шапки пана
procedure UpdateAddPanButton;
procedure OnPanRateClick(Sender: TObject); // «NNN kHz» шапки пана N
procedure PanRateMenuItemClick(Sender: TObject);
procedure OnPanRateSelect(Tag: Integer); // выбор rate из flat-меню
procedure OnPanBandClick(Sender: TObject); // «20m» шапки пана N (3.6)
procedure PanBandMenuItemClick(Sender: TObject);
procedure OnPanBandSelect(Tag: Integer); // выбор диапазона из flat-меню
function CurrentAppTheme: TAppTheme; // тек. тема (light/dark)
procedure ApplyPanBand(P: TPanafallPanel; BandIdx: Integer); // Flex-style смена банда пана
function PanBandSelectorVisible: Boolean; // КВ (не XVTR, не Pluto)
procedure OnPanADCClick(Sender: TObject); // бейдж «RX1/RX2» (3.4)
@@ -2829,7 +2830,7 @@ var T: TAppTheme; i: Integer;
T.SliderThumbNorm, T.SliderThumbHot, T.SliderThumbDrag, T.SliderThumbBdr);
end;
begin
if FLightTheme then T := LightTheme else T := DarkTheme;
T := CurrentAppTheme;
Color := T.BG; Font.Color := T.Text;
DP(PanelToolbar); DP(PanelLeft);
@@ -2994,7 +2995,7 @@ end;
procedure TMainForm.StyleButton(B: TFlatButton; Active: Boolean);
var T: TAppTheme;
begin
if FLightTheme then T := LightTheme else T := DarkTheme;
T := CurrentAppTheme;
B.Active := Active;
B.ClrNorm := T.BtnNorm;
B.ClrActive := T.BtnActive;
@@ -3010,7 +3011,7 @@ end;
procedure TMainForm.StyleSpanButton(B: TFlatButton; Active: Boolean);
var T: TAppTheme;
begin
if FLightTheme then T := LightTheme else T := DarkTheme;
T := CurrentAppTheme;
B.Active := Active;
B.ClrNorm := T.SpanNorm;
B.ClrActive := T.SpanActive;
@@ -3068,7 +3069,7 @@ const
TOP_VFO_FREQ_H = 47;
var T: TAppTheme;
begin
if FLightTheme then T := LightTheme else T := DarkTheme;
T := CurrentAppTheme;
FreqDispA.Frequency := Round(FController.FVfoA);
FreqDispB.Frequency := Round(FController.FVfoB);
@@ -3747,8 +3748,7 @@ begin
FDeviceDialog := TDeviceDialog.Create(Self);
FDeviceDialog.Store := FController.FDeviceStore;
FDeviceDialog.OnDiscover := BtnDiscoverFromDialog;
if FLightTheme then FDeviceDialog.SetTheme(LightTheme)
else FDeviceDialog.SetTheme(DarkTheme);
FDeviceDialog.SetTheme(CurrentAppTheme);
FDeviceDialog.ClearResult; // сбрасываем старый выбор перед открытием
FDeviceDialog.ShowModal;
// Если пользователь нажал CONNECT — результат хранится в DialogResult,
@@ -3836,8 +3836,7 @@ begin
FDeviceDialog := TDeviceDialog.Create(Self);
FDeviceDialog.Store := FController.FDeviceStore;
FDeviceDialog.OnDiscover := BtnDiscoverFromDialog;
if FLightTheme then FDeviceDialog.SetTheme(LightTheme)
else FDeviceDialog.SetTheme(DarkTheme);
FDeviceDialog.SetTheme(CurrentAppTheme);
if FDeviceDialog.ShowModal <> mrOk then Exit;
if not FDeviceDialog.DialogResult.Accepted then Exit;
FPendingIP := FDeviceDialog.DialogResult.IPAddress;
@@ -5468,7 +5467,7 @@ begin
O.OnClose := OnVfoOverlayClose;
O.OnInvalidate := OnVfoOverlayInvalidate;
// Актуальная тема (цвета кнопок флага = как на левой панели).
if FLightTheme then O.SetTheme(LightTheme) else O.SetTheme(DarkTheme);
O.SetTheme(CurrentAppTheme);
end;
// Добавить софт-слайс на частоте Hz (режим/полоса копируются с главного).
@@ -5601,32 +5600,30 @@ procedure TMainForm.OnPanRateClick(Sender: TObject);
// (фильтр PanRateAllowed — кратные 48 кГц). Текущий rate отмечен галкой.
var
P: TPanafallPanel;
MI: TMenuItem;
i, kHz: Integer;
i, kHz, Cur: Integer;
begin
if not (Sender is TPanafallPanel) then Exit;
P := TPanafallPanel(Sender);
if (P.PanId < 1) or not FController.PanDDCActive(P.PanId) then Exit;
if FPanRateMenu = nil then
FPanRateMenu := TPopupMenu.Create(Self);
FPanRateMenu.Items.Clear;
begin
FPanRateMenu := TFlatPopupMenu.Create(Self);
FPanRateMenu.OnSelect := OnPanRateSelect;
end;
FPanRateMenu.SetTheme(CurrentAppTheme);
FPanRateMenu.Clear;
Cur := FController.PanDDCRateKHz(P.PanId);
for i := 0 to High(FController.BackendCaps.RatePresets) do
begin
kHz := FController.BackendCaps.RatePresets[i] div 1000;
if not FController.PanRateAllowed(Word(kHz)) then Continue;
MI := TMenuItem.Create(FPanRateMenu);
MI.Caption := Format('%d kHz', [kHz]);
MI.Tag := kHz;
MI.Checked := kHz = FController.PanDDCRateKHz(P.PanId);
MI.OnClick := PanRateMenuItemClick;
FPanRateMenu.Items.Add(MI);
FPanRateMenu.AddItem(Format('%d kHz', [kHz]), kHz, kHz = Cur);
end;
if FPanRateMenu.Items.Count = 0 then Exit;
FPanRateMenuPanId := P.PanId;
FPanRateMenu.PopUp; // в позиции курсора
FPanRateMenu.PopupBelow(P.HeaderRateLabel); // дочерний контрол формы
end;
procedure TMainForm.PanRateMenuItemClick(Sender: TObject);
procedure TMainForm.OnPanRateSelect(Tag: Integer);
var
P: TPanafallPanel;
PanId: Integer;
@@ -5636,7 +5633,7 @@ begin
if (PanId < 1) or (PanId >= MAX_PANS) then Exit;
P := FPans[PanId];
if P = nil then Exit;
if not FController.SetPanDDCRate(PanId, Word(TMenuItem(Sender).Tag)) then Exit;
if not FController.SetPanDDCRate(PanId, Word(Tag)) then Exit;
// Вьюха/шапка/флаги под новый span (слайсы могли клампнуться к краям).
SyncPanViews;
P.PushAllSliceFlagStates;
@@ -5644,6 +5641,11 @@ begin
MarkPanDirty(P);
end;
function TMainForm.CurrentAppTheme: TAppTheme;
begin
if FLightTheme then Result := LightTheme else Result := DarkTheme;
end;
function TMainForm.PanBandSelectorVisible: Boolean;
// Селектор диапазона пана — только на «настоящих КВ»: не в XVTR и не Pluto
// (у Pluto/трансвертера band-стек другой, сценарий не актуален).
@@ -5656,7 +5658,6 @@ procedure TMainForm.OnPanBandClick(Sender: TObject);
// частоте DDC пана) отмечен галкой.
var
P: TPanafallPanel;
MI: TMenuItem;
i, CurBand: Integer;
begin
if not (Sender is TPanafallPanel) then Exit;
@@ -5665,31 +5666,29 @@ begin
if not PanBandSelectorVisible then Exit;
CurBand := FreqToBandIdx(FController.PanDDCFreq(P.PanId), False);
if FPanBandMenu = nil then
FPanBandMenu := TPopupMenu.Create(Self);
FPanBandMenu.Items.Clear;
begin
FPanBandMenu := TFlatPopupMenu.Create(Self);
FPanBandMenu.OnSelect := OnPanBandSelect;
end;
FPanBandMenu.SetTheme(CurrentAppTheme);
FPanBandMenu.Clear;
for i := 0 to CFG_BAND_COUNT - 1 do
begin
if BandName(i, False) = '' then Continue; // 6m-дубль/пустые слоты
MI := TMenuItem.Create(FPanBandMenu);
MI.Caption := BandName(i, False);
MI.Tag := i;
MI.Checked := i = CurBand;
MI.OnClick := PanBandMenuItemClick;
FPanBandMenu.Items.Add(MI);
FPanBandMenu.AddItem(BandName(i, False), i, i = CurBand);
end;
if FPanBandMenu.Items.Count = 0 then Exit;
FPanBandMenuPanId := P.PanId;
FPanBandMenu.PopUp;
FPanBandMenu.PopupBelow(P.HeaderBandLabel);
end;
procedure TMainForm.PanBandMenuItemClick(Sender: TObject);
procedure TMainForm.OnPanBandSelect(Tag: Integer);
var PanId: Integer;
begin
PanId := FPanBandMenuPanId;
FPanBandMenuPanId := 0;
if (PanId < 1) or (PanId >= MAX_PANS) then Exit;
if FPans[PanId] = nil then Exit;
ApplyPanBand(FPans[PanId], TMenuItem(Sender).Tag);
ApplyPanBand(FPans[PanId], Tag);
end;
procedure TMainForm.ApplyPanBand(P: TPanafallPanel; BandIdx: Integer);
@@ -5791,18 +5790,17 @@ begin
end;
procedure TMainForm.OnPanDisplayClick(Sender: TObject);
// «◑» в шапке пана: показать поповер под кнопкой.
// «◑» в шапке пана: тоггл поповера под кнопкой (дочерний контрол формы).
var
P: TPanafallPanel;
Pop: TPanDisplayPopup;
Pt: TPoint;
begin
if not (Sender is TPanafallPanel) then Exit;
P := TPanafallPanel(Sender);
if P.BtnDisplay = nil then Exit;
if FPanDisplayPopup = nil then
begin
Pop := TPanDisplayPopup.CreateNew(Self);
Pop := TPanDisplayPopup.Create(Self);
Pop.OnApplied := OnPanDisplayApplied;
Pop.OnResetDefault := OnPanDisplayReset;
Pop.OnApplyAll := OnPanDisplayApplyAll;
@@ -5810,9 +5808,9 @@ begin
end
else
Pop := TPanDisplayPopup(FPanDisplayPopup);
// Экранная позиция левого-нижнего угла кнопки.
Pt := P.BtnDisplay.ClientToScreen(Point(0, P.BtnDisplay.Height + 2));
Pop.ShowFor(P, Pt.X, Pt.Y);
// Повторный клик по «◑» — закрыть.
if Pop.Visible then begin Pop.Hide; Exit; end;
Pop.PopupBelow(P, P.BtnDisplay);
end;
procedure TMainForm.OnPanDisplayApplied(P: TPanafallPanel; FullRefresh: Boolean);
@@ -5904,7 +5902,7 @@ begin
if (PanId < 1) or (PanId >= MAX_PANS) then Exit;
P := FPans[PanId];
if (P = nil) or PanFloating(PanId) then Exit;
if FLightTheme then T := LightTheme else T := DarkTheme;
T := CurrentAppTheme;
F := TForm.CreateNew(Self);
F.Caption := Format('EWSDR · PAN %d', [PanId]);
F.Color := T.BG;
@@ -6021,7 +6019,7 @@ procedure TMainForm.ApplyPanHeaderTheme(P: TPanafallPanel);
var T: TAppTheme;
begin
if (P = nil) or (P.HeaderPanel = nil) then Exit;
if FLightTheme then T := LightTheme else T := DarkTheme;
T := CurrentAppTheme;
P.HeaderPanel.Color := T.Panel;
P.HeaderLabel.Font.Color := T.Text;
// Rate-лейбл: кликабельный (паны N) подсвечиваем акцентом селектора.