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
+72 -209
View File
@@ -22,7 +22,8 @@ unit MainForm;
interface interface
uses 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, StdCtrls, ExtCtrls, ComCtrls, Buttons, Menus, Math, Types,
LCLIntf, LCLType, GraphType, LCLIntf, LCLType, GraphType,
HPSDRProtocol, HPSDRNetwork, HPSDRProtocol, HPSDRNetwork,
@@ -180,6 +181,7 @@ type
FDeviceDialog: TDeviceDialog; FDeviceDialog: TDeviceDialog;
FPendingIP: string; // IP устройства для подключения FPendingIP: string; // IP устройства для подключения
FVfoOverlay: TVfoOverlay; // накладка SmartSDR-стиль на спектре FVfoOverlay: TVfoOverlay; // накладка SmartSDR-стиль на спектре
FSampleRateOverlay: TSampleRateOverlay; // оверлей span/hide слева вверху спектра
FPanelHidden: Boolean; // True = левая панель скрыта FPanelHidden: Boolean; // True = левая панель скрыта
FPendingBoardType: Integer; // BoardType устройства для подключения FPendingBoardType: Integer; // BoardType устройства для подключения
@@ -341,12 +343,11 @@ type
FSpectrumTimer: TTimer; FSpectrumTimer: TTimer;
FAfterShowTimer:TTimer; // однократный таймер для пост-инициализации FAfterShowTimer:TTimer; // однократный таймер для пост-инициализации
// ---- Toolbar ---- // ---- Top panel (toolbar + VFO group + S-meter) ----
PanelToolbar: TPanel; PanelToolbar: TPanel;
BtnDiscover: TFlatButton; BtnDiscover: TFlatButton;
BtnStartStop: TFlatButton; // START / STOP (connect+run в одном) BtnStartStop: TFlatButton;
BtnSettings: TFlatButton; // открыть диалог настроек BtnSettings: TFlatButton;
PanelSep: TPanel; // 1px разделитель под тулбаром
// ---- Left panel ---- // ---- Left panel ----
PanelLeft: TPanel; PanelLeft: TPanel;
@@ -419,15 +420,6 @@ type
// ---- Right panel ---- // ---- Right panel ----
PanelRight: TPanel; PanelRight: TPanel;
PanelSpanButtons: TPanel;
LblSpan: TLabel;
BtnSpan48k: TFlatButton;
BtnSpan96k: TFlatButton;
BtnSpan192k: TFlatButton;
BtnSpan384k: TFlatButton;
BtnSpan768k: TFlatButton;
BtnSpan1536k: TFlatButton;
BtnHidePanel: TFlatButton; // скрыть/показать левую панель
PbSpectrum: TPaintBox; PbSpectrum: TPaintBox;
PbRuler: TPaintBox; // полоса частотных меток между спектром и водопадом PbRuler: TPaintBox; // полоса частотных меток между спектром и водопадом
PanelSplitter: TPanel; // перетаскиваемый разделитель спектр/водопад PanelSplitter: TPanel; // перетаскиваемый разделитель спектр/водопад
@@ -445,7 +437,9 @@ type
procedure SetStatusText(Index: Integer; const Text: string); procedure SetStatusText(Index: Integer; const Text: string);
procedure SetRadioOfflineStatus(const ConnText: string; ClearDevice: Boolean = False); procedure SetRadioOfflineStatus(const ConnText: string; ClearDevice: Boolean = False);
procedure ApplyStatusTheme(const T: TAppTheme); procedure ApplyStatusTheme(const T: TAppTheme);
procedure PositionSpanOverlayButtons; procedure PositionSampleRateOverlay;
procedure OnSampleRateSelect(SampleRate: Integer);
procedure OnSampleRateHidePanel;
procedure UpdateVfoDisplay; procedure UpdateVfoDisplay;
function FormatFreq(Hz: Double): string; function FormatFreq(Hz: Double): string;
procedure LayoutTopVfoBlock; procedure LayoutTopVfoBlock;
@@ -551,7 +545,6 @@ type
procedure BtnNBClick(Sender: TObject); procedure BtnNBClick(Sender: TObject);
procedure BtnSNBClick(Sender: TObject); procedure BtnSNBClick(Sender: TObject);
procedure BtnANFClick(Sender: TObject); procedure BtnANFClick(Sender: TObject);
procedure BtnSpanClick(Sender: TObject);
// Веб-интерфейс: callbacks от TWebServer // Веб-интерфейс: callbacks от TWebServer
procedure WebOnFreq(Hz: Double); procedure WebOnFreq(Hz: Double);
procedure WebOnMode(Mode: Integer); procedure WebOnMode(Mode: Integer);
@@ -660,7 +653,6 @@ type
procedure ActivateXvtrBand(Idx: Integer); procedure ActivateXvtrBand(Idx: Integer);
procedure DeactivateXvtr; procedure DeactivateXvtr;
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
procedure BtnHidePanelClick(Sender: TObject);
procedure BtnSettingsClick(Sender: TObject); procedure BtnSettingsClick(Sender: TObject);
procedure PositionVfoOverlay; procedure PositionVfoOverlay;
procedure OnModeFilterSelect(Mode: Integer; FilterBW: Integer); procedure OnModeFilterSelect(Mode: Integer; FilterBW: Integer);
@@ -1127,14 +1119,9 @@ begin
// --- Sample Rate / Span --- // --- Sample Rate / Span ---
// SampleRate — глобальный, не per-band. Не восстанавливаем из диапазона. // SampleRate — глобальный, не per-band. Не восстанавливаем из диапазона.
// Обновляем только FSpanHz для отображения (берём текущий FSampleRate)
FSpanHz := FSampleRate; FSpanHz := FSampleRate;
StyleSpanButton(BtnSpan48k, FSampleRate = 48000); if Assigned(FSampleRateOverlay) then
StyleSpanButton(BtnSpan96k, FSampleRate = 96000); FSampleRateOverlay.SetCurrentRate(FSampleRate);
StyleSpanButton(BtnSpan192k, FSampleRate = 192000);
StyleSpanButton(BtnSpan384k, FSampleRate = 384000);
StyleSpanButton(BtnSpan768k, FSampleRate = 768000);
StyleSpanButton(BtnSpan1536k, FSampleRate = 1536000);
// --- VFO A: центрируем на новой частоте, сбрасываем shift --- // --- VFO A: центрируем на новой частоте, сбрасываем shift ---
FCenterFreq := B.VfoA; // DDC = центр диапазона FCenterFreq := B.VfoA; // DDC = центр диапазона
@@ -1375,12 +1362,6 @@ begin
FCurrentXvtr := -1; FCurrentXvtr := -1;
BuildUI; 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; ResizeSMeter;
ApplyDarkTheme; ApplyDarkTheme;
UpdateVfoDisplay; UpdateVfoDisplay;
@@ -1501,6 +1482,7 @@ const
var var
i, X, Y, W: Integer; i, X, Y, W: Integer;
B: TFlatButton; B: TFlatButton;
SepLine: TPanel;
function MakeBtn(Parent: TWinControl; const Cap: string; function MakeBtn(Parent: TWinControl; const Cap: string;
ALeft, ATop, AW, AH: Integer; Handler: TNotifyEvent): TFlatButton; ALeft, ATop, AW, AH: Integer; Handler: TNotifyEvent): TFlatButton;
@@ -1535,7 +1517,7 @@ begin
PanelToolbar := TPanel.Create(Self); PanelToolbar := TPanel.Create(Self);
PanelToolbar.Parent := Self; PanelToolbar.Parent := Self;
PanelToolbar.Align := alTop; PanelToolbar.Align := alTop;
PanelToolbar.Height := 36; PanelToolbar.Height := 72;
PanelToolbar.BevelOuter := bvNone; PanelToolbar.BevelOuter := bvNone;
PanelToolbar.Color := CLR_PANEL; PanelToolbar.Color := CLR_PANEL;
@@ -1557,7 +1539,13 @@ begin
BtnStartStop.ClrBorder := TColor($00304830); BtnStartStop.ClrBorder := TColor($00304830);
BtnStartStop.ClrText := TColor($0090C090); 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 ---- // ---- Status bar ----
StatusPanel := TMainStatusBar.Create(Self); StatusPanel := TMainStatusBar.Create(Self);
@@ -1570,11 +1558,11 @@ begin
PanelLeft.Width := LEFT_W; PanelLeft.Width := LEFT_W;
PanelLeft.BevelOuter := bvNone; PanelLeft.BevelOuter := bvNone;
Y := 38; // gap below separator: separator at y=66, content starts at y=74 Y := 0;
// Compact top VFO group // Compact top VFO group
PanelTopVfoGroup := TPanel.Create(Self); PanelTopVfoGroup := TPanel.Create(Self);
PanelTopVfoGroup.Parent := Self; PanelTopVfoGroup.Parent := PanelToolbar;
PanelTopVfoGroup.SetBounds(0, 0, TOP_VFO_W * 2 + 64, 60); PanelTopVfoGroup.SetBounds(0, 0, TOP_VFO_W * 2 + 64, 60);
PanelTopVfoGroup.BevelOuter := bvNone; PanelTopVfoGroup.BevelOuter := bvNone;
PanelTopVfoGroup.Color := TColor($00181818); PanelTopVfoGroup.Color := TColor($00181818);
@@ -1940,59 +1928,15 @@ begin
PanelRight.BevelOuter := bvNone; PanelRight.BevelOuter := bvNone;
PanelRight.OnResize := RightPanelResize; PanelRight.OnResize := RightPanelResize;
PanelSpanButtons := TPanel.Create(Self); FSampleRateOverlay := TSampleRateOverlay.Create(Self);
PanelSpanButtons.Parent := PanelRight; FSampleRateOverlay.Parent := PanelRight;
PanelSpanButtons.Align := alTop; FSampleRateOverlay.OnSpanSelect := OnSampleRateSelect;
PanelSpanButtons.Height := 38; FSampleRateOverlay.OnHidePanel := OnSampleRateHidePanel;
PanelSpanButtons.BevelOuter := bvNone; FSampleRateOverlay.SetCurrentRate(FSampleRate);
LblSpan := TLabel.Create(Self); // S-метр — внутри PanelToolbar, справа
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 вертикально
PanelSMeterRight := TPanel.Create(Self); PanelSMeterRight := TPanel.Create(Self);
PanelSMeterRight.Parent := Self; PanelSMeterRight.Parent := PanelToolbar;
PanelSMeterRight.BevelOuter := bvNone; PanelSMeterRight.BevelOuter := bvNone;
PanelSMeterRight.Color := CLR_PANEL; PanelSMeterRight.Color := CLR_PANEL;
PbSMeterRight := TPaintBox.Create(Self); PbSMeterRight := TPaintBox.Create(Self);
@@ -2033,16 +1977,6 @@ begin
PbWaterfall.OnMouseMove := PbWaterfallMouseMove; PbWaterfall.OnMouseMove := PbWaterfallMouseMove;
PbWaterfall.OnMouseUp := PbWaterfallMouseUp; 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 // Initial layout
ResizeSpectrumPanels; ResizeSpectrumPanels;
end; end;
@@ -2089,7 +2023,7 @@ begin
P := PanelRight.ScreenToClient(PanelSplitter.ClientToScreen(Point(X, Y))); P := PanelRight.ScreenToClient(PanelSplitter.ClientToScreen(Point(X, Y)));
DeltaY := P.Y - FSplitterDragY0; DeltaY := P.Y - FSplitterDragY0;
TopOff := PanelSpanButtons.Height; TopOff := 0;
AvailH := PanelRight.ClientHeight - TopOff - RULER_H - SPLITTER_H; AvailH := PanelRight.ClientHeight - TopOff - RULER_H - SPLITTER_H;
if AvailH <= 0 then Exit; if AvailH <= 0 then Exit;
@@ -2127,7 +2061,7 @@ begin
P := PanelRight.ScreenToClient(PanelSplitter.ClientToScreen(Point(X, Y))); P := PanelRight.ScreenToClient(PanelSplitter.ClientToScreen(Point(X, Y)));
DeltaY := P.Y - FSplitterDragY0; DeltaY := P.Y - FSplitterDragY0;
TopOff := PanelSpanButtons.Height; TopOff := 0;
AvailH := PanelRight.ClientHeight - TopOff - RULER_H - SPLITTER_H; AvailH := PanelRight.ClientHeight - TopOff - RULER_H - SPLITTER_H;
if AvailH <= 0 then Exit; if AvailH <= 0 then Exit;
@@ -2173,7 +2107,7 @@ var
TopVfoAW, TopVfoBW, NeedFreqAW, NeedFreqBW, FreqTextW, GroupW: Integer; TopVfoAW, TopVfoBW, NeedFreqAW, NeedFreqBW, FreqTextW, GroupW: Integer;
XferX, XferY: Integer; XferX, XferY: Integer;
begin 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; (PanelTopVfoA = nil) or (PanelTopVfoB = nil) then Exit;
Canvas.Font.Name := FreqDispB.FontName; Canvas.Font.Name := FreqDispB.FontName;
@@ -2217,16 +2151,14 @@ begin
DynGap := XFER_SIDE_GAP + ExtraPerSide; DynGap := XFER_SIDE_GAP + ExtraPerSide;
GroupW := TopVfoAW + DynGap + XFER_PANEL_W + DynGap + TopVfoBW; 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; if BlockH < 56 then BlockH := 56;
// Центрируем группу в доступном пространстве // Центрируем группу в доступном пространстве
GroupLeft := SafeLeft + (AvailW - GroupW) div 2; GroupLeft := SafeLeft + (AvailW - GroupW) div 2;
GroupTop := TOP_OFFSET; GroupTop := TOP_OFFSET;
// Ось выравнивания: центр S-метра на форме (SMTop=3, SMBot=63) → y=33 CenterY := PanelToolbar.Height div 2 - GroupTop;
// Относительно PanelTopVfoGroup (GroupTop=2): CenterY = 33 - 2 = 31
CenterY := (PanelToolbar.Height + 30) div 2 - GroupTop;
PanelTopVfoGroup.SetBounds(GroupLeft, GroupTop, GroupW, BlockH); PanelTopVfoGroup.SetBounds(GroupLeft, GroupTop, GroupW, BlockH);
PanelTopVfoGroup.Visible := True; PanelTopVfoGroup.Visible := True;
@@ -2264,15 +2196,11 @@ begin
FreqDispB.SetBounds(FreqDispB.Left, FreqTop, FreqDispB.SetBounds(FreqDispB.Left, FreqTop,
FreqTextW + VFO_FREQ_LEFT_PAD + VFO_FREQ_RIGHT_PAD, TOP_VFO_FREQ_H); 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; end;
procedure TMainForm.ResizeSMeter; procedure TMainForm.ResizeSMeter;
// PanelSMeterRight на главной форме (Parent=Self). // PanelSMeterRight на главной форме (Parent=Self).
// По Y: от самого верха формы до низа PanelSpanButtons (Toolbar+Span = 66px). // По Y: от самого верха формы до низа тулбара.
var var
FW: Integer; FW: Integer;
RightW: Integer; // ширина PanelRight RightW: Integer; // ширина PanelRight
@@ -2281,8 +2209,7 @@ var
SMTop: Integer; SMTop: Integer;
SMBot: Integer; SMBot: Integer;
begin begin
if (PanelSMeterRight = nil) or (PanelToolbar = nil) or if (PanelSMeterRight = nil) or (PanelToolbar = nil) or (PanelLeft = nil) then Exit;
(PanelSpanButtons = nil) or (PanelLeft = nil) then Exit;
LayoutTopVfoBlock; LayoutTopVfoBlock;
@@ -2294,15 +2221,13 @@ begin
// Ширина S-метра = 32% от PanelRight // Ширина S-метра = 32% от PanelRight
SMW := Round(RightW * SMETER_WIDTH_RATIO); SMW := Round(RightW * SMETER_WIDTH_RATIO);
// X: прижат к правому краю формы // X: прижат к правому краю PanelToolbar
SMLeft := FW - SMW - SMETER_MARGIN; SMLeft := FW - SMW - SMETER_MARGIN;
// Y: от верха формы+margin до низа PanelSpanButtons-margin
SMTop := SMETER_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.SetBounds(SMLeft, SMTop, SMW, SMBot - SMTop);
PanelSMeterRight.BringToFront;
end; end;
procedure TMainForm.ResizeSpectrumPanels; procedure TMainForm.ResizeSpectrumPanels;
@@ -2320,7 +2245,7 @@ begin
if Assigned(FSpecView) then SyncSpecViewFreq; if Assigned(FSpecView) then SyncSpecViewFreq;
RW := PanelRight.ClientWidth; RW := PanelRight.ClientWidth;
RH := PanelRight.ClientHeight; RH := PanelRight.ClientHeight;
TopOff := PanelSpanButtons.Height; TopOff := 0;
// S-метр позиционируется отдельно в ResizeSMeter // S-метр позиционируется отдельно в ResizeSMeter
ResizeSMeter; ResizeSMeter;
@@ -2334,7 +2259,7 @@ begin
FSpectrumWidth := RW; FSpectrumWidth := RW;
FSpectrumHeight := 0; FSpectrumHeight := 0;
FWaterfallHeight := 0; FWaterfallHeight := 0;
PositionSpanOverlayButtons; PositionSampleRateOverlay;
Exit; Exit;
end; end;
@@ -2351,7 +2276,7 @@ begin
PbRuler.Visible := True; PbRuler.Visible := True;
PanelSplitter.Visible := False; PanelSplitter.Visible := False;
PbWaterfall.Visible := False; PbWaterfall.Visible := False;
PositionSpanOverlayButtons; PositionSampleRateOverlay;
FSpectrumWidth := RW; FSpectrumWidth := RW;
FSpectrumHeight := SH; FSpectrumHeight := SH;
FWaterfallHeight := 0; FWaterfallHeight := 0;
@@ -2380,7 +2305,7 @@ begin
PbWaterfall.Visible := True; PbWaterfall.Visible := True;
PbRuler.SetBounds(0, TopOff + WH, RW, RULER_H); PbRuler.SetBounds(0, TopOff + WH, RW, RULER_H);
PbRuler.Visible := True; PbRuler.Visible := True;
PositionSpanOverlayButtons; PositionSampleRateOverlay;
FSpectrumWidth := RW; FSpectrumWidth := RW;
FSpectrumHeight := 0; FSpectrumHeight := 0;
FWaterfallHeight := WH; FWaterfallHeight := WH;
@@ -2424,7 +2349,7 @@ begin
// Водопад — под сплиттером // Водопад — под сплиттером
PbWaterfall.SetBounds(0, TopOff + SH + RULER_H + SPLITTER_H, RW, WH); PbWaterfall.SetBounds(0, TopOff + SH + RULER_H + SPLITTER_H, RW, WH);
PositionSpanOverlayButtons; PositionSampleRateOverlay;
// Обновляем переменные размеров // Обновляем переменные размеров
FSpectrumWidth := RW; FSpectrumWidth := RW;
@@ -2471,7 +2396,7 @@ begin
if PanelFMCTCSS <> nil then DP(PanelFMCTCSS); if PanelFMCTCSS <> nil then DP(PanelFMCTCSS);
if PanelFMStep <> nil then DP(PanelFMStep); if PanelFMStep <> nil then DP(PanelFMStep);
DP(PanelRX); DP(PanelTX); DP(PanelRX); DP(PanelTX);
DP(PanelRight); DP(PanelSpanButtons); DP(PanelRight);
PbSpectrum.Color := T.BG; PbSpectrum.Color := T.BG;
PbWaterfall.Color := T.BG; PbWaterfall.Color := T.BG;
@@ -2481,7 +2406,7 @@ begin
// Метки // Метки
DL(LblAGCTop, False); DL(LblAGCTop, False);
DL(LblDrv); DL(LblSpan); DL(LblDrv);
if LblFMSQ <> nil then DL(LblFMSQ, False); if LblFMSQ <> nil then DL(LblFMSQ, False);
// Кнопки тулбара // Кнопки тулбара
@@ -2518,17 +2443,10 @@ begin
StyleButton(BtnMOX, BtnMOX.Active); StyleButton(BtnMOX, BtnMOX.Active);
StyleButton(BtnTUN, BtnTUN.Active); StyleButton(BtnTUN, BtnTUN.Active);
// Span / VFO-copy / HidePanel // VFO-copy
StyleSpanButton(BtnSpan48k, BtnSpan48k.Active);
StyleSpanButton(BtnSpan96k, BtnSpan96k.Active);
StyleSpanButton(BtnSpan192k, BtnSpan192k.Active);
StyleSpanButton(BtnSpan384k, BtnSpan384k.Active);
StyleSpanButton(BtnSpan768k, BtnSpan768k.Active);
StyleSpanButton(BtnSpan1536k, BtnSpan1536k.Active);
StyleSpanButton(BtnVfoACopyB, False); StyleSpanButton(BtnVfoACopyB, False);
StyleSpanButton(BtnVfoSwap, False); StyleSpanButton(BtnVfoSwap, False);
StyleSpanButton(BtnVfoBCopyA, False); StyleSpanButton(BtnVfoBCopyA, False);
if BtnHidePanel <> nil then StyleSpanButton(BtnHidePanel, False);
// Ползунки // Ползунки
DS(TrkAGC); DS(TrkVolume); DS(TrkDrive); DS(TrkAGC); DS(TrkVolume); DS(TrkDrive);
@@ -2618,59 +2536,17 @@ begin
B.Font.Size := 7; B.Font.Size := 7;
end; end;
procedure TMainForm.PositionSpanOverlayButtons; procedure TMainForm.PositionSampleRateOverlay;
const const
BTN_TOP = 6; MARGIN = 4;
GAP = 3;
W48 = 40;
W96 = 40;
W192 = 44;
W384 = 44;
W768 = 44;
W1536 = 50;
HBTN = 18;
var
X, NeedW, BaseX, BaseY: Integer;
begin begin
if (PbSpectrum = nil) or (BtnSpan48k = nil) then Exit; if (FSampleRateOverlay = nil) or (PbSpectrum = nil) then Exit;
NeedW := W48 + W96 + W192 + W384 + W768 + W1536 + GAP * 5 + 8; FSampleRateOverlay.SetBounds(
if PbSpectrum.Width < NeedW then PbSpectrum.Left + MARGIN,
begin PbSpectrum.Top + MARGIN,
BtnSpan48k.Visible := False; FSampleRateOverlay.Width,
BtnSpan96k.Visible := False; FSampleRateOverlay.Height);
BtnSpan192k.Visible := False; FSampleRateOverlay.BringToFront;
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;
end; end;
// =========================================================================== // ===========================================================================
@@ -3954,6 +3830,8 @@ begin
begin begin
FSampleRate := G_Settings.SampleRate; FSampleRate := G_Settings.SampleRate;
FSpanHz := FSampleRate; FSpanHz := FSampleRate;
if Assigned(FSampleRateOverlay) then
FSampleRateOverlay.SetCurrentRate(FSampleRate);
end; end;
BtnNR.Tag := EnsureRange(G_Settings.NRMode, 0, 4); UpdateNRButton; BtnNR.Tag := EnsureRange(G_Settings.NRMode, 0, 4); UpdateNRButton;
BtnNB.Tag := EnsureRange(G_Settings.NBMode, 0, 2); UpdateNBButton; BtnNB.Tag := EnsureRange(G_Settings.NBMode, 0, 2); UpdateNBButton;
@@ -5390,13 +5268,12 @@ begin
FSettings.SaveWebSettings(W); FSettings.SaveWebSettings(W);
end; end;
procedure TMainForm.BtnHidePanelClick(Sender: TObject); procedure TMainForm.OnSampleRateHidePanel;
begin begin
FPanelHidden := not FPanelHidden; FPanelHidden := not FPanelHidden;
if FPanelHidden then if FPanelHidden then
begin begin
PanelLeft.Width := 0; PanelLeft.Width := 0;
BtnHidePanel.Caption := '▶';
FVfoOverlay.Parent := PanelRight; FVfoOverlay.Parent := PanelRight;
FVfoOverlay.Width := 260; FVfoOverlay.Width := 260;
FVfoOverlay.Height := 136; // OVL_H_NORM — расширяется сам при открытии AGC-пикера FVfoOverlay.Height := 136; // OVL_H_NORM — расширяется сам при открытии AGC-пикера
@@ -5409,9 +5286,10 @@ begin
else else
begin begin
PanelLeft.Width := 232; PanelLeft.Width := 232;
BtnHidePanel.Caption := '◀';
FVfoOverlay.Visible := False; FVfoOverlay.Visible := False;
end; end;
if Assigned(FSampleRateOverlay) then
FSampleRateOverlay.SetPanelHidden(FPanelHidden);
ResizeSpectrumPanels; ResizeSpectrumPanels;
ResizeSMeter; ResizeSMeter;
end; end;
@@ -5492,25 +5370,20 @@ begin
FBandCache[FCurrentBand].AGCMode := FAGCMode; FBandCache[FCurrentBand].AGCMode := FAGCMode;
end; end;
procedure TMainForm.BtnSpanClick(Sender: TObject); procedure TMainForm.OnSampleRateSelect(SampleRate: Integer);
var var
NewRate: Integer; NewRate: Integer;
DDCRate: Word; DDCRate: Word;
begin begin
NewRate := (Sender as TFlatButton).Tag; NewRate := SampleRate;
if NewRate = FSampleRate then Exit; if NewRate = FSampleRate then Exit;
FSampleRate := NewRate; FSampleRate := NewRate;
FSpanHz := NewRate; FSpanHz := NewRate;
DDCRate := NewRate div 1000; DDCRate := NewRate div 1000;
// Обновляем кнопки сразу — UI отзывчив if Assigned(FSampleRateOverlay) then
StyleSpanButton(BtnSpan48k, FSampleRate = 48000); FSampleRateOverlay.SetCurrentRate(NewRate);
StyleSpanButton(BtnSpan96k, FSampleRate = 96000);
StyleSpanButton(BtnSpan192k, FSampleRate = 192000);
StyleSpanButton(BtnSpan384k, FSampleRate = 384000);
StyleSpanButton(BtnSpan768k, FSampleRate = 768000);
StyleSpanButton(BtnSpan1536k, FSampleRate = 1536000);
Application.ProcessMessages; Application.ProcessMessages;
FBandCache[FCurrentBand].SpanHz := FSpanHz; // для обратной совместимости FBandCache[FCurrentBand].SpanHz := FSpanHz; // для обратной совместимости
@@ -5762,27 +5635,17 @@ begin
end; end;
procedure TMainForm.SyncWebSpan; procedure TMainForm.SyncWebSpan;
const
ValidSpans: array[0..5] of Integer = (48000, 96000, 192000, 384000, 768000, 1536000);
var var
ValidSpans: array[0..5] of Integer;
Found: Boolean;
i: Integer; i: Integer;
Btn: TFlatButton;
begin begin
ValidSpans[0]:=48000; ValidSpans[1]:=96000; ValidSpans[2]:=192000; for i := 0 to High(ValidSpans) do
ValidSpans[3]:=384000; ValidSpans[4]:=768000; ValidSpans[5]:=1536000; if ValidSpans[i] = FWebSyncInt then
Found := False; begin
for i := 0 to 5 do OnSampleRateSelect(FWebSyncInt);
if ValidSpans[i] = FWebSyncInt then begin Found := True; Break; end; Exit;
if not Found then Exit; end;
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);
end; end;
procedure TMainForm.WebOnVolume(V: Integer); 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"/> <Filename Value="VfoOverlay.pas"/>
<IsPartOfProject Value="True"/> <IsPartOfProject Value="True"/>
</Unit> </Unit>
<Unit>
<Filename Value="SampleRateOverlay.pas"/>
<IsPartOfProject Value="True"/>
</Unit>
<Unit> <Unit>
<Filename Value="AudioOutput.pas"/> <Filename Value="AudioOutput.pas"/>
<IsPartOfProject Value="True"/> <IsPartOfProject Value="True"/>