mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
830 lines
24 KiB
ObjectPascal
830 lines
24 KiB
ObjectPascal
unit VfoOverlay;
|
|
|
|
{$mode objfpc}{$H+}
|
|
|
|
interface
|
|
|
|
uses
|
|
Classes, SysUtils, Controls, Graphics, Types, Math;
|
|
|
|
const
|
|
VFO_OVERLAY_ALPHA = 200;
|
|
|
|
type
|
|
TModeFilterEvent = procedure(Mode: Integer; FilterBW: Integer) of object;
|
|
TDSPChangeEvent = procedure(NRMode, NBMode: Integer; SNBOn, ANFOn: Boolean) of object;
|
|
TAGCChangeEvent = procedure(AGCMode: Integer) of object;
|
|
|
|
TVfoOverlay = class(TCustomControl)
|
|
private
|
|
FMode: Integer;
|
|
FFilterBW: Integer;
|
|
FVfoHz: Double;
|
|
FSMeterDB: Double;
|
|
FOnSelect: TModeFilterEvent;
|
|
FOnDSPChange: TDSPChangeEvent;
|
|
FOnAGCChange: TAGCChangeEvent;
|
|
FOnInvalidate: TNotifyEvent;
|
|
FModeFilter: array[0..7] of Integer;
|
|
|
|
FNRMode: Integer; // 0=off, 1..4=NR/NR2/NR3/NR4
|
|
FNBMode: Integer; // 0=off, 1=NB, 2=NB2
|
|
FSNBOn: Boolean;
|
|
FANFOn: Boolean;
|
|
FAGCMode: Integer; // 0=FAST 1=MED 2=SLOW 3=LONG 4=OFF
|
|
FAGCPickerOpen: Boolean;
|
|
|
|
FHitRects: array[0..24] of TRect;
|
|
FHitActions: array[0..24] of Integer;
|
|
FHitCount: Integer;
|
|
FHotIdx: Integer;
|
|
FCacheBitmap: TBitmap;
|
|
FCacheDirty: Boolean;
|
|
|
|
procedure DrawSelf(C: TCanvas; W, H: Integer);
|
|
procedure DrawSMeterBar(C: TCanvas; X, Y, BW, BH: Integer);
|
|
procedure DrawModeRow(C: TCanvas; X, Y, RowW, RowH: Integer);
|
|
procedure DrawFilterRow(C: TCanvas; X, Y, RowW, RowH: Integer);
|
|
procedure DrawDSPRow(C: TCanvas; X, Y, RowW, RowH: Integer);
|
|
procedure DrawAGCPickerRow(C: TCanvas; X, Y, RowW, RowH: Integer);
|
|
procedure RegisterHit(HR: TRect; HV: Integer);
|
|
function CalcSLabel: string;
|
|
function FormatFreq(Hz: Double): string;
|
|
function GetFilterBWForMode(ModeIdx, FilterIdx: Integer): Integer;
|
|
function GetFilterLblForMode(ModeIdx, FilterIdx: Integer): string;
|
|
function GetFilterCountForMode(ModeIdx: Integer): Integer;
|
|
procedure RequestInvalidate;
|
|
|
|
protected
|
|
procedure Paint; override;
|
|
procedure MouseDown(Button: TMouseButton; Shift: TShiftState;
|
|
X, Y: Integer); override;
|
|
procedure MouseMove(Shift: TShiftState; X, Y: Integer); override;
|
|
procedure MouseLeave; override;
|
|
|
|
public
|
|
constructor Create(AOwner: TComponent); override;
|
|
destructor Destroy; override;
|
|
procedure DrawOverlay(Target: TBitmap; W, H: Integer);
|
|
procedure DrawOverlayBitmap(Target: TBitmap);
|
|
function HandleMouseDown(Button: TMouseButton; X, Y: Integer): Boolean;
|
|
function HandleMouseMove(X, Y: Integer): Boolean;
|
|
function HandleMouseLeave: Boolean;
|
|
procedure SetState(AMode, ABW: Integer; AVfoHz, ASMeterDB: Double);
|
|
procedure SetDSPState(ANRMode, ANBMode: Integer; ASNBOn, AANFOn: Boolean;
|
|
AAGCMode: Integer);
|
|
procedure UpdateSMeter(DB: Double);
|
|
procedure UpdateVfo(AVfoHz: Double);
|
|
property HotIdx: Integer read FHotIdx;
|
|
property OnSelect: TModeFilterEvent read FOnSelect write FOnSelect;
|
|
property OnDSPChange: TDSPChangeEvent read FOnDSPChange write FOnDSPChange;
|
|
property OnAGCChange: TAGCChangeEvent read FOnAGCChange write FOnAGCChange;
|
|
property OnInvalidate: TNotifyEvent read FOnInvalidate write FOnInvalidate;
|
|
end;
|
|
|
|
implementation
|
|
|
|
const
|
|
KEY_COLOR = TColor($00FF00FF);
|
|
|
|
HIT_NR = -9;
|
|
HIT_NB = -10;
|
|
HIT_SNB = -11;
|
|
HIT_ANF = -12;
|
|
HIT_AGC = -13;
|
|
|
|
HIT_AGC_PICK_BASE = 100000; // 100000+N — выбор режима AGC (>всех BW фильтров)
|
|
|
|
AGC_NAMES: array[0..4] of string = ('FAST', 'MED', 'SLOW', 'LONG', 'OFF');
|
|
|
|
OVL_H_NORM = 136; // высота без пикера AGC
|
|
OVL_H_AGC = 159; // высота с открытым пикером (136 + GAP(3) + ROW_H(20))
|
|
|
|
CLR_PANEL_BG = TColor($00141414);
|
|
CLR_BORDER = TColor($00505050);
|
|
CLR_TEXT = TColor($00E8E8E8);
|
|
CLR_DIM = TColor($00787878);
|
|
CLR_ACCENT = TColor($0040FF80);
|
|
CLR_BTN_NORM = TColor($00282828);
|
|
CLR_BTN_HOT = TColor($00383838);
|
|
CLR_BTN_ACT = TColor($00183828);
|
|
CLR_FREQ = TColor($0050FF80);
|
|
CLR_SVAL = TColor($0030C8FF);
|
|
CLR_BAR_GRN = TColor($0018A030);
|
|
CLR_BAR_RED = TColor($000030C0);
|
|
CLR_BAR_BG = TColor($00101010);
|
|
|
|
// Режимы — ТОЧНО как в MainForm.pas
|
|
// 0=LSB, 1=USB, 2=DSB, 3=CWL, 4=CWU, 5=FM, 6=AM, 7=SAM
|
|
OVL_MODE_COUNT = 8;
|
|
OVL_MODE_NAMES: array[0..7] of string = (
|
|
'LSB','USB','DSB','CWL','CWU','FM','AM','SAM');
|
|
|
|
// Фильтры SSB (LSB=0, USB=1)
|
|
SSB_BW: array[0..5] of Integer = (1800,2100,2400,2700,3300,3800);
|
|
SSB_LBL: array[0..5] of string = ('1.8K','2.1K','2.4K','2.7K','3.3K','3.8K');
|
|
|
|
// Фильтры DSB (DSB=2)
|
|
DSB_BW: array[0..5] of Integer = (16000,12000,10000,8000,5200,4000);
|
|
DSB_LBL: array[0..5] of string = ('16K','12K','10K','8K','5.2K','4K');
|
|
|
|
// Фильтры CW (CWL=3, CWU=4)
|
|
CW_BW: array[0..5] of Integer = (500,250,150,100,50,1000);
|
|
CW_LBL: array[0..5] of string = ('500','250','150','100','50','1K');
|
|
|
|
// Фильтры FM (FM=5) — только NFM и FM, как в MainForm
|
|
FM_BW: array[0..1] of Integer = (11000, 16000);
|
|
FM_LBL: array[0..1] of string = ('NFM', 'FM');
|
|
|
|
// Фильтры AM/SAM (AM=6, SAM=7)
|
|
AM_BW: array[0..5] of Integer = (20000,16000,12000,10000,8000,6000);
|
|
AM_LBL: array[0..5] of string = ('20K','16K','12K','10K','8K','6K');
|
|
|
|
// S-шкала: S1..S9
|
|
DB_S: array[1..9] of Double = (-121,-115,-109,-103,-97,-91,-85,-79,-73);
|
|
|
|
procedure BlendBitmapKey(Target, Source: TBitmap; DstX, DstY: Integer; Alpha: Byte);
|
|
var
|
|
Y, X, SX0, SY0, SX1, SY1, TY, InvA: Integer;
|
|
SrcRow, DstRow: PByte;
|
|
begin
|
|
if (Target = nil) or (Source = nil) or (Alpha = 0) then Exit;
|
|
SX0 := 0;
|
|
SY0 := 0;
|
|
SX1 := Source.Width;
|
|
SY1 := Source.Height;
|
|
if DstX < 0 then begin SX0 := -DstX; DstX := 0; end;
|
|
if DstY < 0 then begin SY0 := -DstY; DstY := 0; end;
|
|
if DstX + (SX1 - SX0) > Target.Width then SX1 := SX0 + Target.Width - DstX;
|
|
if DstY + (SY1 - SY0) > Target.Height then SY1 := SY0 + Target.Height - DstY;
|
|
if (SX1 <= SX0) or (SY1 <= SY0) then Exit;
|
|
|
|
InvA := 255 - Alpha;
|
|
Target.BeginUpdate(False);
|
|
Source.BeginUpdate(False);
|
|
try
|
|
for Y := SY0 to SY1 - 1 do
|
|
begin
|
|
TY := DstY + (Y - SY0);
|
|
SrcRow := PByte(Source.ScanLine[Y]);
|
|
DstRow := PByte(Target.ScanLine[TY]);
|
|
if (SrcRow = nil) or (DstRow = nil) then Continue;
|
|
Inc(SrcRow, SX0 * 4);
|
|
Inc(DstRow, DstX * 4);
|
|
for X := SX0 to SX1 - 1 do
|
|
begin
|
|
// KEY_COLOR is clFuchsia in pf32bit B,G,R order.
|
|
if not ((SrcRow[0] = $FF) and (SrcRow[1] = $00) and (SrcRow[2] = $FF)) then
|
|
begin
|
|
DstRow[0] := Byte((Alpha * SrcRow[0] + InvA * DstRow[0]) div 255);
|
|
DstRow[1] := Byte((Alpha * SrcRow[1] + InvA * DstRow[1]) div 255);
|
|
DstRow[2] := Byte((Alpha * SrcRow[2] + InvA * DstRow[2]) div 255);
|
|
end;
|
|
Inc(SrcRow, 4);
|
|
Inc(DstRow, 4);
|
|
end;
|
|
end;
|
|
finally
|
|
Source.EndUpdate(False);
|
|
Target.EndUpdate(False);
|
|
end;
|
|
end;
|
|
|
|
function TVfoOverlay.GetFilterBWForMode(ModeIdx, FilterIdx: Integer): Integer;
|
|
begin
|
|
case ModeIdx of
|
|
0,1: Result := SSB_BW[FilterIdx];
|
|
2: Result := DSB_BW[FilterIdx];
|
|
3,4: Result := CW_BW[FilterIdx];
|
|
5: Result := FM_BW[FilterIdx];
|
|
else Result := AM_BW[FilterIdx]; // 6=AM, 7=SAM
|
|
end;
|
|
end;
|
|
|
|
function TVfoOverlay.GetFilterLblForMode(ModeIdx, FilterIdx: Integer): string;
|
|
begin
|
|
case ModeIdx of
|
|
0,1: Result := SSB_LBL[FilterIdx];
|
|
2: Result := DSB_LBL[FilterIdx];
|
|
3,4: Result := CW_LBL[FilterIdx];
|
|
5: Result := FM_LBL[FilterIdx];
|
|
else Result := AM_LBL[FilterIdx];
|
|
end;
|
|
end;
|
|
|
|
function TVfoOverlay.GetFilterCountForMode(ModeIdx: Integer): Integer;
|
|
begin
|
|
if ModeIdx = 5 then Result := 2 // FM: только NFM и FM
|
|
else Result := 6;
|
|
end;
|
|
|
|
function DBmToSLabel(DB: Double): string;
|
|
var
|
|
I, Over: Integer;
|
|
begin
|
|
if DB >= DB_S[9] then
|
|
begin
|
|
Over := Round(DB - DB_S[9]);
|
|
if Over < 5 then
|
|
Result := 'S9'
|
|
else
|
|
begin
|
|
Over := ((Over + 5) div 10) * 10;
|
|
if Over = 0 then Over := 10;
|
|
Result := 'S9+' + IntToStr(Over);
|
|
end;
|
|
end
|
|
else if DB <= DB_S[1] then
|
|
Result := 'S1'
|
|
else
|
|
begin
|
|
Result := 'S1';
|
|
for I := 1 to 8 do
|
|
if DB >= DB_S[I] then
|
|
Result := 'S' + IntToStr(I);
|
|
end;
|
|
end;
|
|
|
|
constructor TVfoOverlay.Create(AOwner: TComponent);
|
|
begin
|
|
inherited Create(AOwner);
|
|
ControlStyle := ControlStyle + [csOpaque];
|
|
FMode := 1; // USB по умолчанию (как в MainForm)
|
|
FFilterBW := 2700;
|
|
FVfoHz := 14200000;
|
|
FSMeterDB := -121;
|
|
FHitCount := 0;
|
|
FHotIdx := -1;
|
|
// Дефолтные фильтры для каждого режима (запоминаются при переключении)
|
|
FModeFilter[0] := 2700; // LSB
|
|
FModeFilter[1] := 2700; // USB
|
|
FModeFilter[2] := 8000; // DSB
|
|
FModeFilter[3] := 500; // CWL
|
|
FModeFilter[4] := 500; // CWU
|
|
FModeFilter[5] := 11000; // FM (NFM по умолчанию)
|
|
FModeFilter[6] := 8000; // AM
|
|
FModeFilter[7] := 8000; // SAM
|
|
FNRMode := 0;
|
|
FNBMode := 0;
|
|
FSNBOn := False;
|
|
FANFOn := False;
|
|
FAGCMode := 0;
|
|
FAGCPickerOpen := False;
|
|
FCacheBitmap := TBitmap.Create;
|
|
FCacheBitmap.PixelFormat := pf32bit;
|
|
FCacheDirty := True;
|
|
Width := 260;
|
|
Height := OVL_H_NORM;
|
|
Cursor := crHandPoint;
|
|
end;
|
|
|
|
destructor TVfoOverlay.Destroy;
|
|
begin
|
|
FCacheBitmap.Free;
|
|
inherited Destroy;
|
|
end;
|
|
|
|
procedure TVfoOverlay.RegisterHit(HR: TRect; HV: Integer);
|
|
begin
|
|
if FHitCount > High(FHitRects) then Exit;
|
|
FHitRects[FHitCount] := HR;
|
|
FHitActions[FHitCount] := HV;
|
|
Inc(FHitCount);
|
|
end;
|
|
|
|
procedure TVfoOverlay.RequestInvalidate;
|
|
begin
|
|
FCacheDirty := True;
|
|
inherited Invalidate;
|
|
if Assigned(FOnInvalidate) then FOnInvalidate(Self);
|
|
end;
|
|
|
|
function TVfoOverlay.FormatFreq(Hz: Double): string;
|
|
var
|
|
iM, iK, iH: Integer;
|
|
begin
|
|
iM := Trunc(Hz / 1e6);
|
|
iK := Trunc((Hz - iM * 1e6) / 1000);
|
|
iH := Round(Hz - iM * 1e6 - iK * 1000);
|
|
Result := Format('%d.%3.3d.%3.3d', [iM, iK, iH]);
|
|
end;
|
|
|
|
function TVfoOverlay.CalcSLabel: string;
|
|
begin
|
|
Result := DBmToSLabel(FSMeterDB);
|
|
end;
|
|
|
|
procedure TVfoOverlay.DrawSMeterBar(C: TCanvas; X, Y, BW, BH: Integer);
|
|
const
|
|
DB_MIN = -127.0;
|
|
DB_MAX = -13.0;
|
|
S_POS: array[0..7] of Double = (-121,-109,-97,-85,-73,-53,-33,-13);
|
|
S_LBL: array[0..7] of string = ('1','3','5','7','S9','+20','+40','+60');
|
|
var
|
|
I, BarEnd, S9X, PX, BarH: Integer;
|
|
T: Double;
|
|
Lbl: string;
|
|
TW: Integer;
|
|
begin
|
|
BarH := BH - 11;
|
|
|
|
C.Brush.Color := CLR_BAR_BG;
|
|
C.Brush.Style := bsSolid;
|
|
C.Pen.Style := psClear;
|
|
C.FillRect(Rect(X, Y, X+BW, Y+BarH));
|
|
|
|
S9X := X + Round((-73 - DB_MIN) / (DB_MAX - DB_MIN) * BW);
|
|
|
|
T := Max(0.0, Min(1.0, (FSMeterDB - DB_MIN) / (DB_MAX - DB_MIN)));
|
|
BarEnd := X + Round(T * BW);
|
|
|
|
if BarEnd > X + 1 then
|
|
begin
|
|
if BarEnd <= S9X then
|
|
begin
|
|
C.Brush.Color := CLR_BAR_GRN;
|
|
C.FillRect(Rect(X+1, Y+1, BarEnd, Y+BarH-1));
|
|
end
|
|
else
|
|
begin
|
|
C.Brush.Color := CLR_BAR_GRN;
|
|
C.FillRect(Rect(X+1, Y+1, S9X, Y+BarH-1));
|
|
C.Brush.Color := CLR_BAR_RED;
|
|
C.FillRect(Rect(S9X, Y+1, Min(BarEnd, X+BW-1), Y+BarH-1));
|
|
end;
|
|
end;
|
|
|
|
C.Pen.Style := psSolid;
|
|
C.Pen.Color := CLR_BORDER;
|
|
C.Brush.Style := bsClear;
|
|
C.Rectangle(X, Y, X+BW, Y+BarH);
|
|
|
|
C.Font.Name := 'Courier New';
|
|
C.Font.Size := 5;
|
|
C.Font.Style := [];
|
|
for I := 0 to High(S_POS) do
|
|
begin
|
|
T := (S_POS[I] - DB_MIN) / (DB_MAX - DB_MIN);
|
|
PX := X + Round(T * BW);
|
|
Lbl := S_LBL[I];
|
|
TW := C.TextWidth(Lbl);
|
|
if I < 4 then C.Font.Color := CLR_DIM
|
|
else C.Font.Color := CLR_ACCENT;
|
|
C.Pen.Color := CLR_DIM;
|
|
C.Pen.Style := psSolid;
|
|
C.MoveTo(PX, Y+BarH); C.LineTo(PX, Y+BarH+2);
|
|
C.TextOut(PX - TW div 2, Y+BarH+2, Lbl);
|
|
end;
|
|
end;
|
|
|
|
procedure TVfoOverlay.DrawModeRow(C: TCanvas; X, Y, RowW, RowH: Integer);
|
|
var
|
|
I, BX, BRight: Integer;
|
|
HR: TRect;
|
|
IsAct, IsHot: Boolean;
|
|
begin
|
|
BX := X;
|
|
for I := 0 to OVL_MODE_COUNT - 1 do
|
|
begin
|
|
BRight := X + ((I + 1) * RowW) div OVL_MODE_COUNT;
|
|
HR := Rect(BX, Y, BRight, Y + RowH);
|
|
IsAct := (I = FMode);
|
|
IsHot := (FHotIdx >= 0) and (FHitActions[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_BORDER;
|
|
C.Brush.Style := bsClear; C.Rectangle(HR);
|
|
|
|
if IsAct then C.Font.Color := CLR_ACCENT
|
|
else C.Font.Color := CLR_TEXT;
|
|
C.Font.Size := 6; C.Font.Style := []; C.Font.Name := 'Courier New';
|
|
C.Brush.Style := bsClear;
|
|
C.TextOut(BX + (BRight - BX - C.TextWidth(OVL_MODE_NAMES[I])) div 2,
|
|
Y + (RowH - C.TextHeight('A')) div 2,
|
|
OVL_MODE_NAMES[I]);
|
|
|
|
RegisterHit(HR, -(I + 1)); // -1=LSB, -2=USB, -3=DSB, -4=CWL, -5=CWU, -6=FM, -7=AM, -8=SAM
|
|
BX := BRight + 1;
|
|
end;
|
|
end;
|
|
|
|
procedure TVfoOverlay.DrawFilterRow(C: TCanvas; X, Y, RowW, RowH: Integer);
|
|
var
|
|
FILT_COUNT: Integer;
|
|
I, BX, BRight, BV: Integer;
|
|
HR: TRect;
|
|
Lbl: string;
|
|
IsAct: Boolean;
|
|
begin
|
|
FILT_COUNT := GetFilterCountForMode(FMode);
|
|
BX := X;
|
|
for I := 0 to FILT_COUNT - 1 do
|
|
begin
|
|
BRight := X + ((I + 1) * RowW) div FILT_COUNT;
|
|
HR := Rect(BX, Y, BRight, Y + RowH);
|
|
BV := GetFilterBWForMode(FMode, I);
|
|
IsAct := (BV = FFilterBW);
|
|
Lbl := GetFilterLblForMode(FMode, I);
|
|
|
|
if IsAct then C.Brush.Color := CLR_BTN_ACT
|
|
else if (FHotIdx >= 0) and (FHitActions[FHotIdx] = BV) 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_BORDER;
|
|
C.Brush.Style := bsClear; C.Rectangle(HR);
|
|
|
|
if IsAct then C.Font.Color := CLR_ACCENT
|
|
else C.Font.Color := CLR_TEXT;
|
|
C.Font.Size := 7; C.Font.Name := 'Courier New';
|
|
C.Brush.Style := bsClear;
|
|
C.TextOut(BX + (BRight - BX - C.TextWidth(Lbl)) div 2,
|
|
Y + (RowH - C.TextHeight('A')) div 2, Lbl);
|
|
|
|
RegisterHit(HR, BV); // BV всегда > 0 (BW в Гц, минимум 25)
|
|
BX := BRight + 1;
|
|
end;
|
|
end;
|
|
|
|
procedure TVfoOverlay.DrawDSPRow(C: TCanvas; X, Y, RowW, RowH: Integer);
|
|
const
|
|
DSP_COUNT = 5;
|
|
DSP_HITS: array[0..4] of Integer = (HIT_NR, HIT_NB, HIT_SNB, HIT_ANF, HIT_AGC);
|
|
var
|
|
I, BX, BRight: Integer;
|
|
HR: TRect;
|
|
Lbl: string;
|
|
IsAct, IsHot: Boolean;
|
|
begin
|
|
BX := X;
|
|
for I := 0 to DSP_COUNT - 1 do
|
|
begin
|
|
BRight := X + ((I + 1) * RowW) div DSP_COUNT;
|
|
HR := Rect(BX, Y, BRight, Y + RowH);
|
|
case I of
|
|
0: begin
|
|
case FNRMode of
|
|
1: Lbl := 'NR';
|
|
2: Lbl := 'NR2';
|
|
3: Lbl := 'NR3';
|
|
4: Lbl := 'NR4';
|
|
else Lbl := 'NR';
|
|
end;
|
|
IsAct := FNRMode <> 0;
|
|
end;
|
|
1: begin
|
|
if FNBMode = 2 then Lbl := 'NB2' else Lbl := 'NB';
|
|
IsAct := FNBMode <> 0;
|
|
end;
|
|
2: begin Lbl := 'SNB'; IsAct := FSNBOn; end;
|
|
3: begin Lbl := 'ANF'; IsAct := FANFOn; end;
|
|
4: begin Lbl := AGC_NAMES[FAGCMode]; IsAct := FAGCPickerOpen; end;
|
|
else Lbl := ''; IsAct := False;
|
|
end;
|
|
IsHot := (FHotIdx >= 0) and (FHitActions[FHotIdx] = DSP_HITS[I]);
|
|
|
|
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_BORDER;
|
|
C.Brush.Style := bsClear; C.Rectangle(HR);
|
|
|
|
if IsAct then C.Font.Color := CLR_ACCENT
|
|
else C.Font.Color := CLR_TEXT;
|
|
C.Font.Size := 7; C.Font.Name := 'Courier New';
|
|
C.Brush.Style := bsClear;
|
|
C.TextOut(BX + (BRight - BX - C.TextWidth(Lbl)) div 2,
|
|
Y + (RowH - C.TextHeight('A')) div 2, Lbl);
|
|
|
|
RegisterHit(HR, DSP_HITS[I]);
|
|
BX := BRight + 1;
|
|
end;
|
|
end;
|
|
|
|
procedure TVfoOverlay.DrawAGCPickerRow(C: TCanvas; X, Y, RowW, RowH: Integer);
|
|
const
|
|
AGC_COUNT = 5;
|
|
var
|
|
I, BX, BRight: Integer;
|
|
HR: TRect;
|
|
IsAct, IsHot: Boolean;
|
|
begin
|
|
BX := X;
|
|
for I := 0 to AGC_COUNT - 1 do
|
|
begin
|
|
BRight := X + ((I + 1) * RowW) div AGC_COUNT;
|
|
HR := Rect(BX, Y, BRight, Y + RowH);
|
|
IsAct := (I = FAGCMode);
|
|
IsHot := (FHotIdx >= 0) and
|
|
(FHitActions[FHotIdx] = HIT_AGC_PICK_BASE + I);
|
|
|
|
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_BORDER;
|
|
C.Brush.Style := bsClear; C.Rectangle(HR);
|
|
|
|
if IsAct then C.Font.Color := CLR_ACCENT
|
|
else C.Font.Color := CLR_TEXT;
|
|
C.Font.Size := 7; C.Font.Name := 'Courier New';
|
|
C.Brush.Style := bsClear;
|
|
C.TextOut(BX + (BRight - BX - C.TextWidth(AGC_NAMES[I])) div 2,
|
|
Y + (RowH - C.TextHeight('A')) div 2, AGC_NAMES[I]);
|
|
|
|
RegisterHit(HR, HIT_AGC_PICK_BASE + I);
|
|
BX := BRight + 1;
|
|
end;
|
|
end;
|
|
|
|
procedure TVfoOverlay.DrawSelf(C: TCanvas; W, H: Integer);
|
|
const
|
|
PAD = 4;
|
|
FREQ_H = 18;
|
|
INFO_H = 14;
|
|
SM_H = 27;
|
|
ROW_H = 20;
|
|
GAP = 3;
|
|
var
|
|
FreqStr, SStr: string;
|
|
FW, CurY: Integer;
|
|
begin
|
|
FHitCount := 0;
|
|
|
|
C.Brush.Color := CLR_PANEL_BG;
|
|
C.Brush.Style := bsSolid;
|
|
C.Pen.Color := CLR_BORDER;
|
|
C.Pen.Style := psSolid;
|
|
C.Pen.Width := 1;
|
|
C.RoundRect(0, 0, W, H, 6, 6);
|
|
|
|
CurY := PAD;
|
|
|
|
// Частота
|
|
FreqStr := FormatFreq(FVfoHz);
|
|
C.Font.Name := 'Courier New'; C.Font.Size := 11; C.Font.Style := [fsBold];
|
|
C.Font.Color := CLR_FREQ;
|
|
FW := C.TextWidth(FreqStr);
|
|
C.Brush.Style := bsClear;
|
|
C.TextOut((W - FW) div 2, CurY, FreqStr);
|
|
Inc(CurY, FREQ_H);
|
|
|
|
// Режим (слева) + S-value (справа)
|
|
SStr := CalcSLabel;
|
|
C.Font.Size := 8; C.Font.Style := [fsBold];
|
|
C.Font.Color := CLR_ACCENT;
|
|
C.TextOut(PAD + 2, CurY, OVL_MODE_NAMES[FMode]);
|
|
C.Font.Color := CLR_SVAL;
|
|
C.TextOut(W - PAD - C.TextWidth(SStr) - 2, CurY, SStr);
|
|
Inc(CurY, INFO_H);
|
|
|
|
// S-метр
|
|
DrawSMeterBar(C, PAD + 4, CurY, W - (PAD + 4) * 2, SM_H);
|
|
Inc(CurY, SM_H + GAP);
|
|
|
|
// Кнопки режимов
|
|
DrawModeRow(C, PAD, CurY, W - PAD * 2, ROW_H);
|
|
Inc(CurY, ROW_H + GAP);
|
|
|
|
// Кнопки фильтров
|
|
DrawFilterRow(C, PAD, CurY, W - PAD * 2, ROW_H);
|
|
Inc(CurY, ROW_H + GAP);
|
|
|
|
// Кнопки DSP (NR, NB, SNB, ANF, AGC)
|
|
DrawDSPRow(C, PAD, CurY, W - PAD * 2, ROW_H);
|
|
|
|
// Пикер AGC (открывается кнопкой AGC)
|
|
if FAGCPickerOpen then
|
|
begin
|
|
Inc(CurY, ROW_H + GAP);
|
|
DrawAGCPickerRow(C, PAD, CurY, W - PAD * 2, ROW_H);
|
|
end;
|
|
end;
|
|
|
|
procedure TVfoOverlay.Paint;
|
|
begin
|
|
DrawSelf(Canvas, Width, Height);
|
|
end;
|
|
|
|
procedure TVfoOverlay.DrawOverlay(Target: TBitmap; W, H: Integer);
|
|
begin
|
|
if (not Visible) or (Target = nil) or (Width <= 0) or (Height <= 0) then Exit;
|
|
if (Left >= W) or (Top >= H) then Exit;
|
|
|
|
if (FCacheBitmap.Width <> Width) or (FCacheBitmap.Height <> Height) then
|
|
begin
|
|
FCacheBitmap.SetSize(Width, Height);
|
|
FCacheDirty := True;
|
|
end;
|
|
if FCacheDirty then
|
|
begin
|
|
FCacheBitmap.Canvas.Brush.Color := KEY_COLOR;
|
|
FCacheBitmap.Canvas.Brush.Style := bsSolid;
|
|
FCacheBitmap.Canvas.Pen.Style := psClear;
|
|
FCacheBitmap.Canvas.FillRect(Rect(0, 0, Width, Height));
|
|
DrawSelf(FCacheBitmap.Canvas, Width, Height);
|
|
FCacheDirty := False;
|
|
end;
|
|
BlendBitmapKey(Target, FCacheBitmap, Left, Top, VFO_OVERLAY_ALPHA);
|
|
end;
|
|
|
|
procedure TVfoOverlay.DrawOverlayBitmap(Target: TBitmap);
|
|
begin
|
|
if (Target = nil) or (Width <= 0) or (Height <= 0) then Exit;
|
|
Target.PixelFormat := pf32bit;
|
|
Target.SetSize(Width, Height);
|
|
Target.Canvas.Brush.Color := KEY_COLOR;
|
|
Target.Canvas.Brush.Style := bsSolid;
|
|
Target.Canvas.Pen.Style := psClear;
|
|
Target.Canvas.FillRect(Rect(0, 0, Width, Height));
|
|
DrawSelf(Target.Canvas, Width, Height);
|
|
FCacheDirty := False;
|
|
end;
|
|
|
|
function TVfoOverlay.HandleMouseDown(Button: TMouseButton; X, Y: Integer): Boolean;
|
|
begin
|
|
Result := False;
|
|
if not Visible then Exit;
|
|
if (X < Left) or (Y < Top) or (X >= Left + Width) or (Y >= Top + Height) then Exit;
|
|
MouseDown(Button, [], X - Left, Y - Top);
|
|
Result := True;
|
|
end;
|
|
|
|
function TVfoOverlay.HandleMouseMove(X, Y: Integer): Boolean;
|
|
var
|
|
I, OldHot, LX, LY: Integer;
|
|
begin
|
|
Result := False;
|
|
if not Visible then Exit;
|
|
LX := X - Left;
|
|
LY := Y - Top;
|
|
OldHot := FHotIdx;
|
|
FHotIdx := -1;
|
|
if (LX >= 0) and (LY >= 0) and (LX < Width) and (LY < Height) then
|
|
for I := 0 to FHitCount - 1 do
|
|
if PtInRect(FHitRects[I], Point(LX, LY)) then
|
|
begin
|
|
FHotIdx := I;
|
|
Break;
|
|
end;
|
|
if FHotIdx <> OldHot then
|
|
begin
|
|
RequestInvalidate;
|
|
Result := True;
|
|
end;
|
|
end;
|
|
|
|
function TVfoOverlay.HandleMouseLeave: Boolean;
|
|
begin
|
|
Result := False;
|
|
if FHotIdx >= 0 then
|
|
begin
|
|
FHotIdx := -1;
|
|
RequestInvalidate;
|
|
Result := True;
|
|
end;
|
|
end;
|
|
|
|
procedure TVfoOverlay.MouseDown(Button: TMouseButton; Shift: TShiftState;
|
|
X, Y: Integer);
|
|
var
|
|
I, HV, NewMode, NewBW: Integer;
|
|
begin
|
|
inherited;
|
|
if Button <> mbLeft then Exit;
|
|
for I := 0 to FHitCount - 1 do
|
|
if PtInRect(FHitRects[I], Point(X, Y)) then
|
|
begin
|
|
HV := FHitActions[I];
|
|
if HV < 0 then
|
|
begin
|
|
NewMode := (-HV) - 1;
|
|
if NewMode < OVL_MODE_COUNT then
|
|
begin
|
|
// Кнопка режима
|
|
if NewMode <> FMode then
|
|
begin
|
|
FModeFilter[FMode] := FFilterBW;
|
|
FMode := NewMode;
|
|
FFilterBW := FModeFilter[FMode];
|
|
RequestInvalidate;
|
|
if Assigned(FOnSelect) then FOnSelect(FMode, FFilterBW);
|
|
end;
|
|
end
|
|
else
|
|
begin
|
|
// Кнопка DSP
|
|
if HV = HIT_AGC then
|
|
begin
|
|
FAGCPickerOpen := not FAGCPickerOpen;
|
|
if FAGCPickerOpen then Height := OVL_H_AGC
|
|
else Height := OVL_H_NORM;
|
|
RequestInvalidate;
|
|
end
|
|
else
|
|
begin
|
|
case HV of
|
|
HIT_NR: FNRMode := (FNRMode + 1) mod 5;
|
|
HIT_NB: FNBMode := (FNBMode + 1) mod 3;
|
|
HIT_SNB: FSNBOn := not FSNBOn;
|
|
HIT_ANF: FANFOn := not FANFOn;
|
|
end;
|
|
RequestInvalidate;
|
|
if Assigned(FOnDSPChange) then
|
|
FOnDSPChange(FNRMode, FNBMode, FSNBOn, FANFOn);
|
|
end;
|
|
end;
|
|
end
|
|
else if HV >= HIT_AGC_PICK_BASE then
|
|
begin
|
|
// Выбор режима AGC из пикера
|
|
FAGCMode := HV - HIT_AGC_PICK_BASE;
|
|
FAGCPickerOpen := False;
|
|
Height := OVL_H_NORM;
|
|
RequestInvalidate;
|
|
if Assigned(FOnAGCChange) then FOnAGCChange(FAGCMode);
|
|
end
|
|
else if HV > 0 then
|
|
begin
|
|
NewBW := HV;
|
|
if NewBW <> FFilterBW then
|
|
begin
|
|
FFilterBW := NewBW;
|
|
FModeFilter[FMode] := FFilterBW;
|
|
RequestInvalidate;
|
|
if Assigned(FOnSelect) then FOnSelect(FMode, FFilterBW);
|
|
end;
|
|
end;
|
|
Break;
|
|
end;
|
|
end;
|
|
|
|
procedure TVfoOverlay.MouseMove(Shift: TShiftState; X, Y: Integer);
|
|
var
|
|
I, OldHot: Integer;
|
|
begin
|
|
inherited;
|
|
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 RequestInvalidate;
|
|
end;
|
|
|
|
procedure TVfoOverlay.MouseLeave;
|
|
begin
|
|
inherited;
|
|
if FHotIdx >= 0 then begin FHotIdx := -1; RequestInvalidate; end;
|
|
end;
|
|
|
|
procedure TVfoOverlay.SetState(AMode, ABW: Integer; AVfoHz, ASMeterDB: Double);
|
|
begin
|
|
FMode := Max(0, Min(OVL_MODE_COUNT - 1, AMode));
|
|
FFilterBW := ABW;
|
|
// Синхронизируем память фильтра для текущего режима
|
|
FModeFilter[FMode] := ABW;
|
|
FVfoHz := AVfoHz;
|
|
FSMeterDB := ASMeterDB;
|
|
RequestInvalidate;
|
|
end;
|
|
|
|
procedure TVfoOverlay.SetDSPState(ANRMode, ANBMode: Integer; ASNBOn, AANFOn: Boolean;
|
|
AAGCMode: Integer);
|
|
begin
|
|
FNRMode := ANRMode;
|
|
FNBMode := ANBMode;
|
|
FSNBOn := ASNBOn;
|
|
FANFOn := AANFOn;
|
|
FAGCMode := AAGCMode;
|
|
RequestInvalidate;
|
|
end;
|
|
|
|
procedure TVfoOverlay.UpdateSMeter(DB: Double);
|
|
begin
|
|
// Не клипаем — передаём как есть, как в основном S-метре
|
|
if Abs(DB - FSMeterDB) > 0.4 then
|
|
begin
|
|
FSMeterDB := DB;
|
|
RequestInvalidate;
|
|
end;
|
|
end;
|
|
|
|
procedure TVfoOverlay.UpdateVfo(AVfoHz: Double);
|
|
begin
|
|
if FVfoHz <> AVfoHz then
|
|
begin
|
|
FVfoHz := AVfoHz;
|
|
RequestInvalidate;
|
|
end;
|
|
end;
|
|
|
|
end.
|