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
+25
View File
@@ -579,6 +579,7 @@ type
procedure OnVfoOverlayAudioDevice(DevIndex: Integer; const DevName: string); procedure OnVfoOverlayAudioDevice(DevIndex: Integer; const DevName: string);
procedure OnVfoOverlaySplit(Sender: TObject); procedure OnVfoOverlaySplit(Sender: TObject);
procedure OnVfoOverlayTxSelect(Sender: TObject); procedure OnVfoOverlayTxSelect(Sender: TObject);
procedure OnVfoOverlayRxMuteTx(Sender: TObject); // RXM — приём слайса на TX
procedure RefreshTxIndicators; // TX-бейджи всех флагов procedure RefreshTxIndicators; // TX-бейджи всех флагов
procedure OnVfoOverlaySliceSelect(Sender: TObject); // выбрать слайс активным procedure OnVfoOverlaySliceSelect(Sender: TObject); // выбрать слайс активным
procedure OnVfoOverlayClose(Sender: TObject); // × — удалить слайс procedure OnVfoOverlayClose(Sender: TObject); // × — удалить слайс
@@ -5457,6 +5458,17 @@ begin
FController.SetTxSlice(CurSliceId); // → Changed(rfTransmitting) → RefreshTxIndicators FController.SetTxSlice(CurSliceId); // → Changed(rfTransmitting) → RefreshTxIndicators
end; end;
procedure TMainForm.OnVfoOverlayRxMuteTx(Sender: TObject);
// Бейдж RXM на флаге слайса: слышим ли этот слайс, пока сами передаём.
// У главного приёмника то же делает кнопка RX MUTE на левой панели.
var S: TCtrlSlice;
begin
if CurSliceId = 0 then Exit; // у флага A бейджа RXM нет (страховка)
if not FController.GetSlice(CurSliceId, S) then Exit;
FController.SetSliceRxMuteOnTx(CurSliceId, not S.RxMuteOnTx);
PushFlagStateAllPans(CurSliceId);
end;
procedure TMainForm.RefreshTxIndicators; procedure TMainForm.RefreshTxIndicators;
// Обновить TX-бейджи (TxSel/Tx) на всех флагах всех панов после смены // Обновить TX-бейджи (TxSel/Tx) на всех флагах всех панов после смены
// TX-слайса / состояния передачи. // TX-слайса / состояния передачи.
@@ -5539,6 +5551,7 @@ begin
O.OnAudioDevice := OnVfoOverlayAudioDevice; O.OnAudioDevice := OnVfoOverlayAudioDevice;
O.OnSplit := OnVfoOverlaySplit; O.OnSplit := OnVfoOverlaySplit;
O.OnTxSelect := OnVfoOverlayTxSelect; O.OnTxSelect := OnVfoOverlayTxSelect;
O.OnRxMuteTx := OnVfoOverlayRxMuteTx;
O.OnSliceSelect := OnVfoOverlaySliceSelect; O.OnSliceSelect := OnVfoOverlaySliceSelect;
O.OnClose := OnVfoOverlayClose; O.OnClose := OnVfoOverlayClose;
O.OnInvalidate := OnVfoOverlayInvalidate; O.OnInvalidate := OnVfoOverlayInvalidate;
@@ -6477,6 +6490,7 @@ begin
Cfg.Pans[i].Slices[n].AGC := Ord(FController.FSlices[s].AGC); Cfg.Pans[i].Slices[n].AGC := Ord(FController.FSlices[s].AGC);
Cfg.Pans[i].Slices[n].Volume := FController.FSlices[s].Volume; Cfg.Pans[i].Slices[n].Volume := FController.FSlices[s].Volume;
Cfg.Pans[i].Slices[n].Muted := FController.FSlices[s].Muted; Cfg.Pans[i].Slices[n].Muted := FController.FSlices[s].Muted;
Cfg.Pans[i].Slices[n].RxMuteOnTx := FController.FSlices[s].RxMuteOnTx;
Cfg.Pans[i].Slices[n].FMSQOn := FController.FSlices[s].FMSQOn; Cfg.Pans[i].Slices[n].FMSQOn := FController.FSlices[s].FMSQOn;
Cfg.Pans[i].Slices[n].FMSQLevel := FController.FSlices[s].FMSQLevel; Cfg.Pans[i].Slices[n].FMSQLevel := FController.FSlices[s].FMSQLevel;
Cfg.Pans[i].Slices[n].DevName := FController.FSlices[s].DevName; Cfg.Pans[i].Slices[n].DevName := FController.FSlices[s].DevName;
@@ -6501,6 +6515,7 @@ begin
EnsureRange(SC.Volume, 0.0, 1.0), DevIdx, SC.DevName, PanId); EnsureRange(SC.Volume, 0.0, 1.0), DevIdx, SC.DevName, PanId);
if Id < 0 then Exit; if Id < 0 then Exit;
if SC.Muted then FController.SetSliceMute(Id, True); if SC.Muted then FController.SetSliceMute(Id, True);
FController.SetSliceRxMuteOnTx(Id, SC.RxMuteOnTx);
if SC.FMSQOn then FController.SetSliceFMSquelch(Id, True, SC.FMSQLevel); if SC.FMSQOn then FController.SetSliceFMSquelch(Id, True, SC.FMSQLevel);
P.CreateSliceFlag(Id); P.CreateSliceFlag(Id);
FActiveSliceId := Id; FActiveSliceId := Id;
@@ -7604,6 +7619,16 @@ begin
SliceId := FPans[i].SliceUnderCursor; SliceId := FPans[i].SliceUnderCursor;
if SliceId <> 0 then begin SlicePan := FPans[i]; Break; end; if SliceId <> 0 then begin SlicePan := FPans[i]; Break; end;
end; end;
// Иначе колесо над спектром/водопадом ДОП. пана крутит его главный слайс
// (первый созданный на этом приёмнике), а не общий VFO. Пан 0 — общий VFO.
if SliceId = 0 then
for i := 1 to MAX_PANS - 1 do
if (FPans[i] <> nil) and FPans[i].CursorOverDisplay then
begin
SliceId := FController.PanPrimarySliceId(i);
if SliceId <> 0 then SlicePan := FPans[i];
Break;
end;
if (SliceId <> 0) and FController.GetSlice(SliceId, Sl) then if (SliceId <> 0) and FController.GetSlice(SliceId, Sl) then
begin begin
if (Sl.Mode = MODE_DMR) and if (Sl.Mode = MODE_DMR) and
+18
View File
@@ -275,6 +275,7 @@ type
function SliceAtPixel(X: Integer): Integer; function SliceAtPixel(X: Integer): Integer;
// SliceId флага под курсором мыши (прямоугольник оверлея; 0 = нет). // SliceId флага под курсором мыши (прямоугольник оверлея; 0 = нет).
function SliceUnderCursor: Integer; function SliceUnderCursor: Integer;
function CursorOverDisplay: Boolean; // курсор над спектром/водопадом пана
// Единый packing всех флагов (A+B+) в один ряд, без наездов. // Единый packing всех флагов (A+B+) в один ряд, без наездов.
procedure LayoutFlags; procedure LayoutFlags;
// Флаг A виден: панель хозяина скрыта (MainFlagPinned) ИЛИ есть слайсы B+. // Флаг A виден: панель хозяина скрыта (MainFlagPinned) ИЛИ есть слайсы B+.
@@ -1142,6 +1143,22 @@ begin
end; end;
end; end;
// Курсор над спектром/водопадом этого пана? (колесо крутит «его» частоту)
function TPanafallPanel.CursorOverDisplay: Boolean;
function Over(C: TControl): Boolean;
var P: TPoint;
begin
Result := False;
if (C = nil) or (not C.Visible) then Exit;
P := C.ScreenToClient(Mouse.CursorPos);
Result := (P.X >= 0) and (P.Y >= 0) and (P.X < C.Width) and (P.Y < C.Height);
end;
begin
Result := Over(FPbSpectrum) or Over(FPbWaterfall);
end;
// Флаг слайса под курсором мыши (в клиентских координатах спектра)? // Флаг слайса под курсором мыши (в клиентских координатах спектра)?
function TPanafallPanel.SliceUnderCursor: Integer; function TPanafallPanel.SliceUnderCursor: Integer;
var P: TPoint; i: Integer; O: TVfoOverlay; var P: TPoint; i: Integer; O: TVfoOverlay;
@@ -1321,6 +1338,7 @@ begin
O.SetExtState(Round(S.Volume * 100), S.Muted, False, O.SetExtState(Round(S.Volume * 100), S.Muted, False,
FController.FTransmitting and (FController.FTxSliceId = SliceId), FController.FTransmitting and (FController.FTxSliceId = SliceId),
FController.FTxSliceId = SliceId); FController.FTxSliceId = SliceId);
O.SetRxMuteTxState(S.RxMuteOnTx); // бейдж RXM
// Список аудио-устройств (общий PA-список) + текущее устройство слайса. // Список аудио-устройств (общий PA-список) + текущее устройство слайса.
if Assigned(FController.FAudioOut) then if Assigned(FController.FAudioOut) then
begin begin
+35
View File
@@ -158,6 +158,7 @@ type
AGC: TWDSPAGCMode; AGC: TWDSPAGCMode;
Volume: Double; // 0..1 Volume: Double; // 0..1
Muted: Boolean; Muted: Boolean;
RxMuteOnTx: Boolean; // True (умолч.) = динамик слайса молчит на передаче
FMSQOn: Boolean; // FM-squelch включён (слайс в режиме FM) FMSQOn: Boolean; // FM-squelch включён (слайс в режиме FM)
FMSQLevel: Integer; // порог squelch 0..100 FMSQLevel: Integer; // порог squelch 0..100
DeviceIndex: Integer; // PortAudio-индекс (-1 = default) DeviceIndex: Integer; // PortAudio-индекс (-1 = default)
@@ -187,6 +188,7 @@ type
const Left, Right: array of Single; Count: Integer); const Left, Right: array of Single; Count: Integer);
procedure OnSliceDMRAudioReady(SliceId: Integer; procedure OnSliceDMRAudioReady(SliceId: Integer;
const PCM: array of Single; Count: Integer); const PCM: array of Single; Count: Integer);
function SliceMutedByTx(Idx: Integer): Boolean; // слайс молчит на передаче?
procedure RefreshSliceShifts; // пересчёт ShiftHz слайсов пана 0 при смене центра procedure RefreshSliceShifts; // пересчёт ShiftHz слайсов пана 0 при смене центра
// --- Панадаптеры на аппаратных DDC (этап 3.1/3.2) --- // --- Панадаптеры на аппаратных DDC (этап 3.1/3.2) ---
procedure ResetPanDDCMaps; // все пан-слоты свободны, DDC→Pan пуст procedure ResetPanDDCMaps; // все пан-слоты свободны, DDC→Pan пуст
@@ -592,6 +594,8 @@ type
procedure SetSliceAGCMode(Id: Integer; AGC: TWDSPAGCMode); procedure SetSliceAGCMode(Id: Integer; AGC: TWDSPAGCMode);
procedure SetSliceVolume(Id: Integer; Vol: Double); // 0..1 procedure SetSliceVolume(Id: Integer; Vol: Double); // 0..1
procedure SetSliceMute(Id: Integer; Mute: Boolean); procedure SetSliceMute(Id: Integer; Mute: Boolean);
procedure SetSliceRxMuteOnTx(Id: Integer; On_: Boolean); // self-monitor слайса на TX
function PanPrimarySliceId(PanId: Integer): Integer; // первый созданный слайс пана
procedure SetSliceDSP(Id, NRMode, NBMode: Integer; SNBOn, ANFOn: Boolean); procedure SetSliceDSP(Id, NRMode, NBMode: Integer; SNBOn, ANFOn: Boolean);
procedure SetSliceFMSquelch(Id: Integer; On_: Boolean; Level: Integer); procedure SetSliceFMSquelch(Id: Integer; On_: Boolean; Level: Integer);
function SliceSMeter(Id: Integer): Double; // dBm, из DSP-потока function SliceSMeter(Id: Integer): Double; // dBm, из DSP-потока
@@ -1334,10 +1338,19 @@ begin
idx := FindSliceIndex(SliceId); idx := FindSliceIndex(SliceId);
if idx < 0 then Exit; if idx < 0 then Exit;
if FSlices[idx].Mode = MODE_DMR then Exit; if FSlices[idx].Mode = MODE_DMR then Exit;
if SliceMutedByTx(idx) then Exit;
if FSlices[idx].Audio <> nil then if FSlices[idx].Audio <> nil then
FSlices[idx].Audio.Write(Left, Right, Count); FSlices[idx].Audio.Write(Left, Right, Count);
end; end;
// Слайс глушится на передаче? В full-duplex (Pluto/QO-100) RX-тракт живёт и во
// время TX, поэтому иначе оператор слышит сам себя на КАЖДОМ слайсе. У главного
// приёмника за это отвечает FRxMuteOnTx (кнопка RX MUTE), у слайса — свой флаг.
function TRadioController.SliceMutedByTx(Idx: Integer): Boolean;
begin
Result := FTransmitting and FSlices[Idx].RxMuteOnTx;
end;
// DSP thread: flat, volume-independent discriminator of the corresponding // DSP thread: flat, volume-independent discriminator of the corresponding
// WDSP slice channel. FeedAudio only copies it into that decoder's ring. // WDSP slice channel. FeedAudio only copies it into that decoder's ring.
procedure TRadioController.OnSliceDemodAudioReady(SliceId: Integer; procedure TRadioController.OnSliceDemodAudioReady(SliceId: Integer;
@@ -1362,6 +1375,7 @@ begin
idx := FindSliceIndex(SliceId); idx := FindSliceIndex(SliceId);
if (idx < 0) or (FSlices[idx].Mode <> MODE_DMR) or if (idx < 0) or (FSlices[idx].Mode <> MODE_DMR) or
FSlices[idx].Muted or not Assigned(FSlices[idx].Audio) then Exit; FSlices[idx].Muted or not Assigned(FSlices[idx].Audio) then Exit;
if SliceMutedByTx(idx) then Exit;
N := Min(Count, Length(PCM)); N := Min(Count, Length(PCM));
SetLength(Left, N * 6); SetLength(Left, N * 6);
SetLength(Right, N * 6); SetLength(Right, N * 6);
@@ -1783,6 +1797,7 @@ begin
FSlices[slot].AGC := AGC; FSlices[slot].AGC := AGC;
FSlices[slot].Volume := Vol; FSlices[slot].Volume := Vol;
FSlices[slot].Muted := False; FSlices[slot].Muted := False;
FSlices[slot].RxMuteOnTx := True; // себя на передаче по умолчанию не слышим
FSlices[slot].FMSQOn := False; // squelch по умолчанию выключен FSlices[slot].FMSQOn := False; // squelch по умолчанию выключен
FSlices[slot].FMSQLevel := 40; FSlices[slot].FMSQLevel := 40;
FSlices[slot].DeviceIndex := DevIndex; FSlices[slot].DeviceIndex := DevIndex;
@@ -1878,6 +1893,26 @@ begin
if Assigned(FDSPEngine) then FDSPEngine.SetSliceMute(Id, Mute); if Assigned(FDSPEngine) then FDSPEngine.SetSliceMute(Id, Mute);
end; end;
procedure TRadioController.SetSliceRxMuteOnTx(Id: Integer; On_: Boolean);
var idx: Integer;
begin
idx := FindSliceIndex(Id);
if idx < 0 then Exit;
FSlices[idx].RxMuteOnTx := On_;
end;
// «Главный» слайс пана = созданный на нём первым. Id монотонно растёт
// (FNextSliceId), поэтому первый = слайс с минимальным Id. 0 = слайсов нет.
function TRadioController.PanPrimarySliceId(PanId: Integer): Integer;
var i: Integer;
begin
Result := 0;
for i := 0 to MAX_SLICES - 1 do
if FSlices[i].Active and (FSlices[i].PanId = PanId) and
((Result = 0) or (FSlices[i].Id < Result)) then
Result := FSlices[i].Id;
end;
procedure TRadioController.SetSliceDSP(Id, NRMode, NBMode: Integer; procedure TRadioController.SetSliceDSP(Id, NRMode, NBMode: Integer;
SNBOn, ANFOn: Boolean); SNBOn, ANFOn: Boolean);
var idx: Integer; var idx: Integer;
+3
View File
@@ -104,6 +104,7 @@ type
AGC: Integer; // Ord(TWDSPAGCMode) AGC: Integer; // Ord(TWDSPAGCMode)
Volume: Double; // 0..1 Volume: Double; // 0..1
Muted: Boolean; Muted: Boolean;
RxMuteOnTx: Boolean; // True = слайс молчит на передаче (self-monitor выкл)
FMSQOn: Boolean; FMSQOn: Boolean;
FMSQLevel: Integer; FMSQLevel: Integer;
DevName: string; // имя PortAudio-устройства ('' = default) DevName: string; // имя PortAudio-устройства ('' = default)
@@ -1783,6 +1784,7 @@ begin
SliceObj.Add('agc', P.Pans[i].Slices[j].AGC); SliceObj.Add('agc', P.Pans[i].Slices[j].AGC);
SliceObj.Add('volume', P.Pans[i].Slices[j].Volume); SliceObj.Add('volume', P.Pans[i].Slices[j].Volume);
SliceObj.Add('muted', P.Pans[i].Slices[j].Muted); SliceObj.Add('muted', P.Pans[i].Slices[j].Muted);
SliceObj.Add('rx_mute_on_tx', P.Pans[i].Slices[j].RxMuteOnTx);
SliceObj.Add('fmsq_on', P.Pans[i].Slices[j].FMSQOn); SliceObj.Add('fmsq_on', P.Pans[i].Slices[j].FMSQOn);
SliceObj.Add('fmsq_level', P.Pans[i].Slices[j].FMSQLevel); SliceObj.Add('fmsq_level', P.Pans[i].Slices[j].FMSQLevel);
SliceObj.Add('dev_name', P.Pans[i].Slices[j].DevName); SliceObj.Add('dev_name', P.Pans[i].Slices[j].DevName);
@@ -1860,6 +1862,7 @@ begin
P.Pans[id].Slices[n].AGC := JI(SliceObj, 'agc', 1); P.Pans[id].Slices[n].AGC := JI(SliceObj, 'agc', 1);
P.Pans[id].Slices[n].Volume := EnsureRange(JD(SliceObj, 'volume', 0.7), 0.0, 1.0); P.Pans[id].Slices[n].Volume := EnsureRange(JD(SliceObj, 'volume', 0.7), 0.0, 1.0);
P.Pans[id].Slices[n].Muted := JB(SliceObj, 'muted', False); P.Pans[id].Slices[n].Muted := JB(SliceObj, 'muted', False);
P.Pans[id].Slices[n].RxMuteOnTx := JB(SliceObj, 'rx_mute_on_tx', True);
P.Pans[id].Slices[n].FMSQOn := JB(SliceObj, 'fmsq_on', False); P.Pans[id].Slices[n].FMSQOn := JB(SliceObj, 'fmsq_on', False);
P.Pans[id].Slices[n].FMSQLevel := JI(SliceObj, 'fmsq_level', 30); P.Pans[id].Slices[n].FMSQLevel := JI(SliceObj, 'fmsq_level', 30);
P.Pans[id].Slices[n].DevName := JS(SliceObj, 'dev_name', ''); P.Pans[id].Slices[n].DevName := JS(SliceObj, 'dev_name', '');
+31 -8
View File
@@ -77,6 +77,7 @@ type
FSplit: Boolean; FSplit: Boolean;
FTx: Boolean; // передача идёт (красный) FTx: Boolean; // передача идёт (красный)
FTxSel: Boolean; // этот слайс выбран для передачи (янтарный) FTxSel: Boolean; // этот слайс выбран для передачи (янтарный)
FRxMuteTx: Boolean; // RXM: приём слайса глушится на передаче (умолч. True)
FSlice: Char; // буква слайса ('A') FSlice: Char; // буква слайса ('A')
FSMeterThreshold: Double; // мин. изменение S-метра (дБ) для перерисовки FSMeterThreshold: Double; // мин. изменение S-метра (дБ) для перерисовки
FDMRText: string; // стабильный краткий статус рядом с полосой FDMRText: string; // стабильный краткий статус рядом с полосой
@@ -105,6 +106,7 @@ type
FOnAudioDevice: TAudioDeviceEvent; FOnAudioDevice: TAudioDeviceEvent;
FOnSplit: TNotifyEvent; FOnSplit: TNotifyEvent;
FOnTxSelect: TNotifyEvent; FOnTxSelect: TNotifyEvent;
FOnRxMuteTx: TNotifyEvent;
FOnInvalidate: TNotifyEvent; FOnInvalidate: TNotifyEvent;
FTheme: TAppTheme; // тема приложения (цвета кнопок и т.п.) FTheme: TAppTheme; // тема приложения (цвета кнопок и т.п.)
@@ -183,6 +185,7 @@ type
AAGCMode: Integer); AAGCMode: Integer);
procedure SetSquelchState(AOn: Boolean; ALevel: Integer); procedure SetSquelchState(AOn: Boolean; ALevel: Integer);
procedure SetExtState(AVolume: Integer; AMuted, ASplit, ATx, ATxSel: Boolean); 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; procedure SetAudioDevices(Names: TStrings; const Indices: array of Integer;
const CurName: string); const CurName: string);
procedure UpdateSMeter(DB: Double); procedure UpdateSMeter(DB: Double);
@@ -218,6 +221,7 @@ type
property OnAudioDevice: TAudioDeviceEvent read FOnAudioDevice write FOnAudioDevice; property OnAudioDevice: TAudioDeviceEvent read FOnAudioDevice write FOnAudioDevice;
property OnSplit: TNotifyEvent read FOnSplit write FOnSplit; property OnSplit: TNotifyEvent read FOnSplit write FOnSplit;
property OnTxSelect: TNotifyEvent read FOnTxSelect write FOnTxSelect; property OnTxSelect: TNotifyEvent read FOnTxSelect write FOnTxSelect;
property OnRxMuteTx: TNotifyEvent read FOnRxMuteTx write FOnRxMuteTx;
property OnSliceSelect: TNotifyEvent read FOnSliceSelect write FOnSliceSelect; property OnSliceSelect: TNotifyEvent read FOnSliceSelect write FOnSliceSelect;
property OnClose: TNotifyEvent read FOnClose write FOnClose; property OnClose: TNotifyEvent read FOnClose write FOnClose;
property OnInvalidate: TNotifyEvent read FOnInvalidate write FOnInvalidate; property OnInvalidate: TNotifyEvent read FOnInvalidate write FOnInvalidate;
@@ -258,9 +262,10 @@ const
HIT_CLOSE = -30; // × закрытия слайса (неглавные) HIT_CLOSE = -30; // × закрытия слайса (неглавные)
HIT_SQL = -31; // тумблер FM-squelch (в DSP-панели) HIT_SQL = -31; // тумблер FM-squelch (в DSP-панели)
HIT_SQL_SLIDER = -32; // трек порога FM-squelch HIT_SQL_SLIDER = -32; // трек порога FM-squelch
HIT_RXMUTE = -33; // бейдж RXM: приём слайса на своей передаче
HIT_AGC_PICK_BASE = 100000; HIT_AGC_PICK_BASE = 100000;
HIT_DEV_BASE = 200000; HIT_DEV_BASE = 200000;
// Свои диапазоны, чтобы не пересекаться с тумблерами (-9..-32) и с базами выше. // Свои диапазоны, чтобы не пересекаться с тумблерами (-9..-33) и с базами выше.
HIT_MODE_BASE = -1000; // -1000 .. -1000-(OVL_MODE_COUNT-1) HIT_MODE_BASE = -1000; // -1000 .. -1000-(OVL_MODE_COUNT-1)
HIT_FILT_BASE = 1000; // 1000 + индекс кнопки в ряду фильтров HIT_FILT_BASE = 1000; // 1000 + индекс кнопки в ряду фильтров
@@ -455,7 +460,7 @@ begin
FNRMode := 0; FNBMode := 0; FSNBOn := False; FANFOn := False; FAGCMode := 0; FNRMode := 0; FNBMode := 0; FSNBOn := False; FANFOn := False; FAGCMode := 0;
FSQLOn := False; FSQLLevel := 40; FSqlDrag := False; FSQLOn := False; FSQLLevel := 40; FSqlDrag := False;
FVolume := 70; FMuted := False; FSplit := False; FTx := False; FVolume := 70; FMuted := False; FSplit := False; FTx := False;
FTxSel := True; FSlice := 'A'; FTxSel := True; FSlice := 'A'; FRxMuteTx := True;
FDMRText := ''; FDMRSynced := False; FDMRText := ''; FDMRSynced := False;
FDMRCandidateText := ''; FDMRCandidateSynced := False; FDMRCandidateText := ''; FDMRCandidateSynced := False;
FDMRCandidateSince := 0; FDMRCandidateSince := 0;
@@ -987,7 +992,7 @@ end;
procedure TVfoOverlay.DrawSelf(C: TCanvas; W, H: Integer); procedure TVfoOverlay.DrawSelf(C: TCanvas; W, H: Integer);
var var
FreqStr, SStr, BWStr, DMRStr: string; FreqStr, SStr, BWStr, DMRStr: string;
CurY, X, RW, i, DMRX, DMRRight: Integer; CurY, X, RW, i, DMRX, DMRRight, TxRight: Integer;
R: TRect; R: TRect;
BarL, BarW, MeterX: Integer; BarL, BarW, MeterX: Integer;
begin begin
@@ -1005,16 +1010,25 @@ begin
RW := W - PAD*2; RW := W - PAD*2;
CurY := PAD; 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); 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); // бейдж = выбор слайса RegisterHit(Rect(X, CurY, X+16, CurY+HDR_BADGE_H), HIT_SLICE_SEL); // бейдж = выбор слайса
// Кнопка закрытия × — только у неглавных слайсов (главный приёмник не закрыть). // Кнопка закрытия × — только у неглавных слайсов (главный приёмник не закрыть).
// Место освободилось от SPLIT (его у слайсов нет) — ставим вплотную к TX. // Она крайняя справа, TX и RXM выстраиваются левее неё.
TxRight := W - PAD;
if not FIsMain then if not FIsMain then
begin 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 '×' DrawBadge(C, R, #$C3#$97, CLR_BTN_NORM, CLR_DIM); // UTF-8 '×'
RegisterHit(R, HIT_CLOSE); 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; end;
// ширина фильтра // ширина фильтра
if FFilterBW >= 1000 then BWStr := Format('%.1fk', [FFilterBW/1000.0]) if FFilterBW >= 1000 then BWStr := Format('%.1fk', [FFilterBW/1000.0])
@@ -1029,7 +1043,7 @@ begin
begin begin
DMRStr := FDMRText; DMRStr := FDMRText;
DMRX := X + 22 + C.TextWidth(BWStr) + 8; 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 while (Length(DMRStr) > 1) and
(C.TextWidth(DMRStr) > DMRRight - DMRX) do (C.TextWidth(DMRStr) > DMRRight - DMRX) do
SetLength(DMRStr, Length(DMRStr) - 1); SetLength(DMRStr, Length(DMRStr) - 1);
@@ -1038,7 +1052,7 @@ begin
C.TextOut(DMRX, CurY + 1, DMRStr); C.TextOut(DMRX, CurY + 1, DMRStr);
end; end;
// TX badge: a DMR slice is visibly disabled and has no hit target. // 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 if not TxSelectionAllowed then
DrawBadge(C, R, 'TX', CLR_CARD_BG, CLR_DIM) DrawBadge(C, R, 'TX', CLR_CARD_BG, CLR_DIM)
else if FTx then DrawBadge(C, R, 'TX', CLR_TX_ON, clWhite) else if FTx then DrawBadge(C, R, 'TX', CLR_TX_ON, clWhite)
@@ -1205,6 +1219,8 @@ begin
FOnTxSelect(Self); FOnTxSelect(Self);
Exit; Exit;
end; 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_SLICE_SEL: begin if Assigned(FOnSliceSelect) then FOnSliceSelect(Self); Exit; end;
HIT_CLOSE: begin if Assigned(FOnClose) then FOnClose(Self); Exit; end; HIT_CLOSE: begin if Assigned(FOnClose) then FOnClose(Self); Exit; end;
HIT_AUD_UP: begin if FAudScroll > 0 then Dec(FAudScroll); HIT_AUD_UP: begin if FAudScroll > 0 then Dec(FAudScroll);
@@ -1487,6 +1503,13 @@ begin
RequestInvalidate; RequestInvalidate;
end; 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); procedure TVfoOverlay.SetSliceInfo(ASliceId: Integer; ALetter: Char; AIsMain: Boolean);
begin begin
if (FSliceId = ASliceId) and (FSlice = ALetter) and (FIsMain = AIsMain) then Exit; if (FSliceId = ASliceId) and (FSlice = ALetter) and (FIsMain = AIsMain) then Exit;