diff --git a/MainForm.pas b/MainForm.pas index 220df1b..913195b 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -183,6 +183,19 @@ type FPanSplitTop0: Integer; FPanSplitMinTop: Integer; FPanSplitMaxTop: Integer; + // Грид-раскладка: пан 0 сверху во всю ширину, доп. паны колонками снизу. + // Активна при FPanGridMode и >=2 доп. панах в стеке (иначе обычный стек). + FPanGridMode: Boolean; + FPanGridRowShare: Double; // доля высоты верхнего ряда (пан 0), 0.15..0.85 + FPanColShare: array[1..MAX_PANS-1] of Double; // доли ширины колонок + FPanVSplitters: array[1..MAX_PANS-1] of TPanel; // слева от колонки пана i + // Drag вертикального сплиттера (индекс пана справа; 0 = нет). + FPanVSplitDrag: Integer; + FPanVSplitX0: Integer; + FPanVSplitLeft0: Integer; + FPanVSplitMinLeft: Integer; + FPanVSplitMaxLeft: Integer; + FPanWideH: Integer; // wideband-высота последнего LayoutPanStack (для PanMinHeight) FPendingPanClose: Integer; // «×» шапки: PanId к закрытию в тике (0 = нет) // Меню rate пана («NNN kHz» в шапке панов N; создаётся лениво). FPanRateMenu: TPopupMenu; @@ -612,9 +625,17 @@ type procedure PanSplitMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); procedure PanSplitMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer); + procedure OnPanGridToggle(Sender: TObject); // «▦/▤» в ряду зума пана 0 + function PanGridActive: Boolean; // грид реально применён + procedure PanVSplitMouseDown(Sender: TObject; Button: TMouseButton; + Shift: TShiftState; X, Y: Integer); + procedure PanVSplitMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer); + procedure PanVSplitMouseUp(Sender: TObject; Button: TMouseButton; + Shift: TShiftState; X, Y: Integer); procedure PanSplitMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); procedure LayoutPanStack(AWideH: Integer); // регионы панов + Layout каждого + function PanMinHeight(PanId: Integer): Integer; // мин. высота региона пана function Pan0HeaderH: Integer; // высота шапки пана 0 (0 без стека) procedure PbSpectrumDblClick(Sender: TObject); procedure TrkDriveChange(Sender: TObject); @@ -1079,6 +1100,8 @@ begin FPan.OnADCClick := OnPanADCClick; // бейдж RX1/RX2 (3.4, платы с 2 АЦП) FPans[0] := FPan; FPanShare[0] := 1.0; + FPanGridMode := False; + FPanGridRowShare := 0.5; // грид: пан 0 — верхняя половина FPanCbLock := SyncObjs.TCriticalSection.Create; FSpecView.VfoA := FController.FVfoA; FSpecView.VfoB := FController.FVfoB; @@ -2309,6 +2332,7 @@ begin FPan.ZoomInClick := BtnZoomInClick; FPan.ZoomDefClick := BtnZoomDefClick; FPan.ZoomOutClick := BtnZoomOutClick; + FPan.GridClick := OnPanGridToggle; // «▦/▤» грид-раскладка панов FPan.OnSplitterMoved := RightPanelResize; FPan.Build(PanelRight, Max(1, FController.FSpecMSAA)); // Алиасы контролов (исторические имена; постепенная миграция на FPan.*) @@ -2574,13 +2598,37 @@ begin Result := 0; end; +function TMainForm.PanGridActive: Boolean; +// Грид реально применён: включён юзером и в стеке >=2 доп. панов +// (с одним доп. паном грид совпадает со стеком). +var i, N: Integer; +begin + N := 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); +end; + +function TMainForm.PanMinHeight(PanId: Integer): Integer; +// Мин. высота региона пана в стеке/гриде: ниже неё Layout панели клампит +// спектр/водопад к минимумам и виджеты вылезают за регион (паны наезжают). +begin + if FPans[PanId] = nil then Exit(0); + Result := FPans[PanId].MinLayoutHeight( + IfThen(PanId = 0, FPanWideH, 0), + FController.FShowSpectrum, FController.FShowWaterfall); +end; + procedure TMainForm.LayoutPanStack(AWideH: Integer); const PAN_SPLIT_H = 5; var ActiveIds: array[0..MAX_PANS-1] of Integer; - N, i, k, TotalH, UsableH, Y, Hi: Integer; + MinH: array[0..MAX_PANS-1] of Integer; + N, i, k, TotalH, TotalW, UsableH, UsableW, Y, X, Hi, Wi: Integer; + Row0H, RowTop, RowH, M, MinRest, RemainH, BotMin: Integer; ShareSum: Double; + Grid: Boolean; begin // Плавающие (pop-out) паны в стеке не участвуют — у них своё окно. N := 0; @@ -2588,37 +2636,148 @@ begin if (FPans[i] <> nil) and not PanFloating(i) then begin ActiveIds[N] := i; Inc(N); end; if N = 0 then Exit; - TotalH := PanelRight.ClientHeight; - UsableH := TotalH - (N - 1) * PAN_SPLIT_H; - ShareSum := 0; - for k := 0 to N - 1 do ShareSum := ShareSum + FPanShare[ActiveIds[k]]; - if ShareSum <= 0 then ShareSum := 1; - Y := 0; - for k := 0 to N - 1 do + FPanWideH := AWideH; // для PanMinHeight (в т.ч. из сплиттер-драга) + TotalH := PanelRight.ClientHeight; + TotalW := PanelRight.ClientWidth; + Grid := PanGridActive; // пан 0 + >=2 доп. панов колонками + + // Тумблер «▦/▤» в ряду зума пана 0: есть смысл при >=2 доп. панах в стеке. + if FPans[0] <> nil then begin - i := ActiveIds[k]; - if k > 0 then + // Виден при >=2 доп. панах; при включённом гриде — всегда (выключить можно + // даже когда паны закрылись и раскладка сама откатилась в стек). + FPans[0].GridBtnEnabled := FPanGridMode or (N >= 3); + if FPans[0].BtnGrid <> nil then begin - FPanSplitters[i].SetBounds(0, Y, PanelRight.ClientWidth, PAN_SPLIT_H); - Inc(Y, PAN_SPLIT_H); + if FPanGridMode then FPans[0].BtnGrid.Caption := '▤' + else FPans[0].BtnGrid.Caption := '▦'; end; - if k = N - 1 then - Hi := TotalH - Y // последний пан добирает остаток (округления долей) - else - Hi := Round(UsableH * FPanShare[i] / ShareSum); - FPans[i].StackTop := Y; - FPans[i].StackHeight := Hi; + end; + + if not Grid then + begin + // ---- Вертикальный стек (как раньше) ---- + for i := 1 to MAX_PANS - 1 do + if FPanVSplitters[i] <> nil then FPanVSplitters[i].Visible := False; + UsableH := TotalH - (N - 1) * PAN_SPLIT_H; + ShareSum := 0; + for k := 0 to N - 1 do ShareSum := ShareSum + FPanShare[ActiveIds[k]]; + if ShareSum <= 0 then ShareSum := 1; // Видимость шапки — по ОБЩЕМУ числу панов (включая плавающие), как // Pan0HeaderH/UpdatePanHeaders: иначе при pop-out пана N шапка пана 0 // прячется, а wideband остаётся сдвинутым на Pan0HeaderH — спектр - // накрывает линейку частот wideband. - FPans[i].HeaderVisible := PanCount > 1; - if i = 0 then - FPans[i].Layout(AWideH, FController.FShowSpectrum, FController.FShowWaterfall) - else - FPans[i].Layout(0, FController.FShowSpectrum, FController.FShowWaterfall); - Inc(Y, Hi); + // накрывает линейку частот wideband. Ставим ДО расчёта минимумов + // (MinLayoutHeight учитывает шапку). + MinRest := 0; + for k := 0 to N - 1 do + begin + i := ActiveIds[k]; + FPans[i].HeaderVisible := PanCount > 1; + MinH[k] := PanMinHeight(i); + Inc(MinRest, MinH[k]); + end; + Y := 0; + RemainH := UsableH; + for k := 0 to N - 1 do + begin + i := ActiveIds[k]; + Dec(MinRest, MinH[k]); // теперь = сумма минимумов панов ПОСЛЕ текущего + if k > 0 then + begin + FPanSplitters[i].Visible := True; // грид мог спрятать + FPanSplitters[i].SetBounds(0, Y, TotalW, PAN_SPLIT_H); + Inc(Y, PAN_SPLIT_H); + end; + if k = N - 1 then + Hi := RemainH // последний пан добирает остаток (округления долей) + else + begin + Hi := Round(UsableH * FPanShare[i] / ShareSum); + // Доли не смеют ужать пан ниже минимума содержимого (виджеты вылезут + // за регион и паны наедут друг на друга), а раздутый минимумом пан — + // отобрать у оставшихся их минимумы. + if Hi < MinH[k] then Hi := MinH[k]; + if Hi > RemainH - MinRest then Hi := RemainH - MinRest; + if Hi < 1 then Hi := 1; // окно меньше суммы минимумов — деградируем без наезда выше + end; + Dec(RemainH, Hi); + FPans[i].StackTop := Y; + FPans[i].StackHeight := Hi; + FPans[i].StackLeft := 0; + FPans[i].StackWidth := 0; // вся ширина + if i = 0 then + FPans[i].Layout(AWideH, FController.FShowSpectrum, FController.FShowWaterfall) + else + FPans[i].Layout(0, FController.FShowSpectrum, FController.FShowWaterfall); + Inc(Y, Hi); + end; + Exit; end; + + // ---- Грид: пан 0 сверху во всю ширину, доп. паны колонками снизу ---- + FPanGridRowShare := EnsureRange(FPanGridRowShare, 0.15, 0.85); + Row0H := Round((TotalH - PAN_SPLIT_H) * FPanGridRowShare); + // Кламп ряда минимумами содержимого (шапки — до расчёта минимумов): + // ни пан 0, ни колонки нижнего ряда не смеют стать ниже своих минимумов. + FPans[0].HeaderVisible := PanCount > 1; + BotMin := 0; + for k := 1 to N - 1 do + begin + i := ActiveIds[k]; + FPans[i].HeaderVisible := True; + BotMin := Max(BotMin, PanMinHeight(i)); + end; + if Row0H > TotalH - PAN_SPLIT_H - BotMin then + Row0H := TotalH - PAN_SPLIT_H - BotMin; + if Row0H < PanMinHeight(0) then Row0H := PanMinHeight(0); + FPans[0].StackTop := 0; + FPans[0].StackHeight := Row0H; + FPans[0].StackLeft := 0; + FPans[0].StackWidth := 0; + FPans[0].Layout(AWideH, FController.FShowSpectrum, FController.FShowWaterfall); + + // Горизонтальный сплиттер ряда — сплиттер ПЕРВОЙ колонки (во всю ширину); + // сплиттеры остальных колонок прячем (между колонками — вертикальные). + M := N - 1; // колонок в нижнем ряду + FPanSplitters[ActiveIds[1]].Visible := True; + FPanSplitters[ActiveIds[1]].SetBounds(0, Row0H, TotalW, PAN_SPLIT_H); + for k := 2 to N - 1 do + FPanSplitters[ActiveIds[k]].Visible := False; + + RowTop := Row0H + PAN_SPLIT_H; + RowH := TotalH - RowTop; + UsableW := TotalW - (M - 1) * PAN_SPLIT_H; + ShareSum := 0; + for k := 1 to N - 1 do ShareSum := ShareSum + FPanColShare[ActiveIds[k]]; + if ShareSum <= 0 then ShareSum := 1; + X := 0; + for k := 1 to N - 1 do + begin + i := ActiveIds[k]; + if k > 1 then + begin + FPanVSplitters[i].Visible := True; + FPanVSplitters[i].SetBounds(X, RowTop, PAN_SPLIT_H, RowH); + Inc(X, PAN_SPLIT_H); + end + else if FPanVSplitters[i] <> nil then + FPanVSplitters[i].Visible := False; // у первой колонки левого сплиттера нет + if k = N - 1 then + Wi := TotalW - X + else + Wi := Round(UsableW * FPanColShare[i] / ShareSum); + FPans[i].StackTop := RowTop; + FPans[i].StackHeight := RowH; + FPans[i].StackLeft := X; + FPans[i].StackWidth := Wi; + FPans[i].HeaderVisible := True; + FPans[i].Layout(0, FController.FShowSpectrum, FController.FShowWaterfall); + Inc(X, Wi); + end; + // Вертикальные сплиттеры панов вне нижнего ряда (плавающие) — спрятать. + for i := 1 to MAX_PANS - 1 do + if (FPanVSplitters[i] <> nil) and ((FPans[i] = nil) or PanFloating(i)) then + FPanVSplitters[i].Visible := False; end; // =========================================================================== @@ -2701,6 +2860,7 @@ begin if FPans[i] <> nil then begin if FPans[i].BtnAddPan <> nil then StyleButton(FPans[i].BtnAddPan, False); + if FPans[i].BtnGrid <> nil then StyleButton(FPans[i].BtnGrid, False); if FPans[i].BtnPopPan <> nil then StyleButton(FPans[i].BtnPopPan, False); if FPans[i].BtnClosePan <> nil then StyleButton(FPans[i].BtnClosePan, False); ApplyPanHeaderTheme(FPans[i]); @@ -2708,6 +2868,7 @@ begin begin FPans[i].SetFlagsTheme(T); if FPanSplitters[i] <> nil then FPanSplitters[i].Color := T.Border; + if FPanVSplitters[i] <> nil then FPanVSplitters[i].Color := T.Border; if FPanFloatForm[i] <> nil then FPanFloatForm[i].Color := T.BG; // Зум-бар пана N (пан 0 — через алиас FPanZoomBar ниже). if FPans[i].ZoomBar <> nil then FPans[i].ZoomBar.SetTheme(T); @@ -5371,7 +5532,9 @@ begin FController.PanDDCRateKHz(P.PanId) * 1000.0); P.ZoomBar.SetState(FController.PanZoomFactor(P.PanId), FController.PanPanSlider(P.PanId)); - if P.PbPanZoom <> nil then P.PbPanZoom.Invalidate; + // Инвалидация бара — внутри Set{Freq,State} и ТОЛЬКО при реальной смене: + // нас зовут из тика на каждый пан каждый кадр, безусловный Invalidate = + // полный софт-рендер бара + Qt-рекомпозит на частоте кадров (жор CPU). end; procedure TMainForm.RefreshPanZoom(P: TPanafallPanel); @@ -5382,6 +5545,7 @@ begin P.LayoutFlags; MarkPanDirty(P); if P.PbWaterfall <> nil then P.PbWaterfall.Invalidate; + if P.PbPanZoom <> nil then P.PbPanZoom.Invalidate; // событийный путь — можно end; procedure TMainForm.AddSliceAtFreqPan(P: TPanafallPanel; Hz: Double); @@ -5509,7 +5673,10 @@ begin ResetPanGLCaches(P); P.StackTop := 0; P.StackHeight := 0; // 0/0 = весь родитель (окно) + P.StackLeft := 0; + P.StackWidth := 0; if FPanSplitters[PanId] <> nil then FPanSplitters[PanId].Visible := False; + if FPanVSplitters[PanId] <> nil then FPanVSplitters[PanId].Visible := False; F.Show; UpdatePanHeaders; ResizeSpectrumPanels; // пере-стек оставшихся в главном окне @@ -5761,6 +5928,20 @@ begin Split.OnMouseUp := PanSplitMouseUp; FPanSplitters[PanId] := Split; + // Вертикальный сплиттер слева от колонки пана (грид-раскладка). + Split := TPanel.Create(Self); + Split.Parent := PanelRight; + Split.BevelOuter := bvNone; + Split.Color := TColor($00303030); + Split.Cursor := crHSplit; + Split.Tag := PanId; + Split.Visible := False; // включает LayoutPanStack в грид-режиме + Split.OnMouseDown := PanVSplitMouseDown; + Split.OnMouseMove := PanVSplitMouseMove; + Split.OnMouseUp := PanVSplitMouseUp; + FPanVSplitters[PanId] := Split; + FPanColShare[PanId] := 1.0; + // Публикуем пан для пиксельных колбэков (display-поток) и делим высоту поровну. FPanCbLock.Enter; try @@ -5807,6 +5988,7 @@ begin if (FActiveSliceId <> 0) and not FController.GetSlice(FActiveSliceId, S) then FActiveSliceId := 0; FreeAndNil(FPanSplitters[PanId]); + FreeAndNil(FPanVSplitters[PanId]); P.Free; // Пан был в отдельном окне — окно больше не нужно. Release (не Free): // сюда можно попасть из событий контролов этого же окна («×» шапки → тик). @@ -6042,24 +6224,42 @@ procedure TMainForm.PanSplitMouseDown(Sender: TObject; Button: TMouseButton; const PAN_SPLIT_H = 5; var - BelowId, AboveId, i, TotalH, MinPx: Integer; + BelowId, AboveId, i, TotalH, MinPx, BotMin: Integer; Split: TPanel; begin if Button <> mbLeft then Exit; Split := TPanel(Sender); BelowId := Split.Tag; - // Пан над сплиттером — ближайший активный с меньшим PanId. - AboveId := -1; - for i := BelowId - 1 downto 0 do - if FPans[i] <> nil then begin AboveId := i; Break; end; - if (AboveId < 0) or (FPans[BelowId] = nil) then Exit; TotalH := PanelRight.ClientHeight; if TotalH <= 0 then Exit; - // Границы призрака: каждому пану — минимум 15% стека (= кламп долей в MouseUp). MinPx := Round(0.15 * TotalH); - FPanSplitMinTop := FPans[AboveId].StackTop + MinPx; - FPanSplitMaxTop := FPans[BelowId].StackTop + FPans[BelowId].StackHeight - - MinPx - PAN_SPLIT_H; + if PanGridActive then + begin + // Грид: этот сплиттер — граница ряда пан 0 / нижний ряд колонок. + // Минимум — 15% ИЛИ минимум содержимого (что больше): ниже минимума + // виджеты пана вылезают за регион и паны наезжают друг на друга. + FPanSplitMinTop := Max(MinPx, PanMinHeight(0)); + BotMin := MinPx; // минимум нижнего ряда — макс. по его колонкам + for i := 1 to MAX_PANS - 1 do + if (FPans[i] <> nil) and not PanFloating(i) then + BotMin := Max(BotMin, PanMinHeight(i)); + FPanSplitMaxTop := TotalH - BotMin - PAN_SPLIT_H; + end + else + begin + // Пан над сплиттером — ближайший активный с меньшим PanId. + AboveId := -1; + for i := BelowId - 1 downto 0 do + if FPans[i] <> nil then begin AboveId := i; Break; end; + if (AboveId < 0) or (FPans[BelowId] = nil) then Exit; + // Границы призрака: каждому пану — минимум 15% стека ИЛИ минимум его + // содержимого (что больше; = кламп раздачи в LayoutPanStack). + FPanSplitMinTop := FPans[AboveId].StackTop + + Max(MinPx, PanMinHeight(AboveId)); + FPanSplitMaxTop := FPans[BelowId].StackTop + FPans[BelowId].StackHeight + - Max(MinPx, PanMinHeight(BelowId)) - PAN_SPLIT_H; + end; + if FPanSplitMaxTop < FPanSplitMinTop then FPanSplitMaxTop := FPanSplitMinTop; FPanSplitDrag := BelowId; FPanSplitY0 := Mouse.CursorPos.Y; FPanSplitTop0 := Split.Top; @@ -6088,13 +6288,20 @@ begin BelowId := FPanSplitDrag; FPanSplitDrag := 0; TPanel(Sender).Color := TColor($00303030); + TotalH := PanelRight.ClientHeight; + if TotalH <= 0 then Exit; + if PanGridActive then + begin + // Грид: сплиттер задаёт долю высоты верхнего ряда (пан 0) напрямую. + FPanGridRowShare := EnsureRange(TPanel(Sender).Top / TotalH, 0.15, 0.85); + ResizeSpectrumPanels; + Exit; + end; // Пан над сплиттером — ближайший активный с меньшим PanId. AboveId := -1; for i := BelowId - 1 downto 0 do if FPans[i] <> nil then begin AboveId := i; Break; end; if (AboveId < 0) or (FPans[BelowId] = nil) then Exit; - TotalH := PanelRight.ClientHeight; - if TotalH <= 0 then Exit; // Дельта — по финальному положению призрака (он уже клампился в Move). DeltaY := TPanel(Sender).Top - FPanSplitTop0; DShare := DeltaY / TotalH; @@ -6108,6 +6315,87 @@ begin ResizeSpectrumPanels; end; +procedure TMainForm.OnPanGridToggle(Sender: TObject); +begin + FPanGridMode := not FPanGridMode; + ResizeSpectrumPanels; +end; + +procedure TMainForm.PanVSplitMouseDown(Sender: TObject; Button: TMouseButton; + Shift: TShiftState; X, Y: Integer); +const + PAN_SPLIT_W = 5; +var + RightId, LeftId, i, TotalW, MinPx: Integer; + Split: TPanel; +begin + if (Button <> mbLeft) or not PanGridActive then Exit; + Split := TPanel(Sender); + RightId := Split.Tag; + // Колонка слева — ближайший застекованный доп. пан с меньшим PanId. + LeftId := -1; + for i := RightId - 1 downto 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; + // Каждой колонке — минимум 15% ширины (= кламп долей в MouseUp). + MinPx := Round(0.15 * TotalW); + FPanVSplitMinLeft := FPans[LeftId].StackLeft + MinPx; + FPanVSplitMaxLeft := FPans[RightId].StackLeft + FPans[RightId].StackWidth + - MinPx - PAN_SPLIT_W; + FPanVSplitDrag := RightId; + FPanVSplitX0 := Mouse.CursorPos.X; + FPanVSplitLeft0 := Split.Left; + Split.Color := TColor($005050A0); +end; + +procedure TMainForm.PanVSplitMouseMove(Sender: TObject; Shift: TShiftState; + X, Y: Integer); +var NewLeft: Integer; +begin + // Живой призрак: двигаем сам сплиттер, пересчёт долей — по MouseUp. + if (FPanVSplitDrag = 0) or (TPanel(Sender).Tag <> FPanVSplitDrag) then Exit; + NewLeft := FPanVSplitLeft0 + (Mouse.CursorPos.X - FPanVSplitX0); + if NewLeft < FPanVSplitMinLeft then NewLeft := FPanVSplitMinLeft; + if NewLeft > FPanVSplitMaxLeft then NewLeft := FPanVSplitMaxLeft; + TPanel(Sender).Left := NewLeft; +end; + +procedure TMainForm.PanVSplitMouseUp(Sender: TObject; Button: TMouseButton; + Shift: TShiftState; X, Y: Integer); +var + RightId, LeftId, i, DeltaX, TotalW: Integer; + DShare, MinShare, ShareSum: Double; +begin + if FPanVSplitDrag = 0 then Exit; + RightId := FPanVSplitDrag; + FPanVSplitDrag := 0; + TPanel(Sender).Color := TColor($00303030); + LeftId := -1; + for i := RightId - 1 downto 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 + if (FPans[i] <> nil) and not PanFloating(i) then + ShareSum := ShareSum + FPanColShare[i]; + if ShareSum <= 0 then ShareSum := 1; + DeltaX := TPanel(Sender).Left - FPanVSplitLeft0; + DShare := DeltaX / TotalW * ShareSum; + MinShare := 0.15 * ShareSum; + if FPanColShare[LeftId] + DShare < MinShare then + DShare := MinShare - FPanColShare[LeftId]; + if FPanColShare[RightId] - DShare < MinShare then + DShare := FPanColShare[RightId] - MinShare; + FPanColShare[LeftId] := FPanColShare[LeftId] + DShare; + FPanColShare[RightId] := FPanColShare[RightId] - DShare; + ResizeSpectrumPanels; +end; + procedure TMainForm.OnVfoOverlaySliceSelect(Sender: TObject); begin if Sender is TVfoOverlay then diff --git a/PanafallPanel.pas b/PanafallPanel.pas index 7853391..116078d 100644 --- a/PanafallPanel.pas +++ b/PanafallPanel.pas @@ -110,6 +110,10 @@ type FOnPanClose: TNotifyEvent; FOnAddPan: TNotifyEvent; FAddPanWanted: Boolean; // хозяин: показывать «⊞» в ряду пан/зума + FBtnGrid: TFlatButton; // «▦/▤» тумблер грид-раскладки (пан 0) + FGridBtnWanted: Boolean; // хозяин: показывать тумблер (>=2 доп. панов) + FStackLeft: Integer; // регион пана: X (0 при полной ширине) + FStackWidth: Integer; // ширина региона (0 = вся ширина родителя) procedure BtnClosePanClick(Sender: TObject); procedure BtnAddPanClick(Sender: TObject); @@ -135,7 +139,7 @@ type procedure PanZoomMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); procedure PanZoomDblClick(Sender: TObject); - procedure PositionPanZoomBar(BottomY, RW, H: Integer; AVisible: Boolean); + procedure PositionPanZoomBar(X0, BottomY, RW, H: Integer; AVisible: Boolean); public // Обработчики мыши спектра/водопада и кликов зум-кнопок хозяина — // задать ДО вызова Build (Build подвешивает их на создаваемые контролы). @@ -150,6 +154,7 @@ type ZoomInClick: TNotifyEvent; ZoomDefClick: TNotifyEvent; ZoomOutClick: TNotifyEvent; + GridClick: TNotifyEvent; // «▦/▤» тумблер грид-раскладки (пан 0) constructor Create(AOwner: TComponent; AUseOpenGL: Boolean); reintroduce; destructor Destroy; override; @@ -164,6 +169,10 @@ type // над спектром. Show-флаги приходят от контроллера (FShowSpectrum/ // FShowWaterfall). procedure Layout(ATopOffset: Integer; AShowSpectrum, AShowWaterfall: Boolean); + // Мин. высота региона, при которой Layout не вылезает за него (те же + // константы/DPI, что в Layout; учитывает текущую HeaderVisible). + function MinLayoutHeight(ATopOffset: Integer; + AShowSpectrum, AShowWaterfall: Boolean): Integer; property View: TSpectrumView read FView; property PbSpectrum: TControl read FPbSpectrum; @@ -186,6 +195,9 @@ type property PanId: Integer read FPanId write FPanId; property StackTop: Integer read FStackTop write FStackTop; property StackHeight: Integer read FStackHeight write FStackHeight; + // Грид-раскладка: регион пана может быть колонкой (StackWidth=0 = вся ширина). + property StackLeft: Integer read FStackLeft write FStackLeft; + property StackWidth: Integer read FStackWidth write FStackWidth; // Шапка пана: текст ставит хозяин (частота), rate — отдельным лейблом // (SetHeaderRate); у панов N>0 хозяин делает его кликабельным (селектор // rate, RateClickable + OnRateClick). «×» → OnPanClose. @@ -212,6 +224,9 @@ type property ShowZoomRow: Boolean read FShowZoomRow write FShowZoomRow; // «⊞» в ряду пан/зума (пан 0): показать/спрятать решает хозяин. property AddPanEnabled: Boolean read FAddPanWanted write FAddPanWanted; + // «▦/▤» тумблер грид-раскладки (пан 0, виден при >=2 доп. панах). + property GridBtnEnabled: Boolean read FGridBtnWanted write FGridBtnWanted; + property BtnGrid: TFlatButton read FBtnGrid; property OnPanClose: TNotifyEvent read FOnPanClose write FOnPanClose; property OnAddPan: TNotifyEvent read FOnAddPan write FOnAddPan; // Пользователь отпустил сплиттер (ratio уже обновлён) — хозяин делает @@ -288,6 +303,14 @@ begin // (Owner=Self) освободит inherited. FreeAndNil(FZoomBar); FreeAndNil(FView); + // Кнопки ряда зума создаются MakeFlatBtn с Owner=КОНТЕЙНЕР (не Self) — + // inherited их не освободит, и после гибели пана они оставались жить + // на родителе (STOP → «блуждающие» −/⌂/+ поверх окна). + FreeAndNil(FBtnZoomOut); + FreeAndNil(FBtnZoomDef); + FreeAndNil(FBtnZoomIn); + FreeAndNil(FBtnAddPan); + FreeAndNil(FBtnGrid); inherited Destroy; end; @@ -385,6 +408,11 @@ begin FBtnAddPan.Hint := 'Add panadapter'; FBtnAddPan.ShowHint := True; FBtnAddPan.Visible := False; + // Тумблер грид-раскладки (пан 0; глифом рулит хозяин: ▦ = в грид, ▤ = в стек). + FBtnGrid := MakeFlatBtn(AParent, '▦', 0, 0, 10, 10, GridClick); + FBtnGrid.Hint := 'Grid layout'; + FBtnGrid.ShowHint := True; + FBtnGrid.Visible := False; // ---- Шапка пана (тонкая полоска над спектром; видна при панах >= 2) ---- FHeaderPanel := TPanel.Create(Self); @@ -427,6 +455,7 @@ begin FBtnZoomDef.Parent := NewParent; FBtnZoomIn.Parent := NewParent; FBtnAddPan.Parent := NewParent; + FBtnGrid.Parent := NewParent; FHeaderPanel.Parent := NewParent; // дети шапки едут вместе с ней end; @@ -538,31 +567,39 @@ begin FBtnPopPan.SetBounds(RW - 2 * BtnS - 6, 2, BtnS, BtnS); end; -procedure TPanafallPanel.PositionPanZoomBar(BottomY, RW, H: Integer; +procedure TPanafallPanel.PositionPanZoomBar(X0, BottomY, RW, H: Integer; AVisible: Boolean); -var BtnW, Gap, StripW, X, NBtns: Integer; WantAdd: Boolean; +var BtnW, Gap, StripW, X, NBtns: Integer; WantAdd, WantGrid: Boolean; begin if (FPbPanZoom = nil) or (FBtnZoomIn = nil) then Exit; - // «⊞» добавления пана живёт в этом же ряду; хозяин включает его флагом - // AddPanEnabled (Visible кнопки управляется здесь вместе с рядом). - WantAdd := FAddPanWanted and (FBtnAddPan <> nil); + // «⊞» добавления пана и «▦/▤» грид-тумблер живут в этом же ряду; хозяин + // включает их флагами AddPanEnabled/GridBtnEnabled (Visible — здесь). + WantAdd := FAddPanWanted and (FBtnAddPan <> nil); + WantGrid := FGridBtnWanted and (FBtnGrid <> nil); FPbPanZoom.Visible := AVisible; FBtnZoomIn.Visible := AVisible; FBtnZoomDef.Visible := AVisible; FBtnZoomOut.Visible := AVisible; if FBtnAddPan <> nil then FBtnAddPan.Visible := AVisible and WantAdd; + if FBtnGrid <> nil then FBtnGrid.Visible := AVisible and WantGrid; if not AVisible then Exit; Gap := MulDiv(2, Screen.PixelsPerInch, 96); BtnW := H; // квадратные кнопки в высоту строки NBtns := 3; - if WantAdd then NBtns := 4; + if WantAdd then Inc(NBtns); + if WantGrid then Inc(NBtns); StripW := RW - NBtns * BtnW - (NBtns + 1) * Gap; if StripW < 20 then StripW := 20; - FPbPanZoom.SetBounds(0, BottomY, StripW, H); - X := StripW + Gap; + FPbPanZoom.SetBounds(X0, BottomY, StripW, H); + X := X0 + StripW + Gap; FBtnZoomOut.SetBounds(X, BottomY, BtnW, H); Inc(X, BtnW + Gap); FBtnZoomDef.SetBounds(X, BottomY, BtnW, H); Inc(X, BtnW + Gap); FBtnZoomIn.SetBounds(X, BottomY, BtnW, H); + if WantGrid then + begin + Inc(X, BtnW + Gap); + FBtnGrid.SetBounds(X, BottomY, BtnW, H); + end; if WantAdd then begin Inc(X, BtnW + Gap); @@ -571,6 +608,31 @@ begin FPbPanZoom.Invalidate; end; +function TPanafallPanel.MinLayoutHeight(ATopOffset: Integer; + AShowSpectrum, AShowWaterfall: Boolean): Integer; +const + SPLITTER_H = 5; + MIN_SH = 60; // = Layout + MIN_WH = 40; // = Layout +var + RULER_H, PANZOOM_H, HEADER_H: Integer; +begin + RULER_H := MulDiv(18, Screen.PixelsPerInch, 96); + PANZOOM_H := MulDiv(22, Screen.PixelsPerInch, 96); + HEADER_H := MulDiv(20, Screen.PixelsPerInch, 96); + Result := ATopOffset; + if FHeaderVisible then Inc(Result, HEADER_H); + if (AShowSpectrum or AShowWaterfall) and FShowZoomRow then + Inc(Result, PANZOOM_H); + if AShowSpectrum then Inc(Result, MIN_SH + RULER_H); + if AShowWaterfall then + begin + Inc(Result, MIN_WH); + if AShowSpectrum then Inc(Result, SPLITTER_H) + else Inc(Result, RULER_H); // водопад-only тоже рисует линейку + end; +end; + procedure TPanafallPanel.Layout(ATopOffset: Integer; AShowSpectrum, AShowWaterfall: Boolean); const @@ -578,20 +640,29 @@ const MIN_SH = 60; // минимальная высота спектра MIN_WH = 40; // минимальная высота водопада var - RW, RH, SH, WH, TopOff: Integer; + RW, RH, SH, WH, TopOff, X0: Integer; AvailH: Integer; RULER_H: Integer; PANZOOM_H: Integer; HEADER_H: Integer; ShowPZ: Boolean; + EffRatio: Double; begin if (FParent = nil) or (FPbSpectrum = nil) then Exit; FShowSpectrum := AShowSpectrum; FShowWaterfall := AShowWaterfall; FSplitAvailH := 0; // валидно только в режиме «спектр+водопад» (ниже) RULER_H := MulDiv(18, Screen.PixelsPerInch, 96); - RW := FParent.ClientWidth; - // Регион пана в стеке: [FStackTop .. RH). 0/0 = весь родитель (один пан). + // Регион пана — прямоугольник [X0..X0+RW) × [FStackTop..RH): грид-раскладка + // ставит паны колонками; 0/0 по каждой оси = весь родитель. + X0 := FStackLeft; + if FStackWidth > 0 then + RW := FStackWidth + else + begin + X0 := 0; + RW := FParent.ClientWidth; + end; if FStackHeight > 0 then RH := FStackTop + FStackHeight else @@ -603,14 +674,14 @@ begin FHeaderPanel.Visible := FHeaderVisible; if FHeaderVisible then begin - FHeaderPanel.SetBounds(0, FStackTop, RW, HEADER_H); + FHeaderPanel.SetBounds(X0, FStackTop, RW, HEADER_H); PositionHeaderChildren(RW, HEADER_H); end; end; // Резервируем низ под панель пана/зума (видна, если виден спектр или водопад). PANZOOM_H := MulDiv(22, Screen.PixelsPerInch, 96); ShowPZ := (AShowSpectrum or AShowWaterfall) and FShowZoomRow; - PositionPanZoomBar(RH - PANZOOM_H, RW, PANZOOM_H, ShowPZ); + PositionPanZoomBar(X0, RH - PANZOOM_H, RW, PANZOOM_H, ShowPZ); if ShowPZ then RH := RH - PANZOOM_H; TopOff := ATopOffset + FStackTop; if FHeaderVisible then Inc(TopOff, HEADER_H); @@ -634,9 +705,9 @@ begin AvailH := RH - TopOff - RULER_H; if AvailH < MIN_SH then AvailH := MIN_SH; SH := AvailH; - FPbSpectrum.SetBounds(0, TopOff, RW, SH); + FPbSpectrum.SetBounds(X0, TopOff, RW, SH); FPbSpectrum.Visible := True; - FPbRuler.SetBounds(0, TopOff + SH, RW, RULER_H); + FPbRuler.SetBounds(X0, TopOff + SH, RW, RULER_H); FPbRuler.Visible := True; FSplitter.Visible := False; FPbWaterfall.Visible := False; @@ -663,9 +734,9 @@ begin WH := AvailH; FPbSpectrum.Visible := False; FSplitter.Visible := False; - FPbWaterfall.SetBounds(0, TopOff, RW, WH); + FPbWaterfall.SetBounds(X0, TopOff, RW, WH); FPbWaterfall.Visible := True; - FPbRuler.SetBounds(0, TopOff + WH, RW, RULER_H); + FPbRuler.SetBounds(X0, TopOff + WH, RW, RULER_H); FPbRuler.Visible := True; FSpectrumWidth := RW; FSpectrumHeight := 0; @@ -694,24 +765,29 @@ begin FSplitTopOff := TopOff; FSplitAvailH := AvailH; - // Ограничиваем соотношение, чтобы каждая зона имела минимальный размер - if FSplitterRatio < MIN_SH / AvailH then - FSplitterRatio := MIN_SH / AvailH; - if FSplitterRatio > 1.0 - MIN_WH / AvailH then - FSplitterRatio := 1.0 - MIN_WH / AvailH; + // Ограничиваем соотношение, чтобы каждая зона имела минимальный размер. + // Кламп — в ЛОКАЛЬНУЮ переменную: FSplitterRatio правит только юзер + // (drag сплиттера). Иначе в маленьком регионе (стек панов) кламп + // перезаписывал ratio (60/AvailH → ~0.7) и после закрытия панов + // пропорция «50/50» не восстанавливалась. + EffRatio := FSplitterRatio; + if EffRatio < MIN_SH / AvailH then + EffRatio := MIN_SH / AvailH; + if EffRatio > 1.0 - MIN_WH / AvailH then + EffRatio := 1.0 - MIN_WH / AvailH; - SH := Round(AvailH * FSplitterRatio); + SH := Round(AvailH * EffRatio); WH := AvailH - SH; if WH < MIN_WH then WH := MIN_WH; // Спектр - FPbSpectrum.SetBounds(0, TopOff, RW, SH); + FPbSpectrum.SetBounds(X0, TopOff, RW, SH); // Линейка частот — всегда прижата к низу спектра - FPbRuler.SetBounds(0, TopOff + SH, RW, RULER_H); + FPbRuler.SetBounds(X0, TopOff + SH, RW, RULER_H); // Сплиттер — между линейкой и водопадом - FSplitter.SetBounds(0, TopOff + SH + RULER_H, RW, SPLITTER_H); + FSplitter.SetBounds(X0, TopOff + SH + RULER_H, RW, SPLITTER_H); // Водопад — под сплиттером - FPbWaterfall.SetBounds(0, TopOff + SH + RULER_H + SPLITTER_H, RW, WH); + FPbWaterfall.SetBounds(X0, TopOff + SH + RULER_H + SPLITTER_H, RW, WH); // Обновляем переменные размеров FSpectrumWidth := RW; diff --git a/doc/SLICES_PLAN.md b/doc/SLICES_PLAN.md index 1269638..cee03d3 100644 --- a/doc/SLICES_PLAN.md +++ b/doc/SLICES_PLAN.md @@ -573,6 +573,77 @@ RATEPROBE удалены.** Наблюдение юзера: смена rate Г BtnZoomIn/Def/Out всех панов). Урок: PanFromSender работает только для контролов с Owner=панель (TLabel шапки — да, MakeFlatBtn-кнопки — нет). +**СТАТУС «грид-раскладка панов» (2026-07-12): РЕАЛИЗОВАНА, собирается, НА +ЭКРАНЕ НЕ ПРОВЕРЕНА, НЕ закоммичена.** +- Панель: регион пана обобщён до прямоугольника — `StackLeft/StackWidth` + (0/0 = вся ширина), Layout смещает ВСЁ по X0 (шапка/спектр/линейка/ + сплиттер/водопад/ряд зума); `PositionPanZoomBar` получил X0 и 5-ю кнопку; + новая кнопка `FBtnGrid` («▦»/«▤», глифом рулит хозяин) + `GridClick` + (задать до Build) + `GridBtnEnabled`; ReparentTo переносит кнопку. +- MainForm: `FPanGridMode` (сессия), `FPanGridRowShare` (доля верхнего ряда, + 0.15..0.85, дефолт 0.5), `FPanColShare[1..]` (доли колонок, init 1.0, + нормируются суммой), `FPanVSplitters[1..]` (вертикальный сплиттер СЛЕВА от + колонки пана i, crHSplit, создаётся в CreatePanadapter, free в + ClosePanadapter). `PanGridActive` = режим включён И >=2 доп. панов в стеке + (иначе автоткат в стек, режим помнится). LayoutPanStack: ветка грида — пан 0 + верхний ряд во всю ширину (wideband внутри как в стеке), паны N колонками; + горизонтальный сплиттер ряда = сплиттер ПЕРВОЙ колонки во всю ширину + (PanSplitMouseDown/Up ветвятся по PanGridActive: в гриде правят + FPanGridRowShare напрямую), сплиттеры остальных колонок скрыты; вертикальные + PanVSplitMouse* — зеркало горизонтальных (призрак, min 15% ширины, + DShare=DeltaX/TotalW*ShareSum). Кнопка «▦/▤» видна при FPanGridMode или + >=3 застекованных панах (выключить можно всегда). PopOutPan обнуляет + StackLeft/Width и прячет V-сплиттер. Темизация: V-сплиттеры T.Border, + BtnGrid через StyleButton. +- Флаги/мышь/зум панов грид не трогает: всё в координатах канвы спектра, + канва целиком переезжает в регион-колонку. +- Проверить на экране: тумблер при 3 панах, оба типа сплиттеров, wideband в + гриде, pop-out/возврат из грида, закрытие пана в гриде (автоткат), + узкие колонки (зум/клик-тюн/drag), отсутствие регрессий при 1-2 панах. + +**ФИКСЫ ПО ПЕРВОМУ ЭКРАННОМУ ТЕСТУ ГРИДА (2026-07-12, собирается, не +проверено):** три независимых бага из фидбека юзера. +1. **Наезд панов при выходе из грида (и вообще при 4 панах в стеке).** + LayoutPanStack делил высоту по долям (после add/close все доли равные, + при 4 панах = 25%), а Layout панели клампит спектр/водопад к минимумам + (MIN_SH=60/MIN_WH=40) — виджеты вылезали за регион, следующий пан + рисовался поверх (пан 0 терял водопад и ряд кнопок). Фикс: + `TPanafallPanel.MinLayoutHeight` (те же константы/DPI, что Layout) + + `PanMinHeight(PanId)` в MainForm (пан 0 += wideband `FPanWideH`); + раздача в стеке клампит каждый пан минимумом и режет раздутые так, + чтобы оставшимся хватило на их минимумы; в гриде Row0H клампится + минимумом пана 0 и минимумом нижнего ряда (макс. по колонкам); + драг межпанного сплиттера ограждён `Max(15%, минимум содержимого)` + в обеих ветках (стек/грид). +2. **«Блуждающие» кнопки после STOP (− − ⌂ +, кнопки на линейке, ⊞ накрыт).** + MakeFlatBtn создаёт кнопки с Owner=КОНТЕЙНЕР (не панель) → при гибели + пана (STOP → DestroyAllExtraPans → P.Free) кнопки −/⌂/+/⊞/▦ НЕ + освобождались и оставались жить на PanelRight в старых координатах, + а PanOfZoomButton, не найдя пан, роутил их клики на главный зум. + Фикс: TPanafallPanel.Destroy явно освобождает FBtnZoomOut/Def/In, + FBtnAddPan, FBtnGrid (FBtnPopPan/ClosePan — Owner=FHeaderPanel, + умирают с ним). Перезапуск программы «лечил», потому что утечка — + в пределах процесса. +3. **Жор CPU (+~40% за пан).** SyncPanViews (тик, частота кадров) звал + SyncPanZoomUI с БЕЗУСЛОВНЫМ `PbPanZoom.Invalidate` на каждый пан каждый + кадр (регрессия cb6bdfa) = полный софт-рендер зум-бара + Qt-рекомпозит + @~50fps × N панов. Фикс: инвалидация только внутри + ZoomBar.SetFreq/SetState (они no-op при тех же значениях), событийный + Invalidate перенесён в RefreshPanZoom. Дополнительно жор усиливался + багом 1 (наезжающие GL-виджеты сверх окна). Замер юзера — после фикса. + +Ответ на вопрос «почему только 3 доп. пана»: MAX_PANS=4 (WDSPEngine) — +пан 0 + 3 доп.; 6 было у СЛАЙСОВ (MAX_SLICES=6 доп.). Поднять можно (P2 +позволяет много DDC), но сначала подтвердить CPU после фиксов. + +Фиксы 1-3 ПОДТВЕРЖДЕНЫ юзером на экране 2026-07-12 («работает», CPU со +всеми панами 17-20%). Добит хвост: +4. **Пропорция спектр/водопад пана 0 не восстанавливалась после закрытия + панов (50/50 → 70/30).** Layout клампил ratio минимумами С ЗАПИСЬЮ в + FSplitterRatio: в маленьком регионе (стек панов) MIN_SH/AvailH ≈ 0.7 + запоминался навсегда. Фикс: кламп в локальную EffRatio, FSplitterRatio + меняет только drag сплиттера юзером. + ### 3.6 — Pluto+ (2r2t): второй пан на втором антенном входе Опционально, после 3.0-3.3. Обычного Pluto НЕ касается (там один RX разведён).