unit VisMgrForm;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.ComCtrls, ListViewEx, VisCtl,
  Vcl.ExtCtrls, Vcl.Menus, Vcl.AppEvnts, Generics.Defaults, Generics.Collections;

type
  TVisMgr = class(TForm)
    lvItems: TListViewEx;
    RefreshTimer: TTimer;
    pmItems: TPopupMenu;
    mnuMetadata: TMenuItem;
    mnuSettings: TMenuItem;
    N1: TMenuItem;
    mnuShow: TMenuItem;
    sbStatus: TStatusBar;
    ApplicationEvents: TApplicationEvents;
    N2: TMenuItem;
    mnuRemove: TMenuItem;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure FormShow(Sender: TObject);
    procedure RefreshTimerTimer(Sender: TObject);
    procedure pmItemsPopup(Sender: TObject);
    procedure mnuMetadataClick(Sender: TObject);
    procedure mnuSettingsClick(Sender: TObject);
    procedure mnuShowClick(Sender: TObject);
    procedure lvItemsKeyDown(Sender: TObject; var Key: Word;
      Shift: TShiftState);
    procedure lvItemsDblClick(Sender: TObject);
    procedure lvItemsSelCntChange(Sender: TObject);
    procedure ApplicationEventsHint(Sender: TObject);
    procedure mnuRemoveClick(Sender: TObject);
  private
    procedure VisObjListChanged(Sender: TObject);
    procedure RefreshListDelayed;
    function SelectedObject: TVisObj;
    function SelectedObjects: TArray<TVisObj>;
  public
    procedure RefreshList;
  end;

var
  VisMgr: TVisMgr;

implementation

uses
  ClientDefs, FormFader, WinMgrForm, ObjectMetadataForm, ClientVisuals, VisCtl2D,
  rgl;

{$R *.dfm}

{ TVisMgr }

procedure TVisMgr.ApplicationEventsHint(Sender: TObject);
begin
  if Application.Hint.IsEmpty then
    lvItemsSelCntChange(Self)
  else
    sbStatus.SimpleText := Application.Hint;
end;

procedure TVisMgr.FormCreate(Sender: TObject);
begin
  TFormFader.Create(Self);
  TWindowWatcher.Create(Self);
  VisCtl.VisObjListChanged := VisObjListChanged;
end;

procedure TVisMgr.FormDestroy(Sender: TObject);
begin
  VisCtl.VisObjListChanged := nil;
end;

procedure TVisMgr.FormShow(Sender: TObject);
begin
  RefreshListDelayed;
end;

procedure TVisMgr.lvItemsDblClick(Sender: TObject);
begin
  mnuShowClick(Sender);
end;

procedure TVisMgr.lvItemsKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
  case Key of
    VK_RETURN:
      mnuShowClick(Sender);
    VK_DELETE:
      mnuRemoveClick(Sender)
  end;
end;

procedure TVisMgr.lvItemsSelCntChange(Sender: TObject);
begin
  sbStatus.SimpleText := Format('%d object(s), %d selected',
    [lvItems.Items.count, lvItems.SelCount]);
end;

procedure TVisMgr.mnuMetadataClick(Sender: TObject);
begin

  var Obj := SelectedObject;

  if Assigned(Obj) then
    ObjectMetadataForm.SetMetadata(Self, Obj);

end;

procedure TVisMgr.mnuRemoveClick(Sender: TObject);
begin

  Screen.Cursor := crHourGlass;
  try

    var Objs := SelectedObjects;

    for var Obj in Objs do
    begin

      if Obj is TDrawable then
        TDrawable(Obj).Control.RemoveObject(TDrawable(Obj))
      else if Obj is TDrawable3D then
        try
          TDrawable3D(Obj).Control.RemoveObject(TDrawable3D(Obj));
        except
          on ERglError do Continue;
        end;

    end;

  finally
    Screen.Cursor := crDefault;
  end;

end;

procedure TVisMgr.mnuSettingsClick(Sender: TObject);
begin

  var Obj := SelectedObject;

  if Assigned(Obj) then
    Obj.ShowOptionsForm(Self);

end;

procedure TVisMgr.mnuShowClick(Sender: TObject);
begin

  var Obj := SelectedObject;

  if Assigned(Obj) then
    TVisualization.ShowVisCtl(Obj.GUID);

end;

procedure TVisMgr.pmItemsPopup(Sender: TObject);
begin

  var SingleSel := lvItems.SelCount = 1;

  mnuShow.Enabled := SingleSel;
  mnuSettings.Enabled := SingleSel;
  mnuMetadata.Enabled := SingleSel;
  mnuRemove.Enabled := lvItems.SelCount > 0;

end;

procedure TVisMgr.RefreshList;

  procedure AddObject(const AVisObj: TVisObj);

    function BaseType(AVisObj: TVisObj): string;
    begin
      if AVisObj is TDrawable then
        Result := '2D object'
      else if AVisObj is TDrawable3D then
        Result := '3D object'
      else
        Result := 'object';
    end;

    function Declassify(const S: string): string;
    begin
      if (S <> '') and (S[1] = 'T') then
        Result := Copy(S, 2)
      else
        Result := S;
    end;

  begin
    var LItem := lvItems.Items.Add;
    LItem.Caption := AVisObj.Name;
    LItem.SubItems.Add(BaseType(AVisObj) + ' > ' + Declassify(AVisObj.ClassName));
    LItem.SubItems.Add(AVisObj.Title);
    LItem.SubItems.Add(AVisObj.Description);
    LItem.SubItems.Add(AVisObj.GUID.ToString);
  end;

begin

  lvItems.Items.BeginUpdate;
  try
    lvItems.Clear;
    for var P in TDrawable.Instances do
      AddObject(P.Value);
    for var P in TDrawable3D.Instances do
      AddObject(P.Value);
  finally
    lvItems.Items.EndUpdate;
    lvItems.EnableSorting([]);
    if lvItems.SortColumn = -1 then
      lvItems.SortColumn := 0;
    lvItems.Sort(lvItems.SortColumn, lvItems.SortAscending);
    lvItemsSelCntChange(Self);
  end;

end;

procedure TVisMgr.RefreshListDelayed;
begin
  // The delay serves two purposes:
  // 1. It batches changes for performance and flicker reduction.
  // 2. It allows for a newly created obj to have all its metadata set before it
  //    is included in the list.
  RestartTimer(RefreshTimer);
end;

procedure TVisMgr.RefreshTimerTimer(Sender: TObject);
begin
  RefreshTimer.Enabled := False;
  RefreshList;
end;

function TVisMgr.SelectedObject: TVisObj;
begin

  if lvItems.SelCount <> 1 then
    Exit(nil);

  if lvItems.Selected = nil then
    Exit(nil);

  if lvItems.Selected.SubItems.Count < 4 then
    Exit(nil);

  var IDstr := lvItems.Selected.SubItems[3];

  if IDstr.IsEmpty then
    Exit(nil);

  var ID := TGUID.Create(IDstr);

  if ID.IsEmpty then
    Exit(nil);

  var D2D := TDrawable(nil);
  if TDrawable.TryGetDrawableByGUID(ID, D2D) then
    Exit(D2D);

  var D3D := TDrawable3D(nil);
  if TDrawable3D.TryGetDrawableByGUID(ID, D3D) then
    Exit(D3D);

  Result := nil;

end;

function TVisMgr.SelectedObjects: TArray<TVisObj>;
begin

  var L := TList<TVisObj>.Create;
  try

    var Idxs := lvItems.GetSelectedIndicesFast;

    for var Idx in Idxs do
    begin

      var LItem := lvItems.Items[Idx];

      if Assigned(LItem) and (LItem.SubItems.Count >= 4) then
      begin

        var IDstr := LItem.SubItems[3];

        if not IDstr.IsEmpty then
        begin

          var ID := TGUID.Create(IDstr);

          if not ID.IsEmpty then
          begin

            var D2D := TDrawable(nil);
            if TDrawable.TryGetDrawableByGUID(ID, D2D) then
              L.Add(D2D);

            var D3D := TDrawable3D(nil);
            if TDrawable3D.TryGetDrawableByGUID(ID, D3D) then
              L.Add(D3D);

          end;

        end;

      end;

    end;

    Result := L.ToArray;

  finally
    L.Free;
  end;

end;

procedure TVisMgr.VisObjListChanged(Sender: TObject);
begin
  if Visible then
    RefreshListDelayed;
end;

end.
