feat(slices): per-pan tuning wheel and per-slice RX mute on TX

Mouse wheel over an extra panadapter now retunes that pan's primary
slice (first created on that receiver) instead of the main VFO; a slice
flag under the cursor still wins, pan 0 keeps driving the VFO.

Slice flags get a clickable RXM badge: RX audio of the slice is muted
while transmitting (default), so the operator no longer hears himself on
every slice in full duplex. State persists with the workspace.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 22:35:19 +03:00
co-authored by Claude Opus 4.8
parent fa5a95ca5d
commit 28ae832b71
5 changed files with 112 additions and 8 deletions
+31 -8
View File
@@ -77,6 +77,7 @@ type
FSplit: Boolean;
FTx: Boolean; // передача идёт (красный)
FTxSel: Boolean; // этот слайс выбран для передачи (янтарный)
FRxMuteTx: Boolean; // RXM: приём слайса глушится на передаче (умолч. True)
FSlice: Char; // буква слайса ('A')
FSMeterThreshold: Double; // мин. изменение S-метра (дБ) для перерисовки
FDMRText: string; // стабильный краткий статус рядом с полосой
@@ -105,6 +106,7 @@ type
FOnAudioDevice: TAudioDeviceEvent;
FOnSplit: TNotifyEvent;
FOnTxSelect: TNotifyEvent;
FOnRxMuteTx: TNotifyEvent;
FOnInvalidate: TNotifyEvent;
FTheme: TAppTheme; // тема приложения (цвета кнопок и т.п.)
@@ -183,6 +185,7 @@ type
AAGCMode: Integer);
procedure SetSquelchState(AOn: Boolean; ALevel: Integer);
procedure SetExtState(AVolume: Integer; AMuted, ASplit, ATx, ATxSel: Boolean);
procedure SetRxMuteTxState(AOn: Boolean); // бейдж RXM (self-monitor на TX)
procedure SetAudioDevices(Names: TStrings; const Indices: array of Integer;
const CurName: string);
procedure UpdateSMeter(DB: Double);
@@ -218,6 +221,7 @@ type
property OnAudioDevice: TAudioDeviceEvent read FOnAudioDevice write FOnAudioDevice;
property OnSplit: TNotifyEvent read FOnSplit write FOnSplit;
property OnTxSelect: TNotifyEvent read FOnTxSelect write FOnTxSelect;
property OnRxMuteTx: TNotifyEvent read FOnRxMuteTx write FOnRxMuteTx;
property OnSliceSelect: TNotifyEvent read FOnSliceSelect write FOnSliceSelect;
property OnClose: TNotifyEvent read FOnClose write FOnClose;
property OnInvalidate: TNotifyEvent read FOnInvalidate write FOnInvalidate;
@@ -258,9 +262,10 @@ const
HIT_CLOSE = -30; // × закрытия слайса (неглавные)
HIT_SQL = -31; // тумблер FM-squelch (в DSP-панели)
HIT_SQL_SLIDER = -32; // трек порога FM-squelch
HIT_RXMUTE = -33; // бейдж RXM: приём слайса на своей передаче
HIT_AGC_PICK_BASE = 100000;
HIT_DEV_BASE = 200000;
// Свои диапазоны, чтобы не пересекаться с тумблерами (-9..-32) и с базами выше.
// Свои диапазоны, чтобы не пересекаться с тумблерами (-9..-33) и с базами выше.
HIT_MODE_BASE = -1000; // -1000 .. -1000-(OVL_MODE_COUNT-1)
HIT_FILT_BASE = 1000; // 1000 + индекс кнопки в ряду фильтров
@@ -455,7 +460,7 @@ begin
FNRMode := 0; FNBMode := 0; FSNBOn := False; FANFOn := False; FAGCMode := 0;
FSQLOn := False; FSQLLevel := 40; FSqlDrag := False;
FVolume := 70; FMuted := False; FSplit := False; FTx := False;
FTxSel := True; FSlice := 'A';
FTxSel := True; FSlice := 'A'; FRxMuteTx := True;
FDMRText := ''; FDMRSynced := False;
FDMRCandidateText := ''; FDMRCandidateSynced := False;
FDMRCandidateSince := 0;
@@ -987,7 +992,7 @@ end;
procedure TVfoOverlay.DrawSelf(C: TCanvas; W, H: Integer);
var
FreqStr, SStr, BWStr, DMRStr: string;
CurY, X, RW, i, DMRX, DMRRight: Integer;
CurY, X, RW, i, DMRX, DMRRight, TxRight: Integer;
R: TRect;
BarL, BarW, MeterX: Integer;
begin
@@ -1005,16 +1010,25 @@ begin
RW := W - PAD*2;
CurY := PAD;
{ --- Шапка: главный [A] BW ... SPLIT [TX]; слайс [B] BW ... [×] [TX] --- }
{ --- Шапка: главный [A] BW ... SPLIT [TX]; слайс [B] BW ... [RXM] [TX] [×] --- }
DrawBadge(C, Rect(X, CurY, X+16, CurY+HDR_BADGE_H), FSlice, SliceColor(FSlice), CLR_BADGE_TXT);
RegisterHit(Rect(X, CurY, X+16, CurY+HDR_BADGE_H), HIT_SLICE_SEL); // бейдж = выбор слайса
// Кнопка закрытия × — только у неглавных слайсов (главный приёмник не закрыть).
// Место освободилось от SPLIT (его у слайсов нет) — ставим вплотную к TX.
// Она крайняя справа, TX и RXM выстраиваются левее неё.
TxRight := W - PAD;
if not FIsMain then
begin
R := Rect(W - PAD - 26 - 6 - 14, CurY, W - PAD - 26 - 6, CurY + HDR_BADGE_H);
R := Rect(W - PAD - 14, CurY, W - PAD, CurY + HDR_BADGE_H);
DrawBadge(C, R, #$C3#$97, CLR_BTN_NORM, CLR_DIM); // UTF-8 '×'
RegisterHit(R, HIT_CLOSE);
TxRight := W - PAD - 14 - 6;
// RXM — приём слайса на СВОЕЙ передаче. Подсвечен (умолч.) = глушим, т.е.
// себя не слышим; погашен = self-monitor (свой downlink через транспондер).
// У главного приёмника то же самое делает кнопка RX MUTE на левой панели.
R := Rect(TxRight - 26 - 4 - 32, CurY, TxRight - 26 - 4, CurY + HDR_BADGE_H);
if FRxMuteTx then DrawBadge(C, R, 'RXM', CLR_ACCENT, CLR_BADGE_TXT)
else DrawBadge(C, R, 'RXM', CLR_BTN_NORM, CLR_DIM);
RegisterHit(R, HIT_RXMUTE);
end;
// ширина фильтра
if FFilterBW >= 1000 then BWStr := Format('%.1fk', [FFilterBW/1000.0])
@@ -1029,7 +1043,7 @@ begin
begin
DMRStr := FDMRText;
DMRX := X + 22 + C.TextWidth(BWStr) + 8;
DMRRight := W - PAD - 26 - 6 - 14 - 5; // до кнопок × и TX
DMRRight := TxRight - 26 - 4 - 32 - 5; // до бейджей RXM, TX и ×
while (Length(DMRStr) > 1) and
(C.TextWidth(DMRStr) > DMRRight - DMRX) do
SetLength(DMRStr, Length(DMRStr) - 1);
@@ -1038,7 +1052,7 @@ begin
C.TextOut(DMRX, CurY + 1, DMRStr);
end;
// TX badge: a DMR slice is visibly disabled and has no hit target.
R := Rect(W - PAD - 26, CurY, W - PAD, CurY + HDR_BADGE_H);
R := Rect(TxRight - 26, CurY, TxRight, CurY + HDR_BADGE_H);
if not TxSelectionAllowed then
DrawBadge(C, R, 'TX', CLR_CARD_BG, CLR_DIM)
else if FTx then DrawBadge(C, R, 'TX', CLR_TX_ON, clWhite)
@@ -1205,6 +1219,8 @@ begin
FOnTxSelect(Self);
Exit;
end;
HIT_RXMUTE: begin FRxMuteTx := not FRxMuteTx; RequestInvalidate;
if Assigned(FOnRxMuteTx) then FOnRxMuteTx(Self); Exit; end;
HIT_SLICE_SEL: begin if Assigned(FOnSliceSelect) then FOnSliceSelect(Self); Exit; end;
HIT_CLOSE: begin if Assigned(FOnClose) then FOnClose(Self); Exit; end;
HIT_AUD_UP: begin if FAudScroll > 0 then Dec(FAudScroll);
@@ -1487,6 +1503,13 @@ begin
RequestInvalidate;
end;
procedure TVfoOverlay.SetRxMuteTxState(AOn: Boolean);
begin
if FRxMuteTx = AOn then Exit;
FRxMuteTx := AOn;
RequestInvalidate;
end;
procedure TVfoOverlay.SetSliceInfo(ASliceId: Integer; ALetter: Char; AIsMain: Boolean);
begin
if (FSliceId = ASliceId) and (FSlice = ALetter) and (FIsMain = AIsMain) then Exit;