feat(tx): TX-профиль на слайс — пилюля в шапке флага

Передатчик один, профиль до сих пор был один на устройство: активный профиль
звучал независимо от того, какой слайс взял передачу. С панадаптером на FT8 и
вторым на FM это значит, что компрессия и EQ голосового профиля уходили в
эфир на FT8 (FMRAW прикрыт сам — ApplyTXModeSettings глушит там всю обработку,
а DIGU/DIGL нет). У Flex это решается дисциплиной и внешним API; делаем в радио.

Привязка «источник передачи → профиль» (-1 = не переключать, звучит активный):
TCtrlSlice.TXProfile для B..G, TTXProfileList.MainBind для главного (слайс A).
Персист: ключ tx_profile в слайсе пана и main_bind в секции tx_profiles.
SetTxSlice зовёт ApplyBoundTXProfile ДО пуша режима и цепи, иначе
ApplyTXSettingsToDSP лёг бы на старый режим. Привязали источник, который
передаёт прямо сейчас, — применяем сразу.

Привязки — индексы, поэтому RemapTXProfileBindings: удаление профиля из
середины снимает привязки на него и сдвигает те, что правее; Factory-сброс
снимает все (список заменён целиком).

UI: пилюля с именем профиля в первой строке флага сразу за шириной фильтра
(ЛКМ — fly-out список, первая строка «As active»). Правая граница считается
до SPLIT у главного и до RXM у слайса; не рисуем при пустом списке, в DMR
(передачи нет) и в FMRAW (профиль там ни на что не влияет). Прокрутка списка
общая с AUD — блок стрелок вынесен в DrawScrollArrows.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-04 17:06:08 +03:00
co-authored by Claude Opus 5
parent 8563c4e4cc
commit 013d0406ac
5 changed files with 397 additions and 41 deletions
+18
View File
@@ -533,6 +533,7 @@ type
procedure OnTXProfileRenameFromSettings(Idx: Integer; const AName: string); procedure OnTXProfileRenameFromSettings(Idx: Integer; const AName: string);
procedure OnTXProfileDeleteFromSettings(Idx: Integer); procedure OnTXProfileDeleteFromSettings(Idx: Integer);
procedure OnTXProfileFactoryFromSettings(Sender: TObject); procedure OnTXProfileFactoryFromSettings(Sender: TObject);
procedure OnVfoOverlayTXProfile(Idx: Integer);
procedure OnChannelStoreChanged(Sender: TObject); procedure OnChannelStoreChanged(Sender: TObject);
procedure OnChannelActiveChanged(Idx: Integer; const Name: string; Active: Boolean); procedure OnChannelActiveChanged(Idx: Integer; const Name: string; Active: Boolean);
procedure PbSpectrumMouseDown(Sender: TObject; Button: TMouseButton; procedure PbSpectrumMouseDown(Sender: TObject; Button: TMouseButton;
@@ -5708,6 +5709,10 @@ begin
FController.FNetwork.Device.BoardType, FPendingBoardType)); FController.FNetwork.Device.BoardType, FPendingBoardType));
PushTXProfilesToSettings; PushTXProfilesToSettings;
end; end;
// Пилюли профиля на флагах: список общий, привязки могли съехать после
// удаления/сброса — обновляем на всех панах.
for i := 0 to MAX_PANS - 1 do
if FPans[i] <> nil then FPans[i].PushAllFlagTXProfiles;
end; end;
procedure TMainForm.PushTXProfilesToSettings; procedure TMainForm.PushTXProfilesToSettings;
@@ -6098,6 +6103,16 @@ begin
PushFlagStateAllPans(CurSliceId); PushFlagStateAllPans(CurSliceId);
end; end;
procedure TMainForm.OnVfoOverlayTXProfile(Idx: Integer);
// Пилюля TX-профиля во флаге: «этот источник передачи звучит вот так».
// Idx = -1 — привязки нет, передаёт активный профиль.
begin
// Привязка главного живёт в секции профилей (контроллер её уже сохранил),
// привязка слайса уедет в конфиг с общим персистом панов — как RxMuteOnTx.
FController.SetSliceTXProfile(CurSliceId, Idx);
PushFlagStateAllPans(CurSliceId);
end;
procedure TMainForm.RefreshTxIndicators; procedure TMainForm.RefreshTxIndicators;
// Обновить TX-бейджи (TxSel/Tx) на всех флагах всех панов после смены // Обновить TX-бейджи (TxSel/Tx) на всех флагах всех панов после смены
// TX-слайса / состояния передачи. // TX-слайса / состояния передачи.
@@ -6198,6 +6213,7 @@ begin
O.OnSplit := OnVfoOverlaySplit; O.OnSplit := OnVfoOverlaySplit;
O.OnTxSelect := OnVfoOverlayTxSelect; O.OnTxSelect := OnVfoOverlayTxSelect;
O.OnRxMuteTx := OnVfoOverlayRxMuteTx; O.OnRxMuteTx := OnVfoOverlayRxMuteTx;
O.OnTXProfile := OnVfoOverlayTXProfile;
O.OnSliceSelect := OnVfoOverlaySliceSelect; O.OnSliceSelect := OnVfoOverlaySliceSelect;
O.OnClose := OnVfoOverlayClose; O.OnClose := OnVfoOverlayClose;
O.OnInvalidate := OnVfoOverlayInvalidate; O.OnInvalidate := OnVfoOverlayInvalidate;
@@ -7160,6 +7176,7 @@ begin
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;
Cfg.Pans[i].Slices[n].InDevName := FController.FSlices[s].InDevName; Cfg.Pans[i].Slices[n].InDevName := FController.FSlices[s].InDevName;
Cfg.Pans[i].Slices[n].TXProfile := FController.FSlices[s].TXProfile;
Inc(n); Inc(n);
end; end;
end; end;
@@ -7186,6 +7203,7 @@ begin
FController.FAudioIn.FindDeviceByName(SC.InDevName), SC.InDevName); FController.FAudioIn.FindDeviceByName(SC.InDevName), SC.InDevName);
if SC.Muted then FController.SetSliceMute(Id, True); if SC.Muted then FController.SetSliceMute(Id, True);
FController.SetSliceRxMuteOnTx(Id, SC.RxMuteOnTx); FController.SetSliceRxMuteOnTx(Id, SC.RxMuteOnTx);
FController.SetSliceTXProfile(Id, SC.TXProfile); // «этот слайс звучит вот так»
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;
+32
View File
@@ -286,6 +286,8 @@ type
procedure PushAllSliceFlagStates; // все флаги панели (после смены rate и т.п.) procedure PushAllSliceFlagStates; // все флаги панели (после смены rate и т.п.)
procedure PushMainFlagExtState; procedure PushMainFlagExtState;
procedure PushMainFlagAudioDevices; procedure PushMainFlagAudioDevices;
procedure PushFlagTXProfiles(O: TVfoOverlay; SliceId: Integer);
procedure PushAllFlagTXProfiles;
procedure UpdateSliceDMRFlags; procedure UpdateSliceDMRFlags;
// Тик: S-метр/частота главного флага + позиции + троттленный S-метр слайсов. // Тик: S-метр/частота главного флага + позиции + троттленный S-метр слайсов.
procedure TickFlags; procedure TickFlags;
@@ -1321,6 +1323,23 @@ begin
end; end;
end; end;
// Список TX-профилей (общий) + привязка ЭТОГО флага (SliceId 0 = главный).
procedure TPanafallPanel.PushFlagTXProfiles(O: TVfoOverlay; SliceId: Integer);
var
L: TStringList;
i: Integer;
begin
if (O = nil) or (FController = nil) then Exit;
L := TStringList.Create;
try
for i := 0 to FController.TXProfileCount - 1 do
L.Add(FController.TXProfileName(i));
O.SetTXProfiles(L, FController.SliceTXProfile(SliceId));
finally
L.Free;
end;
end;
// Заливает во флаг слайса его текущее состояние из контроллера. // Заливает во флаг слайса его текущее состояние из контроллера.
procedure TPanafallPanel.PushSliceFlagState(SliceId: Integer); procedure TPanafallPanel.PushSliceFlagState(SliceId: Integer);
var var
@@ -1352,6 +1371,7 @@ begin
// Auto TX (настройка CAT-порта слайса) — бейдж TX подписан AutoTX. // Auto TX (настройка CAT-порта слайса) — бейдж TX подписан AutoTX.
O.SetAutoTxState(FController.SliceAutoTx(SliceId)); O.SetAutoTxState(FController.SliceAutoTx(SliceId));
O.SetRxMuteTxState(S.RxMuteOnTx); // бейдж RXM O.SetRxMuteTxState(S.RxMuteOnTx); // бейдж RXM
PushFlagTXProfiles(O, SliceId); // пилюля TX-профиля слайса
// Список аудио-устройств (общий PA-список) + текущее устройство слайса. // Список аудио-устройств (общий PA-список) + текущее устройство слайса.
if Assigned(FController.FAudioOut) then if Assigned(FController.FAudioOut) then
begin begin
@@ -1399,6 +1419,18 @@ begin
FController.FSplitTxB and (FController.FTxSliceId = 0), FController.FSplitTxB and (FController.FTxSliceId = 0),
FController.FTransmitting and (FController.FTxSliceId = 0), FController.FTransmitting and (FController.FTxSliceId = 0),
FController.FTxSliceId = 0); FController.FTxSliceId = 0);
PushFlagTXProfiles(FMainFlag, 0);
end;
// Обновить пилюли TX-профиля на всех флагах панели (список профилей общий:
// переименовали/удалили — видно везде).
procedure TPanafallPanel.PushAllFlagTXProfiles;
var i: Integer;
begin
if Assigned(FMainFlag) then PushFlagTXProfiles(FMainFlag, 0);
for i := 0 to High(FSliceFlags) do
if FSliceFlags[i] <> nil then
PushFlagTXProfiles(FSliceFlags[i], FSliceFlags[i].SliceId);
end; end;
procedure TPanafallPanel.PushMainFlagAudioDevices; procedure TPanafallPanel.PushMainFlagAudioDevices;
+84
View File
@@ -178,6 +178,9 @@ type
InDeviceIndex: Integer; // PortAudio-индекс ввода (-1 = общий) InDeviceIndex: Integer; // PortAudio-индекс ввода (-1 = общий)
InDevName: string; InDevName: string;
AudioIn: TAudioInput; // НЕ владеет: ссылка на поток из mic-пула AudioIn: TAudioInput; // НЕ владеет: ссылка на поток из mic-пула
// TX-профиль слайса: индекс в FTXProfiles или -1 = «не переключать».
// Применяется, когда слайс становится TX-источником (SetTxSlice).
TXProfile: Integer;
DMR: TDMRSliceDecoder; // владеет; nil для не-DMR слайса DMR: TDMRSliceDecoder; // владеет; nil для не-DMR слайса
end; end;
@@ -596,6 +599,11 @@ type
procedure RenameTXProfile(Idx: Integer; const AName: string); procedure RenameTXProfile(Idx: Integer; const AName: string);
procedure DeleteTXProfile(Idx: Integer); procedure DeleteTXProfile(Idx: Integer);
procedure ResetTXProfilesToFactory; // заводской набор заново procedure ResetTXProfilesToFactory; // заводской набор заново
// Профиль на TX-источник: -1 = не переключать (звучит активный).
function SliceTXProfile(SliceId: Integer): Integer;
procedure SetSliceTXProfile(SliceId, ProfIdx: Integer);
procedure RemapTXProfileBindings(Deleted: Integer); // после удаления/замены списка
procedure ApplyBoundTXProfile; // профиль текущего TX-источника → в эфир
procedure SaveTXProfilesToConfig; procedure SaveTXProfilesToConfig;
// ---- Управление диапазонами (состояние + DSP; рендер через события) ---- // ---- Управление диапазонами (состояние + DSP; рендер через события) ----
@@ -2148,6 +2156,7 @@ begin
FSlices[slot].RxMuteOnTx := True; // себя на передаче по умолчанию не слышим FSlices[slot].RxMuteOnTx := True; // себя на передаче по умолчанию не слышим
FSlices[slot].FMSQOn := False; // squelch по умолчанию выключен FSlices[slot].FMSQOn := False; // squelch по умолчанию выключен
FSlices[slot].FMSQLevel := 40; FSlices[slot].FMSQLevel := 40;
FSlices[slot].TXProfile := -1; // звучит тот профиль, что активен
FSlices[slot].NRMode := 0; // DSP слайса стартует выключенным FSlices[slot].NRMode := 0; // DSP слайса стартует выключенным
FSlices[slot].NBMode := 0; FSlices[slot].NBMode := 0;
FSlices[slot].SNB := False; FSlices[slot].SNB := False;
@@ -2940,6 +2949,9 @@ begin
else if FTransmitting then SetMOX(False); else if FTransmitting then SetMOX(False);
end; end;
FTxSliceId := SliceId; FTxSliceId := SliceId;
// Профиль, привязанный к новому источнику, — ДО пуша режима и цепи: иначе
// ApplyTXSettingsToDSP внутри SelectTXProfile лёг бы на старый режим.
ApplyBoundTXProfile;
// Передающий тракт под режим нового источника (idle-канал, применится к PTT). // Передающий тракт под режим нового источника (idle-канал, применится к PTT).
if FWDSPReady and Assigned(FDSPEngine) then if FWDSPReady and Assigned(FDSPEngine) then
begin begin
@@ -5033,6 +5045,9 @@ begin
for i := Idx to FTXProfiles.Count - 2 do for i := Idx to FTXProfiles.Count - 2 do
FTXProfiles.Items[i] := FTXProfiles.Items[i + 1]; FTXProfiles.Items[i] := FTXProfiles.Items[i + 1];
Dec(FTXProfiles.Count); Dec(FTXProfiles.Count);
// Привязки флагов — это ИНДЕКСЫ: после сдвига списка их надо переписать,
// иначе слайс молча начнёт включать соседний профиль.
RemapTXProfileBindings(Idx);
if FTXProfiles.ActiveIdx > Idx then Dec(FTXProfiles.ActiveIdx) if FTXProfiles.ActiveIdx > Idx then Dec(FTXProfiles.ActiveIdx)
else if FTXProfiles.ActiveIdx = Idx then else if FTXProfiles.ActiveIdx = Idx then
begin begin
@@ -5047,6 +5062,74 @@ begin
Changed(rfTXProfile); Changed(rfTXProfile);
end; end;
procedure TRadioController.RemapTXProfileBindings(Deleted: Integer);
// Профиль удалён из середины списка: привязку на него снимаем, привязки на
// профили правее — сдвигаем. Deleted < 0 = список заменён целиком, снимаем все.
procedure Fix(var B: Integer);
begin
if B < 0 then Exit;
if Deleted < 0 then B := -1
else if B = Deleted then B := -1
else if B > Deleted then Dec(B);
end;
var i: Integer;
begin
Fix(FTXProfiles.MainBind);
for i := 0 to High(FSlices) do
if FSlices[i].Active then Fix(FSlices[i].TXProfile);
end;
function TRadioController.SliceTXProfile(SliceId: Integer): Integer;
// Привязка «этот источник передачи → этот профиль». -1 = не переключать.
var idx: Integer;
begin
Result := -1;
if SliceId <= 0 then
Result := FTXProfiles.MainBind
else
begin
idx := FindSliceIndex(SliceId);
if idx >= 0 then Result := FSlices[idx].TXProfile;
end;
if (Result < 0) or (Result >= FTXProfiles.Count) then Result := -1;
end;
procedure TRadioController.SetSliceTXProfile(SliceId, ProfIdx: Integer);
var idx: Integer;
begin
if (ProfIdx < -1) or (ProfIdx >= FTXProfiles.Count) then ProfIdx := -1;
if SliceId <= 0 then
begin
if FTXProfiles.MainBind = ProfIdx then Exit;
FTXProfiles.MainBind := ProfIdx;
SaveTXProfilesToConfig;
end
else
begin
idx := FindSliceIndex(SliceId);
if (idx < 0) or (FSlices[idx].TXProfile = ProfIdx) then Exit;
FSlices[idx].TXProfile := ProfIdx;
// Персист слайсов делает MainForm на общем сохранении панов — здесь только
// состояние. Привязку главного пишем сразу: она живёт в секции профилей.
end;
// Привязали источник, который передаёт прямо сейчас, — применяем немедленно,
// иначе профиль подхватился бы только при следующей смене TX-источника.
if SliceId = FTxSliceId then ApplyBoundTXProfile;
Changed(rfTXProfile);
end;
procedure TRadioController.ApplyBoundTXProfile;
// Профиль текущего TX-источника → в эфир. Ничего не делает, если источник ни к
// чему не привязан (-1) или нужный профиль уже активен.
var P: Integer;
begin
P := SliceTXProfile(FTxSliceId);
if (P < 0) or (P = FTXProfiles.ActiveIdx) then Exit;
SelectTXProfile(P);
end;
procedure TRadioController.ResetTXProfilesToFactory; procedure TRadioController.ResetTXProfilesToFactory;
// Пересобирает заводской набор поверх текущего списка. База — ЖИВЫЕ настройки, // Пересобирает заводской набор поверх текущего списка. База — ЖИВЫЕ настройки,
// поэтому «Default» = микрофонный вход, полоса, динамика и мощность как сейчас, // поэтому «Default» = микрофонный вход, полоса, динамика и мощность как сейчас,
@@ -5055,6 +5138,7 @@ procedure TRadioController.ResetTXProfilesToFactory;
// пересоздаётся. // пересоздаётся.
begin begin
DefaultTXProfiles(FTXSettings, FDrivePercent, FTXProfiles); DefaultTXProfiles(FTXSettings, FDrivePercent, FTXProfiles);
RemapTXProfileBindings(-1); // список другой — старые привязки бессмысленны
// ActiveIdx гасим перед Select: иначе StoreActiveTXProfile внутри него // ActiveIdx гасим перед Select: иначе StoreActiveTXProfile внутри него
// впишет в свежий «Default» живой EQ и сброс тембра тут же отменится. // впишет в свежий «Default» живой EQ и сброс тембра тут же отменится.
FTXProfiles.ActiveIdx := -1; FTXProfiles.ActiveIdx := -1;
+12
View File
@@ -177,6 +177,7 @@ type
FMSQLevel: Integer; FMSQLevel: Integer;
DevName: string; // имя PortAudio-устройства вывода ('' = default) DevName: string; // имя PortAudio-устройства вывода ('' = default)
InDevName: string; // имя устройства ВВОДА слайса ('' = общий вход) InDevName: string; // имя устройства ВВОДА слайса ('' = общий вход)
TXProfile: Integer; // TX-профиль слайса: -1 = какой активен, тот и звучит
end; end;
TPanCfg = record TPanCfg = record
@@ -349,6 +350,9 @@ type
TTXProfileList = record TTXProfileList = record
Count: Integer; Count: Integer;
ActiveIdx: Integer; ActiveIdx: Integer;
// Привязка профиля к ГЛАВНОМУ приёмнику (слайс A). У слайсов B..G то же
// самое лежит в TSliceCfg.TXProfile; -1 = ничего не переключать.
MainBind: Integer;
Items: array[0..TXPROF_MAX-1] of TTXProfile; Items: array[0..TXPROF_MAX-1] of TTXProfile;
end; end;
@@ -1938,6 +1942,7 @@ begin
FillChar(L, SizeOf(L), 0); FillChar(L, SizeOf(L), 0);
L.Count := 0; L.Count := 0;
L.ActiveIdx := 0; L.ActiveIdx := 0;
L.MainBind := -1; // главный приёмник профиль не переключает
// «Default» — текущее состояние устройства (микрофонный вход, полоса, // «Default» — текущее состояние устройства (микрофонный вход, полоса,
// динамика, мощность), но с заводским EQ: после миграции старого конфига // динамика, мощность), но с заводским EQ: после миграции старого конфига
// звук в эфире не меняется, а сброс к заводским возвращает ровный тембр. // звук в эфире не меняется, а сброс к заводским возвращает ровный тембр.
@@ -1970,6 +1975,7 @@ begin
FillChar(L, SizeOf(L), 0); FillChar(L, SizeOf(L), 0);
L.Count := 0; L.Count := 0;
L.ActiveIdx := 0; L.ActiveIdx := 0;
L.MainBind := -1;
Result := False; Result := False;
MacStr := MacToStr(MAC); MacStr := MacToStr(MAC);
if FRoot.Find(MacStr) = nil then Exit; if FRoot.Find(MacStr) = nil then Exit;
@@ -2030,6 +2036,8 @@ begin
if L.Count = 0 then Exit; // пустой список = как будто секции нет if L.Count = 0 then Exit; // пустой список = как будто секции нет
L.ActiveIdx := JI(Root, 'active', 0); L.ActiveIdx := JI(Root, 'active', 0);
if (L.ActiveIdx < 0) or (L.ActiveIdx >= L.Count) then L.ActiveIdx := 0; if (L.ActiveIdx < 0) or (L.ActiveIdx >= L.Count) then L.ActiveIdx := 0;
L.MainBind := JI(Root, 'main_bind', -1);
if (L.MainBind < -1) or (L.MainBind >= L.Count) then L.MainBind := -1;
Result := True; Result := True;
end; end;
@@ -2042,6 +2050,7 @@ var
begin begin
Root := EnsureObj(GetDevObj(MacToStr(MAC)), 'tx_profiles'); Root := EnsureObj(GetDevObj(MacToStr(MAC)), 'tx_profiles');
JW(Root, 'active', EnsureRange(L.ActiveIdx, 0, Max(0, L.Count - 1))); JW(Root, 'active', EnsureRange(L.ActiveIdx, 0, Max(0, L.Count - 1)));
JW(Root, 'main_bind', EnsureRange(L.MainBind, -1, Max(-1, L.Count - 1)));
// Список пересоздаётся целиком: удаление профиля не должно оставлять хвост. // Список пересоздаётся целиком: удаление профиля не должно оставлять хвост.
Idx := Root.IndexOfName('list'); Idx := Root.IndexOfName('list');
if Idx >= 0 then Root.Delete(Idx); if Idx >= 0 then Root.Delete(Idx);
@@ -2732,6 +2741,7 @@ begin
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);
SliceObj.Add('in_dev_name', P.Pans[i].Slices[j].InDevName); SliceObj.Add('in_dev_name', P.Pans[i].Slices[j].InDevName);
SliceObj.Add('tx_profile', P.Pans[i].Slices[j].TXProfile);
end; end;
end; end;
end; end;
@@ -2812,6 +2822,8 @@ begin
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', '');
P.Pans[id].Slices[n].InDevName := JS(SliceObj, 'in_dev_name', ''); P.Pans[id].Slices[n].InDevName := JS(SliceObj, 'in_dev_name', '');
P.Pans[id].Slices[n].TXProfile := EnsureRange(
JI(SliceObj, 'tx_profile', -1), -1, TXPROF_MAX - 1);
end; end;
end; end;
end; end;
+251 -41
View File
@@ -57,7 +57,10 @@ type
TVolumeEvent = procedure(Vol: Integer) of object; TVolumeEvent = procedure(Vol: Integer) of object;
TAudioDeviceEvent = procedure(DevIndex: Integer; const DevName: string) of object; TAudioDeviceEvent = procedure(DevIndex: Integer; const DevName: string) of object;
TFlyPanel = (fpNone, fpMode, fpDsp, fpAud); TFlyPanel = (fpNone, fpMode, fpDsp, fpAud, fpTxProf);
// Выбран TX-профиль для этого флага: Idx = -1 («как активный») или индекс.
TTXProfilePickEvent = procedure(Idx: Integer) of object;
TVfoOverlay = class(TCustomControl) TVfoOverlay = class(TCustomControl)
private private
@@ -102,6 +105,13 @@ type
FCurInDev: string; // текущий вход слайса ('' = общий вход) FCurInDev: string; // текущий вход слайса ('' = общий вход)
FAudInTab: Boolean; // AUD-панель показывает вход (IN), а не выход FAudInTab: Boolean; // AUD-панель показывает вход (IN), а не выход
// TX-профили: список имён (общий на все флаги) + привязка ЭТОГО флага.
// -1 = флаг ничего не переключает, передаёт активный профиль.
FProfNames: TStringList;
FProfBound: Integer;
FProfScroll: Integer; // прокрутка списка профилей
FProfListRect: TRect; // область списка профилей (зона колёсика)
FPanel: TFlyPanel; // открытая fly-out панель FPanel: TFlyPanel; // открытая fly-out панель
FAudScroll: Integer; // прокрутка списка аудио-устройств (верхний индекс) FAudScroll: Integer; // прокрутка списка аудио-устройств (верхний индекс)
FAudListRect: TRect; // область списка AUD (лок. коорд.) — зона колёсика FAudListRect: TRect; // область списка AUD (лок. коорд.) — зона колёсика
@@ -121,6 +131,7 @@ type
FOnSplit: TNotifyEvent; FOnSplit: TNotifyEvent;
FOnTxSelect: TNotifyEvent; FOnTxSelect: TNotifyEvent;
FOnRxMuteTx: TNotifyEvent; FOnRxMuteTx: TNotifyEvent;
FOnTXProfile: TTXProfilePickEvent;
FOnInvalidate: TNotifyEvent; FOnInvalidate: TNotifyEvent;
FTheme: TAppTheme; // тема приложения (цвета кнопок и т.п.) FTheme: TAppTheme; // тема приложения (цвета кнопок и т.п.)
@@ -156,6 +167,13 @@ type
procedure DrawModePanel(C: TCanvas; X, Y, RowW: Integer); procedure DrawModePanel(C: TCanvas; X, Y, RowW: Integer);
procedure DrawDspPanel(C: TCanvas; X, Y, RowW: Integer); procedure DrawDspPanel(C: TCanvas; X, Y, RowW: Integer);
procedure DrawAudPanel(C: TCanvas; X, Y, RowW: Integer); procedure DrawAudPanel(C: TCanvas; X, Y, RowW: Integer);
procedure DrawProfPanel(C: TCanvas; X, Y, RowW: Integer);
// Стрелки прокрутки справа от списка (общие для AUD и профилей).
procedure DrawScrollArrows(C: TCanvas; X, Y, RowW, RowWidth, PanelH: Integer;
CanUp, CanDn: Boolean; HitUp, HitDn: Integer);
function ProfRowCount: Integer; // «— как активный» + профили
function ProfRowName(I: Integer): string;
procedure ScrollProfToCurrent;
// Строки активной вкладки AUD (OUT — устройства вывода, IN — ввода; // Строки активной вкладки AUD (OUT — устройства вывода, IN — ввода;
// у IN первая строка псевдо: «общий вход» / «нет»). // у IN первая строка псевдо: «общий вход» / «нет»).
function AudRowCount: Integer; function AudRowCount: Integer;
@@ -206,6 +224,8 @@ type
procedure SetDSPState(ANRMode, ANBMode: Integer; ASNBOn, AANFOn: Boolean; procedure SetDSPState(ANRMode, ANBMode: Integer; ASNBOn, AANFOn: Boolean;
AAGCMode: Integer); AAGCMode: Integer);
procedure SetSquelchState(AOn: Boolean; ALevel: Integer); procedure SetSquelchState(AOn: Boolean; ALevel: Integer);
// Список TX-профилей (одинаков у всех флагов) + привязка этого флага.
procedure SetTXProfiles(ANames: TStrings; ABound: 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 SetRxMuteTxState(AOn: Boolean); // бейдж RXM (self-monitor на TX)
// Auto TX слайса (настройка CAT): бейдж TX превращается в AutoTX. // Auto TX слайса (настройка CAT): бейдж TX превращается в AutoTX.
@@ -257,6 +277,7 @@ type
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 OnRxMuteTx: TNotifyEvent read FOnRxMuteTx write FOnRxMuteTx;
property OnTXProfile: TTXProfilePickEvent read FOnTXProfile write FOnTXProfile;
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;
@@ -300,8 +321,12 @@ const
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_RXMUTE = -33; // бейдж RXM: приём слайса на своей передаче
HIT_TXPROF = -36; // пилюля TX-профиля в шапке (открыть список)
HIT_PROF_UP = -37; // прокрутка списка профилей
HIT_PROF_DN = -38;
HIT_AGC_PICK_BASE = 100000; HIT_AGC_PICK_BASE = 100000;
HIT_DEV_BASE = 200000; HIT_DEV_BASE = 200000;
HIT_PROF_BASE = 300000; // 300000 + строка списка (0 = «как активный»)
// Свои диапазоны, чтобы не пересекаться с тумблерами (-9..-33) и с базами выше. // Свои диапазоны, чтобы не пересекаться с тумблерами (-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 + индекс кнопки в ряду фильтров
@@ -319,6 +344,9 @@ const
IGAP = 2; IGAP = 2;
AUD_ROW_H = 17; // высота строки устройства в AUD-панели AUD_ROW_H = 17; // высота строки устройства в AUD-панели
AUD_MAX_VIS = 6; // максимум видимых устройств (дальше — прокрутка) AUD_MAX_VIS = 6; // максимум видимых устройств (дальше — прокрутка)
PROF_MAX_VIS = 6; // столько же строк у списка TX-профилей
PROF_PILL_W = 92; // ширина пилюли профиля в шапке
PROF_PILL_MIN = 44; // уже этого не рисуем — шапка занята бейджами
OVL_BASE_H = PAD + HDR_H + MET_H + FRQ_H + BAR_H + PAD; // = 96 OVL_BASE_H = PAD + HDR_H + MET_H + FRQ_H + BAR_H + PAD; // = 96
// Палитра. Структурные цвета (фон/текст/акценты) берутся из темы — // Палитра. Структурные цвета (фон/текст/акценты) берутся из темы —
@@ -519,6 +547,10 @@ begin
FInDevNames := TStringList.Create; FInDevNames := TStringList.Create;
FCurInDev := ''; FCurInDev := '';
FAudInTab := False; // AUD открывается на вкладке OUT FAudInTab := False; // AUD открывается на вкладке OUT
FProfNames := TStringList.Create;
FProfBound := -1; // флаг не переключает профиль
FProfScroll := 0;
FProfListRect := Rect(0, 0, 0, 0);
FCacheBitmap := TBitmap.Create; FCacheBitmap := TBitmap.Create;
FCacheBitmap.PixelFormat := pf32bit; FCacheBitmap.PixelFormat := pf32bit;
FCacheDirty := True; FCacheDirty := True;
@@ -558,6 +590,7 @@ end;
destructor TVfoOverlay.Destroy; destructor TVfoOverlay.Destroy;
begin begin
FProfNames.Free;
FInDevNames.Free; FInDevNames.Free;
FDevNames.Free; FDevNames.Free;
FCacheBitmap.Free; FCacheBitmap.Free;
@@ -660,6 +693,7 @@ begin
// ряд вкладок OUT/IN + список устройств активной вкладки // ряд вкладок OUT/IN + список устройств активной вкладки
fpAud: Result := ROW_H + IGAP + fpAud: Result := ROW_H + IGAP +
Min(Max(1, AudRowCount), AUD_MAX_VIS) * AUD_ROW_H; Min(Max(1, AudRowCount), AUD_MAX_VIS) * AUD_ROW_H;
fpTxProf: Result := Min(ProfRowCount, PROF_MAX_VIS) * AUD_ROW_H;
else Result := 0; else Result := 0;
end; end;
end; end;
@@ -674,6 +708,7 @@ procedure TVfoOverlay.SetPanel(P: TFlyPanel);
begin begin
if FPanel = P then FPanel := fpNone else FPanel := P; if FPanel = P then FPanel := fpNone else FPanel := P;
if FPanel = fpAud then ScrollAudToCurrent; // сразу показать выбранное if FPanel = fpAud then ScrollAudToCurrent; // сразу показать выбранное
if FPanel = fpTxProf then ScrollProfToCurrent;
Height := ComputeHeight; Height := ComputeHeight;
RequestInvalidate; RequestInvalidate;
end; end;
@@ -996,15 +1031,143 @@ begin
end; end;
end; end;
function TVfoOverlay.ProfRowCount: Integer;
// Строка 0 — псевдо «— как активный», дальше сами профили.
begin
Result := FProfNames.Count + 1;
end;
function TVfoOverlay.ProfRowName(I: Integer): string;
begin
if I <= 0 then Result := 'As active'
else if I - 1 < FProfNames.Count then Result := FProfNames[I - 1]
else Result := '';
end;
procedure TVfoOverlay.ScrollProfToCurrent;
var Cnt, Cur: Integer;
begin
FProfScroll := 0;
Cnt := ProfRowCount;
if Cnt <= PROF_MAX_VIS then Exit;
Cur := FProfBound + 1; // -1 → строка 0
FProfScroll := Min(Max(0, Cur - PROF_MAX_VIS div 2), Cnt - PROF_MAX_VIS);
end;
procedure TVfoOverlay.SetTXProfiles(ANames: TStrings; ABound: Integer);
var Same: Boolean;
begin
Same := (ABound = FProfBound) and (ANames <> nil) and
(ANames.Count = FProfNames.Count) and (ANames.Text = FProfNames.Text);
if Same then Exit;
if ANames <> nil then FProfNames.Assign(ANames) else FProfNames.Clear;
if (ABound < -1) or (ABound >= FProfNames.Count) then ABound := -1;
FProfBound := ABound;
if FPanel = fpTxProf then
begin
ScrollProfToCurrent;
Height := ComputeHeight; // список мог стать длиннее/короче
end;
RequestInvalidate;
end;
procedure TVfoOverlay.DrawProfPanel(C: TCanvas; X, Y, RowW: Integer);
const
ARROW_W = 15;
var
I, Slot, VisN, RowWidth, MaxScroll, Cnt: Integer;
HR: TRect;
Nm, Full: string;
Cur, Scrollable: Boolean;
begin
C.Font.Name := 'Courier New';
C.Font.Size := 7;
C.Font.Style := [];
Cnt := ProfRowCount;
VisN := Min(Cnt, PROF_MAX_VIS);
FProfListRect := Rect(X, Y, X + RowW, Y + VisN * AUD_ROW_H);
Scrollable := Cnt > PROF_MAX_VIS;
MaxScroll := Cnt - VisN;
if FProfScroll > MaxScroll then FProfScroll := MaxScroll;
if FProfScroll < 0 then FProfScroll := 0;
if Scrollable then RowWidth := RowW - ARROW_W - 2 else RowWidth := RowW;
for Slot := 0 to VisN - 1 do
begin
I := FProfScroll + Slot;
HR := Rect(X, Y + Slot*AUD_ROW_H, X + RowWidth, Y + Slot*AUD_ROW_H + 16);
Full := ProfRowName(I);
Nm := Full;
Cur := (I - 1) = FProfBound;
if Cur then C.Brush.Color := CLR_ACCENT_D
else if (FHotIdx >= 0) and (FHitActions[FHotIdx] = HIT_PROF_BASE + I) then
C.Brush.Color := CLR_BTN_HOT
else C.Brush.Color := CLR_BTN_NORM;
C.Brush.Style := bsSolid;
C.Pen.Color := C.Brush.Color;
C.Pen.Style := psSolid;
C.RoundRect(HR.Left, HR.Top, HR.Right, HR.Bottom, 4, 4);
if Cur then C.Font.Color := CLR_ACCENT else C.Font.Color := CLR_TEXT;
C.Brush.Style := bsClear;
while (Length(Nm) > 1) and (C.TextWidth(Nm + '…') > RowWidth - 8) do
SetLength(Nm, Length(Nm) - 1);
if Nm <> Full then Nm := Nm + '…';
C.TextOut(HR.Left + 4, HR.Top + (16 - C.TextHeight('A')) div 2, Nm);
RegisterHit(HR, HIT_PROF_BASE + I);
end;
if not Scrollable then Exit;
DrawScrollArrows(C, X, Y, RowW, RowWidth, VisN * AUD_ROW_H - 1,
FProfScroll > 0, FProfScroll < MaxScroll,
HIT_PROF_UP, HIT_PROF_DN);
end;
procedure TVfoOverlay.DrawScrollArrows(C: TCanvas; X, Y, RowW, RowWidth,
PanelH: Integer; CanUp, CanDn: Boolean; HitUp, HitDn: Integer);
// Полоса прокрутки справа от списка: ▲ / ▼. Общая для AUD и TX-профилей.
var
HR: TRect;
ArrowX, MidY, CY: Integer;
Tri: array[0..2] of TPoint;
begin
ArrowX := X + RowWidth + 2;
MidY := Y + PanelH div 2;
HR := Rect(ArrowX, Y, X + RowW, MidY - 1);
C.Brush.Color := CLR_BTN_NORM; C.Brush.Style := bsSolid;
C.Pen.Color := CLR_BTN_NORM; C.Pen.Style := psSolid;
C.RoundRect(HR.Left, HR.Top, HR.Right, HR.Bottom, 4, 4);
CY := (HR.Top + HR.Bottom) div 2;
if CanUp then C.Brush.Color := CLR_TEXT else C.Brush.Color := CLR_DIM;
C.Pen.Color := C.Brush.Color;
Tri[0] := Point((HR.Left+HR.Right) div 2, CY - 3);
Tri[1] := Point((HR.Left+HR.Right) div 2 - 4, CY + 2);
Tri[2] := Point((HR.Left+HR.Right) div 2 + 4, CY + 2);
C.Polygon(Tri);
RegisterHit(HR, HitUp);
HR := Rect(ArrowX, MidY + 1, X + RowW, Y + PanelH);
C.Brush.Color := CLR_BTN_NORM; C.Brush.Style := bsSolid;
C.Pen.Color := CLR_BTN_NORM; C.Pen.Style := psSolid;
C.RoundRect(HR.Left, HR.Top, HR.Right, HR.Bottom, 4, 4);
CY := (HR.Top + HR.Bottom) div 2;
if CanDn then C.Brush.Color := CLR_TEXT else C.Brush.Color := CLR_DIM;
C.Pen.Color := C.Brush.Color;
Tri[0] := Point((HR.Left+HR.Right) div 2, CY + 3);
Tri[1] := Point((HR.Left+HR.Right) div 2 - 4, CY - 2);
Tri[2] := Point((HR.Left+HR.Right) div 2 + 4, CY - 2);
C.Polygon(Tri);
RegisterHit(HR, HitDn);
end;
procedure TVfoOverlay.DrawAudPanel(C: TCanvas; X, Y, RowW: Integer); procedure TVfoOverlay.DrawAudPanel(C: TCanvas; X, Y, RowW: Integer);
const const
ARROW_W = 15; ARROW_W = 15;
var var
I, Slot, VisN, RowWidth, MaxScroll, PanelH, ArrowX, MidY, CY, Cnt: Integer; I, Slot, VisN, RowWidth, MaxScroll, Cnt: Integer;
HR, TabR: TRect; HR, TabR: TRect;
Nm: string; Nm: string;
Cur, Scrollable, CanUp, CanDn: Boolean; Cur, Scrollable: Boolean;
Tri: array[0..2] of TPoint;
begin begin
// Ряд вкладок: OUT (куда отдаём звук) / IN (откуда берём модуляцию). // Ряд вкладок: OUT (куда отдаём звук) / IN (откуда берём модуляцию).
TabR := Rect(X, Y, X + (RowW - 4) div 2, Y + ROW_H); TabR := Rect(X, Y, X + (RowW - 4) div 2, Y + ROW_H);
@@ -1062,50 +1225,20 @@ begin
end; end;
if not Scrollable then Exit; if not Scrollable then Exit;
DrawScrollArrows(C, X, Y, RowW, RowWidth, VisN * AUD_ROW_H - 1,
// Полоса прокрутки справа: ▲ (верх) / ▼ (низ) + счётчик позиции. FAudScroll > 0, FAudScroll < MaxScroll,
PanelH := VisN * AUD_ROW_H - 1; HIT_AUD_UP, HIT_AUD_DN);
ArrowX := X + RowWidth + 2;
MidY := Y + PanelH div 2;
CanUp := FAudScroll > 0;
CanDn := FAudScroll < MaxScroll;
// верхняя стрелка
HR := Rect(ArrowX, Y, X + RowW, MidY - 1);
C.Brush.Color := CLR_BTN_NORM; C.Brush.Style := bsSolid;
C.Pen.Color := CLR_BTN_NORM; C.Pen.Style := psSolid;
C.RoundRect(HR.Left, HR.Top, HR.Right, HR.Bottom, 4, 4);
CY := (HR.Top + HR.Bottom) div 2;
if CanUp then C.Brush.Color := CLR_TEXT else C.Brush.Color := CLR_DIM;
C.Pen.Color := C.Brush.Color;
Tri[0] := Point((HR.Left+HR.Right) div 2, CY - 3);
Tri[1] := Point((HR.Left+HR.Right) div 2 - 4, CY + 2);
Tri[2] := Point((HR.Left+HR.Right) div 2 + 4, CY + 2);
C.Polygon(Tri);
RegisterHit(HR, HIT_AUD_UP);
// нижняя стрелка
HR := Rect(ArrowX, MidY + 1, X + RowW, Y + PanelH);
C.Brush.Color := CLR_BTN_NORM; C.Brush.Style := bsSolid;
C.Pen.Color := CLR_BTN_NORM; C.Pen.Style := psSolid;
C.RoundRect(HR.Left, HR.Top, HR.Right, HR.Bottom, 4, 4);
CY := (HR.Top + HR.Bottom) div 2;
if CanDn then C.Brush.Color := CLR_TEXT else C.Brush.Color := CLR_DIM;
C.Pen.Color := C.Brush.Color;
Tri[0] := Point((HR.Left+HR.Right) div 2, CY + 3);
Tri[1] := Point((HR.Left+HR.Right) div 2 - 4, CY - 2);
Tri[2] := Point((HR.Left+HR.Right) div 2 + 4, CY - 2);
C.Polygon(Tri);
RegisterHit(HR, HIT_AUD_DN);
end; end;
{ ---- Основная отрисовка ---- } { ---- Основная отрисовка ---- }
procedure TVfoOverlay.DrawSelf(C: TCanvas; W, H: Integer); procedure TVfoOverlay.DrawSelf(C: TCanvas; W, H: Integer);
var var
FreqStr, SStr, BWStr, DMRStr, TxStr: string; FreqStr, SStr, BWStr, DMRStr, TxStr, PStr: string;
CurY, X, RW, i, DMRX, DMRRight, TxRight, TxW: Integer; CurY, X, RW, i, DMRX, DMRRight, TxRight, TxW: Integer;
ProfLeft, ProfRight, ProfW: Integer;
R: TRect; R: TRect;
Tri3: array[0..2] of TPoint;
BarL, BarW, MeterX: Integer; BarL, BarW, MeterX: Integer;
begin begin
FHitCount := 0; FHitCount := 0;
@@ -1154,6 +1287,7 @@ begin
C.Font.Name := 'Courier New'; C.Font.Size := 8; C.Font.Style := []; C.Font.Name := 'Courier New'; C.Font.Size := 8; C.Font.Style := [];
C.Font.Color := CLR_DIM; C.Brush.Style := bsClear; C.Font.Color := CLR_DIM; C.Brush.Style := bsClear;
C.TextOut(X + 22, CurY + 1, BWStr); C.TextOut(X + 22, CurY + 1, BWStr);
ProfLeft := X + 22 + C.TextWidth(BWStr) + 8;
// Краткий статус дополнительного DMR-слайса. Он обновляется только при // Краткий статус дополнительного DMR-слайса. Он обновляется только при
// устойчивом изменении sync/slot/CC/TG, поэтому не заставляет GL-текстуру // устойчивом изменении sync/slot/CC/TG, поэтому не заставляет GL-текстуру
// перезаливаться на каждом burst. // перезаливаться на каждом burst.
@@ -1186,6 +1320,45 @@ begin
else DrawBadge(C, R, 'SPLIT', CLR_BTN_NORM, CLR_DIM); else DrawBadge(C, R, 'SPLIT', CLR_BTN_NORM, CLR_DIM);
RegisterHit(R, HIT_SPLIT); RegisterHit(R, HIT_SPLIT);
end; end;
// Пилюля TX-профиля этого флага — сразу за шириной фильтра. Правая граница:
// до SPLIT (главный) либо до RXM (слайс). У DMR передачи нет вовсе, у FMRAW
// профиль ни на что не влияет (ApplyTXModeSettings глушит всю обработку —
// компрессор, leveler, ALC, phase rot, EQ, mic gain), у пустого списка
// выбирать нечего — не рисуем. Тесная шапка (узкий флаг) — тоже.
if FIsMain then ProfRight := W - PAD - 26 - 44 - 6
else ProfRight := TxRight - TxW - 4 - 32 - 6;
if (FProfNames.Count > 0) and (FMode <> OVL_MODE_DMR) and
(FMode <> OVL_MODE_FMRAW) and
(ProfRight - ProfLeft >= PROF_PILL_MIN) then
begin
ProfW := Min(PROF_PILL_W, ProfRight - ProfLeft);
R := Rect(ProfLeft, CurY, ProfLeft + ProfW, CurY + HDR_BADGE_H);
if FProfBound >= 0 then C.Brush.Color := CLR_ACCENT_D
else if (FHotIdx >= 0) and (FHitActions[FHotIdx] = HIT_TXPROF) then
C.Brush.Color := CLR_BTN_HOT
else C.Brush.Color := CLR_BTN_NORM;
C.Brush.Style := bsSolid;
C.Pen.Color := C.Brush.Color; C.Pen.Style := psSolid;
C.RoundRect(R.Left, R.Top, R.Right, R.Bottom, 4, 4);
if FProfBound >= 0 then PStr := ProfRowName(FProfBound + 1)
else PStr := 'Profile'; // привязки нет — просто ярлык
C.Font.Size := 7;
if FProfBound >= 0 then C.Font.Color := CLR_ACCENT else C.Font.Color := CLR_DIM;
C.Brush.Style := bsClear;
while (Length(PStr) > 1) and (C.TextWidth(PStr) > ProfW - 14) do
SetLength(PStr, Length(PStr) - 1);
C.TextOut(R.Left + 4, R.Top + (HDR_BADGE_H - C.TextHeight('A')) div 2, PStr);
// Треугольник «список» справа — рисуем, а не пишем: в Courier New
// юникодных стрелок может не оказаться.
C.Brush.Color := C.Font.Color; C.Brush.Style := bsSolid;
C.Pen.Color := C.Font.Color;
Tri3[0] := Point(R.Right - 9, CurY + 5);
Tri3[1] := Point(R.Right - 3, CurY + 5);
Tri3[2] := Point(R.Right - 6, CurY + 9);
C.Polygon(Tri3);
C.Font.Size := 8;
RegisterHit(R, HIT_TXPROF);
end;
Inc(CurY, HDR_H); Inc(CurY, HDR_H);
{ --- Метр: индикаторы + сигнал-бар --- } { --- Метр: индикаторы + сигнал-бар --- }
@@ -1259,6 +1432,7 @@ begin
fpMode: DrawModePanel(C, PAD, CurY, W - PAD*2); fpMode: DrawModePanel(C, PAD, CurY, W - PAD*2);
fpDsp: DrawDspPanel(C, PAD, CurY, W - PAD*2); fpDsp: DrawDspPanel(C, PAD, CurY, W - PAD*2);
fpAud: DrawAudPanel(C, PAD, CurY, W - PAD*2); fpAud: DrawAudPanel(C, PAD, CurY, W - PAD*2);
fpTxProf: DrawProfPanel(C, PAD, CurY, W - PAD*2);
end; end;
end; end;
end; end;
@@ -1339,6 +1513,12 @@ begin
end; end;
HIT_RXMUTE: begin FRxMuteTx := not FRxMuteTx; RequestInvalidate; HIT_RXMUTE: begin FRxMuteTx := not FRxMuteTx; RequestInvalidate;
if Assigned(FOnRxMuteTx) then FOnRxMuteTx(Self); Exit; end; if Assigned(FOnRxMuteTx) then FOnRxMuteTx(Self); Exit; end;
HIT_TXPROF: begin SetPanel(fpTxProf); Exit; end;
HIT_PROF_UP: begin if FProfScroll > 0 then Dec(FProfScroll);
RequestInvalidate; Exit; end;
HIT_PROF_DN: begin if FProfScroll < ProfRowCount - PROF_MAX_VIS then
Inc(FProfScroll);
RequestInvalidate; 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);
@@ -1360,6 +1540,18 @@ begin
end; end;
end; end;
// Строка списка TX-профилей: 0 = «как активный» (-1), дальше индексы профилей.
if HV >= HIT_PROF_BASE then
begin
NewMode := HV - HIT_PROF_BASE - 1; // -1 = «как активный»
if (NewMode < -1) or (NewMode >= FProfNames.Count) then Exit;
FProfBound := NewMode;
FPanel := fpNone; Height := ComputeHeight;
RequestInvalidate;
if Assigned(FOnTXProfile) then FOnTXProfile(NewMode);
Exit;
end;
// Устройство аудио (строка активной вкладки OUT/IN) // Устройство аудио (строка активной вкладки OUT/IN)
if HV >= HIT_DEV_BASE then if HV >= HIT_DEV_BASE then
begin begin
@@ -1533,7 +1725,20 @@ var
MaxScroll, Old: Integer; MaxScroll, Old: Integer;
begin begin
Result := False; Result := False;
if (not Visible) or (FPanel <> fpAud) then Exit; if not Visible then Exit;
if FPanel = fpTxProf then
begin
if not PtInRect(FProfListRect, Point(X - Left, Y - Top)) then Exit;
Result := True;
MaxScroll := ProfRowCount - PROF_MAX_VIS;
if MaxScroll <= 0 then Exit;
Old := FProfScroll;
if WheelDelta > 0 then Dec(FProfScroll) else Inc(FProfScroll);
FProfScroll := EnsureRange(FProfScroll, 0, MaxScroll);
if FProfScroll <> Old then RequestInvalidate;
Exit;
end;
if FPanel <> fpAud then Exit;
if not PtInRect(FAudListRect, Point(X - Left, Y - Top)) then Exit; if not PtInRect(FAudListRect, Point(X - Left, Y - Top)) then Exit;
Result := True; // зона наша — колесо в спектр не пускаем Result := True; // зона наша — колесо в спектр не пускаем
MaxScroll := AudRowCount - AUD_MAX_VIS; MaxScroll := AudRowCount - AUD_MAX_VIS;
@@ -1635,6 +1840,11 @@ begin
FDMRCandidateText := ''; FDMRCandidateText := '';
FDMRCandidateSince := 0; FDMRCandidateSince := 0;
end; end;
// Ушли в режим, где профиль ни на что не влияет: пилюли уже нет, открытый
// список закрываем сами — иначе он висел бы без своей кнопки.
if (FPanel = fpTxProf) and
((FMode = OVL_MODE_DMR) or (FMode = OVL_MODE_FMRAW)) then
FPanel := fpNone;
Height := ComputeHeight; Height := ComputeHeight;
RequestInvalidate; RequestInvalidate;
end; end;