unit ce_shortcutseditor;

{$I ce_defines.inc}

interface

uses
  Classes, SysUtils, FileUtil, TreeFilterEdit, Forms, Controls, Menus, Graphics,
  ExtCtrls, LCLProc, ComCtrls, Buttons, LCLType, PropEdits, RTTIGrids,
  strutils, ce_sharedres, ce_observer, ce_interfaces, ce_common,
  ce_writableComponent, ce_dialogs, EditBtn;

type

  TShortcutItem = class(TCollectionItem)
  private
    fIdentifier: string;
    fData: TShortcut;
    fDeclarator: ICEEditableShortCut;
    property declarator: ICEEditableShortCut read fDeclarator write fDeclarator;
  published
    property identifier: string read fIdentifier write fIdentifier;
    property data: TShortcut read fData write fData;
  public
    function combination: string;
    procedure assign(source: TPersistent); override;
  end;

  TShortCutCollection = class(TWritableLfmTextComponent)
  private
    fItems: TCollection;
    procedure setItems(value: TCollection);
    function getCount: Integer;
    function getItem(index: Integer): TShortcutItem;
  published
    property items: TCollection read fItems write setItems;
  public
    constructor create(AOwner: TComponent); override;
    destructor destroy; override;
    procedure assign(source: TPersistent); override;
    //
    function findIdentifier(const identifier: string): boolean;
    function findShortcut(aShortcut: Word): TShortcutItem;
    //
    property count: Integer read getCount;
    property item[index: Integer]: TShortcutItem read getItem; default;
  end;

  TEditableShortcut = class(TPersistent)
  public
    value: TShortCut;
  published
    property shortcut: TShortCut read value write value;
  end;

  { TCEShortcutEditor }

  TCEShortcutEditor = class(TFrame, ICEEditableOptions)
    btnClear: TSpeedButton;
    btnEdit: TSpeedButton;
    Panel1: TPanel;
    fltItems: TTreeFilterEdit;
    Panel2: TPanel;
    propedit: TTIPropertyGrid;
    tree: TTreeView;
    procedure btnClearClick(Sender: TObject);
    procedure btnEditClick(Sender: TObject);
    function fltItemsFilterItem(Item: TObject; out Done: Boolean): Boolean;
    procedure shortcutCatcherExit(Sender: TObject);
    procedure shortcutCatcherMouseLeave(Sender: TObject);
    procedure propeditModified(Sender: TObject);
    procedure treeSelectionChanged(Sender: TObject);
  private
    fObservers: TCEEditableShortCutSubject;
    fShortcuts: TShortCutCollection;
    fBackup: TShortCutCollection;
    fHasChanged: boolean;
    propvalue: TEditableShortcut;
    fHasScaled: boolean;
    //
    procedure updateScaling;
    function optionedWantCategory(): string;
    function optionedWantEditorKind: TOptionEditorKind;
    function optionedWantContainer: TPersistent;
    procedure optionedEvent(event: TOptionEditorEvent);
    function optionedOptionsModified: boolean;
    //
    function findCategory(const aName: string; aData: Pointer): TTreeNode;
    function findCategory(const aShortcutItem: TShortcutItem): string;
    function sortCategories(Cat1, Cat2: TTreeNode): integer;
    procedure receiveShortcuts;
    procedure updateEditCtrls;
    procedure sendShortcuts;
    function anItemIsSelected: boolean;
  public
    constructor create(TheOwner: TComponent); override;
    destructor destroy; override;
  end;

implementation
{$R *.lfm}

var
  CEShortcutEditor: TCEShortcutEditor;

{$REGION TShortCutCollection ---------------------------------------------------}
function TShortcutItem.combination: string;
begin
  result := ShortCutToText(fData);
end;

procedure TShortcutItem.assign(source: TPersistent);
var
  src: TShortcutItem;
begin
  if source is TShortcutItem then
  begin
    src := TShortcutItem(source);
    fData:= src.fData;
    fIdentifier:= src.fIdentifier;
    fDeclarator := src.fDeclarator;
  end
  else inherited;
end;

constructor TShortCutCollection.create(AOwner: TComponent);
begin
  inherited;
  fItems := TCollection.Create(TShortcutItem);
end;

destructor TShortCutCollection.destroy;
begin
  fItems.Free;
  inherited;
end;

procedure TShortCutCollection.assign(source: TPersistent);
begin
  if source is TShortCutCollection then
    fItems.Assign(TShortCutCollection(source).fItems)
  else
    inherited;
end;

procedure TShortCutCollection.setItems(value: TCollection);
begin
  fItems.Assign(value);
end;

function TShortCutCollection.getCount: Integer;
begin
  exit(fItems.Count);
end;

function TShortCutCollection.getItem(index: Integer): TShortcutItem;
begin
  exit(TShortcutItem(fItems.Items[index]));
end;

function TShortCutCollection.findIdentifier(const identifier: string): boolean;
var
  i: Integer;
begin
  result := false;
  for i := 0 to count-1 do
    if item[i].identifier = identifier then
      exit(true);
end;

function TShortCutCollection.findShortcut(aShortcut: Word): TShortcutItem;
var
  i: Integer;
begin
  result := nil;
  for i := 0 to count-1 do
    if item[i].data = aShortcut then
      exit(item[i]);
end;
{$ENDREGION}

{$REGION Standard Comp/Object things -------------------------------------------}
constructor TCEShortcutEditor.create(TheOwner: TComponent);
begin
  inherited;
  propvalue := TEditableShortcut.Create;
  fObservers := TCEEditableShortCutSubject.create;
  fShortcuts := TShortCutCollection.create(self);
  fBackup := TShortCutCollection.create(self);
  EntitiesConnector.addObserver(self);
  propedit.TIObject := propvalue;
  propedit.PropertyEditorHook.AddHandlerModified(@propeditModified);
end;

destructor TCEShortcutEditor.destroy;
begin
  propvalue.Free;
  fObservers.Free;
  inherited;
end;

procedure TCEShortcutEditor.updateScaling;
begin
  if fHasScaled then
    exit;
  fHasScaled := true;
  case GetIconScaledSize of
    iss16:
    begin
      AssignPng(btnClear, 'CLEAN');
      AssignPng(btnEdit, 'SHORTCUTS');
      AssignPng(fltItems.Glyph, 'FILTER_CLEAR')
    end;
    iss24:
    begin
      AssignPng(btnClear, 'CLEAN24');
      AssignPng(btnEdit, 'SHORTCUTS24');
      AssignPng(fltItems.Glyph, 'FILTER_CLEAR24')
    end;
    iss32:
    begin
      AssignPng(btnClear, 'CLEAN32');
      AssignPng(btnEdit, 'SHORTCUTS32');
      AssignPng(fltItems.Glyph, 'FILTER_CLEAR32')
    end;
  end;
  panel2.Height:=scaleY(30, 96);
  propedit.DefaultItemHeight:= scaleY(26, 96);
  propedit.BuildPropertyList();
end;

function TCEShortcutEditor.anItemIsSelected: boolean;
begin
  result := true;
  if tree.Selected.isNil or (tree.Selected.Level = 0) or tree.Selected.Data.isNil then
    result := false;
end;
{$ENDREGION}

{$REGION ICEEditableOptions ----------------------------------------------------}
function TCEShortcutEditor.optionedWantCategory(): string;
begin
  exit('Shortcuts');
end;

function TCEShortcutEditor.optionedWantEditorKind: TOptionEditorKind;
begin
  exit(oekControl);
end;

function TCEShortcutEditor.optionedWantContainer: TPersistent;
begin
  updateScaling;
  receiveShortcuts;
  exit(self);
end;

procedure TCEShortcutEditor.optionedEvent(event: TOptionEditorEvent);
begin
  case event of
    oeeSelectCat: receiveShortcuts;
    oeeCancel:
    begin
      fShortcuts.assign(fBackup);
      sendShortcuts;
      fHasChanged := false;
    end;
    oeeAccept:
    begin
      fBackup.assign(fShortcuts);
      sendShortcuts;
      fHasChanged := false;
    end;
  end;
end;

function TCEShortcutEditor.optionedOptionsModified: boolean;
begin
  exit(fHasChanged);
end;
{$ENDREGION}

{$REGION shortcut editor things ------------------------------------------------}
procedure TCEShortcutEditor.treeSelectionChanged(Sender: TObject);
begin
  updateEditCtrls;
end;

procedure TCEShortcutEditor.shortcutCatcherExit(Sender: TObject);
begin
  updateEditCtrls;
end;

procedure TCEShortcutEditor.shortcutCatcherMouseLeave(Sender: TObject);
begin
  updateEditCtrls;
end;

procedure TCEShortcutEditor.propeditModified(Sender: TObject);
var
  i: integer;
  sh: TShortCut;
  sht: string;
  dup: TShortcutItem = nil;
const
  msg = '"%s" is already assigned in the same category by "%s". The new shortcut will be ignored';
begin
  if not anItemIsSelected then
    exit;
  sh := propvalue.value;
  sht := shortCutToText(sh);
  if sht.isEmpty then
    exit;
  for i:= 0 to tree.Selected.Parent.Count-1 do
    if i <> tree.Selected.Index then
      if TShortcutItem(tree.Selected.Parent.Items[i].Data).data = sh then
        dup := TShortcutItem(tree.Selected.Parent.Items[i].Data);
  if dup.isNotNil then
    dlgOkInfo(format(msg,[ShortCutToText(sh), dup.identifier]))
  else if TShortcutItem(tree.Selected.Data).data <> sh then
  begin
    TShortcutItem(tree.Selected.Data).data := sh;
    fHasChanged := true;
  end;
  updateEditCtrls;
end;

procedure TCEShortcutEditor.btnClearClick(Sender: TObject);
begin
  if not anItemIsSelected then
    exit;
  if TShortcutItem(tree.Selected.Data).data <> 0 then
  begin
    TShortcutItem(tree.Selected.Data).data := 0;
    fHasChanged := true;
  end;
  updateEditCtrls;
end;

procedure TCEShortcutEditor.btnEditClick(Sender: TObject);
begin
  if not anItemIsSelected then
    exit;
  propedit.Rows[0].Editor.Edit;
end;

function TCEShortcutEditor.fltItemsFilterItem(Item: TObject; out Done: Boolean): Boolean;
var
  shc: TShortcutItem;
begin
  if fltItems.Filter.isBlank then
  begin
    result := true;
    done := true;
  end
  else
  begin
    result := false;
    done := false;
    // see TTreeFilterEdit: they pass TObject(TTreeNode.Data) and not a TTreeNode
    if Pointer(item).isNil then
      exit;
    shc := TShortcutItem(item);
    result := AnsiContainsText(shc.combination, fltItems.Filter);
    done := true;
  end;
end;

procedure TCEShortcutEditor.updateEditCtrls;
var
  shc: TShortcutItem;
begin
  if not anItemIsSelected then
    exit;
  shc := TShortcutItem(tree.Selected.Data);
  if propvalue.value <> shc.data then
  begin
    propvalue.value := shc.data;
    propedit.BuildPropertyList;
  end;
end;

function TCEShortcutEditor.findCategory(const aName: string; aData: Pointer): TTreeNode;
var
  i: integer;
begin
  result := nil;
  for i:= 0 to tree.Items.Count-1 do
    if tree.Items[i].Text = aName then
      if tree.Items[i].Data = aData then
        exit(tree.Items[i]);
end;

function TCEShortcutEditor.findCategory(const aShortcutItem: TShortcutItem): string;
var
  i, j: integer;
begin
  result := '';
  for i := 0 to tree.Items.Count-1 do
    for j:= 0 to tree.Items.Item[i].Count-1 do
      if tree.Items.Item[i].Items[j].Data = Pointer(aShortcutItem) then
        exit(tree.Items.Item[i].Text);
end;

function TCEShortcutEditor.sortCategories(Cat1, Cat2: TTreeNode): integer;
begin
  result := CompareText(Cat1.Text, Cat2.Text);
end;

procedure TCEShortcutEditor.receiveShortcuts;
var
  i: Integer;
  obs: ICEEditableShortCut;
  cat: string;
  sht: word;
  idt: string;
  itm: TShortcutItem;
procedure addItem();
var
  prt: TTreeNode;
begin
  // root category
  if cat.isEmpty or idt.isEmpty then
    exit;
  prt := findCategory(cat, obs);
  if prt.isNil then
    prt := tree.Items.AddObject(nil, cat, obs);
  // item as child
  itm := TShortcutItem(fShortcuts.items.Add);
  itm.identifier := idt;
  itm.data:= sht;
  itm.declarator := obs;
  tree.Items.AddChildObject(prt, idt, itm);
  cat := '';
  idt := '';
end;
begin
  tree.Items.Clear;
  fShortcuts.items.Clear;
  fBackup.items.Clear;
  cat := '';
  idt := '';
  for i:= 0 to fObservers.observersCount-1 do
  begin
    obs := fObservers.observers[i] as ICEEditableShortCut;
    if obs.scedWantFirst then
    begin
      while obs.scedWantNext(cat, idt, sht) do
        addItem();
      addItem();
    end;
  end;
  tree.Items.SortTopLevelNodes(@sortCategories);
  fBackup.Assign(fShortcuts);
end;

procedure TCEShortcutEditor.sendShortcuts;
var
  i: integer;
  shc: TShortcutItem;
  decl: ICEEditableShortCut = nil;
  cat: string;
begin
  for i := 0 to fShortcuts.count-1 do
  begin
    shc := fShortcuts[i];
    decl:= shc.declarator;
    if decl = nil then
      continue;
    cat := findCategory(shc);
    if cat.isEmpty then
      continue;
    decl.scedSendItem(cat, shc.identifier, shc.data);
    if i = fShortcuts.count-1 then
      decl.scedSendDone
    // fShortcuts is always sorted by declarator, cf. receiveShortcuts()
    else if decl <> fShortcuts[i+1].declarator then
      decl.scedSendDone;
  end;
end;

{$ENDREGION}

initialization
  CEShortcutEditor := TCEShortcutEditor.Create(nil);
finalization
  CEShortcutEditor.Free;
end.

