Replace PanelSpanButtons with SampleRateOverlay overlay

- Add SampleRateOverlay.pas: TCustomControl overlay (top-left of spectrum)
  with hide-panel button and 6 span buttons (48k–1536k), same hit-test
  pattern as VfoOverlay
- Consolidate toolbar: move PanelTopVfoGroup and PanelSMeterRight into
  PanelToolbar (height 72) instead of floating on the form
- Remove PanelSpanButtons (alTop spacer) that caused the ~1cm black strip
  at the top of the spectrum
- Add separator line inside PanelToolbar anchored to its bottom edge,
  with 16px left/right margins
- Fix left panel blocks starting Y offset (was 38, now 0)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-22 15:31:42 +03:00
co-authored by Claude Sonnet 4.6
parent 24ff61cb05
commit ea6e57b8b8
3 changed files with 320 additions and 212 deletions
+75 -212
View File
@@ -22,7 +22,8 @@ unit MainForm;
interface
uses
Classes, SysUtils, StrUtils, FreqDisplay, DeviceForm, VfoOverlay, FlatButton, FlatSlider, FlatDropDown, AppTheme, Forms, Controls, Graphics, Dialogs,
Classes, SysUtils, StrUtils, FreqDisplay, DeviceForm, VfoOverlay, SampleRateOverlay,
FlatButton, FlatSlider, FlatDropDown, AppTheme, Forms, Controls, Graphics, Dialogs,
StdCtrls, ExtCtrls, ComCtrls, Buttons, Menus, Math, Types,
LCLIntf, LCLType, GraphType,
HPSDRProtocol, HPSDRNetwork,
@@ -179,8 +180,9 @@ type
FDeviceCount: Integer;
FDeviceDialog: TDeviceDialog;
FPendingIP: string; // IP устройства для подключения
FVfoOverlay: TVfoOverlay; // накладка SmartSDR-стиль на спектре
FPanelHidden: Boolean; // True = левая панель скрыта
FVfoOverlay: TVfoOverlay; // накладка SmartSDR-стиль на спектре
FSampleRateOverlay: TSampleRateOverlay; // оверлей span/hide слева вверху спектра
FPanelHidden: Boolean; // True = левая панель скрыта
FPendingBoardType: Integer; // BoardType устройства для подключения
// ---- DSP + Audio ----
@@ -341,12 +343,11 @@ type
FSpectrumTimer: TTimer;
FAfterShowTimer:TTimer; // однократный таймер для пост-инициализации
// ---- Toolbar ----
// ---- Top panel (toolbar + VFO group + S-meter) ----
PanelToolbar: TPanel;
BtnDiscover: TFlatButton;
BtnStartStop: TFlatButton; // START / STOP (connect+run в одном)
BtnSettings: TFlatButton; // открыть диалог настроек
PanelSep: TPanel; // 1px разделитель под тулбаром
BtnStartStop: TFlatButton;
BtnSettings: TFlatButton;
// ---- Left panel ----
PanelLeft: TPanel;
@@ -418,16 +419,7 @@ type
BtnTUN: TFlatButton;
// ---- Right panel ----
PanelRight: TPanel;
PanelSpanButtons: TPanel;
LblSpan: TLabel;
BtnSpan48k: TFlatButton;
BtnSpan96k: TFlatButton;
BtnSpan192k: TFlatButton;
BtnSpan384k: TFlatButton;
BtnSpan768k: TFlatButton;
BtnSpan1536k: TFlatButton;
BtnHidePanel: TFlatButton; // скрыть/показать левую панель
PanelRight: TPanel;
PbSpectrum: TPaintBox;
PbRuler: TPaintBox; // полоса частотных меток между спектром и водопадом
PanelSplitter: TPanel; // перетаскиваемый разделитель спектр/водопад
@@ -445,7 +437,9 @@ type
procedure SetStatusText(Index: Integer; const Text: string);
procedure SetRadioOfflineStatus(const ConnText: string; ClearDevice: Boolean = False);
procedure ApplyStatusTheme(const T: TAppTheme);
procedure PositionSpanOverlayButtons;
procedure PositionSampleRateOverlay;
procedure OnSampleRateSelect(SampleRate: Integer);
procedure OnSampleRateHidePanel;
procedure UpdateVfoDisplay;
function FormatFreq(Hz: Double): string;
procedure LayoutTopVfoBlock;
@@ -551,7 +545,6 @@ type
procedure BtnNBClick(Sender: TObject);
procedure BtnSNBClick(Sender: TObject);
procedure BtnANFClick(Sender: TObject);
procedure BtnSpanClick(Sender: TObject);
// Веб-интерфейс: callbacks от TWebServer
procedure WebOnFreq(Hz: Double);
procedure WebOnMode(Mode: Integer);
@@ -660,7 +653,6 @@ type
procedure ActivateXvtrBand(Idx: Integer);
procedure DeactivateXvtr;
// -------------------------------------------------------------------------
procedure BtnHidePanelClick(Sender: TObject);
procedure BtnSettingsClick(Sender: TObject);
procedure PositionVfoOverlay;
procedure OnModeFilterSelect(Mode: Integer; FilterBW: Integer);
@@ -1127,14 +1119,9 @@ begin
// --- Sample Rate / Span ---
// SampleRate — глобальный, не per-band. Не восстанавливаем из диапазона.
// Обновляем только FSpanHz для отображения (берём текущий FSampleRate)
FSpanHz := FSampleRate;
StyleSpanButton(BtnSpan48k, FSampleRate = 48000);
StyleSpanButton(BtnSpan96k, FSampleRate = 96000);
StyleSpanButton(BtnSpan192k, FSampleRate = 192000);
StyleSpanButton(BtnSpan384k, FSampleRate = 384000);
StyleSpanButton(BtnSpan768k, FSampleRate = 768000);
StyleSpanButton(BtnSpan1536k, FSampleRate = 1536000);
if Assigned(FSampleRateOverlay) then
FSampleRateOverlay.SetCurrentRate(FSampleRate);
// --- VFO A: центрируем на новой частоте, сбрасываем shift ---
FCenterFreq := B.VfoA; // DDC = центр диапазона
@@ -1375,12 +1362,6 @@ begin
FCurrentXvtr := -1;
BuildUI;
StyleSpanButton(BtnSpan48k, FSampleRate = 48000);
StyleSpanButton(BtnSpan96k, FSampleRate = 96000);
StyleSpanButton(BtnSpan192k, FSampleRate = 192000);
StyleSpanButton(BtnSpan384k, FSampleRate = 384000);
StyleSpanButton(BtnSpan768k, FSampleRate = 768000);
StyleSpanButton(BtnSpan1536k, FSampleRate = 1536000);
ResizeSMeter;
ApplyDarkTheme;
UpdateVfoDisplay;
@@ -1501,6 +1482,7 @@ const
var
i, X, Y, W: Integer;
B: TFlatButton;
SepLine: TPanel;
function MakeBtn(Parent: TWinControl; const Cap: string;
ALeft, ATop, AW, AH: Integer; Handler: TNotifyEvent): TFlatButton;
@@ -1535,7 +1517,7 @@ begin
PanelToolbar := TPanel.Create(Self);
PanelToolbar.Parent := Self;
PanelToolbar.Align := alTop;
PanelToolbar.Height := 36;
PanelToolbar.Height := 72;
PanelToolbar.BevelOuter := bvNone;
PanelToolbar.Color := CLR_PANEL;
@@ -1557,7 +1539,13 @@ begin
BtnStartStop.ClrBorder := TColor($00304830);
BtnStartStop.ClrText := TColor($0090C090);
// ---- Separator line ----
// Линия-разделитель внутри PanelToolbar, прижата к его нижнему краю
SepLine := TPanel.Create(Self);
SepLine.Parent := PanelToolbar;
SepLine.BevelOuter := bvNone;
SepLine.Color := TColor($00606060);
SepLine.SetBounds(16, PanelToolbar.Height - 10, PanelToolbar.Width - 32, 2);
SepLine.Anchors := [akLeft, akRight, akBottom];
// ---- Status bar ----
StatusPanel := TMainStatusBar.Create(Self);
@@ -1570,11 +1558,11 @@ begin
PanelLeft.Width := LEFT_W;
PanelLeft.BevelOuter := bvNone;
Y := 38; // gap below separator: separator at y=66, content starts at y=74
Y := 0;
// Compact top VFO group
PanelTopVfoGroup := TPanel.Create(Self);
PanelTopVfoGroup.Parent := Self;
PanelTopVfoGroup.Parent := PanelToolbar;
PanelTopVfoGroup.SetBounds(0, 0, TOP_VFO_W * 2 + 64, 60);
PanelTopVfoGroup.BevelOuter := bvNone;
PanelTopVfoGroup.Color := TColor($00181818);
@@ -1940,59 +1928,15 @@ begin
PanelRight.BevelOuter := bvNone;
PanelRight.OnResize := RightPanelResize;
PanelSpanButtons := TPanel.Create(Self);
PanelSpanButtons.Parent := PanelRight;
PanelSpanButtons.Align := alTop;
PanelSpanButtons.Height := 38;
PanelSpanButtons.BevelOuter := bvNone;
FSampleRateOverlay := TSampleRateOverlay.Create(Self);
FSampleRateOverlay.Parent := PanelRight;
FSampleRateOverlay.OnSpanSelect := OnSampleRateSelect;
FSampleRateOverlay.OnHidePanel := OnSampleRateHidePanel;
FSampleRateOverlay.SetCurrentRate(FSampleRate);
LblSpan := TLabel.Create(Self);
LblSpan.Parent := PanelSpanButtons; LblSpan.Left := 4; LblSpan.Top := 8;
LblSpan.Caption := 'SPAN:'; LblSpan.Font.Color := CLR_TEXTDIM;
LblSpan.Font.Name := 'Courier New'; LblSpan.Font.Size := 7;
BtnSpan48k := MakeBtn(PanelSpanButtons, '48k', 50, 3, 46, BTN_SM, BtnSpanClick);
BtnSpan96k := MakeBtn(PanelSpanButtons, '96k', 99, 3, 46, BTN_SM, BtnSpanClick);
BtnSpan192k := MakeBtn(PanelSpanButtons, '192k', 148, 3, 52, BTN_SM, BtnSpanClick);
BtnSpan384k := MakeBtn(PanelSpanButtons, '384k', 203, 3, 52, BTN_SM, BtnSpanClick);
BtnSpan768k := MakeBtn(PanelSpanButtons, '768k', 258, 3, 52, BTN_SM, BtnSpanClick);
BtnSpan1536k := MakeBtn(PanelSpanButtons, '1536k', 313, 3, 58, BTN_SM, BtnSpanClick);
BtnSpan48k.Tag := 48000;
BtnSpan96k.Tag := 96000;
BtnSpan192k.Tag := 192000;
BtnSpan384k.Tag := 384000;
BtnSpan768k.Tag := 768000;
BtnSpan1536k.Tag := 1536000;
StyleSpanButton(BtnSpan48k, False);
StyleSpanButton(BtnSpan96k, False);
StyleSpanButton(BtnSpan192k, True);
StyleSpanButton(BtnSpan384k, False);
StyleSpanButton(BtnSpan768k, False);
StyleSpanButton(BtnSpan1536k, False);
BtnHidePanel := MakeBtn(PanelRight, '◀', 0, 0, 28, 28, BtnHidePanelClick);
BtnHidePanel.Font.Size := 10;
BtnHidePanel.ClrNorm := TColor($00543F18);
BtnHidePanel.ClrHot := TColor($00624B1D);
BtnHidePanel.ClrActive := TColor($00624B1D);
BtnHidePanel.ClrBorder := TColor($00685624);
BtnHidePanel.ClrText := TColor($00D8E6EE);
BtnHidePanel.ClrTextAct := TColor($00F0FAFF);
BtnHidePanel.BringToFront;
// Разделитель: на уровне формы, под всеми верхними элементами
PanelSep := TPanel.Create(Self);
PanelSep.Parent := Self;
PanelSep.Align := alNone;
PanelSep.BevelOuter := bvNone;
PanelSep.ParentColor := False;
PanelSep.Color := TColor($00606060);
PanelSep.SetBounds(16, PanelToolbar.Height + 30, ClientWidth - 32, 2);
// Большой S-метр — на главной форме поверх двух панелей
// S-метр: Parent=Self, перекрывает Toolbar+SpanButtons вертикально
// S-метр — внутри PanelToolbar, справа
PanelSMeterRight := TPanel.Create(Self);
PanelSMeterRight.Parent := Self;
PanelSMeterRight.Parent := PanelToolbar;
PanelSMeterRight.BevelOuter := bvNone;
PanelSMeterRight.Color := CLR_PANEL;
PbSMeterRight := TPaintBox.Create(Self);
@@ -2033,16 +1977,6 @@ begin
PbWaterfall.OnMouseMove := PbWaterfallMouseMove;
PbWaterfall.OnMouseUp := PbWaterfallMouseUp;
// SPAN кнопки как оверлей на спектре (в стиле web)
BtnSpan48k.Parent := PanelRight;
BtnSpan96k.Parent := PanelRight;
BtnSpan192k.Parent := PanelRight;
BtnSpan384k.Parent := PanelRight;
BtnSpan768k.Parent := PanelRight;
BtnSpan1536k.Parent := PanelRight;
LblSpan.Visible := False;
PositionSpanOverlayButtons;
// Initial layout
ResizeSpectrumPanels;
end;
@@ -2089,7 +2023,7 @@ begin
P := PanelRight.ScreenToClient(PanelSplitter.ClientToScreen(Point(X, Y)));
DeltaY := P.Y - FSplitterDragY0;
TopOff := PanelSpanButtons.Height;
TopOff := 0;
AvailH := PanelRight.ClientHeight - TopOff - RULER_H - SPLITTER_H;
if AvailH <= 0 then Exit;
@@ -2127,7 +2061,7 @@ begin
P := PanelRight.ScreenToClient(PanelSplitter.ClientToScreen(Point(X, Y)));
DeltaY := P.Y - FSplitterDragY0;
TopOff := PanelSpanButtons.Height;
TopOff := 0;
AvailH := PanelRight.ClientHeight - TopOff - RULER_H - SPLITTER_H;
if AvailH <= 0 then Exit;
@@ -2173,7 +2107,7 @@ var
TopVfoAW, TopVfoBW, NeedFreqAW, NeedFreqBW, FreqTextW, GroupW: Integer;
XferX, XferY: Integer;
begin
if (PanelToolbar = nil) or (PanelSpanButtons = nil) or (PanelTopVfoGroup = nil) or
if (PanelToolbar = nil) or (PanelTopVfoGroup = nil) or
(PanelTopVfoA = nil) or (PanelTopVfoB = nil) then Exit;
Canvas.Font.Name := FreqDispB.FontName;
@@ -2217,16 +2151,14 @@ begin
DynGap := XFER_SIDE_GAP + ExtraPerSide;
GroupW := TopVfoAW + DynGap + XFER_PANEL_W + DynGap + TopVfoBW;
BlockH := PanelToolbar.Height + 30 - (MARGIN * 2); // 30 = span row content, not full PanelSpanButtons
BlockH := PanelToolbar.Height - (MARGIN * 2) - 8;
if BlockH < 56 then BlockH := 56;
// Центрируем группу в доступном пространстве
GroupLeft := SafeLeft + (AvailW - GroupW) div 2;
GroupTop := TOP_OFFSET;
// Ось выравнивания: центр S-метра на форме (SMTop=3, SMBot=63) → y=33
// Относительно PanelTopVfoGroup (GroupTop=2): CenterY = 33 - 2 = 31
CenterY := (PanelToolbar.Height + 30) div 2 - GroupTop;
CenterY := PanelToolbar.Height div 2 - GroupTop;
PanelTopVfoGroup.SetBounds(GroupLeft, GroupTop, GroupW, BlockH);
PanelTopVfoGroup.Visible := True;
@@ -2264,15 +2196,11 @@ begin
FreqDispB.SetBounds(FreqDispB.Left, FreqTop,
FreqTextW + VFO_FREQ_LEFT_PAD + VFO_FREQ_RIGHT_PAD, TOP_VFO_FREQ_H);
if PanelSep <> nil then
PanelSep.SetBounds(16, PanelToolbar.Height + 30, ClientWidth - 32, 2);
PanelTopVfoGroup.BringToFront;
end;
procedure TMainForm.ResizeSMeter;
// PanelSMeterRight на главной форме (Parent=Self).
// По Y: от самого верха формы до низа PanelSpanButtons (Toolbar+Span = 66px).
// По Y: от самого верха формы до низа тулбара.
var
FW: Integer;
RightW: Integer; // ширина PanelRight
@@ -2281,8 +2209,7 @@ var
SMTop: Integer;
SMBot: Integer;
begin
if (PanelSMeterRight = nil) or (PanelToolbar = nil) or
(PanelSpanButtons = nil) or (PanelLeft = nil) then Exit;
if (PanelSMeterRight = nil) or (PanelToolbar = nil) or (PanelLeft = nil) then Exit;
LayoutTopVfoBlock;
@@ -2294,15 +2221,13 @@ begin
// Ширина S-метра = 32% от PanelRight
SMW := Round(RightW * SMETER_WIDTH_RATIO);
// X: прижат к правому краю формы
// X: прижат к правому краю PanelToolbar
SMLeft := FW - SMW - SMETER_MARGIN;
// Y: от верха формы+margin до низа PanelSpanButtons-margin
SMTop := SMETER_MARGIN;
SMBot := PanelToolbar.Height + 30 - SMETER_MARGIN; // 30 = span row content, ends before separator
SMBot := PanelToolbar.Height - SMETER_MARGIN - 9;
PanelSMeterRight.SetBounds(SMLeft, SMTop, SMW, SMBot - SMTop);
PanelSMeterRight.BringToFront;
end;
procedure TMainForm.ResizeSpectrumPanels;
@@ -2320,7 +2245,7 @@ begin
if Assigned(FSpecView) then SyncSpecViewFreq;
RW := PanelRight.ClientWidth;
RH := PanelRight.ClientHeight;
TopOff := PanelSpanButtons.Height;
TopOff := 0;
// S-метр позиционируется отдельно в ResizeSMeter
ResizeSMeter;
@@ -2334,7 +2259,7 @@ begin
FSpectrumWidth := RW;
FSpectrumHeight := 0;
FWaterfallHeight := 0;
PositionSpanOverlayButtons;
PositionSampleRateOverlay;
Exit;
end;
@@ -2351,7 +2276,7 @@ begin
PbRuler.Visible := True;
PanelSplitter.Visible := False;
PbWaterfall.Visible := False;
PositionSpanOverlayButtons;
PositionSampleRateOverlay;
FSpectrumWidth := RW;
FSpectrumHeight := SH;
FWaterfallHeight := 0;
@@ -2380,7 +2305,7 @@ begin
PbWaterfall.Visible := True;
PbRuler.SetBounds(0, TopOff + WH, RW, RULER_H);
PbRuler.Visible := True;
PositionSpanOverlayButtons;
PositionSampleRateOverlay;
FSpectrumWidth := RW;
FSpectrumHeight := 0;
FWaterfallHeight := WH;
@@ -2424,7 +2349,7 @@ begin
// Водопад — под сплиттером
PbWaterfall.SetBounds(0, TopOff + SH + RULER_H + SPLITTER_H, RW, WH);
PositionSpanOverlayButtons;
PositionSampleRateOverlay;
// Обновляем переменные размеров
FSpectrumWidth := RW;
@@ -2471,7 +2396,7 @@ begin
if PanelFMCTCSS <> nil then DP(PanelFMCTCSS);
if PanelFMStep <> nil then DP(PanelFMStep);
DP(PanelRX); DP(PanelTX);
DP(PanelRight); DP(PanelSpanButtons);
DP(PanelRight);
PbSpectrum.Color := T.BG;
PbWaterfall.Color := T.BG;
@@ -2481,7 +2406,7 @@ begin
// Метки
DL(LblAGCTop, False);
DL(LblDrv); DL(LblSpan);
DL(LblDrv);
if LblFMSQ <> nil then DL(LblFMSQ, False);
// Кнопки тулбара
@@ -2518,17 +2443,10 @@ begin
StyleButton(BtnMOX, BtnMOX.Active);
StyleButton(BtnTUN, BtnTUN.Active);
// Span / VFO-copy / HidePanel
StyleSpanButton(BtnSpan48k, BtnSpan48k.Active);
StyleSpanButton(BtnSpan96k, BtnSpan96k.Active);
StyleSpanButton(BtnSpan192k, BtnSpan192k.Active);
StyleSpanButton(BtnSpan384k, BtnSpan384k.Active);
StyleSpanButton(BtnSpan768k, BtnSpan768k.Active);
StyleSpanButton(BtnSpan1536k, BtnSpan1536k.Active);
// VFO-copy
StyleSpanButton(BtnVfoACopyB, False);
StyleSpanButton(BtnVfoSwap, False);
StyleSpanButton(BtnVfoBCopyA, False);
if BtnHidePanel <> nil then StyleSpanButton(BtnHidePanel, False);
// Ползунки
DS(TrkAGC); DS(TrkVolume); DS(TrkDrive);
@@ -2618,59 +2536,17 @@ begin
B.Font.Size := 7;
end;
procedure TMainForm.PositionSpanOverlayButtons;
procedure TMainForm.PositionSampleRateOverlay;
const
BTN_TOP = 6;
GAP = 3;
W48 = 40;
W96 = 40;
W192 = 44;
W384 = 44;
W768 = 44;
W1536 = 50;
HBTN = 18;
var
X, NeedW, BaseX, BaseY: Integer;
MARGIN = 4;
begin
if (PbSpectrum = nil) or (BtnSpan48k = nil) then Exit;
NeedW := W48 + W96 + W192 + W384 + W768 + W1536 + GAP * 5 + 8;
if PbSpectrum.Width < NeedW then
begin
BtnSpan48k.Visible := False;
BtnSpan96k.Visible := False;
BtnSpan192k.Visible := False;
BtnSpan384k.Visible := False;
BtnSpan768k.Visible := False;
BtnSpan1536k.Visible := False;
Exit;
end;
BaseX := PbSpectrum.Left;
BaseY := PbSpectrum.Top;
X := BaseX + 56;
BtnSpan48k.SetBounds(X, BaseY + BTN_TOP, W48, HBTN); Inc(X, W48 + GAP);
BtnSpan96k.SetBounds(X, BaseY + BTN_TOP, W96, HBTN); Inc(X, W96 + GAP);
BtnSpan192k.SetBounds(X, BaseY + BTN_TOP, W192, HBTN); Inc(X, W192 + GAP);
BtnSpan384k.SetBounds(X, BaseY + BTN_TOP, W384, HBTN); Inc(X, W384 + GAP);
BtnSpan768k.SetBounds(X, BaseY + BTN_TOP, W768, HBTN); Inc(X, W768 + GAP);
BtnSpan1536k.SetBounds(X, BaseY + BTN_TOP, W1536, HBTN);
if BtnHidePanel <> nil then
BtnHidePanel.SetBounds(BaseX, BaseY + BTN_TOP, 28, HBTN);
BtnSpan48k.Visible := True;
BtnSpan96k.Visible := True;
BtnSpan192k.Visible := True;
BtnSpan384k.Visible := True;
BtnSpan768k.Visible := True;
BtnSpan1536k.Visible := True;
BtnSpan48k.BringToFront;
BtnSpan96k.BringToFront;
BtnSpan192k.BringToFront;
BtnSpan384k.BringToFront;
BtnSpan768k.BringToFront;
BtnSpan1536k.BringToFront;
if BtnHidePanel <> nil then BtnHidePanel.BringToFront;
if (FSampleRateOverlay = nil) or (PbSpectrum = nil) then Exit;
FSampleRateOverlay.SetBounds(
PbSpectrum.Left + MARGIN,
PbSpectrum.Top + MARGIN,
FSampleRateOverlay.Width,
FSampleRateOverlay.Height);
FSampleRateOverlay.BringToFront;
end;
// ===========================================================================
@@ -3954,6 +3830,8 @@ begin
begin
FSampleRate := G_Settings.SampleRate;
FSpanHz := FSampleRate;
if Assigned(FSampleRateOverlay) then
FSampleRateOverlay.SetCurrentRate(FSampleRate);
end;
BtnNR.Tag := EnsureRange(G_Settings.NRMode, 0, 4); UpdateNRButton;
BtnNB.Tag := EnsureRange(G_Settings.NBMode, 0, 2); UpdateNBButton;
@@ -5390,13 +5268,12 @@ begin
FSettings.SaveWebSettings(W);
end;
procedure TMainForm.BtnHidePanelClick(Sender: TObject);
procedure TMainForm.OnSampleRateHidePanel;
begin
FPanelHidden := not FPanelHidden;
if FPanelHidden then
begin
PanelLeft.Width := 0;
BtnHidePanel.Caption := '▶';
FVfoOverlay.Parent := PanelRight;
FVfoOverlay.Width := 260;
FVfoOverlay.Height := 136; // OVL_H_NORM — расширяется сам при открытии AGC-пикера
@@ -5409,9 +5286,10 @@ begin
else
begin
PanelLeft.Width := 232;
BtnHidePanel.Caption := '◀';
FVfoOverlay.Visible := False;
end;
if Assigned(FSampleRateOverlay) then
FSampleRateOverlay.SetPanelHidden(FPanelHidden);
ResizeSpectrumPanels;
ResizeSMeter;
end;
@@ -5492,25 +5370,20 @@ begin
FBandCache[FCurrentBand].AGCMode := FAGCMode;
end;
procedure TMainForm.BtnSpanClick(Sender: TObject);
procedure TMainForm.OnSampleRateSelect(SampleRate: Integer);
var
NewRate: Integer;
DDCRate: Word;
begin
NewRate := (Sender as TFlatButton).Tag;
NewRate := SampleRate;
if NewRate = FSampleRate then Exit;
FSampleRate := NewRate;
FSpanHz := NewRate;
DDCRate := NewRate div 1000;
// Обновляем кнопки сразу — UI отзывчив
StyleSpanButton(BtnSpan48k, FSampleRate = 48000);
StyleSpanButton(BtnSpan96k, FSampleRate = 96000);
StyleSpanButton(BtnSpan192k, FSampleRate = 192000);
StyleSpanButton(BtnSpan384k, FSampleRate = 384000);
StyleSpanButton(BtnSpan768k, FSampleRate = 768000);
StyleSpanButton(BtnSpan1536k, FSampleRate = 1536000);
if Assigned(FSampleRateOverlay) then
FSampleRateOverlay.SetCurrentRate(NewRate);
Application.ProcessMessages;
FBandCache[FCurrentBand].SpanHz := FSpanHz; // для обратной совместимости
@@ -5762,27 +5635,17 @@ begin
end;
procedure TMainForm.SyncWebSpan;
const
ValidSpans: array[0..5] of Integer = (48000, 96000, 192000, 384000, 768000, 1536000);
var
ValidSpans: array[0..5] of Integer;
Found: Boolean;
i: Integer;
Btn: TFlatButton;
begin
ValidSpans[0]:=48000; ValidSpans[1]:=96000; ValidSpans[2]:=192000;
ValidSpans[3]:=384000; ValidSpans[4]:=768000; ValidSpans[5]:=1536000;
Found := False;
for i := 0 to 5 do
if ValidSpans[i] = FWebSyncInt then begin Found := True; Break; end;
if not Found then Exit;
if FWebSyncInt = FSampleRate then Exit;
Btn := nil;
if FWebSyncInt = 48000 then Btn := BtnSpan48k
else if FWebSyncInt = 96000 then Btn := BtnSpan96k
else if FWebSyncInt = 192000 then Btn := BtnSpan192k
else if FWebSyncInt = 384000 then Btn := BtnSpan384k
else if FWebSyncInt = 768000 then Btn := BtnSpan768k
else if FWebSyncInt = 1536000 then Btn := BtnSpan1536k;
if Assigned(Btn) then BtnSpanClick(Btn);
for i := 0 to High(ValidSpans) do
if ValidSpans[i] = FWebSyncInt then
begin
OnSampleRateSelect(FWebSyncInt);
Exit;
end;
end;
procedure TMainForm.WebOnVolume(V: Integer);
+241
View File
@@ -0,0 +1,241 @@
unit SampleRateOverlay;
{
Overlay виджет для выбора sample rate (span) спектра.
Размещается в верхнем левом углу спектрограммы поверх PbSpectrum.
Содержит кнопку скрытия левой панели и ряд кнопок span.
}
{$IFDEF FPC}
{$MODE Delphi}
{$ENDIF}
interface
uses
Classes, SysUtils, Controls, Graphics, Types, Math;
type
TSpanSelectEvent = procedure(SampleRate: Integer) of object;
THidePanelEvent = procedure of object;
TSampleRateOverlay = class(TCustomControl)
private
FCurrentRate: Integer;
FPanelHidden: Boolean;
FOnSpanSelect: TSpanSelectEvent;
FOnHidePanel: THidePanelEvent;
FHitRects: array[0..6] of TRect; // [0]=hide button, [1..6]=span buttons
FHitValues: array[0..6] of Integer; // 0=hide, else sample rate
FHitCount: Integer;
FHotIdx: Integer;
procedure DrawSelf(C: TCanvas; W, H: Integer);
procedure RegisterHit(const HR: TRect; Value: Integer);
protected
procedure Paint; override;
procedure MouseMove(Shift: TShiftState; X, Y: Integer); override;
procedure MouseDown(Button: TMouseButton; Shift: TShiftState;
X, Y: Integer); override;
procedure MouseLeave; override;
public
constructor Create(AOwner: TComponent); override;
procedure SetCurrentRate(Rate: Integer);
procedure SetPanelHidden(Hidden: Boolean);
property OnSpanSelect: TSpanSelectEvent read FOnSpanSelect write FOnSpanSelect;
property OnHidePanel: THidePanelEvent read FOnHidePanel write FOnHidePanel;
end;
implementation
const
SPAN_RATES: array[0..5] of Integer = (48000, 96000, 192000, 384000, 768000, 1536000);
SPAN_NAMES: array[0..5] of string = ('48k', '96k', '192k', '384k', '768k', '1536k');
HIDE_W = 22;
SPAN_H = 18;
PAD_X = 4;
PAD_Y = 4;
BTN_GAP = 3;
CLR_BG = TColor($00141414);
CLR_BORDER = TColor($00505050);
CLR_TEXT = TColor($00D8E6EE);
CLR_BTN_NORM = TColor($00543F18);
CLR_BTN_HOT = TColor($00624B1D);
CLR_BTN_ACT = TColor($00785A20);
CLR_BTN_BDR = TColor($00685624);
function SpanBtnWidth(const Name: string; C: TCanvas): Integer;
begin
C.Font.Name := 'Courier New';
C.Font.Size := 7;
C.Font.Style := [];
Result := C.TextWidth(Name) + 10;
if Result < 36 then Result := 36;
end;
constructor TSampleRateOverlay.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
ControlStyle := ControlStyle + [csOpaque];
FCurrentRate := 192000;
FPanelHidden := False;
FHitCount := 0;
FHotIdx := -1;
Cursor := crHandPoint;
Width := 320;
Height := SPAN_H + PAD_Y * 2;
end;
procedure TSampleRateOverlay.RegisterHit(const HR: TRect; Value: Integer);
begin
if FHitCount > High(FHitRects) then Exit;
FHitRects[FHitCount] := HR;
FHitValues[FHitCount] := Value;
Inc(FHitCount);
end;
procedure TSampleRateOverlay.DrawSelf(C: TCanvas; W, H: Integer);
var
X, I, TW, TH, BtnW: Integer;
HR: TRect;
IsAct, IsHot: Boolean;
Cap: string;
begin
FHitCount := 0;
// Фон
C.Brush.Color := CLR_BG;
C.Brush.Style := bsSolid;
C.Pen.Style := psClear;
C.FillRect(Rect(0, 0, W, H));
// Рамка
C.Pen.Style := psSolid;
C.Pen.Color := CLR_BORDER;
C.Brush.Style := bsClear;
C.Rectangle(0, 0, W, H);
C.Font.Name := 'Courier New';
C.Font.Size := 7;
C.Font.Style := [];
TH := C.TextHeight('A');
X := PAD_X;
// Кнопка скрытия панели (◀ или ▶)
HR := Rect(X, PAD_Y, X + HIDE_W, PAD_Y + SPAN_H);
IsHot := (FHotIdx = 0);
if IsHot then C.Brush.Color := CLR_BTN_HOT
else C.Brush.Color := CLR_BTN_NORM;
C.Brush.Style := bsSolid; C.Pen.Style := psClear;
C.FillRect(HR);
C.Pen.Style := psSolid; C.Pen.Color := CLR_BTN_BDR;
C.Brush.Style := bsClear; C.Rectangle(HR);
C.Font.Size := 8;
if FPanelHidden then Cap := '▶' else Cap := '◀';
TW := C.TextWidth(Cap); C.Font.Size := 7;
C.Font.Color := CLR_TEXT;
C.Brush.Style := bsClear;
C.Font.Size := 8;
C.TextOut(HR.Left + (HIDE_W - TW) div 2,
HR.Top + (SPAN_H - TH) div 2, Cap);
C.Font.Size := 7;
RegisterHit(HR, 0);
Inc(X, HIDE_W + BTN_GAP);
// Кнопки span
for I := 0 to High(SPAN_RATES) do
begin
BtnW := SpanBtnWidth(SPAN_NAMES[I], C);
HR := Rect(X, PAD_Y, X + BtnW, PAD_Y + SPAN_H);
IsAct := (SPAN_RATES[I] = FCurrentRate);
IsHot := (FHotIdx = I + 1);
if IsAct then C.Brush.Color := CLR_BTN_ACT
else if IsHot then C.Brush.Color := CLR_BTN_HOT
else C.Brush.Color := CLR_BTN_NORM;
C.Brush.Style := bsSolid; C.Pen.Style := psClear;
C.FillRect(HR);
C.Pen.Style := psSolid; C.Pen.Color := CLR_BTN_BDR;
C.Brush.Style := bsClear; C.Rectangle(HR);
TW := C.TextWidth(SPAN_NAMES[I]);
C.Font.Color := CLR_TEXT;
C.Brush.Style := bsClear;
C.TextOut(HR.Left + (BtnW - TW) div 2,
HR.Top + (SPAN_H - TH) div 2, SPAN_NAMES[I]);
RegisterHit(HR, SPAN_RATES[I]);
Inc(X, BtnW + BTN_GAP);
end;
end;
procedure TSampleRateOverlay.Paint;
begin
DrawSelf(Canvas, Width, Height);
end;
procedure TSampleRateOverlay.MouseMove(Shift: TShiftState; X, Y: Integer);
var
I, OldHot: Integer;
begin
OldHot := FHotIdx;
FHotIdx := -1;
for I := 0 to FHitCount - 1 do
if PtInRect(FHitRects[I], Point(X, Y)) then
begin
FHotIdx := I;
Break;
end;
if FHotIdx <> OldHot then Invalidate;
inherited MouseMove(Shift, X, Y);
end;
procedure TSampleRateOverlay.MouseDown(Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
var
I: Integer;
begin
if Button <> mbLeft then Exit;
for I := 0 to FHitCount - 1 do
if PtInRect(FHitRects[I], Point(X, Y)) then
begin
if FHitValues[I] = 0 then
begin
if Assigned(FOnHidePanel) then FOnHidePanel;
end
else
begin
if Assigned(FOnSpanSelect) then FOnSpanSelect(FHitValues[I]);
end;
Break;
end;
end;
procedure TSampleRateOverlay.MouseLeave;
begin
if FHotIdx >= 0 then
begin
FHotIdx := -1;
Invalidate;
end;
inherited MouseLeave;
end;
procedure TSampleRateOverlay.SetCurrentRate(Rate: Integer);
begin
if FCurrentRate = Rate then Exit;
FCurrentRate := Rate;
Invalidate;
end;
procedure TSampleRateOverlay.SetPanelHidden(Hidden: Boolean);
begin
if FPanelHidden = Hidden then Exit;
FPanelHidden := Hidden;
Invalidate;
end;
end.
+4
View File
@@ -95,6 +95,10 @@
<Filename Value="VfoOverlay.pas"/>
<IsPartOfProject Value="True"/>
</Unit>
<Unit>
<Filename Value="SampleRateOverlay.pas"/>
<IsPartOfProject Value="True"/>
</Unit>
<Unit>
<Filename Value="AudioOutput.pas"/>
<IsPartOfProject Value="True"/>