feat(slices): per-pan дисплей — палитра/gamma/уровни на пан («◑»-поповер)

Кнопка «◑» в шапке каждого пана открывает компактный поповер настроек
отображения (палитра, gamma, ref/range спектра, авто-уровень водопада +
offset/ручные min-max). Модель «глобальный дефолт + override на пан»: пан 0
правит глобальные поля рендера (зеркалятся в контроллер), паны N ставят
DisplayOverride и персистятся в воркспейс раздельно КВ/трансвертер.

- PanDisplayPopup.pas (new): borderless fsStayOnTop форма, гаснет по
  OnDeactivate; Flat*-контролы; крестик «×» закрытия; дим-хинт «правки
  применяются и сохраняются сами»; кнопки «⟳ Reset»/«⇄ Copy to all».
- TPanafallPanel: FBtnDisplay + OnDisplayClick + DisplayOverride;
  PositionHeaderChildren каскадит [×][⧉][◑] справа по Visible.
- MainForm: OnPanDisplayClick/Applied/Reset/ApplyAll + ApplyGlobalRenderToView
  + MarkPanDisplayDirty. При смене палитры/сбросе чистим историю GL-водопада
  (TSpectrumView.ClearWaterfall) — GL хранит уже раскрашенные пиксели.
- Settings/TPanCfg: под-объект 'display' (палитра/gamma/ref/range/wf-уровни),
  пишется только при override.

Глобальными осознанно оставлены разрешение анализатора, MSAA, скорость
водопада, детектор/усреднение (движок/перф, не косметика).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-13 13:49:19 +03:00
co-authored by Claude Opus 4.8
parent 543453007e
commit 3566f10191
6 changed files with 666 additions and 10 deletions
+357
View File
@@ -0,0 +1,357 @@
unit PanDisplayPopup;
{
TPanDisplayPopup — компактный поповер per-pan настроек отображения
(палитра / gamma / уровни спектра и водопада), открывается кнопкой «◑» в
шапке пана. Правки применяются к P.View живьём; после каждой изменения зовётся
OnApplied(P) — хозяин (MainForm) инвалидирует пан, ставит override-флаг и
зеркалит глобальные (для пана 0). Кнопки «Сброс к дефолту» / «Применить ко
всем» — через OnResetDefault / OnApplyAll.
Форма без рамки, поверх всех, гаснет по потере фокуса (OnDeactivate).
Этап 3.6 плана мультислайсов (doc/SLICES_PLAN.md), per-pan дисплей.
}
{$IFDEF FPC}
{$MODE Delphi}
{$ENDIF}
interface
uses
Classes, SysUtils, Math, Controls, ExtCtrls, StdCtrls, Graphics, Forms,
AppTheme, PanafallPanel,
FlatButton, FlatComboBox, FlatCheckBox, FlatSpinEdit, FlatFloatSpinEdit;
type
TPanDisplayEvent = procedure(P: TPanafallPanel) of object;
// FullRefresh=True ⇒ смена палитры/gamma/авто: чистим историю водопада.
TPanDisplayApplyEvent = procedure(P: TPanafallPanel; FullRefresh: Boolean) of object;
TPanDisplayPopup = class(TForm)
private
FPan: TPanafallPanel; // пан, чьи настройки правим (не владеем)
FLoading: Boolean; // подавляет OnApplied при программной заливке
FBg: TPanel;
FTitle: TLabel;
FCbPalette: TFlatComboBox;
FSeGamma: TFlatFloatSpinEdit;
FSeRef: TFlatSpinEdit;
FSeRange: TFlatSpinEdit;
FChkAuto: TFlatCheckBox;
FSeOffset: TFlatSpinEdit;
FSeMin: TFlatSpinEdit;
FSeMax: TFlatSpinEdit;
FLbOffset: TLabel;
FLbMin: TLabel;
FLbMax: TLabel;
FHint: TLabel;
FBtnClose: TFlatButton;
FBtnReset: TFlatButton;
FBtnAll: TFlatButton;
FOnApplied: TPanDisplayApplyEvent;
FOnResetDefault: TPanDisplayEvent;
FOnApplyAll: TPanDisplayEvent;
function MakeLbl(const Cap: string; ALeft, ATop, AW: Integer): TLabel;
procedure Apply(FullRefresh: Boolean);
procedure CtlChange(Sender: TObject);
procedure PaletteChange(Sender: TObject);
procedure AutoChange(Sender: TObject);
procedure CloseClick(Sender: TObject);
procedure ResetClick(Sender: TObject);
procedure AllClick(Sender: TObject);
procedure DoDeactivate(Sender: TObject);
procedure UpdateWfRowEnable;
public
constructor CreateNew(AOwner: TComponent; Num: Integer = 0); override;
procedure LoadFrom(P: TPanafallPanel);
// Показать под кнопкой (экранные координаты левого-нижнего угла кнопки).
procedure ShowFor(P: TPanafallPanel; ScreenX, ScreenY: Integer);
property OnApplied: TPanDisplayApplyEvent read FOnApplied write FOnApplied;
property OnResetDefault: TPanDisplayEvent read FOnResetDefault write FOnResetDefault;
property OnApplyAll: TPanDisplayEvent read FOnApplyAll write FOnApplyAll;
end;
implementation
const
{$IFDEF WINDOWS}
UI_FONT = 'Segoe UI';
{$ELSE}
UI_FONT = 'Sans';
{$ENDIF}
CLR_BG = TColor($00303030); // рамка (1px кайма формы)
CLR_PANEL = TColor($001A1A1A);
CLR_TEXT = TColor($00E0E0E0);
CLR_TEXTDIM = TColor($00888888);
CLR_ACCENT = TColor($0040FF80);
CLR_INPUT = TColor($00FFFFFF);
CLR_INPUT_TEXT = TColor($00202020);
POP_W = 244;
PAD = 10;
LBLW = 66;
CTLX = 84;
ROWH = 24;
WF_PALETTE_NAMES: array[0..2] of string = ('Classic', 'Inferno', 'Turbo');
constructor TPanDisplayPopup.CreateNew(AOwner: TComponent; Num: Integer);
var
Y: Integer;
function NextRow: Integer;
begin
Result := Y;
Inc(Y, ROWH + 4);
end;
function MakeIntSpin(ATop, AMin, AMax, AInc: Integer): TFlatSpinEdit;
begin
Result := TFlatSpinEdit.Create(Self);
Result.Parent := FBg;
Result.SetBounds(CTLX, ATop, POP_W - CTLX - PAD, ROWH - 2);
Result.MinValue := AMin;
Result.MaxValue := AMax;
Result.Increment := AInc;
Result.Color := CLR_INPUT;
Result.Font.Color := CLR_INPUT_TEXT;
Result.Font.Name := UI_FONT;
Result.Font.Size := 9;
Result.OnChange := CtlChange;
end;
begin
inherited CreateNew(AOwner, Num);
BorderStyle := bsNone;
FormStyle := fsStayOnTop;
Color := CLR_BG;
Font.Name := UI_FONT;
Font.Size := 9;
Font.Color := CLR_TEXT;
OnDeactivate := DoDeactivate;
FLoading := False;
FBg := TPanel.Create(Self);
FBg.Parent := Self;
FBg.BevelOuter := bvNone;
FBg.Color := CLR_PANEL;
// 1px рамка = зазор до края формы (цвет формы = CLR_BG).
FBg.SetBounds(1, 1, POP_W - 2, 10);
Y := PAD;
FTitle := MakeLbl('Display', PAD, NextRow + 2, POP_W - 2 * PAD - 18);
FTitle.Font.Color := CLR_ACCENT;
FTitle.Font.Style := [fsBold];
// Крестик закрытия в правом верхнем углу («готово» — правки сохраняются сами).
FBtnClose := MakeFlatBtn(FBg, '×', POP_W - PAD - 16, PAD, 16, 16, CloseClick);
FBtnClose.ClrText := CLR_TEXTDIM;
FBtnClose.ClrTextAct := CLR_TEXT;
// Хинты на кнопках поповера отключены: THintWindow в этой borderless-форме
// позиционирует подсказку не у элемента (у левой панели). Лейблы и так ясны.
// ---- Палитра ----
MakeLbl('Palette', PAD, Y + 3, LBLW);
FCbPalette := TFlatComboBox.Create(Self);
FCbPalette.Parent := FBg;
FCbPalette.SetBounds(CTLX, Y, POP_W - CTLX - PAD, ROWH - 2);
FCbPalette.Items.Add(WF_PALETTE_NAMES[0]);
FCbPalette.Items.Add(WF_PALETTE_NAMES[1]);
FCbPalette.Items.Add(WF_PALETTE_NAMES[2]);
FCbPalette.Color := CLR_INPUT;
FCbPalette.Font.Color := CLR_INPUT_TEXT;
FCbPalette.Font.Name := UI_FONT;
FCbPalette.Font.Size := 9;
FCbPalette.OnChange := PaletteChange; // смена палитры чистит историю водопада
NextRow;
// ---- Gamma ----
MakeLbl('Gamma', PAD, Y + 3, LBLW);
FSeGamma := TFlatFloatSpinEdit.Create(Self);
FSeGamma.Parent := FBg;
FSeGamma.SetBounds(CTLX, Y, POP_W - CTLX - PAD, ROWH - 2);
FSeGamma.MinValue := 0.30;
FSeGamma.MaxValue := 3.00;
FSeGamma.Increment := 0.05;
FSeGamma.DecimalPlaces := 2;
FSeGamma.Color := CLR_INPUT;
FSeGamma.Font.Color := CLR_INPUT_TEXT;
FSeGamma.Font.Name := UI_FONT;
FSeGamma.Font.Size := 9;
// gamma — непрерывный спин: чистить историю на каждый тик = мерцание;
// применяем к новым строкам (как ref/range), без сброса водопада.
FSeGamma.OnChange := CtlChange;
NextRow;
MakeLbl('— Spectrum —', PAD, Y + 3, POP_W - 2 * PAD).Font.Color := CLR_TEXTDIM;
NextRow;
// ---- Ref level / Range ----
MakeLbl('Ref, dBm', PAD, Y + 3, LBLW);
FSeRef := MakeIntSpin(Y, -200, 30, 5);
NextRow;
MakeLbl('Range, dB', PAD, Y + 3, LBLW);
FSeRange := MakeIntSpin(Y, 20, 200, 5);
NextRow;
MakeLbl('— Waterfall —', PAD, Y + 3, POP_W - 2 * PAD).Font.Color := CLR_TEXTDIM;
NextRow;
// ---- Авто-уровень водопада ----
FChkAuto := TFlatCheckBox.Create(Self);
FChkAuto.Parent := FBg;
FChkAuto.SetBounds(PAD, Y + 2, POP_W - 2 * PAD, ROWH - 4);
FChkAuto.Caption := 'Auto level';
FChkAuto.Font.Color := CLR_TEXT;
FChkAuto.Font.Name := UI_FONT;
FChkAuto.Font.Size := 9;
FChkAuto.OnChange := AutoChange;
NextRow;
FLbOffset := MakeLbl('Offset, dB', PAD, Y + 3, LBLW);
FSeOffset := MakeIntSpin(Y, -60, 60, 1);
NextRow;
FLbMin := MakeLbl('Min, dBm', PAD, Y + 3, LBLW);
FSeMin := MakeIntSpin(Y, -200, 0, 5);
NextRow;
FLbMax := MakeLbl('Max, dBm', PAD, Y + 3, LBLW);
FSeMax := MakeIntSpin(Y, -160, 30, 5);
NextRow;
// ---- Подсказка: правки применяются и сохраняются сами (не нужно «сохранять») ----
FHint := MakeLbl('Changes apply & save automatically', PAD, Y + 2, POP_W - 2 * PAD);
FHint.Font.Color := CLR_TEXTDIM;
Inc(Y, 18);
// ---- Кнопки действий (обе — вторичные, не «сохранение») ----
FBtnReset := MakeFlatBtn(FBg, '⟳ Reset', PAD, Y, (POP_W - 3 * PAD) div 2, ROWH, ResetClick);
FBtnReset.ClrText := CLR_TEXT;
FBtnReset.ClrTextAct := CLR_ACCENT;
FBtnAll := MakeFlatBtn(FBg, '⇄ Copy to all',
PAD + (POP_W - 3 * PAD) div 2 + PAD, Y, (POP_W - 3 * PAD) div 2, ROWH, AllClick);
FBtnAll.ClrText := CLR_TEXT;
FBtnAll.ClrTextAct := CLR_ACCENT;
Inc(Y, ROWH + PAD);
FBg.Height := Y - 1;
ClientWidth := POP_W;
ClientHeight := Y + 1;
end;
function TPanDisplayPopup.MakeLbl(const Cap: string; ALeft, ATop, AW: Integer): TLabel;
begin
Result := TLabel.Create(Self);
Result.Parent := FBg;
Result.Caption := Cap;
Result.Left := ALeft;
Result.Top := ATop;
if AW > 0 then Result.Width := AW;
Result.Font.Color := CLR_TEXT;
Result.Font.Name := UI_FONT;
Result.Font.Size := 9;
end;
procedure TPanDisplayPopup.UpdateWfRowEnable;
var Man: Boolean;
begin
Man := not FChkAuto.Checked;
FLbOffset.Enabled := not Man; FSeOffset.Enabled := not Man;
FLbMin.Enabled := Man; FSeMin.Enabled := Man;
FLbMax.Enabled := Man; FSeMax.Enabled := Man;
end;
procedure TPanDisplayPopup.LoadFrom(P: TPanafallPanel);
begin
FPan := P;
if P = nil then Exit;
FLoading := True;
try
if P.PanId = 0 then FTitle.Caption := 'Display · PAN 0'
else FTitle.Caption := Format('Display · PAN %d', [P.PanId]);
FCbPalette.ItemIndex := EnsureRange(P.View.WfPalette, 0, 2);
FSeGamma.Value := EnsureRange(P.View.WfGamma, 0.30, 3.00);
FSeRef.Value := Round(P.View.SpecRefLevel);
FSeRange.Value := Round(P.View.SpecRange);
FChkAuto.Checked := P.View.WfAGCEnabled;
FSeOffset.Value := Round(P.View.WfAGCOffset);
FSeMin.Value := Round(P.View.WfManualLow);
FSeMax.Value := Round(P.View.WfManualHigh);
// Сброс к дефолту осмыслен только для панов N (пан 0 = сам дефолт).
FBtnReset.Visible := P.PanId > 0;
UpdateWfRowEnable;
finally
FLoading := False;
end;
end;
procedure TPanDisplayPopup.Apply(FullRefresh: Boolean);
begin
if FLoading or (FPan = nil) then Exit;
FPan.View.WfPalette := FCbPalette.ItemIndex;
FPan.View.WfGamma := FSeGamma.Value;
FPan.View.SpecRefLevel := FSeRef.Value;
FPan.View.SpecRange := FSeRange.Value;
FPan.View.WfAGCEnabled := FChkAuto.Checked;
FPan.View.WfAGCOffset := FSeOffset.Value;
FPan.View.WfManualLow := FSeMin.Value;
FPan.View.WfManualHigh := FSeMax.Value;
if Assigned(FOnApplied) then FOnApplied(FPan, FullRefresh);
end;
procedure TPanDisplayPopup.CtlChange(Sender: TObject);
begin
Apply(False); // ref/range/offset/min/max: применяются к новым строкам
end;
procedure TPanDisplayPopup.PaletteChange(Sender: TObject);
begin
Apply(True); // палитра (дискретный выбор): перекрасить весь водопад
end;
procedure TPanDisplayPopup.AutoChange(Sender: TObject);
begin
UpdateWfRowEnable;
Apply(True); // смена режима уровня меняет отображение всей истории
end;
procedure TPanDisplayPopup.CloseClick(Sender: TObject);
begin
Hide;
end;
procedure TPanDisplayPopup.ResetClick(Sender: TObject);
begin
if (FPan = nil) or not Assigned(FOnResetDefault) then Exit;
FOnResetDefault(FPan);
LoadFrom(FPan); // перезалить контролы значениями глобального дефолта
end;
procedure TPanDisplayPopup.AllClick(Sender: TObject);
begin
if (FPan = nil) or not Assigned(FOnApplyAll) then Exit;
FOnApplyAll(FPan);
end;
procedure TPanDisplayPopup.DoDeactivate(Sender: TObject);
begin
Hide;
end;
procedure TPanDisplayPopup.ShowFor(P: TPanafallPanel; ScreenX, ScreenY: Integer);
var L, T: Integer;
begin
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;
SetBounds(L, T, Width, Height);
Show;
end;
end.