{ Copyright (C) 2005

 *****************************************************************************
  See the file COPYING.modifiedLGPL.txt, included in this distribution,
  for details about the license.
 *****************************************************************************

  Author: Lagunov Aleksey

  Abstract:
    Property Editor for TTreeView.
}

unit TreeViewPropEdit;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils,
  // LCL
  LCLType, Forms, Dialogs, Buttons, Controls, StdCtrls, ComCtrls, ImgList, Spin,
  ButtonPanel, ExtCtrls,
  // IdeIntf
  PropEdits, ComponentEditors, ObjInspStrConsts, IDEImagesIntf, IDEWindowIntf;

type

  { TTreeViewItemsEditorForm }

  TTreeViewItemsEditorForm = class(TForm)
    btnApply: TBitBtn;
    ButtonPanel: TButtonPanel;
    lblImageIndex: TLabel;
    lblSelectedIndex: TLabel;
    lblStateIndex: TLabel;
    dlgOpen: TOpenDialog;
    dlgSave: TSaveDialog;
    pnlStateIndex: TPanel;
    pnlSelectedIndex: TPanel;
    pnlImageIndex: TPanel;
    pnlImageIndexes: TPanel;
    spnImageIndex: TSpinEdit;
    spnSelectedIndex: TSpinEdit;
    spnStateIndex: TSpinEdit;
    ToolBar: TToolBar;
    tbNewItem: TToolButton;
    tbNewSubItem: TToolButton;
    tb1: TToolButton;
    tbDelete: TToolButton;
    tb2: TToolButton;
    tbRename: TToolButton;
    tb3: TToolButton;
    tbMoveDown: TToolButton;
    tbMoveUp: TToolButton;
    tb4: TToolButton;
    tbSave: TToolButton;
    tbOpen: TToolButton;
    treEditor: TTreeView;
    procedure btnApplyClick(Sender: TObject);
    procedure FormActivate(Sender: TObject);
    procedure FormClose(Sender: TObject; var {%H-}CloseAction: TCloseAction);
    procedure FormCreate(Sender: TObject);
    procedure FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
    procedure spnIndexChange(Sender: TObject);
    procedure tbDeleteClick(Sender: TObject);
    procedure tbMoveClick(Sender: TObject);
    procedure tbNewItemClick(Sender: TObject);
    procedure tbOpenClick(Sender: TObject);
    procedure tbRenameClick(Sender: TObject);
    procedure tbSaveClick(Sender: TObject);
    procedure treEditorSelectionChanged(Sender: TObject);
  private
    fModified: Boolean;
    fTreeView: TCustomTreeView;
    procedure FinishNodeEditing;
    procedure LoadFromTree(aTreeView: TCustomTreeView);
    procedure SaveToSourceTree;
    procedure UpdateEnabledStates;
    procedure UpdateImageHints;
  public
    property Modified: Boolean read fModified write fModified;
  end;

  { TTreeViewItemsProperty }

  TTreeViewItemsProperty = class(TClassPropertyEditor)
  public
    procedure Edit; override;
    function GetAttributes: TPropertyAttributes; override;
  end;

  { TTreeViewComponentEditor }

  TTreeViewComponentEditor = class(TComponentEditor)
  public
    procedure ExecuteVerb(Index: Integer); override;
    function GetVerb(Index: Integer): string; override;
    function GetVerbCount: Integer; override;
  end;

implementation

{$R *.lfm}

// The function returns true if the tree has been changed
function EditTreeView(ATreeView: TCustomTreeView): boolean;
begin
  with TTreeViewItemsEditorForm.Create(Application) do
    try
      LoadFromTree(ATreeView);
      if ShowModal = mrOK then
        SaveToSourceTree;
      Result := Modified;
    finally
      Free;
    end;
end;

{ TTreeViewItemsEditorForm }

procedure TTreeViewItemsEditorForm.FormActivate(Sender: TObject);
begin
  Constraints.MinWidth := pnlStateIndex.Left + pnlStateIndex.Width + pnlImageIndex.Left;
end;

procedure TTreeViewItemsEditorForm.FormCreate(Sender: TObject);
begin
  fModified := false;

  { Captions }

  // form
  self    .Caption := sccsTrEdtCaption;
  btnApply.Caption := sccsTrEdtApply;

  // toolbar
  ToolBar.ShowHint := true;
  tbNewItem   .Hint := sccsTrEdtNewItem    + ' [Ctrl+N]';
  tbNewSubItem.Hint := sccsTrEdtNewSubItem + ' [Ctrl+Shift+N]';
  tbDelete    .Hint := sccsTrEdtDelete     + ' [Del]';
  tbRename    .Hint := sccsTrEdtRename     + ' [F2]';
  tbMoveDown  .Hint := rscdMoveDown        + ' [Ctrl+Shift+Down]';
  tbMoveUp    .Hint := rscdMoveUp          + ' [Ctrl+Shift+Up]';
  tbSave      .Hint := sccsTrEdtSave       + ' [Ctrl+S]';
  tbOpen      .Hint := sccsTrEdtOpen       + ' [Ctrl+O]';

  // image indexes
  lblImageIndex   .Caption := Format(sccsTrEdtLabelImageIndex, ['']);
  lblSelectedIndex.Caption := Format(sccsTrEdtLabelSelIndex  , ['']);
  lblStateIndex   .Caption := Format(sccsTrEdtLabelStateIndex, ['']);

  // dialogs
  dlgOpen.Title := sccsTrEdtOpenDialog;
  dlgSave.Title := sccsTrEdtSaveDialog;

  // button panel
  ButtonPanel.ShowHint := true;
  btnApply.TabOrder := 1;

  { Images }

  // form
  btnApply.LoadGlyphFromResource(idButtonRetry);

  // toolbar
  ToolBar.Images := IDEImages.Images_16;
  tbNewItem   .ImageIndex := IDEImages.LoadImage('new_item');
  tbNewSubItem.ImageIndex := IDEImages.LoadImage('new_subitem');
  tbDelete    .ImageIndex := IDEImages.LoadImage('item_delete');
  tbRename    .ImageIndex := IDEImages.LoadImage('item_rename');
  tbMoveDown  .ImageIndex := IDEImages.LoadImage('item_down');
  tbMoveUp    .ImageIndex := IDEImages.LoadImage('item_up');
  tbSave      .ImageIndex := IDEImages.LoadImage('items_save');
  tbOpen      .ImageIndex := IDEImages.LoadImage('items_load');

  { Layout }

  IDEDialogLayoutList.ApplyLayout(self);
end;

procedure TTreeViewItemsEditorForm.FormClose(Sender: TObject;
  var CloseAction: TCloseAction);
begin
  IDEDialogLayoutList.SaveLayout(self);
end;

procedure TTreeViewItemsEditorForm.FormKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
var
  lWasEdited: Boolean;
begin
  // form actions
  if (Key = VK_RETURN) and (Shift = [ssCtrl]) then
  begin
    ModalResult := mrOK;
    Key := 0;
  end
  else if (Key = VK_RETURN) and (Shift = [ssShift]) then
  begin
    btnApplyClick(Sender);
    Key := 0;
  end
  else if (Key = VK_ESCAPE) and (Shift = []) then
  begin
    // pressing [Esc] in an open in-place editor cancel text editing
    if not treEditor.IsEditing then
    begin
      ModalResult := mrCancel;
      Key := 0;
    end;
  end

  // create item
  else if (Key = VK_N) and (Shift = [ssCtrl]) then
  begin
    tbNewItemClick(tbNewItem); // "Sender" is the pressed button
    Key := 0;
  end
  else if (Key = VK_N) and (Shift = [ssCtrl, ssShift]) then
  begin
    tbNewItemClick(tbNewSubItem); // "Sender" is the pressed button
    Key := 0;
  end

  // delete item
  else if (Key = VK_DELETE) and (Shift = []) then
  begin
    // check to prevent it from working in TEdit or TSpinEdit
    // or when renaming node
    if treEditor.Focused and not treEditor.IsEditing then
    begin
      tbDeleteClick(Sender);
      Key := 0;
    end;
  end

  // rename item
  // the location in the FormKeyDown makes it independent of the current focus
  else if (Key = VK_F2) and (Shift = []) then
  begin
    if not treEditor.IsEditing then
      if Assigned(treEditor.Selected) then
        treEditor.Selected.EditText;
    Key := 0;
  end

  // move item
  else if (Key = VK_DOWN) and (Shift = [ssCtrl, ssShift]) then
  begin
    tbMoveClick(tbMoveDown); // "Sender" is the pressed button
    Key := 0;
  end
  else if (Key = VK_UP) and (Shift = [ssCtrl, ssShift]) then
  begin
    tbMoveClick(tbMoveUp); // "Sender" is the pressed button
    Key := 0;
  end

  // move selection
  else if (Key in [VK_DOWN, VK_UP]) and (Shift = []) then
  begin
    if treEditor.IsEditing then
    begin
      // close text editor
      // select next
      if Key = VK_DOWN then
        treEditor.MoveToNextNode(true)
      else
        treEditor.MoveToPrevNode(true);
      // open text editor
      if Assigned(treEditor.Selected) then
        treEditor.Selected.EditText;

      Key := 0;
    end;
  end

  // save and load in/from file
  else if (Key = VK_S) and (Shift = [ssCtrl]) then
  begin
    tbSaveClick(Sender);
    Key := 0;
  end
  else if (Key in [VK_O]) and (Shift = [ssCtrl]) then
  begin
    tbOpenClick(Sender);
    Key := 0;
  end;
end;

procedure TTreeViewItemsEditorForm.tbRenameClick(Sender: TObject);
begin
  if Assigned(treEditor.Selected) then
    treEditor.Selected.EditText;
end;

procedure TTreeViewItemsEditorForm.tbNewItemClick(Sender: TObject);
var
  lNewName: String;
begin
  FinishNodeEditing;

  lNewName := sccsTrEdtItem + IntToStr(treEditor.Items.Count);
  if Sender = tbNewItem then
    treEditor.Selected := treEditor.Items.Add(treEditor.Selected, lNewName)
  else if Sender = tbNewSubItem then
    treEditor.Selected := treEditor.Items.AddChild(treEditor.Selected, lNewName)
  else
    raise Exception.Create('[tbNewItemClick] Unknown Sender');

  if Assigned(treEditor.Selected) then
  begin
    treEditor.SetFocus;
    treEditor.Selected.EditText;
  end;

  UpdateEnabledStates;
end;

procedure TTreeViewItemsEditorForm.tbMoveClick(Sender: TObject);
var
  lCurNode: TTreeNode;
begin
  lCurNode := treEditor.Selected;
  if lCurNode = nil then exit;

  FinishNodeEditing;

  if Sender = tbMoveUp then
  begin
    if lCurNode.GetPrevSibling = nil then exit;
    lCurNode.MoveTo(lCurNode.GetPrevSibling, naInsert);
  end
  else if Sender = tbMoveDown then
  begin
    if lCurNode.GetNextSibling = nil then exit;
    lCurNode.MoveTo(lCurNode.GetNextSibling, naInsertBehind);
  end else
    raise Exception.Create('[tbMoveClick] Unknown Sender');

  treEditor.SetFocus;

  UpdateEnabledStates;
end;

procedure TTreeViewItemsEditorForm.treEditorSelectionChanged(Sender: TObject);
  //
  procedure UpdateImageIndexes;
  begin
    // Remove events to avoid cyclic calling
    spnImageIndex   .OnChange := nil;
    spnSelectedIndex.OnChange := nil;
    spnStateIndex   .OnChange := nil;
    try
      // Read the indexes of the selected item
      spnImageIndex   .Value := treEditor.Selected.ImageIndex;
      spnSelectedIndex.Value := treEditor.Selected.SelectedIndex;
      spnStateIndex   .Value := treEditor.Selected.StateIndex;
    finally
      // Restore events
      spnImageIndex   .OnChange := @spnIndexChange;
      spnSelectedIndex.OnChange := @spnIndexChange;
      spnStateIndex   .OnChange := @spnIndexChange;
    end;
  end;
  //
begin
  if Assigned(treEditor.Selected) then
  begin
    UpdateImageIndexes;
    UpdateImageHints;
  end;

  UpdateEnabledStates;
end;

procedure TTreeViewItemsEditorForm.btnApplyClick(Sender: TObject);
begin
  FinishNodeEditing;
  SaveToSourceTree;
end;

procedure TTreeViewItemsEditorForm.tbDeleteClick(Sender: TObject);
var
  lNextNode: TTreeNode;
begin
  if Assigned(treEditor.Selected) then
  begin
    // find node for new selection
    lNextNode := treEditor.Selected.GetNextSibling;
    if lNextNode = nil then
      lNextNode := treEditor.Selected.GetPrevSibling;
    if lNextNode = nil then
      lNextNode := treEditor.Selected.Parent;

    // delete
    treEditor.Items.Delete(treEditor.Selected);

    // select "next" node
    if lNextNode <> nil then
      treEditor.Selected := lNextNode;

    UpdateEnabledStates;
  end;
end;

procedure TTreeViewItemsEditorForm.tbOpenClick(Sender: TObject);

  function ConfirmTreeReplace: boolean;
  begin
    if treEditor.Items.Count > 0 then
      result := QuestionDlg(sccsTrEdtConfirmationCaption, sccsTrEdtConfirmationTreeReplace,
        TMsgDlgType.mtConfirmation, [mrYes, sccsTrEdtYes, mrNo, sccsTrEdtNo,
        mrCancel, sccsTrEdtCancel], 0) = mrYes
    else
      result := true;
  end;

begin
  FinishNodeEditing;

  if ConfirmTreeReplace and dlgOpen.Execute then
  begin
    treEditor.LoadFromFile(dlgOpen.FileName);

    treEditor.FullExpand;
    treEditor.Selected := treEditor.Items.GetFirstNode;
    treEditor.SetFocus;
    UpdateEnabledStates;
  end;
end;

procedure TTreeViewItemsEditorForm.tbSaveClick(Sender: TObject);

  function ImagesFound: boolean;
  var
    i: Integer;
  begin
    for i := 0 to treEditor.Items.Count - 1 do
      if (treEditor.Items[i].ImageIndex    >= 0) or
         (treEditor.Items[i].SelectedIndex >= 0) or
         (treEditor.Items[i].StateIndex    >= 0)
      then
        exit(true);
    result := false;
  end;

  function ConfirmImagesLoss: boolean;
  begin
    if ImagesFound then
      result := QuestionDlg(sccsTrEdtConfirmationCaption, sccsTrEdtConfirmationImagesLoss,
        TMsgDlgType.mtConfirmation, [mrYes, sccsTrEdtYes, mrNo, sccsTrEdtNo,
        mrCancel, sccsTrEdtCancel], 0) = mrYes
    else
      result := true;
  end;

  function ConfirmFileReplace: boolean;
  begin
    if FileExists(dlgSave.FileName) then
      result := QuestionDlg(sccsTrEdtConfirmationCaption, sccsTrEdtConfirmationFileReplace,
        TMsgDlgType.mtConfirmation, [mrYes, sccsTrEdtYes, mrNo, sccsTrEdtNo,
        mrCancel, sccsTrEdtCancel], 0) = mrYes
    else
      result := true;
  end;

begin
  FinishNodeEditing;

  if ConfirmImagesLoss and dlgSave.Execute and ConfirmFileReplace then
    treEditor.SaveToFile(dlgSave.FileName);
end;

procedure TTreeViewItemsEditorForm.spnIndexChange(Sender: TObject);
begin
  if Assigned(treEditor.Selected) then
  begin
    treEditor.Selected.ImageIndex := spnImageIndex.Value;
    treEditor.Selected.StateIndex := spnStateIndex.Value;
    if Sender = spnImageIndex then
      spnSelectedIndex.Value := spnImageIndex.Value
    else
      treEditor.Selected.SelectedIndex := spnSelectedIndex.Value;
    UpdateImageHints;
  end;
end;

// Show hints and "*" in label for invalid index
procedure TTreeViewItemsEditorForm.UpdateImageHints;
  //
  procedure UpdateImageHint(aSpinEdit: TSpinEdit;
    aIsStateImages: boolean; aLabel: TLabel; aCaption: string);
  const
    MAYBE_ASTERISK: array[boolean] of String[1] = ('', '*');
  var
    lImageList: TCustomImageList;
    lPropName: string;
  begin
    if aIsStateImages then
    begin
      lImageList := treEditor.StateImages;
      lPropName := fTreeView.Name + '.StateImages';
    end else begin
      lImageList := treEditor.Images;
      lPropName := fTreeView.Name + '.Images';
    end;

    // check valid index
    if aSpinEdit.Value >= 0 then
    begin
      // check assign
      if lImageList = nil then
      begin
        aLabel.Hint := Format(sccsTrEdtImageListNotAssigned, [lPropName]);
        aLabel.ShowHint := true;
      end else begin
        // check count
        if aSpinEdit.Value >= lImageList.Count then
        begin
          aLabel.Hint := Format(sccsTrEdtInvalidIndex, [lPropName, lImageList.Count]);
          aLabel.ShowHint := true;
        end else
          aLabel.ShowHint := false;
      end;
    end else
      aLabel.ShowHint := false;

    // show asterisk if necessary
    aLabel.Caption := Format(aCaption, [MAYBE_ASTERISK[aLabel.ShowHint]]);
  end;
  //
begin
  if Assigned(treEditor.Selected) then
  begin
    UpdateImageHint(spnImageIndex,    false, lblImageIndex,    sccsTrEdtLabelImageIndex);
    UpdateImageHint(spnSelectedIndex, false, lblSelectedIndex, sccsTrEdtLabelSelIndex);
    UpdateImageHint(spnStateIndex,    true,  lblStateIndex,    sccsTrEdtLabelStateIndex);
  end;
end;

procedure TTreeViewItemsEditorForm.FinishNodeEditing;
begin
  if treEditor.IsEditing then
    if treEditor.Items.Count > 0 then
      treEditor.Items[0].EndEdit(false);
    // Quick-and-dirty way to end editing and accepting the edited text
    // no matter if the edited node was selected or not.
end;

procedure TTreeViewItemsEditorForm.LoadFromTree(aTreeView: TCustomTreeView);
begin
  fTreeView := aTreeView;
  if Assigned(aTreeView) then
  begin
    // images
    treEditor.Images      := aTreeView.Images;
    treEditor.StateImages := aTreeView.StateImages;
    // items
    treEditor.Items.Assign(aTreeView.Items);
  end;
  treEditor.Selected := treEditor.Items.GetFirstNode;
  UpdateEnabledStates;
end;

procedure TTreeViewItemsEditorForm.SaveToSourceTree;
begin
  if Assigned(fTreeView) then
    fTreeView.Items.Assign(treEditor.Items);
  fModified := true;
end;

procedure TTreeViewItemsEditorForm.UpdateEnabledStates;
var
  lCurNode: TTreeNode;
  lAssigned: Boolean;
begin
  lCurNode := treEditor.Selected;
  lAssigned := Assigned(lCurNode);

  // control states
  pnlImageIndexes .Enabled := lAssigned;
  tbDelete        .Enabled := lAssigned;
  tbRename        .Enabled := lAssigned;
  tbMoveUp        .Enabled := lAssigned and Assigned(lCurNode.GetPrevSibling);
  tbMoveDown      .Enabled := lAssigned and Assigned(lCurNode.GetNextSibling);
  tbSave          .Enabled := treEditor.Items.Count > 0;

  // clear disabled fields
  if lCurNode = nil then
  begin
    spnImageIndex   .Value := -1;
    spnSelectedIndex.Value := -1;
    spnStateIndex   .Value := -1;
  end;
end;

{ TTreeViewItemsProperty }

procedure TTreeViewItemsProperty.Edit;
begin
  if EditTreeView(GetComponent(0) as TCustomTreeView) then
    Modified;
end;

function TTreeViewItemsProperty.GetAttributes: TPropertyAttributes;
begin
  result := [paDialog, paReadOnly, paRevertable];
end;

{ TTreeViewComponentEditor }

procedure TTreeViewComponentEditor.ExecuteVerb(Index: Integer);
var
  Hook: TPropertyEditorHook;
begin
  if Index = 0 then
  begin
    GetHook(Hook);
    if EditTreeView(GetComponent as TCustomTreeView) then
      if Assigned(Hook) then
        Hook.Modified(self);
  end;
end;

function TTreeViewComponentEditor.GetVerb(Index: Integer): string;
begin
  if Index = 0 then
    result := sccsTrEdt
  else
    result := '';
end;

function TTreeViewComponentEditor.GetVerbCount: Integer;
begin
  result := 1;
end;

initialization
  RegisterPropertyEditor(ClassTypeInfo(TTreeNodes), TTreeView, 'Items', TTreeViewItemsProperty);
  RegisterComponentEditor(TTreeView, TTreeViewComponentEditor);
end.

