From 6c38777c582d65db272f1804628981af87c7319d Mon Sep 17 00:00:00 2001 From: Vladimir Date: Wed, 22 Jul 2026 12:08:06 +0300 Subject: [PATCH 1/4] =?UTF-8?q?ui(pans):=20=D0=B3=D1=80=D0=B8=D0=B4=20?= =?UTF-8?q?=D0=B8=D0=B7=20=D0=B4=D0=B2=D1=83=D1=85=20=D0=BA=D0=BE=D0=BB?= =?UTF-8?q?=D0=BE=D0=BD=D0=BE=D0=BA=20=E2=80=94=20=D0=BF=D0=B0=D0=BD=200?= =?UTF-8?q?=20=D0=B8=20=D0=B4=D0=BE=D0=BF.=20=D0=BF=D0=B0=D0=BD=20=D0=B1?= =?UTF-8?q?=D0=BE=D0=BA=20=D0=BE=20=D0=B1=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Раньше «▦» имел смысл только при >=2 доп. панах (с одним грид совпадал со стеком) — основной и единственный дополнительный приёмник могли стоять лишь друг под другом. Теперь при ОДНОМ доп. пане грид = две колонки во всю высоту: пан 0 слева, доп. справа, между ними тот же вертикальный сплиттер. Wideband при этом остаётся сверху во всю ширину PanelRight, обе колонки начинаются под ним (StackTop=AWideH, пан 0 раскладывается с ATopOffset=0, шапки панов уходят в свои колонки — Pan0HeaderH над wideband не резервируется). MainForm: ExtraPanCount/PanGridTwoCol; FPanColShare расширен на индекс 0 (пан 0 — тоже колонка), доля персистится через Cfg.Pans[0].ColShare — ключ col_share в Settings уже был, теперь пишется для всех панов; отдельная ветка в LayoutPanStack (горизонтальные сплиттеры спрятаны, кламп 15% ширины на колонку); vsplit-хендлеры ищут левую колонку downto 0 в two-col и нормируют доли с учётом пана 0; PanSplitMouseDown/Up гейтятся not PanGridTwoCol; кнопка «▦» видна уже при одном доп. пане. Отдельно исправлен sync-гард в тике дисплея: он сверял PbWideband.Width с FPan.SpectrumWidth, а в two-col пан 0 — половина ширины при wideband во всю, поэтому условие было вечно истинным и ResizeSpectrumPanels вызывался каждый кадр — раскладка пересчитывалась под курсором (вертикальный сплиттер не тянулся) и зря сбрасывались битмапы. Теперь wideband сверяется с PanelRight.ClientWidth — его настоящим инвариантом. Co-Authored-By: Claude Opus 4.8 --- MainForm.pas | 126 +++++++++++++++++++++++++++++++++++++++------------ ewsdr.lpi | 9 +++- 2 files changed, 104 insertions(+), 31 deletions(-) diff --git a/MainForm.pas b/MainForm.pas index 3334ebb..f0d98f6 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -189,11 +189,12 @@ type FPanSplitTop0: Integer; FPanSplitMinTop: Integer; FPanSplitMaxTop: Integer; - // Грид-раскладка: пан 0 сверху во всю ширину, доп. паны колонками снизу. - // Активна при FPanGridMode и >=2 доп. панах в стеке (иначе обычный стек). + // Грид-раскладка: при >=2 доп. панах — пан 0 сверху во всю ширину, доп. + // паны колонками снизу; при ОДНОМ доп. пане — две колонки бок о бок + // (пан 0 слева, доп. справа). Активна при FPanGridMode и >=1 доп. пане. FPanGridMode: Boolean; FPanGridRowShare: Double; // доля высоты верхнего ряда (пан 0), 0.15..0.85 - FPanColShare: array[1..MAX_PANS-1] of Double; // доли ширины колонок + FPanColShare: array[0..MAX_PANS-1] of Double; // доли ширины колонок FPanVSplitters: array[1..MAX_PANS-1] of TPanel; // слева от колонки пана i FPansRestored: Boolean; // персист панов восстановлен в этой Run-сессии // Drag вертикального сплиттера (индекс пана справа; 0 = нет). @@ -684,6 +685,8 @@ type procedure PanSplitMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer); procedure OnPanGridToggle(Sender: TObject); // «▦/▤» в ряду зума пана 0 function PanGridActive: Boolean; // грид реально применён + function PanGridTwoCol: Boolean; // грид из 2 колонок (пан 0 + 1) + function ExtraPanCount: Integer; // доп. паны в раскладке procedure PanVSplitMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); procedure PanVSplitMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer); @@ -1170,8 +1173,9 @@ begin FPan.OnAddPan := OnAddPanClick; FPan.OnADCClick := OnPanADCClick; // бейдж RX1/RX2 (3.4, платы с 2 АЦП) FPan.OnDisplayClick := OnPanDisplayClick; // «◑» дисплей-поповер (3.6) - FPans[0] := FPan; - FPanShare[0] := 1.0; + FPans[0] := FPan; + FPanShare[0] := 1.0; + FPanColShare[0] := 1.0; // грид «две колонки»: пан 0 — тоже колонка FPanGridMode := False; FPanGridRowShare := 0.5; // грид: пан 0 — верхняя половина FPanCbLock := SyncObjs.TCriticalSection.Create; @@ -2700,7 +2704,7 @@ procedure TMainForm.ResizeSpectrumPanels; const WB_H = 118; var - RW, RH, WideH, WideSpecH: Integer; + RW, RH, WideH, WideSpecH, WideTop: Integer; RULER_H: Integer; PANZOOM_H: Integer; begin @@ -2721,10 +2725,13 @@ begin WideH := Min(MulDiv(WB_H, Screen.PixelsPerInch, 96), Max(40, RH div 2)); if WideH > 0 then WideSpecH := Max(1, WideH - RULER_H); + // Wideband — всегда во всю ширину PanelRight, самой верхней полосой. + // В обычном стеке над ним стоит шапка пана 0; в гриде «две колонки» шапки + // панов уходят в свои колонки ПОД wideband, поэтому он начинается с нуля. + WideTop := IfThen(PanGridTwoCol, 0, Pan0HeaderH); if PbWideband <> nil then begin - // При стеке панов над wideband стоит шапка пана 0. - PbWideband.SetBounds(0, Pan0HeaderH, RW, WideSpecH); + PbWideband.SetBounds(0, WideTop, RW, WideSpecH); PbWideband.Visible := FController.FShowWideband and (WideSpecH > 0); if FController.FShowWideband and (RW > 0) and (WideSpecH > 0) then begin @@ -2734,7 +2741,7 @@ begin end; if PbWidebandRuler <> nil then begin - PbWidebandRuler.SetBounds(0, Pan0HeaderH + WideSpecH, RW, IfThen(FController.FShowWideband and (WideH > 0), RULER_H, 0)); + PbWidebandRuler.SetBounds(0, WideTop + WideSpecH, RW, IfThen(FController.FShowWideband and (WideH > 0), RULER_H, 0)); PbWidebandRuler.Visible := FController.FShowWideband and (WideH > 0); if FController.FShowWideband and (RW > 0) and (WideH > 0) then begin @@ -2768,15 +2775,26 @@ begin Result := 0; end; -function TMainForm.PanGridActive: Boolean; -// Грид реально применён: включён юзером и в стеке >=2 доп. панов -// (с одним доп. паном грид совпадает со стеком). -var i, N: Integer; +function TMainForm.ExtraPanCount: Integer; +// Доп. паны (без пана 0), участвующие в раскладке (плавающие не в счёт). +var i: Integer; begin - N := 0; + Result := 0; for i := 1 to MAX_PANS - 1 do - if (FPans[i] <> nil) and not PanFloating(i) then Inc(N); - Result := FPanGridMode and (N >= 2); + if (FPans[i] <> nil) and not PanFloating(i) then Inc(Result); +end; + +function TMainForm.PanGridActive: Boolean; +// Грид реально применён: включён юзером и есть хотя бы один доп. пан в стеке. +begin + Result := FPanGridMode and (ExtraPanCount >= 1); +end; + +function TMainForm.PanGridTwoCol: Boolean; +// Частный случай грида: пан 0 + ОДИН доп. пан = две колонки бок о бок во всю +// высоту (верхнего ряда/горизонтального сплиттера нет). +begin + Result := FPanGridMode and (ExtraPanCount = 1); end; function TMainForm.PanMinHeight(PanId: Integer): Integer; @@ -2795,7 +2813,7 @@ const var ActiveIds: array[0..MAX_PANS-1] of Integer; MinH: array[0..MAX_PANS-1] of Integer; - N, i, k, TotalH, TotalW, UsableH, UsableW, Y, X, Hi, Wi: Integer; + N, i, k, TotalH, TotalW, UsableH, UsableW, Y, X, Hi, Wi, MinW: Integer; Row0H, RowTop, RowH, M, MinRest, RemainH, BotMin: Integer; ShareSum: Double; Grid: Boolean; @@ -2809,14 +2827,15 @@ begin FPanWideH := AWideH; // для PanMinHeight (в т.ч. из сплиттер-драга) TotalH := PanelRight.ClientHeight; TotalW := PanelRight.ClientWidth; - Grid := PanGridActive; // пан 0 + >=2 доп. панов колонками + Grid := PanGridActive; // 2 колонки (пан 0 + 1 доп.) либо ряд + колонки - // Тумблер «▦/▤» в ряду зума пана 0: есть смысл при >=2 доп. панах в стеке. + // Тумблер «▦/▤» в ряду зума пана 0: есть смысл уже при ОДНОМ доп. пане + // (грид = пан 0 и доп. пан бок о бок вместо «друг под другом»). if FPans[0] <> nil then begin - // Виден при >=2 доп. панах; при включённом гриде — всегда (выключить можно + // Виден при >=1 доп. пане; при включённом гриде — всегда (выключить можно // даже когда паны закрылись и раскладка сама откатилась в стек). - FPans[0].GridBtnEnabled := FPanGridMode or (N >= 3); + FPans[0].GridBtnEnabled := FPanGridMode or (N >= 2); if FPans[0].BtnGrid <> nil then begin if FPanGridMode then FPans[0].BtnGrid.Caption := '▤' @@ -2885,6 +2904,47 @@ begin Exit; end; + if PanGridTwoCol then + begin + // ---- Грид «две колонки»: пан 0 слева, единственный доп. пан справа ---- + // Wideband остаётся сверху во всю ширину (его полосу кладёт + // ResizeSpectrumPanels), обе колонки начинаются ПОД ним и идут до низа; + // между ними вертикальный сплиттер (доли FPanColShare[0]/FPanColShare[доп]). + i := ActiveIds[1]; // единственный застекованный доп. пан + for k := 1 to MAX_PANS - 1 do + begin + if FPanSplitters[k] <> nil then FPanSplitters[k].Visible := False; + if (k <> i) and (FPanVSplitters[k] <> nil) then + FPanVSplitters[k].Visible := False; + end; + FPans[0].HeaderVisible := Pan0HeaderVisible; + FPans[i].HeaderVisible := True; + UsableW := TotalW - PAN_SPLIT_H; + ShareSum := FPanColShare[0] + FPanColShare[i]; + if ShareSum <= 0 then ShareSum := 1; + MinW := Round(0.15 * TotalW); // = кламп драга вертикального сплиттера + Wi := Round(UsableW * FPanColShare[0] / ShareSum); + if Wi < MinW then Wi := MinW; + if Wi > UsableW - MinW then Wi := UsableW - MinW; + if Wi < 1 then Wi := 1; + RowTop := AWideH; // низ полосы wideband = верх обеих колонок + RowH := TotalH - RowTop; + FPans[0].StackTop := RowTop; + FPans[0].StackHeight := RowH; + FPans[0].StackLeft := 0; + FPans[0].StackWidth := Wi; + // ATopOffset=0: wideband вне региона пана 0 (он уже учтён в StackTop). + FPans[0].Layout(0, FController.FShowSpectrum, FController.FShowWaterfall); + FPanVSplitters[i].Visible := True; + FPanVSplitters[i].SetBounds(Wi, RowTop, PAN_SPLIT_H, RowH); + FPans[i].StackTop := RowTop; + FPans[i].StackHeight := RowH; + FPans[i].StackLeft := Wi + PAN_SPLIT_H; + FPans[i].StackWidth := TotalW - Wi - PAN_SPLIT_H; + FPans[i].Layout(0, FController.FShowSpectrum, FController.FShowWaterfall); + Exit; + end; + // ---- Грид: пан 0 сверху во всю ширину, доп. паны колонками снизу ---- FPanGridRowShare := EnsureRange(FPanGridRowShare, 0.15, 0.85); Row0H := Round((TotalH - PAN_SPLIT_H) * FPanGridRowShare); @@ -3466,8 +3526,13 @@ begin // Важно: проверяем только видимые панели — когда спектр скрыт, // PbSpectrum.Height может быть ненулевым при FPan.SpectrumHeight=0, // что вызвало бы сброс буфера водопада на каждом тике. + // Wideband сверяем с шириной PanelRight, а НЕ со спектром пана 0: в гриде + // «две колонки» пан 0 — половина ширины, а wideband остаётся во всю (иначе + // условие вечно истинно и тик дёргает ResizeSpectrumPanels каждый кадр — + // раскладка пересчитывается под курсором и сплиттеры не тянутся). if (FPan.SpectrumWidth = 0) or - (FController.FShowWideband and (PbWideband.Width <> FPan.SpectrumWidth)) or + (FController.FShowWideband and (PanelRight <> nil) and + (PbWideband.Width <> PanelRight.ClientWidth)) or (FController.FShowSpectrum and ((PbSpectrum.Width <> FPan.SpectrumWidth) or (PbSpectrum.Height <> FPan.SpectrumHeight))) or (FController.FShowWaterfall and not FController.FShowSpectrum and @@ -6714,6 +6779,7 @@ begin for i := 0 to MAX_PANS - 1 do begin Cfg.Pans[i].StackShare := FPanShare[i]; + Cfg.Pans[i].ColShare := FPanColShare[i]; // пан 0 — колонка в гриде «2 колонки» if FPans[i] <> nil then Cfg.Pans[i].SplitRatio := FPans[i].SplitterRatio; if i > 0 then begin @@ -6724,7 +6790,6 @@ begin Cfg.Pans[i].ADC := FController.PanADC(i); Cfg.Pans[i].Zoom := FController.PanZoomFactor(i); Cfg.Pans[i].PanSlider := FController.PanPanSlider(i); - Cfg.Pans[i].ColShare := FPanColShare[i]; // Per-pan дисплей (3.6): пишем override только если пан его задал. if (FPans[i] <> nil) and FPans[i].DisplayOverride then begin @@ -6822,7 +6887,8 @@ begin FPanGridRowShare := Cfg.GridRowShare; // Пан 0: восстановить его доп. слайсы (главный VFO — не слайс) + доли. - FPanShare[0] := Cfg.Pans[0].StackShare; + FPanShare[0] := Cfg.Pans[0].StackShare; + FPanColShare[0] := Cfg.Pans[0].ColShare; FPan.SplitterRatio := Cfg.Pans[0].SplitRatio; for k := 0 to High(Cfg.Pans[0].Slices) do RestoreSliceInto(FPan, 0, Cfg.Pans[0].Slices[k]); @@ -7086,6 +7152,7 @@ var Split: TPanel; begin if Button <> mbLeft then Exit; + if PanGridTwoCol then Exit; // две колонки: горизонтальных сплиттеров нет Split := TPanel(Sender); BelowId := Split.Tag; TotalH := PanelRight.ClientHeight; @@ -7148,7 +7215,7 @@ begin TPanel(Sender).Color := TColor($00303030); TotalH := PanelRight.ClientHeight; if TotalH <= 0 then Exit; - if PanGridActive then + if PanGridActive and not PanGridTwoCol then begin // Грид: сплиттер задаёт долю высоты верхнего ряда (пан 0) напрямую. FPanGridRowShare := EnsureRange(TPanel(Sender).Top / TotalH, 0.15, 0.85); @@ -7190,9 +7257,10 @@ begin if (Button <> mbLeft) or not PanGridActive then Exit; Split := TPanel(Sender); RightId := Split.Tag; - // Колонка слева — ближайший застекованный доп. пан с меньшим PanId. + // Колонка слева — ближайший застекованный пан с меньшим PanId (в гриде из + // двух колонок левая колонка — сам пан 0, в ряду колонок — доп. пан). LeftId := -1; - for i := RightId - 1 downto 1 do + for i := RightId - 1 downto IfThen(PanGridTwoCol, 0, 1) do if (FPans[i] <> nil) and not PanFloating(i) then begin LeftId := i; Break; end; if (LeftId < 0) or (FPans[RightId] = nil) then Exit; TotalW := PanelRight.ClientWidth; @@ -7231,14 +7299,14 @@ begin FPanVSplitDrag := 0; TPanel(Sender).Color := TColor($00303030); LeftId := -1; - for i := RightId - 1 downto 1 do + for i := RightId - 1 downto IfThen(PanGridTwoCol, 0, 1) do if (FPans[i] <> nil) and not PanFloating(i) then begin LeftId := i; Break; end; if (LeftId < 0) or (FPans[RightId] = nil) then Exit; TotalW := PanelRight.ClientWidth; if TotalW <= 0 then Exit; // Доли колонок нормируются суммой (init 1.0 на пан) — пиксели → доли через неё. ShareSum := 0; - for i := 1 to MAX_PANS - 1 do + for i := IfThen(PanGridTwoCol, 0, 1) to MAX_PANS - 1 do if (FPans[i] <> nil) and not PanFloating(i) then ShareSum := ShareSum + FPanColShare[i]; if ShareSum <= 0 then ShareSum := 1; diff --git a/ewsdr.lpi b/ewsdr.lpi index 421591e..36c821f 100644 --- a/ewsdr.lpi +++ b/ewsdr.lpi @@ -17,9 +17,9 @@ - + - + @@ -130,6 +130,11 @@ + + + + + From 3bdcfa06748860112508d78fc9bdb7f4c2ab5e6a Mon Sep 17 00:00:00 2001 From: Vladimir Date: Wed, 22 Jul 2026 12:11:57 +0300 Subject: [PATCH 2/4] =?UTF-8?q?ui(pans):=20=D1=84=D0=BB=D0=B0=D0=B3=20A=20?= =?UTF-8?q?=D0=BF=D0=BE=D1=8F=D0=B2=D0=BB=D1=8F=D0=B5=D1=82=D1=81=D1=8F=20?= =?UTF-8?q?=D0=B2=D0=BC=D0=B5=D1=81=D1=82=D0=B5=20=D1=81=20=D0=BF=D0=B5?= =?UTF-8?q?=D1=80=D0=B2=D1=8B=D0=BC=20=D0=B4=D0=BE=D0=BF.=20=D0=BF=D0=B0?= =?UTF-8?q?=D0=BD=D0=B0=D0=B4=D0=B0=D0=BF=D1=82=D0=B5=D1=80=D0=BE=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Доп. панадаптер приходит со своим слайсом и сразу рисует его флаг, а главный приёмник оставался без флага — [A] показывался только когда слайс создавали на самом пане 0 (инвариант «панель скрыта ИЛИ есть слайсы B+»). TPanafallPanel: третье слагаемое инварианта — MainFlagForced, выставляется хозяином. MainForm.ResizeSpectrumPanels после LayoutPanStack ставит FPan.MainFlagForced := PanCount > 1 и зовёт UpdateMainFlagVisibility; это единая точка — добавление/закрытие пана, restore персиста и STOP все идут через пересчёт раскладки. Сворачивание левой панели (MainFlagPinned) на это не влияет: пока жив хотя бы один доп. пан, флаг A остаётся. Co-Authored-By: Claude Opus 4.8 --- MainForm.pas | 8 ++++++++ PanafallPanel.pas | 16 +++++++++++----- ewsdr.lpi | 5 +++-- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/MainForm.pas b/MainForm.pas index f0d98f6..7d17be9 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -2755,6 +2755,14 @@ begin // Стек панов: регионы по долям, каждый пан раскладывает свой стек // спектр/линейка/сплиттер/водопад/пан-зум внутри своего региона. LayoutPanStack(WideH); + // Доп. панадаптер приходит со своим слайсом и сразу показывает его флаг — + // главный приёмник должен быть представлен флагом A с того же момента + // (раньше флаг A появлялся только от слайса, созданного на самом пане 0). + if FPan <> nil then + begin + FPan.MainFlagForced := PanCount > 1; + FPan.UpdateMainFlagVisibility; + end; PositionSampleRateOverlay; end; diff --git a/PanafallPanel.pas b/PanafallPanel.pas index 9f221d2..adaebc4 100644 --- a/PanafallPanel.pas +++ b/PanafallPanel.pas @@ -84,6 +84,7 @@ type FActiveFlag: TVfoOverlay; // чьё событие сейчас обрабатывается FPendingSliceClose: Integer; // SliceId к удалению после мыши (0=нет) FMainFlagPinned: Boolean; // True = левая панель хозяина скрыта + FMainFlagForced: Boolean; // True = есть доп. панадаптеры FLastSliceMeterMs: QWord; // троттлинг S-метра слайсов (~10 Гц) FLastSliceDMRMs: QWord; // семантический DMR-статус (~4 Гц) FOnWireFlag: TWireFlagEvent; @@ -278,7 +279,8 @@ type function CursorOverDisplay: Boolean; // курсор над спектром/водопадом пана // Единый packing всех флагов (A+B+) в один ряд, без наездов. procedure LayoutFlags; - // Флаг A виден: панель хозяина скрыта (MainFlagPinned) ИЛИ есть слайсы B+. + // Флаг A виден: панель хозяина скрыта (MainFlagPinned) ИЛИ есть слайсы B+ + // ИЛИ хозяин требует его принудительно (MainFlagForced — доп. панадаптеры). procedure UpdateMainFlagVisibility; procedure PushSliceFlagState(SliceId: Integer); procedure PushAllSliceFlagStates; // все флаги панели (после смены rate и т.п.) @@ -297,6 +299,9 @@ type property Controller: TRadioController read FController write FController; property MainFlag: TVfoOverlay read FMainFlag; property MainFlagPinned: Boolean read FMainFlagPinned write FMainFlagPinned; + // Хозяин форсит флаг A (есть доп. панадаптеры — их слайсы должны быть + // представлены флагом и на главном приёмнике). + property MainFlagForced: Boolean read FMainFlagForced write FMainFlagForced; // Проводка семантических событий флага (MODE/DSP/AUD/…) — хозяин. property OnWireFlag: TWireFlagEvent read FOnWireFlag write FOnWireFlag; // Слайс закрыт «×» — хозяину сбросить активный слайс и т.п. @@ -1286,15 +1291,16 @@ begin end; // Флаг главного приёмника (слайс A) виден, когда СКРЫТА левая панель хозяина -// (MainFlagPinned) ЛИБО есть доп. слайсы (B+): тогда A всегда представлен -// флагом рядом с B/C…, а сворачивание/раскрытие панели при живых слайсах не -// дёргает его появление. +// (MainFlagPinned) ЛИБО есть доп. слайсы (B+) ЛИБО живут доп. панадаптеры +// (MainFlagForced — их слайсы уже показаны флагами, главный приёмник должен +// быть представлен так же): тогда A всегда представлен флагом рядом с B/C…, +// а сворачивание/раскрытие панели при живых слайсах не дёргает его появление. procedure TPanafallPanel.UpdateMainFlagVisibility; var ShouldShow: Boolean; begin if (FMainFlag = nil) or (FController = nil) then Exit; - ShouldShow := FMainFlagPinned or (Length(FSliceFlags) > 0); + ShouldShow := FMainFlagPinned or FMainFlagForced or (Length(FSliceFlags) > 0); if ShouldShow = FMainFlag.Visible then Exit; if ShouldShow then begin diff --git a/ewsdr.lpi b/ewsdr.lpi index 36c821f..f7ae74f 100644 --- a/ewsdr.lpi +++ b/ewsdr.lpi @@ -17,9 +17,9 @@ - + - + @@ -135,6 +135,7 @@ + From c857aa9d4b1650985bd33a10d091a3ba32a28cab Mon Sep 17 00:00:00 2001 From: Vladimir Date: Wed, 22 Jul 2026 12:16:49 +0300 Subject: [PATCH 3/4] =?UTF-8?q?fix(slices):=20squelch=20=D1=81=D0=BB=D0=B0?= =?UTF-8?q?=D0=B9=D1=81=D0=B0=20=D0=B3=D0=B5=D0=B9=D1=82=D0=B8=D1=82=D1=81?= =?UTF-8?q?=D1=8F=20=D1=80=D0=B5=D0=B6=D0=B8=D0=BC=D0=BE=D0=BC=20=E2=80=94?= =?UTF-8?q?=20=D1=83=D1=85=D0=BE=D0=B4=20=D0=B8=D0=B7=20FM=20=D0=B1=D0=BE?= =?UTF-8?q?=D0=BB=D1=8C=D1=88=D0=B5=20=D0=BD=D0=B5=20=D0=B3=D0=BB=D1=83?= =?UTF-8?q?=D1=88=D0=B8=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Юзер: на слайсе включил FM с шумодавом, переключил модуляцию — звука нет, а шумодава в не-FM режимах нет, выключить нечем. Блок fmsq WDSP стоит в тракте и глушит звук по шумовому выходу FM- дискриминатора. У главного приёмника он всегда гейтится режимом (ApplyModeFilter: SetFMSquelch((FMode = MODE_FM) and FFMSQOn, …), в не-FM ветке — явное выключение), а у слайсов SetSliceFMSquelch дёргал SetRXAFMSQRun(Chan,1) безусловно и после ухода из FM squelch оставался. WDSPEngine: новый ApplySliceFMSQ(Idx) — FMSQOn and (Mode = MODE_FM), то же правило, что у главного. Зовётся из SetSliceMode (ветка без пересоздания канала) и SetSliceFMSquelch, тем же гейтом накрыт OpenSliceChannel (пересоздание канала при смене rate/WFM). Выбор юзера не теряется: FMSQOn остаётся в записи слайса и в персисте, при возврате в FM восстанавливается. Co-Authored-By: Claude Opus 4.8 --- WDSPEngine.pas | 24 +++++++++++++++++++++--- ewsdr.lpi | 5 +++-- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/WDSPEngine.pas b/WDSPEngine.pas index 5e8de55..5593579 100644 --- a/WDSPEngine.pas +++ b/WDSPEngine.pas @@ -539,6 +539,7 @@ type procedure ApplySNBToChan(Chan: Integer; On_: Boolean); procedure ApplyANFToChan(Chan: Integer; On_: Boolean); procedure ApplyFMSQToChan(Chan: Integer; Enable: Boolean; Level: Integer); + procedure ApplySliceFMSQ(Idx: Integer); // squelch слайса с гейтом по режиму procedure ApplyNoiseFilterState; procedure ProcessTXBlock; // вызывается из TTXDSPThread procedure FeedTXDisplaySample(const I, Q: Double); @@ -2394,7 +2395,8 @@ begin ApplyNBToChan(Chan, FSlices[Idx].NBMode); ApplySNBToChan(Chan, FSlices[Idx].SNBOn); ApplyANFToChan(Chan, FSlices[Idx].ANFOn); - ApplyFMSQToChan(Chan, FSlices[Idx].FMSQOn, FSlices[Idx].FMSQLevel); + ApplyFMSQToChan(Chan, FSlices[Idx].FMSQOn and (FSlices[Idx].Mode = MODE_FM), + FSlices[Idx].FMSQLevel); // Decoder input must not disappear or change scale with speaker volume. if FSlices[Idx].Mode in [MODE_DMR, MODE_FMRAW] then SetRXAPanelGain1(Chan, 1.0) @@ -2693,6 +2695,9 @@ begin SetRXAPanelGain1(FSlices[idx].Chan, 1.0) else SetRXAPanelGain1(FSlices[idx].Chan, FSlices[idx].Volume); + // Ушли из FM — снять squelch (иначе слайс остаётся заглушенным, а + // шумодава в не-FM режимах нет); вернулись в FM — восстановить выбор. + ApplySliceFMSQ(idx); end; finally FSliceLock.Leave; @@ -2821,13 +2826,26 @@ begin idx := FindSliceIdx(Id); if idx < 0 then Exit; FSlices[idx].FMSQOn := Enable; FSlices[idx].FMSQLevel := Level; - if FSlices[idx].Opened then - ApplyFMSQToChan(FSlices[idx].Chan, Enable, Level); + ApplySliceFMSQ(idx); finally FSliceLock.Leave; end; end; +// Squelch слайса с гейтом по режиму — как у главного приёмника +// (ApplyModeFilter: включён только в MODE_FM). Блок fmsq WDSP стоит в тракте +// и глушит звук по шумовому выходу FM-дискриминатора: оставшись включённым +// после ухода из FM, он затыкает слайс намертво, а органов управления +// шумодавом в не-FM режимах нет. Пользовательский выбор при этом сохраняется +// в FMSQOn и возвращается вместе с режимом FM. +procedure TWDSPEngine.ApplySliceFMSQ(Idx: Integer); +begin + if (Idx < 0) or (Idx > High(FSlices)) or not FSlices[Idx].Opened then Exit; + ApplyFMSQToChan(FSlices[Idx].Chan, + FSlices[Idx].FMSQOn and (FSlices[Idx].Mode = MODE_FM), + FSlices[Idx].FMSQLevel); +end; + function TWDSPEngine.GetSliceSMeter(Id: Integer): Double; var idx: Integer; begin diff --git a/ewsdr.lpi b/ewsdr.lpi index f7ae74f..4792265 100644 --- a/ewsdr.lpi +++ b/ewsdr.lpi @@ -17,9 +17,9 @@ - + - + @@ -136,6 +136,7 @@ + From 02288292ff56d55ead4d87204904a52994736d99 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Wed, 22 Jul 2026 19:22:30 +0300 Subject: [PATCH 4/4] =?UTF-8?q?feat(pans):=20=D1=81=D0=B5=D1=82=D0=BA?= =?UTF-8?q?=D0=B0=20FM-=D0=BA=D0=B0=D0=BD=D0=B0=D0=BB=D0=BE=D0=B2=20=D0=B8?= =?UTF-8?q?=20=D1=88=D0=B0=D0=B3=20=D1=82=D1=8E=D0=BD=D0=B8=D0=BD=D0=B3?= =?UTF-8?q?=D0=B0=20=D0=BD=D0=B0=20=D0=B4=D0=BE=D0=BF.=20=D0=BF=D0=B0?= =?UTF-8?q?=D0=BD=D0=B0=D0=B4=D0=B0=D0=BF=D1=82=D0=B5=D1=80=D0=B0=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Доп. паны не имеют своего FMode — режим берётся со слайсов пана: PanHasFMSlice(PanId) = есть слайс в FM/DMR/FMRAW. По нему SyncPanZoomUI выставляет View.FMGridStepHz = FM_STEP_HZ[FFMStepIdx], как SyncSpecViewFreq делает для главного пана (сеттер сам гасит кэши спектра и линейки). Тюнинг слайса привязан к той же сетке: • колесо над слайсом/доп. паном — было зашито 12500 и только DMR/FMRAW, теперь FM_STEP_HZ[FFMStepIdx] во всех трёх канальных режимах (гейт FFMStepOn); • PanClickTune — там была та же зашитая 12500, теперь шаг режима целевого слайса, как в DoSpectrumClick главного пана. Co-Authored-By: Claude Opus 4.8 --- MainForm.pas | 41 ++++++++++++++++++++++++++++++++--------- RadioController.pas | 16 ++++++++++++++++ 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/MainForm.pas b/MainForm.pas index 7d17be9..7f8e4ca 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -5983,12 +5983,24 @@ end; procedure TMainForm.SyncPanZoomUI(P: TPanafallPanel); // Вьюха и зум-бар пана ← контроллер (после смены зума/центра/rate пана). -var VC, VS: Double; +var VC, VS, StepHz: Double; begin if (P = nil) or (P.PanId = 0) then Exit; FController.GetPanViewWindow(P.PanId, VC, VS); P.View.CenterFreq := VC; P.View.SpanHz := VS; + // Сетка каналов по шагу FM — как на главном пане, но по режиму слайсов + // ЭТОГО пана (у доп. панов нет своего FMode). + if FController.PanHasFMSlice(P.PanId) then + StepHz := FM_STEP_HZ[FController.FFMStepIdx] + else + StepHz := 0; + if Abs(P.View.FMGridStepHz - StepHz) > 0.5 then + begin + P.View.FMGridStepHz := StepHz; // сеттер сам сбросит кэши спектра/линейки + MarkPanDirty(P); + if P.PbRuler <> nil then P.PbRuler.Invalidate; + end; P.ZoomBar.SetFreq(FController.PanDDCFreq(P.PanId), FController.PanDDCRateKHz(P.PanId) * 1000.0); P.ZoomBar.SetState(FController.PanZoomFactor(P.PanId), @@ -6422,7 +6434,7 @@ procedure TMainForm.PanClickTune(P: TPanafallPanel; X, W: Integer); var VC, VS, LoHz, HiHz: Double; Hz, StepHz: Int64; - Id: Integer; + Id, ModeHere: Integer; S: TCtrlSlice; begin PanViewWindow(P, VC, VS); @@ -6432,11 +6444,18 @@ begin Id := FActiveSliceId else Id := P.FirstSliceId; - StepHz := 100; - if ((Id <> 0) and FController.GetSlice(Id, S) and - (S.Mode in [MODE_DMR, MODE_FMRAW])) or - ((Id = 0) and (FController.FMode in [MODE_DMR, MODE_FMRAW])) then - StepHz := 12500; + // Канальные режимы: клик садится на ту же сетку, что нарисована на пане. + if Id <> 0 then + begin + if FController.GetSlice(Id, S) then ModeHere := S.Mode + else ModeHere := FController.FMode; + end + else + ModeHere := FController.FMode; + if (ModeHere in [MODE_FM, MODE_DMR, MODE_FMRAW]) and FController.FFMStepOn then + StepHz := FM_STEP_HZ[FController.FFMStepIdx] + else + StepHz := 100; Hz := Round(VC + (X / W - 0.5) * VS); Hz := ((Hz + StepHz div 2) div StepHz) * StepHz; FController.CapturedSpan(LoHz, HiHz, P.PanId); @@ -8003,8 +8022,12 @@ begin end; if (SliceId <> 0) and FController.GetSlice(SliceId, Sl) then begin - if (Sl.Mode in [MODE_DMR, MODE_FMRAW]) and - not ((ssCtrl in Shift) or (ssShift in Shift)) then Step := 12500 + // Канальные режимы: шаг = выбранный шаг сетки (тот же, что рисуется на + // пане), как на главном панадаптере. Ctrl/Shift — обычные мелкие шаги. + if (Sl.Mode in [MODE_FM, MODE_DMR, MODE_FMRAW]) and + FController.FFMStepOn and + not ((ssCtrl in Shift) or (ssShift in Shift)) then + Step := FM_STEP_HZ[FController.FFMStepIdx] else if (ssCtrl in Shift) and (ssShift in Shift) then Step := 1000000 else if ssCtrl in Shift then Step := 1000 else if ssShift in Shift then Step := 10 diff --git a/RadioController.pas b/RadioController.pas index 996f726..0d26626 100644 --- a/RadioController.pas +++ b/RadioController.pas @@ -649,6 +649,7 @@ type procedure SetSliceMute(Id: Integer; Mute: Boolean); procedure SetSliceRxMuteOnTx(Id: Integer; On_: Boolean); // self-monitor слайса на TX function PanPrimarySliceId(PanId: Integer): Integer; // первый созданный слайс пана + function PanHasFMSlice(PanId: Integer): Boolean; // есть слайс в канальном режиме (FM/DMR/FMRAW)? procedure SetSliceDSP(Id, NRMode, NBMode: Integer; SNBOn, ANFOn: Boolean); procedure SetSliceFMSquelch(Id: Integer; On_: Boolean; Level: Integer); function SliceSMeter(Id: Integer): Double; // dBm, из DSP-потока @@ -2093,6 +2094,21 @@ begin Result := FSlices[i].Id; end; +function TRadioController.PanHasFMSlice(PanId: Integer): Boolean; +// Пан «канальный», если хотя бы один его слайс сидит в FM/DMR/FMRAW: под это +// UI рисует сетку частот по шагу FFMStepIdx (как на главном панадаптере). +var i: Integer; +begin + Result := False; + for i := 0 to MAX_SLICES - 1 do + if FSlices[i].Active and (FSlices[i].PanId = PanId) and + (FSlices[i].Mode in [MODE_FM, MODE_DMR, MODE_FMRAW]) then + begin + Result := True; + Exit; + end; +end; + procedure TRadioController.SetSliceDSP(Id, NRMode, NBMode: Integer; SNBOn, ANFOn: Boolean); var idx: Integer;