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 -30
View File
@@ -8,7 +8,10 @@ unit PanDisplayPopup;
зеркалит глобальные (для пана 0). Кнопки «Сброс к дефолту» / «Применить ко
всем» — через OnResetDefault / OnApplyAll.
Форма без рамки, поверх всех, гаснет по потере фокуса (OnDeactivate).
Реализован как ДОЧЕРНИЙ контрол верхнеуровневой формы (не отдельное окно):
Wayland не даёт клиенту позиционировать свои top-level окна — они
центрируются компоновщиком. Дочерний контрол позиционируется в клиентских
координатах под кнопкой «◑». Закрытие — крестиком «×» или повторным «◑».
Этап 3.6 плана мультислайсов (doc/SLICES_PLAN.md), per-pan дисплей.
}
@@ -19,7 +22,7 @@ unit PanDisplayPopup;
interface
uses
Classes, SysUtils, Math, Controls, ExtCtrls, StdCtrls, Graphics, Forms,
Classes, SysUtils, Math, Types, Controls, ExtCtrls, StdCtrls, Graphics, Forms,
AppTheme, PanafallPanel,
FlatButton, FlatComboBox, FlatCheckBox, FlatSpinEdit, FlatFloatSpinEdit;
@@ -28,7 +31,7 @@ type
// FullRefresh=True ⇒ смена палитры/gamma/авто: чистим историю водопада.
TPanDisplayApplyEvent = procedure(P: TPanafallPanel; FullRefresh: Boolean) of object;
TPanDisplayPopup = class(TForm)
TPanDisplayPopup = class(TCustomControl)
private
FPan: TPanafallPanel; // пан, чьи настройки правим (не владеем)
FLoading: Boolean; // подавляет OnApplied при программной заливке
@@ -61,13 +64,14 @@ type
procedure CloseClick(Sender: TObject);
procedure ResetClick(Sender: TObject);
procedure AllClick(Sender: TObject);
procedure DoDeactivate(Sender: TObject);
procedure UpdateWfRowEnable;
protected
procedure Paint; override;
public
constructor CreateNew(AOwner: TComponent; Num: Integer = 0); override;
constructor Create(AOwner: TComponent); override;
procedure LoadFrom(P: TPanafallPanel);
// Показать под кнопкой (экранные координаты левого-нижнего угла кнопки).
procedure ShowFor(P: TPanafallPanel; ScreenX, ScreenY: Integer);
// Показать под контролом-якорем (кнопкой «◑») как дочерний контрол формы.
procedure PopupBelow(P: TPanafallPanel; Anchor: TControl);
property OnApplied: TPanDisplayApplyEvent read FOnApplied write FOnApplied;
property OnResetDefault: TPanDisplayEvent read FOnResetDefault write FOnResetDefault;
property OnApplyAll: TPanDisplayEvent read FOnApplyAll write FOnApplyAll;
@@ -97,7 +101,7 @@ const
WF_PALETTE_NAMES: array[0..2] of string = ('Classic', 'Inferno', 'Turbo');
constructor TPanDisplayPopup.CreateNew(AOwner: TComponent; Num: Integer);
constructor TPanDisplayPopup.Create(AOwner: TComponent);
var
Y: Integer;
@@ -123,14 +127,13 @@ var
end;
begin
inherited CreateNew(AOwner, Num);
BorderStyle := bsNone;
FormStyle := fsStayOnTop;
inherited Create(AOwner);
ControlStyle := ControlStyle + [csOpaque];
Visible := False;
Color := CLR_BG;
Font.Name := UI_FONT;
Font.Size := 9;
Font.Color := CLR_TEXT;
OnDeactivate := DoDeactivate;
FLoading := False;
FBg := TPanel.Create(Self);
@@ -235,9 +238,18 @@ begin
FBtnAll.ClrTextAct := CLR_ACCENT;
Inc(Y, ROWH + PAD);
FBg.Height := Y - 1;
ClientWidth := POP_W;
ClientHeight := Y + 1;
FBg.Height := Y - 1;
Width := POP_W;
Height := Y + 1;
end;
procedure TPanDisplayPopup.Paint;
begin
// 1px рамка вокруг FBg (FBg = CLR_PANEL перекрывает нутро).
Canvas.Brush.Style := bsSolid;
Canvas.Brush.Color := CLR_BG;
Canvas.Pen.Style := psClear;
Canvas.FillRect(0, 0, Width, Height);
end;
function TPanDisplayPopup.MakeLbl(const Cap: string; ALeft, ATop, AW: Integer): TLabel;
@@ -334,24 +346,29 @@ begin
FOnApplyAll(FPan);
end;
procedure TPanDisplayPopup.DoDeactivate(Sender: TObject);
begin
Hide;
end;
procedure TPanDisplayPopup.ShowFor(P: TPanafallPanel; ScreenX, ScreenY: Integer);
var L, T: Integer;
procedure TPanDisplayPopup.PopupBelow(P: TPanafallPanel; Anchor: TControl);
var
Root: TCustomForm;
L, T: Integer;
Cl: TPoint;
begin
if Anchor = nil then Exit;
Root := GetParentForm(Anchor);
if Root = nil then Exit;
LoadFrom(P);
L := ScreenX;
T := ScreenY;
// Кламп в экран.
if L + Width > Screen.Width then L := Screen.Width - Width - 4;
if L < 0 then L := 4;
if T + Height > Screen.Height then T := ScreenY - Height - 4; // раскрыть вверх
if T < 0 then T := 4;
// Позиция под якорем в КЛИЕНТСКИХ координатах формы (не top-level окно!).
Cl := Root.ScreenToClient(Anchor.ClientToScreen(Point(0, Anchor.Height + 2)));
L := Cl.X;
T := Cl.Y;
if L + Width > Root.ClientWidth then L := Root.ClientWidth - Width;
if L < 0 then L := 0;
if T + Height > Root.ClientHeight then
T := Root.ScreenToClient(Anchor.ClientToScreen(Point(0, 0))).Y - Height; // вверх
if T < 0 then T := 0;
Parent := Root;
SetBounds(L, T, Width, Height);
Show;
Visible := True;
BringToFront;
end;
end.