mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 18:43:51 +00:00
feat(filter): произвольные кромки фильтра — таблица по режиму, редактор, drag, TX-полоска
Фильтр главного приёмника перестал быть одним числом. Теперь это таблица (Name, Low, High) на режим: 10 пресетов + VAR1/VAR2, знаковые кромки (у LSB своя строка, у USB своя), паритет с Thetis FilterForm. Слайсы независимые кромки умели и раньше — главный приёмник настраивался только пресетами. Модель (Settings/RadioController): - TFilterSlot/TFilterSet/TFilterTable + заводские дефолты из прежних FILT_*_BW и FILT_*_NAMES (переехали в Settings — из них строится таблица). - FFilterLo/FFilterHi = истина, FFilterBW производная (FilterBWFromEdges), поэтому все прежние читатели ширины (web, зум, band-cache, CAT) не тронуты. - ApplyFilterSlot — единственная точка «слот → состояние тракта». - SetFilterEdges: произвольные кромки уходят в VAR1 с автопереключением, пресеты нельзя испортить случайным движением мыши или энкодера. - Персист: секция "filters" под MAC, пишутся только режимы, отличные от заводских; секция режима сносится целиком, иначе возврат слота к заводскому оставлял бы старый ключ. - Диапазон помнит только выбранный слот: кромки живут в таблице, второго источника истины у VAR нет. UI: - 12 кнопок фильтра (2 ряда по 6), подписи из живой таблицы. - FilterPopup — поповер редактора по ПКМ на кнопке фильтра, по образцу PureSignalPopup (дочерний контрол формы, не top-level: Wayland). Слева слоты, справа Name/Low/High/Width, внизу форма полосы на знаковой оси. - Перетаскивание края полосы на спектре: ±5px, снап 10 Гц, Alt = симметрично (не Ctrl — он создаёт слайс). Отказ на передаче и когда полоса на экране уже 12px, иначе захват съедал бы клик-тюн. Отрисовка: - Полоса главного VFO рисуется по РЕАЛЬНЫМ кромкам приёмника, слайса — по его кромкам; вывод из режима и ширины остался фолбэком. Пара→ширина→пара было преобразованием с потерями: несимметричный фильтр из слайс-CAT (ZZFL/ZZFH) рисовался не там, где звучал. - На передаче полоса считается по TX-фильтру (TXSignedEdges), а не по приёмной ширине — TX-полоса от RX не зависит (модель Thetis). - CalcFilterBandXFor больше не держит свою копию таблицы знака боковой: единственная таблица теперь FilterEdgesFromBW. Управление: - CAT ZZFL/ZZFH заработали на основном порту (были заглушками). - Андромеда: энкодеры #4/#5 — независимые Filter High/Low, как на железе G2 (было: #4 ширина, #5 не реализован). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+247
-137
@@ -22,7 +22,8 @@ unit MainForm;
|
||||
interface
|
||||
|
||||
uses
|
||||
Classes, SysUtils, StrUtils, FreqDisplay, DeviceForm, VfoOverlay, SampleRateOverlay, BandPlanOverlay,
|
||||
Classes, SysUtils, StrUtils, FreqDisplay, DeviceForm, FilterPopup,
|
||||
VfoOverlay, SampleRateOverlay, BandPlanOverlay,
|
||||
FlatButton, FlatSlider, FlatDropDown, AppTheme, Forms, Controls, Graphics, Dialogs,
|
||||
StdCtrls, ExtCtrls, Buttons, Menus, Math, Types, SyncObjs,
|
||||
OpenGLContextEx,
|
||||
@@ -68,23 +69,12 @@ const
|
||||
WDSP_WISDOM_FILE = 'wdspWisdom00';
|
||||
|
||||
BAND_COUNT = 12; // макс. слотов среди планов (Pluto VHF/UHF = 12, HF = 11)
|
||||
// Полосы/дефолты фильтров (FILT_COUNT, FILT_*_BW, FILT_*_DEF, FILT_FM_*)
|
||||
// перенесены в Settings.pas — нужны TRadioController. Здесь остаются только
|
||||
// UI-подписи кнопок (NAMES).
|
||||
FILT_SSB_NAMES: array[0..FILT_COUNT-1] of string =
|
||||
('5.0k','4.4k','3.8k','3.3k','2.9k','2.7k','2.4k','2.1k','1.8k','1.0k');
|
||||
FILT_CW_NAMES: array[0..FILT_COUNT-1] of string =
|
||||
('1.0k','800','600','500','400','250','150','100','50','25');
|
||||
FILT_DSB_NAMES: array[0..FILT_COUNT-1] of string =
|
||||
('16k','12k','10k','8.0k','6.6k','5.2k','4.0k','3.1k','2.9k','2.4k');
|
||||
FILT_AM_NAMES: array[0..FILT_COUNT-1] of string =
|
||||
('20k','18k','16k','12k','10k','9.0k','8.0k','7.0k','6.0k','5.0k');
|
||||
FILT_DIGI_NAMES: array[0..FILT_COUNT-1] of string =
|
||||
('3.0k','2.7k','2.4k','2.1k','1.8k','1.5k','1.2k','900','600','300');
|
||||
FILT_FM_NAMES: array[0..1] of string = ('NFM', 'FM');
|
||||
FILT_WFM_NAMES: array[0..FILT_WFM_COUNT-1] of string = ('200k', '160k', '120k');
|
||||
FILT_RAW_NAMES: array[0..FILT_RAW_COUNT-1] of string =
|
||||
('6.25k', '10k', '12.5k', '20k', '25k');
|
||||
// Полосы/дефолты/подписи фильтров (FILT_COUNT, FILT_*_BW, FILT_*_DEF,
|
||||
// FILT_*_NAMES) живут в Settings.pas: из них строится заводская таблица
|
||||
// фильтров, а подписи кнопок теперь берутся из живой таблицы контроллера.
|
||||
FILT_GRID_COLS = 6; // сетка кнопок фильтра: 2 ряда по 6 (12 = 10 + VAR1/2)
|
||||
EDGE_GRAB_PX = 5; // зона захвата края полосы фильтра мышью, px
|
||||
EDGE_SNAP_HZ = 10; // снап края при перетаскивании
|
||||
|
||||
// CTCSS tones (38 standard tones, no None — toggle via CTCSS button)
|
||||
// CTCSS_COUNT и CTCSS_TONES перенесены в FMRepeater (нужны TRadioController).
|
||||
@@ -132,11 +122,17 @@ type
|
||||
// ---- Network ----
|
||||
// Список устройств (saved+discovered) → FController.FDeviceStore (общий с web).
|
||||
FDeviceDialog: TDeviceDialog;
|
||||
FFilterPop: TFilterPopup; // поповер редактора фильтров (ПКМ по кнопке FILTER)
|
||||
FPendingIP: string; // IP устройства для подключения
|
||||
// Флаг главного (слайс A): создаётся здесь (проводка событий), живёт в
|
||||
// раскладке/диспетчере FPan (AttachMainFlag). Флаги B+ — целиком в FPan.
|
||||
FVfoOverlay: TVfoOverlay;
|
||||
FActiveSliceId: Integer; // выбранный активный слайс (0 = главный)
|
||||
// Перетаскивание края полосы фильтра: -1 левый, +1 правый, 0 не тянем.
|
||||
// Кромки на момент нажатия — чтобы неподвижный край не «плыл» от снапа.
|
||||
FEdgeDrag: Integer;
|
||||
FEdgeDragLo: Integer;
|
||||
FEdgeDragHi: Integer;
|
||||
FSliceDrag: Boolean; // тащим маркер несущей слайса по спектру
|
||||
FSliceDragId: Integer; // какой слайс тащим
|
||||
FSampleRateOverlay: TSampleRateOverlay; // оверлей span/hide слева вверху спектра
|
||||
@@ -298,7 +294,7 @@ type
|
||||
BtnMode: array[0..MODE_COUNT-1] of TFlatButton;
|
||||
|
||||
PanelFilter: TPanel;
|
||||
BtnFilter: array[0..FILT_COUNT-1] of TFlatButton;
|
||||
BtnFilter: array[0..FILT_SLOTS-1] of TFlatButton; // 10 пресетов + VAR1/VAR2
|
||||
BtnCTun: TFlatButton;
|
||||
BtnBeacon: TFlatButton; // QO-100 beacon lock (видим только на Pluto в трансвертере)
|
||||
BtnDUP: TFlatButton;
|
||||
@@ -467,6 +463,16 @@ type
|
||||
procedure BtnBandClick(Sender: TObject);
|
||||
procedure BtnModeClick(Sender: TObject);
|
||||
procedure BtnFilterClick(Sender: TObject);
|
||||
procedure BtnFilterMouseDown(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
procedure LayoutFilterButtons(PresetCount: Integer);
|
||||
procedure PushFilterEdgesToView; // кромки RX/TX в SpectrumView
|
||||
procedure RefreshFilterCaptions; // подписи кнопок без сброса выбора
|
||||
function FilterEdgeAtPixel(X: Integer): Integer; // -1 левый край, +1 правый, 0 мимо
|
||||
procedure DoFilterEdgeDrag(X: Integer; Shift: TShiftState);
|
||||
procedure OpenFilterEditor(SlotIdx: Integer);
|
||||
procedure OnFilterSlotEdited(Mode, Idx: Integer; const S: TFilterSlot);
|
||||
procedure OnFilterSlotReset(Mode, Idx: Integer); // Idx < 0 = весь режим
|
||||
procedure BtnCTunClick(Sender: TObject);
|
||||
procedure BtnBeaconClick(Sender: TObject);
|
||||
procedure BtnBeaconMouseDown(Sender: TObject; Button: TMouseButton;
|
||||
@@ -980,7 +986,9 @@ begin
|
||||
FController.FAGCTop := 90; // −90 dBm
|
||||
// default: зависит от режима, будет сброшен в UpdateFilterButtons
|
||||
FController.FCTun := False;
|
||||
FController.FFilterBW := 2700;
|
||||
// Фильтр (индекс + кромки + производная ширина) — одной точкой, чтобы
|
||||
// состояние не расползалось: ширину отдельно больше не выставляем.
|
||||
FController.SelectFilterSlotState(FController.FFilter);
|
||||
FSpecDrag := False;
|
||||
FController.FDriveLevel := 0;
|
||||
FController.FPAMaxPower := 100.0;
|
||||
@@ -1426,6 +1434,7 @@ begin
|
||||
// TX со слайса краснеет полоса ТОГО слайса (DrawSliceFilterMarkers).
|
||||
FSpecView.TXOverlay := FController.FTransmitting and
|
||||
(FController.FTxSliceId = 0);
|
||||
PushFilterEdgesToView; // на передаче полоса рисуется по TX-фильтру
|
||||
ApplySpecViewGridFromState; // сетка спектра под активный тракт
|
||||
RefreshTxIndicators; // TX-бейджи флагов (какой слайс передаёт)
|
||||
UpdateTXControlAvailability;
|
||||
@@ -1526,17 +1535,29 @@ begin
|
||||
SavedFilter := FController.FFilter;
|
||||
SavedFilterBW := FController.FFilterBW;
|
||||
UpdateFilterButtons; // подписи + FM-панели под режим
|
||||
FController.FFilter := SavedFilter;
|
||||
FController.FFilterBW := SavedFilterBW;
|
||||
for i := 0 to FILT_COUNT - 1 do StyleButton(BtnFilter[i], i = FController.FFilter);
|
||||
// Возвращаем СЛОТ целиком: кромки — истина, одной ширины мало.
|
||||
FController.SelectFilterSlotState(SavedFilter);
|
||||
for i := 0 to FILT_SLOTS - 1 do StyleButton(BtnFilter[i], i = FController.FFilter);
|
||||
PushFilterEdgesToView;
|
||||
// Редактор всегда показывает фильтры ТЕКУЩЕГО режима — иначе правка
|
||||
// ушла бы в чужую таблицу.
|
||||
if (FFilterPop <> nil) and FFilterPop.Visible then
|
||||
begin
|
||||
if FController.FMode = MODE_DMR then FFilterPop.Hide
|
||||
else FFilterPop.LoadFrom(FController, FController.FMode,
|
||||
FController.FFilter);
|
||||
end;
|
||||
SyncSpecViewFreq; // иначе полоса фильтра рисуется под старый режим
|
||||
UpdateTXControlAvailability;
|
||||
UpdateDMRStatus;
|
||||
end;
|
||||
rfFilter:
|
||||
begin
|
||||
for i := 0 to FILT_COUNT - 1 do StyleButton(BtnFilter[i], i = FController.FFilter);
|
||||
for i := 0 to FILT_SLOTS - 1 do StyleButton(BtnFilter[i], i = FController.FFilter);
|
||||
PushFilterEdgesToView;
|
||||
SyncSpecViewFreq;
|
||||
if (FFilterPop <> nil) and FFilterPop.Visible then
|
||||
FFilterPop.SyncSelection(FController.FMode, FController.FFilter);
|
||||
end;
|
||||
rfFMDeviation:
|
||||
if (TrkRawDev <> nil) and (LblRawDev <> nil) then
|
||||
@@ -1775,6 +1796,7 @@ begin
|
||||
rfFMSQ, rfFMSQLevel]) then
|
||||
begin
|
||||
FVfoOverlay.SetState(FController.FMode, FController.FFilterBW, FController.FVfoA, FController.FLastSMeter);
|
||||
FVfoOverlay.SetFilterEdges(FController.FilterLo, FController.FilterHi);
|
||||
FVfoOverlay.SetDSPState(FController.FNRMode, FController.FNBMode, FController.FSNB, FController.FANF, FController.FAGCMode);
|
||||
FVfoOverlay.SetSquelchState(FController.FFMSQOn, FController.FFMSQLevel);
|
||||
end;
|
||||
@@ -2315,20 +2337,22 @@ begin
|
||||
|
||||
Inc(Y, MODE_PANEL_H + 2);
|
||||
|
||||
// Filter selector (10 кнопок в 2 ряда по 5)
|
||||
// Filter selector (12 кнопок в 2 ряда по 6: 10 пресетов + VAR1/VAR2).
|
||||
// ПКМ по любой из них открывает редактор фильтров.
|
||||
PanelFilter := TPanel.Create(Self);
|
||||
PanelFilter.Parent := PanelLeft;
|
||||
PanelFilter.SetBounds(0, Y, LEFT_W, 74);
|
||||
PanelFilter.BevelOuter := bvNone;
|
||||
MakeLbl(PanelFilter, 'FILTER', 4, 2);
|
||||
for i := 0 to FILT_COUNT - 1 do
|
||||
for i := 0 to FILT_SLOTS - 1 do
|
||||
begin
|
||||
B := MakeBtn(PanelFilter, '---',
|
||||
LeftPanelButtonLeft(LEFT_W, 5, i mod 5),
|
||||
16 + (i div 5) * 27,
|
||||
LeftPanelButtonWidth(LEFT_W, 5, i mod 5),
|
||||
LeftPanelButtonLeft(LEFT_W, FILT_GRID_COLS, i mod FILT_GRID_COLS),
|
||||
16 + (i div FILT_GRID_COLS) * 27,
|
||||
LeftPanelButtonWidth(LEFT_W, FILT_GRID_COLS, i mod FILT_GRID_COLS),
|
||||
BTN_SM, BtnFilterClick);
|
||||
B.Tag := i;
|
||||
B.OnMouseDown := BtnFilterMouseDown;
|
||||
BtnFilter[i] := B;
|
||||
end;
|
||||
|
||||
@@ -3121,7 +3145,7 @@ begin
|
||||
// Обычные кнопки — все массивы
|
||||
for i := 0 to BAND_COUNT-1 do StyleButton(BtnBand[i], BtnBand[i].Active);
|
||||
for i := 0 to MODE_COUNT-1 do StyleButton(BtnMode[i], BtnMode[i].Active);
|
||||
for i := 0 to FILT_COUNT-1 do StyleButton(BtnFilter[i], BtnFilter[i].Active);
|
||||
for i := 0 to FILT_SLOTS-1 do StyleButton(BtnFilter[i], BtnFilter[i].Active);
|
||||
for i := 0 to 4 do StyleButton(BtnAGCMode[i],BtnAGCMode[i].Active);
|
||||
StyleButton(BtnCTun, BtnCTun.Active);
|
||||
StyleButton(BtnDUP, BtnDUP.Active);
|
||||
@@ -4454,10 +4478,38 @@ end;
|
||||
// ---------------------------------------------------------------------------
|
||||
// UpdateFilterButtons — обновляет подписи кнопок фильтра под текущий режим
|
||||
// ---------------------------------------------------------------------------
|
||||
// Раскладывает N кнопок фильтра: пресеты режима + VAR1/VAR2 следом. Ряды по
|
||||
// FILT_GRID_COLS, но узкие режимы (FM=2+2, WFM=3+2) занимают один ряд целиком,
|
||||
// растягивая кнопки — как было до появления VAR.
|
||||
procedure TMainForm.LayoutFilterButtons(PresetCount: Integer);
|
||||
var
|
||||
i, Total, Cols, Rows, RowOf, ColOf, InRow: Integer;
|
||||
begin
|
||||
Total := PresetCount + 2; // + VAR1/VAR2
|
||||
if Total <= FILT_GRID_COLS then Cols := Total // FM/WFM: всё в один ряд
|
||||
else Cols := FILT_GRID_COLS;
|
||||
Rows := (Total + Cols - 1) div Cols;
|
||||
for i := 0 to FILT_SLOTS - 1 do
|
||||
begin
|
||||
if i < PresetCount then InRow := i // пресет на своём месте
|
||||
else if i = FILT_VAR1 then InRow := PresetCount // VAR1 сразу за пресетами
|
||||
else if i = FILT_VAR2 then InRow := PresetCount + 1
|
||||
else begin BtnFilter[i].Visible := False; Continue; end;
|
||||
RowOf := InRow div Cols;
|
||||
ColOf := InRow mod Cols;
|
||||
BtnFilter[i].Left := LeftPanelButtonLeft(PanelFilter.Width, Cols, ColOf);
|
||||
BtnFilter[i].Width := LeftPanelButtonWidth(PanelFilter.Width, Cols, ColOf);
|
||||
BtnFilter[i].Top := 16 + RowOf * 27;
|
||||
BtnFilter[i].Caption := FController.FilterSlot(FController.FMode, i).Name;
|
||||
BtnFilter[i].Visible := True;
|
||||
StyleButton(BtnFilter[i], i = FController.FFilter);
|
||||
end;
|
||||
PanelFilter.Height := 16 + Rows * 27 + 4;
|
||||
end;
|
||||
|
||||
procedure TMainForm.UpdateFilterButtons;
|
||||
var
|
||||
i: Integer;
|
||||
Names: array[0..FILT_COUNT-1] of string;
|
||||
DefIdx: Integer;
|
||||
begin
|
||||
// DMR has a mandatory 12.5 kHz channel. There is nothing for the operator
|
||||
@@ -4465,26 +4517,11 @@ begin
|
||||
PanelFilter.Visible := FController.FMode <> MODE_DMR;
|
||||
if FController.FMode = MODE_FM then
|
||||
begin
|
||||
// FM: show only NFM and WFM buttons, resize them to fill the row
|
||||
// FM: NFM/FM + VAR1/VAR2 в один ряд (панель остаётся низкой, SQL/CTCSS
|
||||
// садятся сразу под ней).
|
||||
CloseCTCSSPopup;
|
||||
if FController.FFilter > 1 then FController.FFilter := FILT_FM_DEF; // sanitize, keep NFM/WFM as-is
|
||||
FController.FFilterBW := FILT_FM_BW[FController.FFilter];
|
||||
FController.FFMDeviation := FILT_FM_DEV[FController.FFilter];
|
||||
BtnFilter[0].Caption := FILT_FM_NAMES[0];
|
||||
BtnFilter[0].Left := LeftPanelButtonLeft(PanelFilter.Width, 2, 0);
|
||||
BtnFilter[0].Width := LeftPanelButtonWidth(PanelFilter.Width, 2, 0);
|
||||
BtnFilter[0].Top := 16;
|
||||
BtnFilter[0].Visible := True;
|
||||
StyleButton(BtnFilter[0], FController.FFilter = 0);
|
||||
BtnFilter[1].Caption := FILT_FM_NAMES[1];
|
||||
BtnFilter[1].Left := LeftPanelButtonLeft(PanelFilter.Width, 2, 1);
|
||||
BtnFilter[1].Width := LeftPanelButtonWidth(PanelFilter.Width, 2, 1);
|
||||
BtnFilter[1].Top := 16;
|
||||
BtnFilter[1].Visible := True;
|
||||
StyleButton(BtnFilter[1], FController.FFilter = 1);
|
||||
for i := 2 to FILT_COUNT - 1 do BtnFilter[i].Visible := False;
|
||||
// Shrink panel to fit label + 1 button row so SQL/CTCSS sit right below
|
||||
PanelFilter.Height := BtnFilter[0].Top + BtnFilter[0].Height + 4;
|
||||
FController.SanitizeFilterState;
|
||||
LayoutFilterButtons(FILT_FM_COUNT);
|
||||
if PanelFMSQ <> nil then
|
||||
begin
|
||||
PanelFMSQ.Visible := True;
|
||||
@@ -4536,21 +4573,10 @@ begin
|
||||
|
||||
if FController.FMode = MODE_WFM then
|
||||
begin
|
||||
// WFM: полосы вещательного канала — три кнопки в один ряд.
|
||||
// WFM: полосы вещательного канала — три кнопки + VAR1/VAR2 в один ряд.
|
||||
CloseCTCSSPopup;
|
||||
if FController.FFilter > FILT_WFM_COUNT - 1 then FController.FFilter := FILT_WFM_DEF;
|
||||
FController.FFilterBW := FILT_WFM_BW[FController.FFilter];
|
||||
for i := 0 to FILT_WFM_COUNT - 1 do
|
||||
begin
|
||||
BtnFilter[i].Caption := FILT_WFM_NAMES[i];
|
||||
BtnFilter[i].Left := LeftPanelButtonLeft(PanelFilter.Width, FILT_WFM_COUNT, i);
|
||||
BtnFilter[i].Width := LeftPanelButtonWidth(PanelFilter.Width, FILT_WFM_COUNT, i);
|
||||
BtnFilter[i].Top := 16;
|
||||
BtnFilter[i].Visible := True;
|
||||
StyleButton(BtnFilter[i], i = FController.FFilter);
|
||||
end;
|
||||
for i := FILT_WFM_COUNT to FILT_COUNT - 1 do BtnFilter[i].Visible := False;
|
||||
PanelFilter.Height := BtnFilter[0].Top + BtnFilter[0].Height + 4;
|
||||
FController.SanitizeFilterState;
|
||||
LayoutFilterButtons(FILT_WFM_COUNT);
|
||||
RelayoutBelowBands;
|
||||
Exit;
|
||||
end;
|
||||
@@ -4558,20 +4584,8 @@ begin
|
||||
if FController.FMode = MODE_FMRAW then
|
||||
begin
|
||||
CloseCTCSSPopup;
|
||||
if FController.FFilter > FILT_RAW_COUNT - 1 then
|
||||
FController.FFilter := FILT_RAW_DEF;
|
||||
FController.FFilterBW := FILT_RAW_BW[FController.FFilter];
|
||||
for i := 0 to FILT_RAW_COUNT - 1 do
|
||||
begin
|
||||
BtnFilter[i].Caption := FILT_RAW_NAMES[i];
|
||||
BtnFilter[i].Left := LeftPanelButtonLeft(PanelFilter.Width, FILT_RAW_COUNT, i);
|
||||
BtnFilter[i].Width := LeftPanelButtonWidth(PanelFilter.Width, FILT_RAW_COUNT, i);
|
||||
BtnFilter[i].Top := 16;
|
||||
BtnFilter[i].Visible := True;
|
||||
StyleButton(BtnFilter[i], i = FController.FFilter);
|
||||
end;
|
||||
for i := FILT_RAW_COUNT to FILT_COUNT - 1 do BtnFilter[i].Visible := False;
|
||||
PanelFilter.Height := BtnFilter[0].Top + BtnFilter[0].Height + 4;
|
||||
FController.SanitizeFilterState;
|
||||
LayoutFilterButtons(FILT_RAW_COUNT);
|
||||
if PanelRawDev <> nil then
|
||||
begin
|
||||
TrkRawDev.Position := Round(FController.FRawFMDeviation) div 50;
|
||||
@@ -4585,67 +4599,18 @@ begin
|
||||
if FController.FMode = MODE_DMR then
|
||||
begin
|
||||
CloseCTCSSPopup;
|
||||
FController.FFilter := 0;
|
||||
FController.FFilterBW := FILT_DMR_BW;
|
||||
FController.FFMDeviation := DMR_FM_DEV;
|
||||
for i := 0 to FILT_COUNT - 1 do BtnFilter[i].Visible := False;
|
||||
FController.SanitizeFilterState;
|
||||
for i := 0 to FILT_SLOTS - 1 do BtnFilter[i].Visible := False;
|
||||
RelayoutBelowBands;
|
||||
Exit;
|
||||
end;
|
||||
|
||||
for i := 0 to FILT_COUNT - 1 do
|
||||
begin
|
||||
BtnFilter[i].Left := LeftPanelButtonLeft(PanelFilter.Width, 5, i mod 5);
|
||||
BtnFilter[i].Top := 16 + (i div 5) * 27;
|
||||
BtnFilter[i].Width := LeftPanelButtonWidth(PanelFilter.Width, 5, i mod 5);
|
||||
BtnFilter[i].Visible := True;
|
||||
end;
|
||||
|
||||
case FController.FMode of
|
||||
MODE_LSB, MODE_USB:
|
||||
begin
|
||||
for i := 0 to FILT_COUNT-1 do Names[i] := FILT_SSB_NAMES[i];
|
||||
DefIdx := FILT_SSB_DEF;
|
||||
end;
|
||||
MODE_DIGU, MODE_DIGL:
|
||||
begin
|
||||
for i := 0 to FILT_COUNT-1 do Names[i] := FILT_DIGI_NAMES[i];
|
||||
DefIdx := FILT_DIGI_DEF;
|
||||
end;
|
||||
MODE_DSB:
|
||||
begin
|
||||
for i := 0 to FILT_COUNT-1 do Names[i] := FILT_DSB_NAMES[i];
|
||||
DefIdx := FILT_DSB_DEF;
|
||||
end;
|
||||
MODE_CWL, MODE_CWU:
|
||||
begin
|
||||
for i := 0 to FILT_COUNT-1 do Names[i] := FILT_CW_NAMES[i];
|
||||
DefIdx := FILT_CW_DEF;
|
||||
end;
|
||||
else begin
|
||||
for i := 0 to FILT_COUNT-1 do Names[i] := FILT_AM_NAMES[i];
|
||||
DefIdx := FILT_AM_DEF;
|
||||
end;
|
||||
end;
|
||||
|
||||
FController.FFilter := DefIdx;
|
||||
case FController.FMode of
|
||||
MODE_LSB, MODE_USB:
|
||||
FController.FFilterBW := FILT_SSB_BW[DefIdx];
|
||||
MODE_DIGU, MODE_DIGL:
|
||||
FController.FFilterBW := FILT_DIGI_BW[DefIdx];
|
||||
MODE_DSB:
|
||||
FController.FFilterBW := FILT_DSB_BW[DefIdx];
|
||||
MODE_CWL, MODE_CWU:
|
||||
FController.FFilterBW := FILT_CW_BW[DefIdx];
|
||||
else FController.FFilterBW := FILT_AM_BW[DefIdx];
|
||||
end;
|
||||
|
||||
for i := 0 to FILT_COUNT-1 do
|
||||
begin
|
||||
BtnFilter[i].Caption := Names[i];
|
||||
StyleButton(BtnFilter[i], i = FController.FFilter);
|
||||
end;
|
||||
// Прочие режимы: смена режима возвращает заводской индекс фильтра (как было
|
||||
// до таблицы), кромки и подписи приезжают из таблицы контроллера.
|
||||
PanelFilter.Height := 74;
|
||||
DefIdx := FilterDefIdx(FController.FMode);
|
||||
FController.SelectFilterSlotState(DefIdx);
|
||||
LayoutFilterButtons(FILT_COUNT);
|
||||
RelayoutBelowBands;
|
||||
end;
|
||||
|
||||
@@ -5105,6 +5070,17 @@ begin
|
||||
UpdateBeaconButton;
|
||||
Exit;
|
||||
end;
|
||||
// ЛКМ по краю полосы фильтра — тянем именно этот край (левый/правый
|
||||
// независимо). Проверяется РАНЬШЕ слайс-маркера и панорамирования, иначе
|
||||
// край у самой несущей (узкий CW) было бы не поймать.
|
||||
if (PbSpectrum.Width > 0) and (FilterEdgeAtPixel(X) <> 0) then
|
||||
begin
|
||||
FEdgeDrag := FilterEdgeAtPixel(X);
|
||||
FEdgeDragLo := FController.FilterLo;
|
||||
FEdgeDragHi := FController.FilterHi;
|
||||
PbSpectrum.Cursor := crSizeWE;
|
||||
Exit;
|
||||
end;
|
||||
// ЛКМ по маркеру несущей слайса — начинаем его перетаскивание (перетюн).
|
||||
if (PbSpectrum.Width > 0) then
|
||||
begin
|
||||
@@ -5164,6 +5140,13 @@ procedure TMainForm.PbSpectrumMouseMove(Sender: TObject; Shift: TShiftState;
|
||||
var
|
||||
VC, VS, NewHz, LoHz, HiHz: Double;
|
||||
begin
|
||||
// Перетаскивание края полосы фильтра.
|
||||
if FEdgeDrag <> 0 then
|
||||
begin
|
||||
if ssLeft in Shift then DoFilterEdgeDrag(X, Shift);
|
||||
Exit;
|
||||
end;
|
||||
|
||||
// Перетаскивание маркера несущей слайса — перетюн в пределах захваченной полосы.
|
||||
if FSliceDrag then
|
||||
begin
|
||||
@@ -5192,6 +5175,8 @@ begin
|
||||
if (Assigned(FVfoOverlay) and (FVfoOverlay.HotIdx >= 0)) or
|
||||
(Assigned(FSampleRateOverlay) and (FSampleRateOverlay.HotIdx >= 0)) then
|
||||
PbSpectrum.Cursor := crHandPoint
|
||||
else if FilterEdgeAtPixel(X) <> 0 then
|
||||
PbSpectrum.Cursor := crSizeWE // над краем фильтра — край можно тянуть
|
||||
else if FPan.SliceAtPixel(X) <> 0 then
|
||||
PbSpectrum.Cursor := crSizeWE // над маркером слайса — его можно тащить
|
||||
else
|
||||
@@ -5211,6 +5196,12 @@ end;
|
||||
procedure TMainForm.PbSpectrumMouseUp(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
begin
|
||||
if FEdgeDrag <> 0 then
|
||||
begin
|
||||
FEdgeDrag := 0;
|
||||
PbSpectrum.Cursor := crDefault;
|
||||
Exit;
|
||||
end;
|
||||
if FSliceDrag then
|
||||
begin
|
||||
FSliceDrag := False;
|
||||
@@ -5335,11 +5326,130 @@ end;
|
||||
|
||||
procedure TMainForm.BtnFilterClick(Sender: TObject);
|
||||
begin
|
||||
// Логика (FFilter/FFilterBW по таблицам режима, ApplyModeFilter, band cache)
|
||||
// — в контроллере; рендер — в OnControllerState(rfFilter).
|
||||
// Логика (слот → кромки, ApplyModeFilter, band cache) — в контроллере;
|
||||
// рендер — в OnControllerState(rfFilter).
|
||||
FController.SetFilterIdx((Sender as TFlatButton).Tag);
|
||||
end;
|
||||
|
||||
function TMainForm.FilterEdgeAtPixel(X: Integer): Integer;
|
||||
// -1 = курсор у левого края полосы фильтра, +1 = у правого, 0 = мимо.
|
||||
// Работает по тем же кромкам, что нарисованы (главный VFO, не слайсы).
|
||||
var
|
||||
VC, VS, Lo, Hi: Double;
|
||||
W, X1, X2: Integer;
|
||||
begin
|
||||
Result := 0;
|
||||
W := PbSpectrum.Width;
|
||||
if (W <= 0) or (FController.FMode = MODE_DMR) then Exit; // DMR: полоса фиксирована
|
||||
if FController.FTransmitting then Exit; // на передаче фильтр мышью не крутим
|
||||
FController.GetViewWindow(VC, VS);
|
||||
if VS <= 0 then Exit;
|
||||
Lo := ActiveVfoFreq + FController.FilterLo;
|
||||
Hi := ActiveVfoFreq + FController.FilterHi;
|
||||
X1 := Round((Lo - VC + VS / 2) / VS * W);
|
||||
X2 := Round((Hi - VC + VS / 2) / VS * W);
|
||||
// Слишком узкая на экране полоса (CW на полном спане) — края слиплись с
|
||||
// несущей, и захват съел бы обычный клик-тюн. Тогда фильтр правится только
|
||||
// в редакторе; чтобы потянуть мышью, достаточно зазумиться.
|
||||
if X2 - X1 < 2 * EDGE_GRAB_PX + 2 then Exit;
|
||||
// Приоритет правому краю: при узкой полосе левый иначе «съедает» оба.
|
||||
if Abs(X - X2) <= EDGE_GRAB_PX then Result := 1
|
||||
else if Abs(X - X1) <= EDGE_GRAB_PX then Result := -1;
|
||||
end;
|
||||
|
||||
procedure TMainForm.DoFilterEdgeDrag(X: Integer; Shift: TShiftState);
|
||||
// Тянем ОДИН край; второй стоит. Alt — симметрично (второй край зеркалит).
|
||||
// Именно Alt, а не Ctrl: Ctrl+ЛКМ по спектру уже создаёт слайс. Модификатор
|
||||
// читаем из Shift обработчика, а не GetKeyState — надёжнее на Wayland.
|
||||
var
|
||||
VC, VS, Hz: Double;
|
||||
W, Lo, Hi, D: Integer;
|
||||
begin
|
||||
W := PbSpectrum.Width;
|
||||
if W <= 0 then Exit;
|
||||
FController.GetViewWindow(VC, VS);
|
||||
if VS <= 0 then Exit;
|
||||
Hz := PixelToFreq(X, W, VC, VS) - ActiveVfoFreq;
|
||||
Lo := FEdgeDragLo;
|
||||
Hi := FEdgeDragHi;
|
||||
if FEdgeDrag > 0 then Hi := Round(Hz / EDGE_SNAP_HZ) * EDGE_SNAP_HZ
|
||||
else Lo := Round(Hz / EDGE_SNAP_HZ) * EDGE_SNAP_HZ;
|
||||
if ssAlt in Shift then
|
||||
begin
|
||||
// Симметрия относительно центра исходной полосы.
|
||||
D := (FEdgeDragLo + FEdgeDragHi) div 2;
|
||||
if FEdgeDrag > 0 then Lo := 2 * D - Hi
|
||||
else Hi := 2 * D - Lo;
|
||||
end;
|
||||
if Hi - Lo < FILT_MIN_WIDTH then Exit; // схлопывать полосу не даём
|
||||
FController.SetFilterEdges(Lo, Hi); // уходит в VAR — пресеты целы
|
||||
FSpectrumDirty := True;
|
||||
end;
|
||||
|
||||
procedure TMainForm.PushFilterEdgesToView;
|
||||
// Кромки в спектр: RX — то, что реально стоит в приёмнике (фильтр может быть
|
||||
// произвольным), TX — из TXSignedEdges движка. Полоса на экране обязана
|
||||
// совпадать с тем, что слышно и что уходит в эфир.
|
||||
var Lo, Hi: Integer;
|
||||
begin
|
||||
if FSpecView = nil then Exit;
|
||||
FSpecView.SetRXEdges(FController.FilterLo, FController.FilterHi);
|
||||
FController.TXFilterEdges(Lo, Hi);
|
||||
FSpecView.SetTXEdges(Lo, Hi);
|
||||
end;
|
||||
|
||||
procedure TMainForm.BtnFilterMouseDown(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
// ПКМ по кнопке фильтра открывает редактор на этом слоте (эталон Thetis:
|
||||
// у них это пункт контекстного меню «RX1 Filter Configure»). ЛКМ не трогаем —
|
||||
// иначе окно вылезало бы при каждом обычном переключении фильтра.
|
||||
begin
|
||||
if Button = mbRight then OpenFilterEditor((Sender as TFlatButton).Tag);
|
||||
end;
|
||||
|
||||
procedure TMainForm.OpenFilterEditor(SlotIdx: Integer);
|
||||
var Anchor: TFlatButton;
|
||||
begin
|
||||
if FController.FMode = MODE_DMR then Exit; // у DMR полоса жёстко задана
|
||||
if FFilterPop = nil then
|
||||
begin
|
||||
FFilterPop := TFilterPopup.Create(Self);
|
||||
FFilterPop.OnSlotEdited := OnFilterSlotEdited;
|
||||
FFilterPop.OnSlotReset := OnFilterSlotReset;
|
||||
end;
|
||||
if FFilterPop.Visible then begin FFilterPop.Hide; Exit; end; // повторный ПКМ закрывает
|
||||
Anchor := BtnFilter[EnsureRange(SlotIdx, 0, FILT_SLOTS - 1)];
|
||||
if (Anchor = nil) or not Anchor.Visible then Anchor := BtnFilter[0];
|
||||
FFilterPop.PopupBelow(FController, Anchor, FController.FMode, SlotIdx);
|
||||
end;
|
||||
|
||||
// Обновляет ТОЛЬКО подписи/раскладку кнопок фильтра. Полный
|
||||
// UpdateFilterButtons для не-FM режимов сбрасывает выбранный фильтр на
|
||||
// заводской — из редактора это отобрало бы у оператора текущий фильтр на
|
||||
// каждом нажатии клавиши.
|
||||
procedure TMainForm.RefreshFilterCaptions;
|
||||
begin
|
||||
if FController.FMode = MODE_DMR then Exit;
|
||||
LayoutFilterButtons(FilterPresetCount(FController.FMode));
|
||||
end;
|
||||
|
||||
procedure TMainForm.OnFilterSlotEdited(Mode, Idx: Integer; const S: TFilterSlot);
|
||||
begin
|
||||
FController.SetFilterSlot(Mode, Idx, S);
|
||||
RefreshFilterCaptions; // подпись слота могла измениться
|
||||
OnControllerState(FController, rfFilter);
|
||||
end;
|
||||
|
||||
procedure TMainForm.OnFilterSlotReset(Mode, Idx: Integer);
|
||||
begin
|
||||
if Idx < 0 then FController.ResetFilterSet(Mode)
|
||||
else FController.ResetFilterSlot(Mode, Idx);
|
||||
if FFilterPop <> nil then
|
||||
FFilterPop.LoadFrom(FController, Mode, FFilterPop.SlotIdx);
|
||||
RefreshFilterCaptions;
|
||||
OnControllerState(FController, rfFilter);
|
||||
end;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FM CTCSS helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user