{******************************************************************************}
{                       CnPack For Delphi/C++Builder                           }
{                     中国人自己的开放源码第三方开发包                         }
{                   (C)Copyright 2001-2015 CnPack 开发组                       }
{                   ------------------------------------                       }
{                                                                              }
{            本开发包是开源的自由软件，您可以遵照 CnPack 的发布协议来修        }
{        改和重新发布这一程序。                                                }
{                                                                              }
{            发布这一开发包的目的是希望它有用，但没有任何担保。甚至没有        }
{        适合特定目的而隐含的担保。更详细的情况请参阅 CnPack 发布协议。        }
{                                                                              }
{            您应该已经和开发包一起收到一份 CnPack 发布协议的副本。如果        }
{        还没有，可访问我们的网站：                                            }
{                                                                              }
{            网站地址：http://www.cnpack.org                                   }
{            电子邮件：master@cnpack.org                                       }
{                                                                              }
{******************************************************************************}

unit CnAOTreeView;
{* |<PRE>
================================================================================
* 软件名称：不可视工具组件包
* 单元名称：自动参数设置 TreeView 组件单元
* 单元作者：周劲羽 (zjy@cnpack.org)
* 开发平台：PWin2000 SP4 + Delphi 5.01
* 兼容测试：PWin9X/2000/XP + Delphi 5/6/7
* 本 地 化：该单元中的字符串均符合本地化处理方式
* 单元标识：$Id$
* 备    注：该单元定义了自动参数设置 TreeView 组件
*           该组件用于在运行时使用树状结构根据设置信息对象显示通用的设置界面。
================================================================================
|</PRE>}

interface

{$I CnPack.inc}

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, ComCtrls, TypInfo,
  {$IFDEF COMPILER6_UP} Variants, {$ENDIF COMPILER6_UP}
  StdCtrls, ImgList, CnSpin, Dialogs, Menus, Math, Forms, CnGraphConsts,
  CnAutoOption;

type

{ TCnAOTreeView }

  EUnsupportedPropKind = class(Exception);

  TCnOptionKind = (
    okUnknown, okGroup, okCustom, okBoolFalse, okBoolTrue, okString,
    okStringCombo, okInteger, okIntegerCombo, okFloat, okDateTime,
    okDate, okTime, okEnum, okSet, okVariant, okFont, okColor, okShortCut,
    okStrings);

  TCreateInplaceEditEvent = procedure(Sender: TObject; InplaceEdit: TControl;
    AOption: TCnBaseOption) of object;
  TGetItemTextEvent = procedure(Sender: TObject; AOption: TCnOptionItem;
    var AText: string) of object; 

  TCnAOTreeView = class(TCustomTreeView)
  private
    FImageList: TImageList;
    FInplaceEdit: TControl;
    FModified: Boolean;
    FOptions: TCnOptionGroup;
    FOnCreateInplaceEdit: TCreateInplaceEditEvent;
    FOnGetItemText: TGetItemTextEvent;
    procedure ApplyInplaceEdit;
    procedure ComboBoxDropDown(Sender: TObject);
    procedure CreateInplaceEdit;
    procedure FreeInplaceEdit;
    procedure OnColorClick(Sender: TObject);
    procedure OnFontClick(Sender: TObject);
    procedure OnInplaceEditEnterExit(Sender: TObject);
    procedure SetNodeImageIndex(Node: TTreeNode; Index: Integer);
    procedure UpdateInplaceEdit;
    procedure SetOptions(Value: TCnOptionGroup);
  protected
    function CanEdit(Node: TTreeNode): Boolean; override;
    procedure Change(Node: TTreeNode); override;
    procedure Click; override;
    function DoClickNode(Node: TTreeNode): Boolean;
    procedure DoCustomDrawItem(Sender: TCustomTreeView; Node: TTreeNode;
      State: TCustomDrawState; var DefaultDraw: Boolean);
    procedure DoEnter; override;
    procedure DoExit; override;
    function GetOptionKind(Option: TCnBaseOption; RaiseError: Boolean = False):
      TCnOptionKind;
    procedure GetSelectedIndex(Node: TTreeNode); override;
    procedure UpdateNode(Node: TTreeNode);
    procedure WMChar(var Message: TWMChar); message WM_CHAR;
    procedure WMHScroll(var Message: TWMHScroll); message WM_HSCROLL;
    procedure WMMouseWheel(var Message: TWMMouseWheel); message WM_MOUSEWHEEL;
    procedure WMVScroll(var Message: TWMVScroll); message WM_VSCROLL;
    property ImageList: TImageList read FImageList;
  public
    constructor Create(AOwner: TComponent); override;
    {* 类构造器 }
    destructor Destroy; override;
    {* 类析构器 }
    procedure ApplyOption;
    {* 应用当前的设置到对象属性 }
    procedure DefaultOption;
    {* 恢复设置为原对象属性的默认值 }
    procedure ResetOption;
    {* 恢复设置为原对象属性的当前值 }
    procedure UpdateTreeView;
    {* 更新设置树 }
    property Modified: Boolean read FModified;
    {* 标识是否有属性被修改 }
    property Options: TCnOptionGroup read FOptions write SetOptions;
    {* 用于设置的参数组对象 }
  published
    property Align;
    property Anchors;
    property AutoExpand;
    property BiDiMode;
    property BorderStyle;
    property BorderWidth;
    property ChangeDelay;
    property Color;
    property Constraints;
    property Ctl3D;
    property DragCursor;
    property DragKind;
    property DragMode;
    property Enabled;
    property Font;
    property HideSelection;
    property HotTrack;
    property Indent;
    property OnChange;
    property OnChanging;
    property OnClick;
    property OnCollapsed;
    property OnCollapsing;
    property OnContextPopup;
    property OnDblClick;
    property OnDeletion;
    property OnDragDrop;
    property OnDragOver;
    property OnEndDock;
    property OnEndDrag;
    property OnEnter;
    property OnExit;
    property OnExpanded;
    property OnExpanding;
    property OnGetSelectedIndex;
    property OnKeyDown;
    property OnKeyPress;
    property OnKeyUp;
    property OnMouseDown;
    property OnMouseMove;
    property OnMouseUp;
    property OnStartDock;
    property OnStartDrag;
    property ParentBiDiMode;
    property ParentColor default False;
    property ParentCtl3D;
    property ParentFont;
    property ParentShowHint;
    property PopupMenu;
    property RightClickSelect;
    property RowSelect;
    property ShowButtons default False;
    property ShowHint;
    property ShowLines default False;
    property ShowRoot;
    property TabOrder;
    property TabStop default True;
    property ToolTips;
    property Visible;

    property OnCreateInplaceEdit: TCreateInplaceEditEvent
      read FOnCreateInplaceEdit write FOnCreateInplaceEdit;
    property OnGetItemText: TGetItemTextEvent read FOnGetItemText write FOnGetItemText;
  end;

implementation

{$IFDEF DEBUG}
uses
  CnDebug;
{$ENDIF}

{$R *.res}

const
  csIdxGroup = 0;
  csIdxCustom = 1;
  csIdxUnChecked = 2;
  csIdxChecked = 3;
  csIdxUnSelected = 4;
  csIdxSelected = 5;
  csIdxString = 6;
  csIdxStringCombo = 7;
  csIdxInteger = 8;
  csIdxIntegerCombo = 9;
  csIdxFloat = 10;
  csIdxEnum = 11;
  csIdxSet = 12;
  csIdxVariant = 13;
  csIdxFont = 14;
  csIdxDateTime = 15;
  csIdxDate = 16;
  csIdxTime = 17;
  csIdxColor = 18;
  csIdxShortCut = 19;
  csIdxStrings = 20;
  csSepStr = ': ';

  csInplaceEditHeight = 20;
  csInplaceEditWidth = 120;
  csInplaceButtonHeight = 20;
  csInplaceButtonWidth = 60;
  csInplaceMemoHeight = 60;
  csInplaceMemoWidth = 120;

  csInplaceSpace = 4;
  csMaxStrLength = 15;

  csImageIndexs: array[TCnOptionKind] of Integer = (
    -1, csIdxGroup, csIdxCustom, csIdxUnChecked, csIdxChecked, csIdxString,
    csIdxStringCombo, csIdxInteger, csIdxIntegerCombo, csIdxFloat, csIdxDateTime,
    csIdxDate, csIdxTime, csIdxEnum, csIdxSet, csIdxVariant, csIdxFont, csIdxColor,
    csIdxShortCut, csIdxStrings);

type
  TWinControlHack = class(TWinControl);

constructor TCnAOTreeView.Create(AOwner: TComponent);
var
  Bitmap: TBitmap;
begin
  inherited;
  FImageList := TImageList.Create(Self);
  Images := FImageList;
  Bitmap := TBitmap.Create;
  try
    Bitmap.LoadFromResourceName(HInstance, 'CNAOTREEVIEW');
    FImageList.AddMasked(Bitmap, Bitmap.TransparentColor);
  finally
    Bitmap.Free;
  end;
  
  ShowLines := False;
  ShowButtons := False;
  ReadOnly := True;
  OnCustomDrawItem := DoCustomDrawItem;
end;

destructor TCnAOTreeView.Destroy;
begin
  FImageList.Free;
  inherited;
end;

procedure TCnAOTreeView.ApplyInplaceEdit;
var
  Node: TTreeNode;
  Item: TCnOptionItem;
  Obj: TObject;

  // 将字符串转换为匹配的格式
  function GetString(PropKind: TTypeKind; const Value: string): Variant;
  var
    C: Char;
    WC: WideChar;
    WS: WideString;
  begin
    case PropKind of
      tkChar:
        begin
          if Value <> '' then
            C := Value[1]
          else
            C := #0;
          Result := C;
        end;
      tkWChar:
        begin
          WS := Value;
          if WS <> '' then
            WC := WS[1]
          else
            WC := #0;
          Result := WC;
        end;
      tkWString {$IFDEF UNICODE_STRING}, tkUString{$ENDIF}:
        begin
          WS := Value;
          Result := WS;
        end;
    else
      Result := Value;
    end;
  end;
begin
  if FInplaceEdit = nil then Exit;

  Node := TTreeNode(FInplaceEdit.Tag);
  Item := TCnOptionItem(Node.Data);
  
  if FInplaceEdit is TDateTimePicker then
  begin
    Item.Value := TDateTimePicker(FInplaceEdit).DateTime;
    FModified := True;
  end
  else if FInplaceEdit is TCnSpinEdit then
  begin
    with TCnSpinEdit(FInplaceEdit) do
    begin
      if (MaxValue > MinValue) and ((MaxValue <> 0) or (MinValue <> 0)) then
      begin
        if Value > MaxValue then
          Value := MaxValue;
        if Value < MinValue then
          Value := MinValue;
      end;
      Item.Value := Value;
    end;
    FModified := True;
  end
  else if FInplaceEdit is TComboBox then
  begin
    case GetOptionKind(Item) of
      okIntegerCombo:
        Item.Value := TComboBox(FInplaceEdit).ItemIndex;
      okStringCombo:
        Item.Value := GetString(Item.PropKind, TComboBox(FInplaceEdit).Text);
    else
      Assert(False);
    end;
    FModified := True;
  end
  else if FInplaceEdit is THotKey then
  begin
    Item.Value := THotKey(FInplaceEdit).HotKey;
    FModified := True;
  end
  else if FInplaceEdit is TEdit then
  begin
    try
      case GetOptionKind(Item) of
        okFloat:
          Item.Value := StrToFloat(TEdit(FInplaceEdit).Text);
        okDateTime:
          Item.Value := StrToDateTime(TEdit(FInplaceEdit).Text);
        okString:
          Item.Value := GetString(Item.PropKind, TEdit(FInplaceEdit).Text);
        okVariant:
          Item.Value := TEdit(FInplaceEdit).Text;
      else
        Assert(False);
      end;
      FModified := True;
    except
      ;
    end;
  end
  else if FInplaceEdit is TMemo then
  begin
    Integer(Obj) := Item.Value;
    if Obj is TStrings then
      TStrings(Obj).Text := TMemo(FInplaceEdit).Lines.Text;
    FModified := True;
  end
  else if FInplaceEdit is TButton then
  begin
    // None
  end
  else
  begin
    // None
  end;

  UpdateNode(Node);
  Repaint;
end;

procedure TCnAOTreeView.ApplyOption;

  procedure DoApplyOption(Option: TCnBaseOption);
  var
    i: Integer;
  begin
    if Option is TCnOptionItem then
      TCnOptionItem(Option).ApplyOption
    else if Option is TCnOptionGroup then
      for i := 0 to TCnOptionGroup(Option).Count - 1 do
        DoApplyOption(TCnOptionGroup(Option)[i]);
  end;
begin
  ApplyInplaceEdit;
  DoApplyOption(FOptions);
end;

function TCnAOTreeView.CanEdit(Node: TTreeNode): Boolean;
begin
  Result := False;
end;

procedure TCnAOTreeView.Change(Node: TTreeNode);
begin
  inherited;
  CreateInplaceEdit;
end;

procedure TCnAOTreeView.Click;
var
  P: TPoint;
  Node: TTreeNode;
begin
  inherited;
  GetCursorPos(P);
  P := ScreenToClient(P);
  Node := GetNodeAt(P.X, P.Y);
  if Node <> nil then
    DoClickNode(Node);
end;

procedure TCnAOTreeView.ComboBoxDropDown(Sender: TObject);
var
  i: Integer;
  MaxWidth: Integer;
  Bitmap: Graphics.TBitmap;
  ComboBox: TComboBox;
begin
  if not (Sender is TComboBox) then
    Exit;

  ComboBox := TComboBox(Sender);
  MaxWidth := ComboBox.Width;
  Bitmap := Graphics.TBitmap.Create;
  try
    Bitmap.Canvas.Font.Assign(ComboBox.Font);
    for i := 0 to ComboBox.Items.Count - 1 do
      MaxWidth := Max(MaxWidth, Bitmap.Canvas.TextWidth(ComboBox.Items[i]) + 10);
  finally;
    Bitmap.Free;
  end;
  if ComboBox.Items.Count > ComboBox.DropDownCount then
    Inc(MaxWidth,  GetSystemMetrics(SM_CXVSCROLL));
  MaxWidth := Min(400, MaxWidth);
  if MaxWidth > ComboBox.Width then
    SendMessage(ComboBox.Handle, CB_SETDROPPEDWIDTH, MaxWidth, 0)
  else
    SendMessage(ComboBox.Handle, CB_SETDROPPEDWIDTH, 0, 0)
end;

procedure TCnAOTreeView.CreateInplaceEdit;
var
  Option: TCnBaseOption;
  Item: TCnOptionItem;

  procedure CreateSpin(Value, MinValue, MaxValue: Integer);
  begin
    FInplaceEdit := TCnSpinEdit.Create(Self);
    FInplaceEdit.Height := csInplaceEditHeight;
    FInplaceEdit.Width := csInplaceEditWidth;
    TCnSpinEdit(FInplaceEdit).Value := Value;
    TCnSpinEdit(FInplaceEdit).MinValue := MinValue;
    TCnSpinEdit(FInplaceEdit).MaxValue := MaxValue;
    UpdateInplaceEdit;
  end;

  procedure CreateDateTimePicker(Kind: TDateTimeKind; Value: TDateTime);
  begin
    FInplaceEdit := TDateTimePicker.Create(Self);
    FInplaceEdit.Height := csInplaceEditHeight;
    FInplaceEdit.Width := csInplaceEditWidth;
    TDateTimePicker(FInplaceEdit).Kind := Kind;
    TDateTimePicker(FInplaceEdit).DateTime := Value;
    UpdateInplaceEdit;
  end;

  procedure CreateEdit(const Value: string);
  begin
    FInplaceEdit := TEdit.Create(Self);
    FInplaceEdit.Height := csInplaceEditHeight;
    FInplaceEdit.Width := csInplaceEditWidth;
    UpdateInplaceEdit;
    TEdit(FInplaceEdit).Text := Value;
  end;

  procedure CreateComboBox(const Value: string; List: TStrings; DropDownList: Boolean);
  begin
    FInplaceEdit := TComboBox.Create(Self);
    FInplaceEdit.Height := csInplaceEditHeight;
    FInplaceEdit.Width := csInplaceEditWidth;
    UpdateInplaceEdit;
    TComboBox(FInplaceEdit).Items.Assign(List);
    TComboBox(FInplaceEdit).OnDropDown := ComboBoxDropDown;
    if DropDownList then
    begin
      TComboBox(FInplaceEdit).Style := csDropDownList;
      TComboBox(FInplaceEdit).ItemIndex := List.IndexOf(Value);
    end
    else
    begin
      TComboBox(FInplaceEdit).Style := csDropDown;
      TComboBox(FInplaceEdit).Text := Value;
    end;
  end;

  procedure CreateButton(Caption: string; OnClick: TNotifyEvent);
  begin
    FInplaceEdit := TButton.Create(Self);
    FInplaceEdit.Height := csInplaceButtonHeight;
    FInplaceEdit.Width := csInplaceButtonWidth;
    UpdateInplaceEdit;
    if Caption = '' then
      Caption := SCnAOCaptionOption;
    TButton(FInplaceEdit).Caption := Caption;
    TButton(FInplaceEdit).OnClick := OnClick;
  end;

  procedure CreateHotKey(ShortCut: TShortCut);
  begin
    FInplaceEdit := THotKey.Create(Self);
    FInplaceEdit.Height := csInplaceEditHeight;
    FInplaceEdit.Width := csInplaceEditWidth;
    UpdateInplaceEdit;
    THotKey(FInplaceEdit).HotKey := ShortCut;
  end;

  procedure CreateMemo(Value: Variant);
  var
    Obj: TPersistent;
  begin
    Integer(Obj) := Value;
    Assert(Obj is TStrings);
    FInplaceEdit := TMemo.Create(Self);
    FInplaceEdit.Width := csInplaceMemoWidth;
    FInplaceEdit.Height := csInplaceMemoHeight;
    UpdateInplaceEdit;
    TMemo(FInplaceEdit).Lines.Assign(Obj);
  end;
begin
  FreeInplaceEdit;
  if (Selected = nil) or (Selected.Data = nil) then Exit;

  Option := TCnBaseOption(Selected.Data);
  Item := TCnOptionItem(Selected.Data);
  case GetOptionKind(Option) of
    okCustom:
      CreateButton(TCnOptionCustom(Option).Caption, TCnOptionCustom(Option).OnClick);
    okString:
      CreateEdit(Item.Value);
    okStringCombo:
      CreateComboBox(Item.Value, Item.List, False);
    okInteger:
      CreateSpin(Item.Value, Item.MinValue, Item.MaxValue);
    okIntegerCombo:
      CreateComboBox(Item.List[Item.Value], Item.List, True);
    okFloat:
      CreateEdit(FloatToStr(Item.Value));
    okDateTime:
      CreateEdit(DateTimeToStr(TDateTime(Item.Value)));
    okDate:
      CreateDateTimePicker(dtkDate, TDate(Item.Value));
    okTime:
      CreateDateTimePicker(dtkTime, TTime(Item.Value));
    okVariant:
      CreateEdit(VarToStr(Item.Value));
    okFont:
      CreateButton(SCnAOCaptionFont, OnFontClick);
    okColor:
      CreateButton(SCnAOCaptionColor, OnColorClick);
    okShortCut:
      CreateHotKey(Item.Value);
    okStrings:
      CreateMemo(Item.Value);
  else
    Exit;
  end;

  if Assigned(FOnCreateInplaceEdit) then
    FOnCreateInplaceEdit(Self, FInplaceEdit, Option);
end;

procedure TCnAOTreeView.DefaultOption;
  procedure DoDefaultOption(Option: TCnBaseOption);
  var
    i: Integer;
  begin
    if Option is TCnOptionItem then
      TCnOptionItem(Option).DefaultOption
    else if Option is TCnOptionGroup then
      for i := 0 to TCnOptionGroup(Option).Count - 1 do
        DoDefaultOption(TCnOptionGroup(Option)[i]);
  end;
begin
  DoDefaultOption(FOptions);
  UpdateTreeView;
end;

function TCnAOTreeView.DoClickNode(Node: TTreeNode): Boolean;
var
  i, Min, Max: Integer;
  Item: TCnOptionItem;
  BoolValue: Boolean;
  EnumInfo: PTypeInfo;
  SetValue: TIntegerSet;
begin
  Result := False;
  if Node = nil then Exit;
  
  if Node.Data = nil then  // 是集合或枚举子项
  begin
    Item := TCnOptionItem(Node.Parent.Data);
    Assert(Item is TCnOptionItem);
    Assert(GetOptionKind(Item) in [okEnum, okSet]);

    case GetOptionKind(Item) of
      okEnum:
        begin
          Item.Value := Node.Index + Item.MinValue;
          for i := 0 to Node.Parent.Count - 1 do
            if i = Node.Index then
              SetNodeImageIndex(Node.Parent.Item[i], csIdxSelected)
            else
              SetNodeImageIndex(Node.Parent.Item[i], csIdxUnSelected)
        end;
      okSet:
        begin
          EnumInfo := GetTypeData(Item.PropInfo^.PropType^)^.CompType^;
          if Node.ImageIndex = csIdxChecked then
            SetNodeImageIndex(Node, csIdxUnChecked)
          else
            SetNodeImageIndex(Node, csIdxChecked);

          SetValue := [];
          Min := GetTypeData(EnumInfo).MinValue;
          Max := GetTypeData(EnumInfo).MaxValue;
          for i := Min to Max do
            if Node.Parent.Item[i].ImageIndex = csIdxChecked then
              Include(SetValue, i + Min);
          Item.Value := Integer(SetValue);
        end;
    end;

    FModified := True;
    Result := True;
  end
  else if GetOptionKind(TCnBaseOption(Node.Data)) in [okBoolFalse, okBoolTrue] then
  begin
    Item := TCnOptionItem(Node.Data);
    
    BoolValue := not Item.Value;
    Item.Value := BoolValue;
    if BoolValue then
      SetNodeImageIndex(Node, csIdxChecked)
    else
      SetNodeImageIndex(Node, csIdxUnChecked);

    FModified := True;
    Result := True;
  end;
end;

procedure TCnAOTreeView.DoCustomDrawItem(Sender: TCustomTreeView;
  Node: TTreeNode; State: TCustomDrawState; var DefaultDraw: Boolean);
begin
  // Todo: 自绘制颜色属性及字体等
  DefaultDraw := True;
end;

procedure TCnAOTreeView.DoEnter;
begin
  inherited;
  Change(Selected);
end;

procedure TCnAOTreeView.DoExit;
begin
  inherited;
  FreeInplaceEdit;
end;

procedure TCnAOTreeView.FreeInplaceEdit;
begin
  if FInplaceEdit <> nil then
  begin
    ApplyInplaceEdit;
    FreeAndNil(FInplaceEdit);
  end;
end;

function TCnAOTreeView.GetOptionKind(Option: TCnBaseOption; RaiseError: Boolean
  = False): TCnOptionKind;
var
  Item: TCnOptionItem;
  BoolValue: Boolean;
  Obj: TObject;
begin
  Result := okUnknown;
  
  if Option is TCnOptionCustom then
    Result := okCustom
  else if Option is TCnOptionGroup then
    Result := okGroup
  else if Option is TCnOptionItem then
  begin
    Item := TCnOptionItem(Option);
    case Item.PropKind of
      tkInteger, tkInt64:
        begin
          if Item.PropInfo^.PropType^ = TypeInfo(TColor) then
            Result := okColor
          else if Item.PropInfo^.PropType^ = TypeInfo(TShortCut) then
            Result := okShortCut
          else if (Item.List.Count > 0) and (Item.Value >= 0) and
            (Item.Value < Item.List.Count) then
            Result := okIntegerCombo
          else
            Result := okInteger;
        end;
      tkFloat:
        begin
          if Item.PropInfo^.PropType^ = TypeInfo(TDateTime) then
            Result := okDateTime
          else if Item.PropInfo^.PropType^ = TypeInfo(TDate) then
            Result := okDate
          else if Item.PropInfo^.PropType^ = TypeInfo(TTime) then
            Result := okTime
          else
            Result := okFloat;
        end;
      tkChar, tkString, tkWChar, tkLString, tkWString{$IFDEF UNICODE_STRING}, tkUString{$ENDIF}:
        begin
          if Item.List.Count > 0 then
            Result := okStringCombo
          else
            Result := okString
        end;
      tkEnumeration:
        begin
          if IsBooleanType(Item.PropInfo^.PropType^) or
            IsBoolType(Item.PropInfo^.PropType^) then
          begin
            BoolValue := Item.Value;
            if BoolValue then
              Result := okBoolTrue
            else
              Result := okBoolFalse;
          end
          else
            Result := okEnum;
        end;
      tkSet:
        begin
          Result := okSet;
        end;
      tkClass:
        begin
          Integer(Obj) := Item.Value;
          if Obj is TFont then
            Result := okFont
          else if Obj is TStrings then
            Result := okStrings
          else if RaiseError then
            raise EUnsupportedPropKind.Create('Unsupported Property Kind: ' +
              Obj.ClassName);
        end;
      tkVariant:
        begin
          Result := okVariant;
        end;
    end;
  end;

  if (Result = okUnknown) and RaiseError then
    if Option is TCnOptionItem then
      raise EUnsupportedPropKind.Create('Unsupported Property Kind: ' +
        GetEnumName(TypeInfo(TTypeKind), Ord(TCnOptionItem(Option).PropKind)))
    else
      raise EUnsupportedPropKind.Create('Unsupported Property Define: ' + Option.Text);
end;

procedure TCnAOTreeView.GetSelectedIndex(Node: TTreeNode);
begin
  Node.SelectedIndex := Node.ImageIndex;
end;

procedure TCnAOTreeView.OnColorClick(Sender: TObject);
var
  Node: TTreeNode;
  Item: TCnOptionItem;
begin
  if FInplaceEdit = nil then Exit;

  Node := TTreeNode(FInplaceEdit.Tag);
  Item := TCnOptionItem(Node.Data);
  with TColorDialog.Create(Self) do
  try
    Color := ColorToRGB(Item.Value);
    if Execute then
      Item.Value := Color;
  finally
    Free;
  end;
end;

procedure TCnAOTreeView.OnFontClick(Sender: TObject);
var
  Node: TTreeNode;
  Item: TCnOptionItem;
  Obj: TFont;
begin
  if FInplaceEdit = nil then Exit;

  Node := TTreeNode(FInplaceEdit.Tag);
  Item := TCnOptionItem(Node.Data);
  Integer(Obj) := Item.Value;
  with TFontDialog.Create(Self) do
  try
    Font.Assign(Obj);
    if Execute then
      Obj.Assign(Font);
  finally
    Free;
  end;
end;

procedure TCnAOTreeView.OnInplaceEditEnterExit(Sender: TObject);
var
  i: Integer;
begin
  if FInplaceEdit <> nil then
  begin
    FInplaceEdit.Invalidate;
    if FInplaceEdit is TWinControl then
      for i := 0 to TWinControl(FInplaceEdit).ControlCount - 1 do
        TWinControl(FInplaceEdit).Controls[i].Invalidate;
  end;
end;

procedure TCnAOTreeView.ResetOption;

  procedure DoResetOption(Option: TCnBaseOption);
  var
    i: Integer;
  begin
    if Option is TCnOptionItem then
      TCnOptionItem(Option).ResetOption
    else if Option is TCnOptionGroup then
      for i := 0 to TCnOptionGroup(Option).Count - 1 do
      try
        DoResetOption(TCnOptionGroup(Option)[i]);
      except
        Application.HandleException(nil);
      end;
  end;
begin
  DoResetOption(FOptions);
  UpdateTreeView;
end;

procedure TCnAOTreeView.SetNodeImageIndex(Node: TTreeNode; Index: Integer);
begin
  Node.ImageIndex := Index;
  Node.SelectedIndex := Index;
end;

procedure TCnAOTreeView.UpdateInplaceEdit;
var
  R1, R2: TRect;
begin
  if (Selected <> nil) and (TopItem <> nil) and (FInplaceEdit <> nil) then
  begin
    FInplaceEdit.Tag := Integer(Selected);
    FInplaceEdit.Parent := Self;
    if FInplaceEdit is TWinControl then
    begin
      TWinControlHack(FInplaceEdit).OnEnter := OnInplaceEditEnterExit;
      TWinControlHack(FInplaceEdit).OnExit := OnInplaceEditEnterExit;
    end;
    Selected.Text := TCnBaseOption(Selected.Data).Text + csSepStr;
    R1 := Selected.DisplayRect(True);
    R2 := TopItem.DisplayRect(True);
    FInplaceEdit.Top := R1.Top - R2.Top;
    FInplaceEdit.Left := R1.Right + csInplaceSpace;
    FInplaceEdit.Invalidate;
    //Invalidate;
    // Todo: 滚动时刷新有时不正常
  end;
end;

procedure TCnAOTreeView.UpdateNode(Node: TTreeNode);
var
  Idx: Integer;
  Item: TCnOptionItem;
  OptionKind: TCnOptionKind;
  OrdValue: Integer;
  EnumInfo: PTypeInfo;

  function FontToStr(Value: Variant): string;
  var
    Obj: TObject;
  begin
    Integer(Obj) := Value;
    if Obj is TFont then
      Result := Format('%s,%d', [TFont(Obj).Name, TFont(Obj).Size])
    else
      Result := '';
  end;

  function StringsToStr(Value: Variant): string;
  var
    Obj: TObject;
  begin
    Integer(Obj) := Value;
    if Obj is TStrings then
      Result := StringReplace(TStrings(Obj).Text, #13#10, ' ', [rfReplaceAll])
    else
      Result := '';
    if Length(Result) > csMaxStrLength - 3 then
      Result := Copy(Result, 1, csMaxStrLength - 3) + '...';
  end;

  procedure SetNodeText(AItem: TCnOptionItem; Text: string);
  begin
    if Assigned(FOnGetItemText) then
      FOnGetItemText(Self, AItem, Text);
    Node.Text := AItem.Text + csSepStr + Text;
  end;
begin
  Assert(Node <> nil);

  if Node.Data = nil then  // 是集合或枚举子项
  begin
    Item := TCnOptionItem(Node.Parent.Data);
    Assert(GetOptionKind(Item) in [okEnum, okSet]);

    case GetOptionKind(Item) of
      okEnum:
        begin
          OrdValue := Item.Value;
          Idx := Node.Index;

          if Idx < Item.List.Count then
            Node.Text := Item.List[Idx - Item.MinValue]
          else
            Node.Text := GetEnumName(Item.PropInfo^.PropType^, Idx);

          if Idx = OrdValue then
            SetNodeImageIndex(Node, csIdxSelected)
          else
            SetNodeImageIndex(Node, csIdxUnSelected);
        end;
      okSet:
        begin
          OrdValue := Item.Value;
          EnumInfo := GetTypeData(Item.PropInfo^.PropType^)^.CompType^;
          Idx := Node.Index;

          if Idx < Item.List.Count then
            Node.Text := Item.List[Idx - GetTypeData(EnumInfo).MinValue]
          else
            Node.Text := GetEnumName(EnumInfo, Idx);

          if Idx in TIntegerSet(OrdValue) then
            SetNodeImageIndex(Node, csIdxChecked)
          else
            SetNodeImageIndex(Node, csIdxUnChecked);
        end;
    end;
  end
  else
  begin
    Item := TCnOptionItem(Node.Data);
    OptionKind := GetOptionKind(TCnBaseOption(Node.Data));
    SetNodeImageIndex(Node, csImageIndexs[OptionKind]);
    case OptionKind of
      okString, okStringCombo:
        SetNodeText(Item, Item.Value);
      okInteger:
        SetNodeText(Item, IntToStr(Item.Value));
      okIntegerCombo:
        SetNodeText(Item, Item.List[Item.Value]);
      okFloat:
        SetNodeText(Item, FloatToStr(Item.Value));
      okDateTime:
        SetNodeText(Item, DateTimeToStr(Item.Value));
      okDate:
        SetNodeText(Item, DateToStr(Item.Value));
      okTime:
        SetNodeText(Item, TimeToStr(Item.Value));
      okVariant:
        SetNodeText(Item, VarToStr(Item.Value));
      okFont:
        SetNodeText(Item, FontToStr(Item.Value));
      okColor:
        SetNodeText(Item, IntToHex(ColorToRGB(Item.Value), 8));
      okShortCut:
        SetNodeText(Item, ShortCutToText(Item.Value));
      okStrings:
        SetNodeText(Item, StringsToStr(Item.Value));
    else
      Node.Text := TCnBaseOption(Node.Data).Text;
    end;
  end;
end;

procedure TCnAOTreeView.UpdateTreeView;

  function AddNode(ParentNode: TTreeNode; AOption: TCnBaseOption): TTreeNode;
  var
    i: Integer;
    EnumInfo: PTypeInfo;
  begin
    Result := Items.AddChildObject(ParentNode, AOption.Text, AOption);
    try
      UpdateNode(Result);
      case GetOptionKind(AOption, True) of
        okGroup:
          begin
            with TCnOptionGroup(AOption) do
              for i := 0 to Count - 1 do
                AddNode(Result, Items[i]);
          end;
        okEnum:
          with TCnOptionItem(AOption) do
          begin
            for i := MinValue to MaxValue do
            begin
              UpdateNode(Items.AddChildObject(Result, '', nil));
            end;
          end;
        okSet:
          with TCnOptionItem(AOption) do
          begin
            EnumInfo := GetTypeData(PropInfo^.PropType^)^.CompType^;
            for i := GetTypeData(EnumInfo).MinValue to GetTypeData(EnumInfo).MaxValue do
              UpdateNode(Items.AddChildObject(Result, '', nil));
          end;
      end;
    except
      Result.Free;
      Application.HandleException(Self);
    end;
  end;
begin
  Items.BeginUpdate;
  try
    Items.Clear;
    AddNode(nil, Options);
    Selected := Items.GetFirstNode;
    FullExpand;
    TopItem := Selected;
    FModified := False;
  finally
    Items.EndUpdate;
  end;
end;

procedure TCnAOTreeView.SetOptions(Value: TCnOptionGroup);
begin
  if FOptions <> Value then
  begin
    FOptions := Value;
    ResetOption;
  end;
end;

procedure TCnAOTreeView.WMChar(var Message: TWMChar);
begin
  if (Char(Message.CharCode) <> ' ') or not DoClickNode(Selected) then
    inherited;
end;

procedure TCnAOTreeView.WMHScroll(var Message: TWMHScroll);
begin
  inherited;
  UpdateInplaceEdit;
end;

procedure TCnAOTreeView.WMMouseWheel(var Message: TWMMouseWheel);
begin
  inherited;
  UpdateInplaceEdit;
end;

procedure TCnAOTreeView.WMVScroll(var Message: TWMVScroll);
begin
  inherited;
  UpdateInplaceEdit;
end;

end.
