{
  Copyright (C)
  2026 - Uladzimir Karpenka, EW8BAK

  This program is free software; you can redistribute it and/or
  modify it under the terms of the GNU General Public License
  as published by the Free Software Foundation; either version 2
  of the License, or (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
}

unit FlatPopupMenu;

{
  TFlatPopupMenu — кастомное popup-меню в стиле приложения (не нативное
  TPopupMenu, чтобы одинаково выглядело на всех платформах и жило в цветовой
  теме). Реализовано как ДОЧЕРНИЙ контрол верхнеуровневой формы (а не отдельное
  окно): Wayland не позволяет клиенту позиционировать свои top-level окна —
  они центрируются компоновщиком. Дочерний контрол позиционируется в клиентских
  координатах формы и работает на всех платформах (тот же приём, что у
  выпадающего списка TFlatComboBox).

  Owner-draw список: hover-подсветка, галка текущего пункта, клавиатура
  (↑↓/Enter/Esc), закрытие по клику вне (MouseCapture) и по потере фокуса.

  Использование:
    Menu.SetTheme(T);
    Menu.Clear;
    Menu.AddItem('20m', BandIdx, Checked);
    Menu.OnSelect := Handler;          // Handler(Tag)
    Menu.PopupBelow(AnchorControl);    // выпадает под якорем

  Селекторы диапазона/samplerate панадаптеров (этап 3.6).
}

{$IFDEF FPC}
  {$MODE Delphi}
{$ENDIF}

interface

uses
  Classes, SysUtils, Math, Types, Controls, Graphics, Forms, LCLType, LCLIntf,
  AppTheme, DpiUtils;

type
  TFlatMenuSelect = procedure(Tag: Integer) of object;

  TFlatPopupMenu = class(TCustomControl)
  private
    FCaptions: array of string;
    FTags:     array of Integer;
    FChecked:  array of Boolean;
    FEnabled:  array of Boolean;
    FHot:      Integer;
    FItemH:    Integer;
    FTheme:    TAppTheme;
    FOnSelect: TFlatMenuSelect;
    function  ItemAt(Y: Integer): Integer;
    procedure Commit(Idx: Integer);
  protected
    procedure Paint; override;
    procedure MouseMove(Shift: TShiftState; X, Y: Integer); override;
    procedure MouseDown(Button: TMouseButton; Shift: TShiftState;
      X, Y: Integer); override;
    procedure MouseLeave; override;
    procedure DoExit; override;
    procedure KeyDown(var Key: Word; Shift: TShiftState); override;
  public
    constructor Create(AOwner: TComponent); override;
    procedure Clear;
    procedure AddItem(const ACaption: string; ATag: Integer;
      AChecked: Boolean = False; AEnabled: Boolean = True);
    procedure SetTheme(const T: TAppTheme);
    procedure PopupBelow(Anchor: TControl);   // выпадает под контролом-якорем
    procedure ClosePopup;
    property OnSelect: TFlatMenuSelect read FOnSelect write FOnSelect;
  end;

implementation

const
{$IFDEF WINDOWS}
  UI_FONT = 'Segoe UI';
{$ELSE}
  UI_FONT = 'Sans';
{$ENDIF}
  BASE_ITEM_H = 22;
  BASE_PAD    = 10;   // левый отступ текста
  BASE_CHECKW = 16;   // колонка галки слева
  BASE_MINW   = 96;

constructor TFlatPopupMenu.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  ControlStyle := ControlStyle + [csOpaque];
  TabStop   := True;
  Visible   := False;
  Font.Name := UI_FONT;
  Font.Size := 9;
  FHot      := -1;
  FTheme    := DarkTheme;
end;

procedure TFlatPopupMenu.SetTheme(const T: TAppTheme);
begin
  FTheme := T;
  Font.Color := T.Text;
  Invalidate;
end;

procedure TFlatPopupMenu.Clear;
begin
  SetLength(FCaptions, 0);
  SetLength(FTags, 0);
  SetLength(FChecked, 0);
  SetLength(FEnabled, 0);
  FHot := -1;
end;

procedure TFlatPopupMenu.AddItem(const ACaption: string; ATag: Integer;
  AChecked: Boolean; AEnabled: Boolean);
var n: Integer;
begin
  n := Length(FCaptions);
  SetLength(FCaptions, n + 1);
  SetLength(FTags,     n + 1);
  SetLength(FChecked,  n + 1);
  SetLength(FEnabled,  n + 1);
  FCaptions[n] := ACaption;
  FTags[n]     := ATag;
  FChecked[n]  := AChecked;
  FEnabled[n]  := AEnabled;
end;

function TFlatPopupMenu.ItemAt(Y: Integer): Integer;
begin
  if FItemH <= 0 then Exit(-1);
  Result := (Y - 1) div FItemH;   // -1: верхняя 1px-рамка
  if (Result < 0) or (Result >= Length(FCaptions)) then Result := -1;
end;

procedure TFlatPopupMenu.Paint;
var
  i, Y, TextY, TextX, W, H: Integer;
  R: TRect;
begin
  W := ClientWidth;
  H := ClientHeight;
  Canvas.Brush.Style := bsSolid;
  Canvas.Brush.Color := FTheme.Panel;
  Canvas.Pen.Style   := psClear;
  Canvas.FillRect(0, 0, W, H);
  Canvas.Font.Assign(Font);

  for i := 0 to High(FCaptions) do
  begin
    Y := 1 + i * FItemH;
    R := Rect(1, Y, W - 1, Y + FItemH);
    if i = FHot then
    begin
      Canvas.Brush.Style := bsSolid;
      Canvas.Brush.Color := FTheme.BtnActive;
      Canvas.Pen.Style   := psClear;
      Canvas.FillRect(R);
    end;
    // Галка текущего пункта.
    Canvas.Brush.Style := bsClear;
    if FChecked[i] then
    begin
      Canvas.Font.Color := FTheme.BtnTextActive;
      Canvas.TextOut(DpiScale(BASE_PAD) - DpiScale(2),
        Y + (FItemH - Canvas.TextHeight('Ag')) div 2, '✓');
    end;
    // Текст пункта.
    if FEnabled[i] then Canvas.Font.Color := FTheme.Text
    else               Canvas.Font.Color := FTheme.TextDim;
    TextX := DpiScale(BASE_PAD) + DpiScale(BASE_CHECKW);
    TextY := Y + (FItemH - Canvas.TextHeight('Ag')) div 2;
    Canvas.TextOut(TextX, TextY, FCaptions[i]);
  end;

  // Рамка.
  Canvas.Brush.Style := bsClear;
  Canvas.Pen.Style   := psSolid;
  Canvas.Pen.Color   := FTheme.Border;
  Canvas.Rectangle(0, 0, W, H);
end;

procedure TFlatPopupMenu.MouseMove(Shift: TShiftState; X, Y: Integer);
var Idx: Integer;
begin
  inherited MouseMove(Shift, X, Y);
  if PtInRect(ClientRect, Point(X, Y)) then Idx := ItemAt(Y) else Idx := -1;
  if Idx = FHot then Exit;
  FHot := Idx;
  Invalidate;
end;

procedure TFlatPopupMenu.MouseLeave;
begin
  inherited MouseLeave;
  if FHot <> -1 then begin FHot := -1; Invalidate; end;
end;

procedure TFlatPopupMenu.Commit(Idx: Integer);
var T: Integer;
begin
  if (Idx < 0) or (Idx > High(FCaptions)) or not FEnabled[Idx] then
  begin
    ClosePopup;
    Exit;
  end;
  T := FTags[Idx];
  ClosePopup;
  if Assigned(FOnSelect) then FOnSelect(T);
end;

procedure TFlatPopupMenu.MouseDown(Button: TMouseButton; Shift: TShiftState;
  X, Y: Integer);
begin
  inherited MouseDown(Button, Shift, X, Y);
  // Клик вне списка (MouseCapture доставляет сюда координаты вне ClientRect) —
  // просто закрыть.
  if (Button <> mbLeft) or not PtInRect(ClientRect, Point(X, Y)) then
  begin
    ClosePopup;
    Exit;
  end;
  Commit(ItemAt(Y));
end;

procedure TFlatPopupMenu.DoExit;
begin
  inherited DoExit;
  ClosePopup;
end;

procedure TFlatPopupMenu.KeyDown(var Key: Word; Shift: TShiftState);
var n: Integer;
begin
  inherited KeyDown(Key, Shift);
  n := Length(FCaptions);
  case Key of
    VK_ESCAPE: begin ClosePopup; Key := 0; end;
    VK_RETURN: begin Commit(FHot); Key := 0; end;
    VK_UP:
      begin
        if n > 0 then FHot := EnsureRange(FHot - 1, 0, n - 1);
        Invalidate; Key := 0;
      end;
    VK_DOWN:
      begin
        if n > 0 then
        begin
          if FHot < 0 then FHot := 0
          else FHot := EnsureRange(FHot + 1, 0, n - 1);
        end;
        Invalidate; Key := 0;
      end;
  end;
end;

procedure TFlatPopupMenu.ClosePopup;
begin
  if not Visible then Exit;
  MouseCapture := False;
  Visible := False;
end;

procedure TFlatPopupMenu.PopupBelow(Anchor: TControl);
var
  Root: TCustomForm;
  bmp: TBitmap;
  i, maxTW, W, H, L, T: Integer;
  ScreenPt, Cl: TPoint;
begin
  if (Anchor = nil) or (Length(FCaptions) = 0) then Exit;
  Root := GetParentForm(Anchor);
  if Root = nil then Exit;

  // Размеры по временному холсту (у контрола Canvas может быть ещё невалиден).
  bmp := TBitmap.Create;
  try
    bmp.Canvas.Font.Assign(Font);
    FItemH := Max(DpiScale(BASE_ITEM_H),
                  bmp.Canvas.TextHeight('Ag') + DpiScale(8));
    maxTW := 0;
    for i := 0 to High(FCaptions) do
      maxTW := Max(maxTW, bmp.Canvas.TextWidth(FCaptions[i]));
  finally
    bmp.Free;
  end;
  W := DpiScale(BASE_PAD) + DpiScale(BASE_CHECKW) + maxTW + DpiScale(BASE_PAD);
  W := Max(W, DpiScale(BASE_MINW));
  H := Length(FCaptions) * FItemH + 2;

  // Позиция под якорем в КЛИЕНТСКИХ координатах формы.
  ScreenPt := Anchor.ClientToScreen(Point(0, Anchor.Height + 1));
  Cl := Root.ScreenToClient(ScreenPt);
  L := Cl.X;
  T := Cl.Y;
  if L + W > Root.ClientWidth  then L := Root.ClientWidth - W;
  if L < 0 then L := 0;
  if T + H > Root.ClientHeight then
    T := Root.ScreenToClient(Anchor.ClientToScreen(Point(0, 0))).Y - H;  // вверх
  if T < 0 then T := 0;

  FHot := -1;
  Parent := Root;
  SetBounds(L, T, W, H);
  Visible := True;
  BringToFront;
  SetFocus;
  MouseCapture := True;
end;

end.
