unit TB2DsgnItemEditor;

{
  Toolbar2000
  Copyright (C) 1998-2008 by Jordan Russell
  All rights reserved.

  The contents of this file are subject to the "Toolbar2000 License"; you may
  not use or distribute this file except in compliance with the
  "Toolbar2000 License". A copy of the "Toolbar2000 License" may be found in
  TB2k-LICENSE.txt or at:
    http://www.jrsoftware.org/files/tb2k/TB2k-LICENSE.txt

  Alternatively, the contents of this file may be used under the terms of the
  GNU General Public License (the "GPL"), in which case the provisions of the
  GPL are applicable instead of those in the "Toolbar2000 License". A copy of
  the GPL may be found in GPL-LICENSE.txt or at:
    http://www.jrsoftware.org/files/tb2k/GPL-LICENSE.txt
  If you wish to allow use of your version of this file only under the terms of
  the GPL and not to allow others to use your version of this file under the
  "Toolbar2000 License", indicate your decision by deleting the provisions
  above and replace them with the notice and other provisions required by the
  GPL. If you do not delete the provisions above, a recipient may use your
  version of this file under either the "Toolbar2000 License" or the GPL.

  $jrsoftware: tb2k/Source/TB2DsgnItemEditor.pas,v 1.63 2008/09/25 18:49:31 jr Exp $
}

interface

{$I TB2Ver.inc}

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  {$IFDEF CLR} System.ComponentModel, {$ENDIF}
  StdCtrls, ExtCtrls, Buttons, ComCtrls, ImgList, Menus,
  TB2Item, TB2Toolbar, TB2Dock,
  {$IFDEF JR_D6}
    DesignIntf, DesignWindows, DesignEditors;
  {$ELSE}
    DsgnIntf, DsgnWnds, LibIntf;
  {$ENDIF}

const
  CM_DEFERUPDATE = WM_USER + 100;

type
  TTBItemEditForm = class(TDesignWindow)
    TreeView: TTreeView;
    ListView: TListView;
    Splitter1: TSplitter;
    Toolbar: TTBToolbar;
    NewSubmenuButton: TTBItem;
    NewItemButton: TTBItem;
    NewSepButton: TTBItem;
    DeleteButton: TTBItem;
    TBSeparatorItem1: TTBSeparatorItem;
    TBPopupMenu1: TTBPopupMenu;
    TBItemContainer1: TTBItemContainer;
    ToolbarItems: TTBSubmenuItem;
    CopyButton: TTBItem;
    CutButton: TTBItem;
    PasteButton: TTBItem;
    MoreMenu: TTBSubmenuItem;
    TBSeparatorItem2: TTBSeparatorItem;
    TBSubmenuItem1: TTBSubmenuItem;
    TConvertMenu: TTBItem;
    TBSeparatorItem3: TTBSeparatorItem;
    MoveUpButton: TTBItem;
    MoveDownButton: TTBItem;
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
    procedure TreeViewChange(Sender: TObject; Node: TTreeNode);
    procedure NewSubmenuButtonClick(Sender: TObject);
    procedure NewItemButtonClick(Sender: TObject);
    procedure ListViewChange(Sender: TObject; Item: TListItem;
      Change: TItemChange);
    procedure DeleteButtonClick(Sender: TObject);
    procedure NewSepButtonClick(Sender: TObject);
    procedure ListViewDragOver(Sender, Source: TObject; X, Y: Integer;
      State: TDragState; var Accept: Boolean);
    procedure ListViewDragDrop(Sender, Source: TObject; X, Y: Integer);
    procedure TreeViewEnter(Sender: TObject);
    procedure TreeViewDragDrop(Sender, Source: TObject; X, Y: Integer);
    procedure TreeViewDragOver(Sender, Source: TObject; X, Y: Integer;
      State: TDragState; var Accept: Boolean);
    procedure CopyButtonClick(Sender: TObject);
    procedure ListViewKeyDown(Sender: TObject; var Key: Word;
      Shift: TShiftState);
    procedure CutButtonClick(Sender: TObject);
    procedure PasteButtonClick(Sender: TObject);
    procedure FormActivate(Sender: TObject);
    procedure ListViewKeyPress(Sender: TObject; var Key: Char);
    procedure ListViewDblClick(Sender: TObject);
    procedure ListViewEnter(Sender: TObject);
    procedure TreeViewKeyDown(Sender: TObject; var Key: Word;
      Shift: TShiftState);
    procedure TConvertMenuClick(Sender: TObject);
    procedure TreeViewKeyPress(Sender: TObject; var Key: Char);
    procedure MoveUpButtonClick(Sender: TObject);
    procedure MoveDownButtonClick(Sender: TObject);
  private
    FParentComponent: TComponent;
    FRootItem, FSelParentItem: TTBCustomItem;
    FNotifyItemList: TList;
    FSettingSel, FRebuildingTree, FRebuildingList: Integer;
    function AddListViewItem(const Index: Integer;
      const Item: TTBCustomItem): TListItem;
    procedure CMDeferUpdate(var Message: TMessage); message CM_DEFERUPDATE;
    procedure Copy;
    procedure CreateNewItem(const AClass: TTBCustomItemClass);
    procedure Cut;
    procedure Delete;
    procedure DeleteItem(const Item: TTBCustomItem);
    function GetItemTreeCaption(AItem: TTBCustomItem): String;
    procedure GetSelItemList(const AList: TList);
    procedure ItemNotification(Ancestor: TTBCustomItem; Relayed: Boolean;
      Action: TTBItemChangedAction; Index: Integer; Item: TTBCustomItem);
    procedure MoreItemClick(Sender: TObject);
    procedure MoveItem(CurIndex, NewIndex: Integer);
    procedure Paste;
    procedure RebuildList;
    procedure RebuildTree;
    procedure SelectInObjectInspector(AList: TList);
    procedure SetSelParentItem(ASelParentItem: TTBCustomItem);
    function TreeViewDragHandler(Sender, Source: TObject; X, Y: Integer;
      Drop: Boolean): Boolean;
    procedure UnregisterAllNotifications;
  protected
    procedure Notification(AComponent: TComponent; Operation: TOperation); override;
    function UniqueName(Component: TComponent): String; override;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    {$IFDEF JR_D6}
    function EditAction(Action: TEditAction): Boolean; override;
    {$ELSE}
    procedure EditAction(Action: TEditAction); override;
    {$ENDIF}
    function GetEditState: TEditState; override;
  end;

  TTBItemsEditor = class(TDefaultEditor)
  public
    procedure Edit; override;
    procedure ExecuteVerb(Index: Integer); override;
    function GetVerb(Index: Integer): String; override;
    function GetVerbCount: Integer; override;
  end;

  TTBItemsPropertyEditor = class(TStringProperty)
  public
    procedure Edit; override;
    function GetAttributes: TPropertyAttributes; override;
    function GetValue: String; override;
  end;

procedure TBRegisterItemClass(AClass: TTBCustomItemClass;
  const ACaption: String; ResInstance: HINST);

implementation

{$R *.DFM}

uses
  {$IFDEF CLR} System.Drawing, System.IO, System.Reflection, {$ENDIF}
  TypInfo, CommCtrl, TB2Version, TB2Common, TB2DsgnConverter;

type
{$IFNDEF JR_D5}
  TDesignerSelectionList = TComponentList;
{$ENDIF}
{$IFDEF JR_D6}
  TDesignerSelectionList = IDesignerSelections;
{$ENDIF}

  TItemClassInfo = class
    ItemClass: TTBCustomItemClass;
    Caption: String;
    ImageIndex: Integer;
  end;

var
  ItemClasses: TList;
  ItemImageList: TImageList;

{$IFNDEF JR_D6}
function CreateSelectionList: TDesignerSelectionList;
begin
  Result := TDesignerSelectionList.Create;
end;
{$ENDIF}

procedure FreeItemClasses;
var
  I: Integer;
  IC: TList;
begin
  if ItemClasses = nil then Exit;
  IC := ItemClasses;
  ItemClasses := nil;
  for I := IC.Count-1 downto 0 do
    TItemClassInfo(IC[I]).Free;
  IC.Free;
end;

{ Note: AFAIK, there is no need for a similar function on .NET since assemblies
  can't be unloaded. When a design-time package is uninstalled, it remains
  loaded until the IDE is restarted. }
{$IFNDEF CLR}
procedure UnregisterModuleItemClasses(AModule: {$IFDEF JR_D5} THandle {$ELSE} Integer {$ENDIF});
var
  I: Integer;
  Info: TItemClassInfo;
begin
  I := 0;
  while I < ItemClasses.Count do begin
    Info := TItemClassInfo(ItemClasses[I]);
    if FindClassHInstance(Info.ItemClass) = AModule then begin
      ItemClasses.Delete(I);
      Info.Free;
    end
    else
      Inc(I);
  end;
  { Note: TTBItemEditForm also holds references to item classes, but since
    Delphi automatically closes all editor forms before compiling/removing
    a package, we don't need to remove them. }
end;
{$ENDIF}

{$IFNDEF CLR}
function LoadItemImage(Instance: HINST; const ResName: String): Integer;
var
  Bmp: TBitmap;
begin
  Bmp := TBitmap.Create;
  try
    Bmp.Handle := LoadBitmap(Instance, {$IFNDEF CLR}PChar{$ENDIF}(ResName));
    if Bmp.Handle = 0 then
      Result := -1
    else
      Result := ItemImageList.AddMasked(Bmp, Bmp.Canvas.Pixels[0, Bmp.Height-1]);
  finally
    Bmp.Free;
  end;
end;
{$ELSE}
function LoadItemImage(const AAssembly: System.Reflection.Assembly;
  const ResName: String): Integer;
var
  Bmp: TBitmap;
  ResStream: System.IO.Stream;
  ResBmp: System.Drawing.Bitmap;
begin
  Bmp := TBitmap.Create;
  try
    ResStream := AAssembly.GetManifestResourceStream(ResName);
    if ResStream = nil then begin
      Result := -1;
      Exit;
    end;
    try
      ResBmp := System.Drawing.Bitmap.Create(ResStream);
      try
        Bmp.LoadFromBitmap(ResBmp);
      finally
        ResBmp.Dispose;
      end;
    finally
      ResStream.Close;
    end;
    Result := ItemImageList.AddMasked(Bmp, Bmp.Canvas.Pixels[0, Bmp.Height-1]);
  finally
    Bmp.Free;
  end;
end;
{$ENDIF}

procedure TBRegisterItemClass(AClass: TTBCustomItemClass;
  const ACaption: String; ResInstance: HINST);
var
  I: Integer;
  Info: TItemClassInfo;
begin
  { Hack for Delphi.NET 2006 bug:
    If you start Delphi, open & rebuild the tb2k_dn10 package only, then open
    the Demo project, the IDE calls the Register procedure on tb2kdsgn_d10 a
    second time, without reloading either of the two packages. As a result,
    the TBRegisterItemClass calls are repeated. To avoid doubled items on the
    editor form's More menu, check if the class was already registered. }
  for I := 0 to ItemClasses.Count-1 do
    if TItemClassInfo(ItemClasses[I]).ItemClass = AClass then
      Exit;
  Info := TItemClassInfo.Create;
  Info.ItemClass := AClass;
  Info.Caption := ACaption;
  {$IFNDEF CLR}
  Info.ImageIndex := LoadItemImage(ResInstance,
    Uppercase(AClass.ClassName {$IFDEF JR_D9} , loInvariantLocale {$ENDIF}));
  {$ELSE}
  Info.ImageIndex := LoadItemImage(Assembly.GetCallingAssembly,
    AClass.ClassName + '.bmp');
  {$ENDIF}
  ItemClasses.Add(Info);
end;

function GetItemClassImage(AClass: TTBCustomItemClass): Integer;
var
  I: Integer;
  Info: TItemClassInfo;
begin
  for I := ItemClasses.Count-1 downto 0 do begin
    Info := TItemClassInfo(ItemClasses[I]);
    if AClass.InheritsFrom(Info.ItemClass) then begin
      Result := Info.ImageIndex;
      if Result >= 0 then
        Exit;
    end;
  end;
  if AClass.InheritsFrom(TTBSubmenuItem) then
    Result := 1
  else if AClass.InheritsFrom(TTBSeparatorItem) then
    Result := 2
  else
    Result := 0;
end;

procedure ShowEditForm(AParentComponent: TComponent; ARootItem: TTBCustomItem;
  const ADesigner: {$IFDEF JR_D6} IDesigner {$ELSE} IFormDesigner {$ENDIF});
var
  I: Integer;
  Form: TCustomForm;
  EditForm: TTBItemEditForm;
begin
  if Assigned(ARootItem.LinkSubitems) then begin
    case MessageDlg(Format('The LinkSubitems property is set to ''%s''. ' +
       'Would you like to edit that item instead?',
       [ARootItem.LinkSubitems.Name]), mtConfirmation, [mbYes, mbNo, mbCancel], 0) of
      mrYes: begin
          AParentComponent := ARootItem.LinkSubitems;
          ARootItem := ARootItem.LinkSubitems;
        end;
      mrCancel: Exit;
    end;
  end;
  for I := 0 to Screen.FormCount-1 do begin
    Form := Screen.Forms[I];
    if Form is TTBItemEditForm then
      if TTBItemEditForm(Form).FRootItem = ARootItem then begin
        Form.Show;
        if Form.WindowState = wsMinimized then
          Form.WindowState := wsNormal;
        Exit;
      end;
  end;
  EditForm := TTBItemEditForm.Create(Application);
  try
    EditForm.Designer := ADesigner;
    EditForm.FParentComponent := AParentComponent;
    AParentComponent.FreeNotification(EditForm);
    EditForm.FRootItem := ARootItem;
    ARootItem.FreeNotification(EditForm);
    EditForm.FSelParentItem := ARootItem;
    EditForm.Caption := 'Editing ' + AParentComponent.Name;
    EditForm.RebuildTree;
    EditForm.RebuildList;
    {$IFDEF JR_D9}
    EditForm.PopupMode := pmExplicit;
    {$ENDIF}
    EditForm.Show;
  except
    EditForm.Free;
    raise;
  end;
end;

function IsSubmenuItem(Item: TTBCustomItem): Boolean;
begin
  Result := tbisSubitemsEditable in Item.GetItemStyle;
end;

procedure ShowVersion;
const
  AboutText =
    '%s'#13#10 +
    'Copyright (C) 1998-2008 by Jordan Russell'#13#10 +
    'For conditions of distribution and use, see LICENSE.TXT.'#13#10 +
    #13#10 +
    'Visit my web site for the latest versions of Toolbar2000:'#13#10 +
    'http://www.jrsoftware.org/';
begin
  MessageDlg(Format(AboutText, [Toolbar2000VersionPropText]), mtInformation,
    [mbOK], 0);
end;


{ TTBItemEditForm }

constructor TTBItemEditForm.Create(AOwner: TComponent);
var
  I: Integer;
  Info: TItemClassInfo;
  Item: TTBItem;
begin
  inherited;
  FNotifyItemList := TList.Create;
  ToolbarItems.SubMenuImages := ItemImageList;
  ListView.SmallImages := ItemImageList;
  { Populate the 'More' menu }
  for I := 0 to ItemClasses.Count-1 do begin
    Info := TItemClassInfo(ItemClasses[I]);
    Item := TTBItem.Create(Self);
    Item.Caption := Info.Caption;
    Item.ImageIndex := GetItemClassImage(Info.ItemClass);
    Item.Tag := {$IFNDEF CLR}Integer{$ELSE}TTag{$ENDIF}(Info.ItemClass);
    Item.OnClick := MoreItemClick;
    MoreMenu.Add(Item);
  end;
end;

destructor TTBItemEditForm.Destroy;
begin
  inherited;
  if Assigned(FNotifyItemList) then begin
    UnregisterAllNotifications;
    FNotifyItemList.Free;
    FNotifyItemList := nil;
  end;
end;

procedure TTBItemEditForm.FormClose(Sender: TObject;
  var Action: TCloseAction);
begin
  Action := caFree;
end;

procedure TTBItemEditForm.FormActivate(Sender: TObject);
begin
  SetSelParentItem(FSelParentItem);
end;

procedure TTBItemEditForm.Notification(AComponent: TComponent;
  Operation: TOperation);
begin
  inherited;
  if (Operation = opRemove) and
     ((AComponent = FParentComponent) or (AComponent = FRootItem)) then
    { Must use Free instead of Close, since Close causes the freeing of the
      form to be delayed until the next message. We have to destroy the form
      immediately, otherwise Delphi will crash when Compile is clicked on the
      TB2k package. }
    Free;
  {}{temp:}
  (*if (Operation = opRemove) and (FNotifyItemList.IndexOf(AComponent) <> -1) then begin
    outputdebugstring(pchar('Still in list: ' + AComponent.name));
    //beep;
  end;*)
end;

function TTBItemEditForm.UniqueName(Component: TComponent): String;
begin
  Result := Designer.UniqueName(Component.ClassName);
end;

function TTBItemEditForm.GetEditState: TEditState;
begin
  Result := [];
  if ActiveControl = ListView then begin
    if Assigned(ListView.Selected) then
      Result := [esCanDelete, esCanCut, esCanCopy];
    if ClipboardComponents then
      Include(Result, esCanPaste);
  end;
end;

{$IFDEF JR_D6}
function TTBItemEditForm.EditAction(Action: TEditAction): Boolean;
{$ELSE}
procedure TTBItemEditForm.EditAction(Action: TEditAction);
{$ENDIF}
begin
  {$IFDEF JR_D6}
  Result := True;
  {$ENDIF}
  case Action of
    eaCut: Cut;
    eaCopy: Copy;
    eaPaste: Paste;
    eaDelete: Delete;
  {$IFDEF JR_D6}
  else
    Result := False;
  {$ENDIF}
  end;
end;

procedure TTBItemEditForm.UnregisterAllNotifications;
var
  I: Integer;
begin
  for I := FNotifyItemList.Count-1 downto 0 do begin
    //outputdebugstring(pchar('Unregall: ' + TTBCustomItem(FNotifyItemList[I]).name));
    TTBCustomItem(FNotifyItemList[I]).UnregisterNotification(ItemNotification);
    FNotifyItemList.Delete(I);
  end;
end;

procedure TTBItemEditForm.ItemNotification(Ancestor: TTBCustomItem;
  Relayed: Boolean; Action: TTBItemChangedAction; Index: Integer;
  Item: TTBCustomItem);
var
  ListItem: TListItem;
  TreeNode: TTreeNode;
  I: Integer;
  C: String;
begin
  { Manipulate the list view when items are inserted, deleted, or their Caption
    changes }
  case Action of
    tbicInserted:
      begin
        if (Ancestor = FSelParentItem) and not Relayed then
          AddListViewItem(Index, Item);
        if IsSubmenuItem(Item) then
          RebuildTree;
      end;
    tbicDeleting:
      if (Ancestor = FSelParentItem) and not Relayed then begin
        ListItem := ListView.FindData(0, Item, True, False);
        if Assigned(ListItem) then
          ListItem.Delete;
      end;
    tbicInvalidateAndResize:
      if (Ancestor = FSelParentItem) and not Relayed then begin
        ListItem := ListView.FindData(0, Item, True, False);
        if Assigned(ListItem) and (ListItem.Caption <> TTBCustomItem(Item).Caption) then
          ListItem.Caption := TTBCustomItem(Item).Caption;
      end;
  end;
  { Update tree view when an item is deleted, or a Caption changes }
  if Action = tbicDeleting then begin
    I := FNotifyItemList.IndexOf(Item);
    if I <> -1 then begin
      //outputdebugstring(pchar('Deleting, so unreging: ' + item.name));
      TTBCustomItem(Item).UnregisterNotification(ItemNotification);
      FNotifyItemList.Delete(I);
    end;
  end;
  if Action in [tbicDeleting, tbicInvalidateAndResize, tbicNameChanged] then begin
    TreeNode := TreeView.Items.GetFirstNode;
    while Assigned(TreeNode) do begin
      if TreeNode.Data = Item then begin
        if Action = tbicDeleting then begin
          TreeNode.Delete;
          if FSelParentItem = Item then
            SetSelParentItem(TTBCustomItem(Item).Parent);
        end
        else begin
          { tbicInvalidateAndResize, tbicNameChanged: }
          C := GetItemTreeCaption(Item);
          if TreeNode.Text <> C then
            TreeNode.Text := C;
        end;
        Break;
      end;
      TreeNode := TreeNode.GetNext;
    end;
  end;
end;

function TTBItemEditForm.GetItemTreeCaption(AItem: TTBCustomItem): String;
begin
  if AItem <> FRootItem then begin
    Result := AItem.Caption;
    if Result = '' then
      Result := '[' + AItem.Name + ']';
  end
  else
    Result := '(Root)';
end;

procedure TTBItemEditForm.RebuildTree;

  procedure Recurse(const AParentItem: TTBCustomItem; const ATreeNode: TTreeNode;
    var FoundSelParentItem: TTreeNode);
  var
    I: Integer;
    NewNode: TTreeNode;
    ChildItem: TTBCustomItem;
  begin
    {}AParentItem.FreeNotification(Self);
    AParentItem.RegisterNotification(ItemNotification);
    FNotifyItemList.Add(AParentItem);
    NewNode := TreeView.Items.AddChild(ATreeNode, GetItemTreeCaption(AParentItem));
    NewNode.Data := AParentItem;
    if AParentItem = FSelParentItem then
      FoundSelParentItem := NewNode;
    for I := 0 to AParentItem.Count-1 do begin
      ChildItem := AParentItem[I];
      if IsSubmenuItem(ChildItem) then
        Recurse(ChildItem, NewNode, FoundSelParentItem);
    end;
  end;

var
  FoundSelParentItem: TTreeNode;
begin
  Inc(FRebuildingTree);
  try
    TreeView.Items.BeginUpdate;
    try
      TreeView.Items.Clear;
      UnregisterAllNotifications;
      FoundSelParentItem := nil;
      Recurse(FRootItem, nil, FoundSelParentItem);
      if FoundSelParentItem = nil then
        SetSelParentItem(FRootItem)
      else
        TreeView.Selected := FoundSelParentItem;
      TreeView.Items[0].Expand(True);
    finally
      TreeView.Items.EndUpdate;
    end;
  finally
    Dec(FRebuildingTree);
  end;
end;

function TTBItemEditForm.AddListViewItem(const Index: Integer;
  const Item: TTBCustomItem): TListItem;
begin
  Result := ListView.Items.Insert(Index);
  Result.Data := Item;
  if not(Item is TTBControlItem) then begin
    Result.Caption := Item.Caption;
    Result.Subitems.Add(Item.ClassName);
    Result.ImageIndex := GetItemClassImage(TTBCustomItemClass(Item.ClassType));
  end
  else begin
    Result.Caption := '(Control)';
    Result.Subitems.Add(Item.ClassName);
    Result.ImageIndex := -1;
  end;
end;

procedure TTBItemEditForm.RebuildList;
var
  ChildItem: TTBCustomItem;
  I: Integer;
begin
  Inc(FRebuildingList);
  try
    ListView.Items.BeginUpdate;
    try
      ListView.Items.Clear;
      if Assigned(FSelParentItem) then begin
        for I := 0 to FSelParentItem.Count-1 do begin
          ChildItem := FSelParentItem[I];
          { Check for csDestroying because deleting an item in the tree view
            causes the parent item to be selected, and the parent item won't
            get a notification that the item is deleting since notifications
            were already sent }
          if not(csDestroying in ChildItem.ComponentState) then
            AddListViewItem(I, ChildItem);
        end;
        { Add an empty item to the end }
        ListView.Items.Add.ImageIndex := -1;
      end;
    finally
      ListView.Items.EndUpdate;
    end;
    { Work around a strange TListView bug(?). Without this, the column header
      isn't painted properly. }
    if HandleAllocated then
      SetWindowPos(ListView.Handle, 0, 0, 0, 0, 0, SWP_FRAMECHANGED or
        SWP_NOACTIVATE or SWP_NOMOVE or SWP_NOSIZE or SWP_NOZORDER);
  finally
    Dec(FRebuildingList);
  end;
end;

procedure TTBItemEditForm.SelectInObjectInspector(AList: TList);
var
  CompList1, CompList2: TDesignerSelectionList;
  I: Integer;
  C: TComponent;
begin
  { Designer.SetSelections will make components appear selected on the form.
    It will also select the component in Object Inspector, but only if the
    form has the focus. TDesignWindow.SetSelection will select the component
    in Object Inspector regardless of whether the form has the focus. }
  CompList1 := CreateSelectionList;
  CompList2 := CreateSelectionList;
  for I := 0 to AList.Count-1 do begin
    C := TComponent(AList[I]);
    { Must check for csDestroying. If SetSelection is passed a component that's
      destroying, Delphi will crash. }
    if not(csDestroying in C.ComponentState) then begin
      CompList1.Add(C);
      CompList2.Add(C);
    end;
  end;
  if CompList1.Count = 0 then begin
    {$IFNDEF JR_D6}
    CompList1.Free;
    CompList2.Free;
    {$ENDIF}
  end
  else begin
    Designer.SetSelections(CompList1);
    { Note: Never pass an empty list to SetSelection or Delphi will crash }
    { History here:
      - 1.34: SetSelection call remarked out because it fixed Delphi 6 issue
        with random AV's after the editor was closed.
      - 1.38: SetSelection call restored because without it, Ctrl+X/C/V didn't
        work.
      - 1.40: SetSelection call disabled on Delphi 6 only because AV problem
        still seems to exist despite another change which I thought fixed it.
        On D6 it isn't necessary to call SetSelection for Ctrl+X/C/V to work.
        Note: Using "ComponentDesigner.SetSelection(Designer, nil, CompList2);"
        instead seems to fix the AV problem, but for consistency with Delphi's
        TMainMenu editor (which only selects items when its parent form is
        focused), I decided not to call SetSelection at all on D6.
    }
    {$IFNDEF JR_D6}
    SetSelection(CompList2);
    {$ENDIF}
  end;
end;

procedure TTBItemEditForm.GetSelItemList(const AList: TList);
var
  ListItem: TListItem;
begin
  ListItem := nil;
  while True do begin
    ListItem := ListView.GetNextItem(ListItem, sdAll, [isSelected]);
    if ListItem = nil then
      Break;
    if Assigned(ListItem.Data) then
      AList.Add(ListItem.Data);
  end;
end;

procedure TTBItemEditForm.SetSelParentItem(ASelParentItem: TTBCustomItem);
{ - Rebuilds the list view to match a new selection (ASelParentItem) in the
    tree view
  - Updates toolbar
  - Selects selected item(s) into Object Inspector }
var
  I: Integer;
  TreeNode: TTreeNode;
  ItemIsSelected: Boolean;
  List: TList;
begin
  if FSettingSel > 0 then
    Exit;
  List := TList.Create;
  Inc(FSettingSel);
  try
    if FSelParentItem <> ASelParentItem then begin
      FSelParentItem := ASelParentItem;
      NewSubmenuButton.Enabled := Assigned(ASelParentItem);
      NewItemButton.Enabled := Assigned(ASelParentItem);
      NewSepButton.Enabled := Assigned(ASelParentItem);
      for I := 0 to MoreMenu.Count-1 do
        MoreMenu[I].Enabled := Assigned(ASelParentItem);
      if not Assigned(TreeView.Selected) or (TreeView.Selected.Data <> FSelParentItem) then begin
        if FSelParentItem = nil then
          TreeView.Selected := nil
        else begin
          TreeNode := TreeView.Items.GetFirstNode;
          while Assigned(TreeNode) do begin
            if TreeNode.Data = FSelParentItem then begin
              TreeView.Selected := TreeNode;
              Break;
            end;
            TreeNode := TreeNode.GetNext;
          end;
        end;
      end;
      RebuildList;
    end;

    ItemIsSelected := (ActiveControl = ListView) and Assigned(ListView.Selected) and
      Assigned(ListView.Selected.Data);
    if ItemIsSelected then
      GetSelItemList(List);

    CutButton.Enabled := ItemIsSelected;
    CopyButton.Enabled := ItemIsSelected;
    PasteButton.Enabled := (ActiveControl = ListView);
    DeleteButton.Enabled := ItemIsSelected or
      ((ActiveControl = TreeView) and (FSelParentItem <> FRootItem));
    MoveUpButton.Enabled := ItemIsSelected and
      (FSelParentItem.IndexOf(TTBCustomItem(List.First)) > 0);
    MoveDownButton.Enabled := ItemIsSelected and
      (FSelParentItem.IndexOf(TTBCustomItem(List.Last)) < FSelParentItem.Count-1);

    if ActiveControl = ListView then begin
      if List.Count = 0 then
        { No item was selected, or the blank item was selected.
          Select the root item so it looks like no item was selected in
          Object Inspector }
        List.Add(FRootItem);
    end
    else if not Assigned(ASelParentItem) or (ASelParentItem = FRootItem) then
      List.Add(FParentComponent)
    else
      List.Add(ASelParentItem);
    SelectInObjectInspector(List);
  finally
    Dec(FSettingSel);
    List.Free;
  end;
end;

procedure TTBItemEditForm.Cut;
begin
  Copy;
  Delete;
end;

procedure TTBItemEditForm.Copy;
var
  SelList: TList;
  CompList: TDesignerSelectionList;
  I: Integer;
  Item: TTBCustomItem;
begin
  if ListView.Selected = nil then Exit;
  CompList := nil;
  SelList := TList.Create;
  try
    GetSelItemList(SelList);
    CompList := CreateSelectionList;
    for I := 0 to SelList.Count-1 do begin
      Item := TTBCustomItem(SelList[I]);
      if Item is TTBControlItem then
        raise EInvalidOperation.Create('Cannot cut or copy TTBControlItems');
      CompList.Add(Item);
    end;
    CopyComponents(FParentComponent.Owner, CompList);
  finally
    {$IFNDEF JR_D6}
    CompList.Free;
    {$ENDIF}
    SelList.Free;
  end;
end;

procedure TTBItemEditForm.Paste;
var
  CompList: TDesignerSelectionList;
begin
  if FSelParentItem = nil then Exit;
  CompList := CreateSelectionList;
  try
    PasteComponents(FParentComponent.Owner, FSelParentItem, CompList);
    if CompList.Count <> 0 then
      Designer.Modified;
  finally
    {$IFNDEF JR_D6}
    CompList.Free;
    {$ENDIF}
  end;
end;

procedure TTBItemEditForm.DeleteItem(const Item: TTBCustomItem);
begin
  if csAncestor in Item.ComponentState then
    raise EInvalidOperation.Create('Items introduced in an ancestor form cannot be deleted');
  //Designer.ValidateRename(Item, Item.Name, '');
  Item.Free;
  Designer.Modified;
end;

procedure TTBItemEditForm.Delete;
var
  List: TList;
  Item: TTBCustomItem;
  ListItem: TListItem;
begin
  List := TList.Create;
  try
    List.Add(FSelParentItem);
    SelectInObjectInspector(List);
  finally
    List.Free;
  end;
  FSelParentItem.ViewBeginUpdate;
  try
    while Assigned(ListView.Selected) do begin
      Item := TTBCustomItem(ListView.Selected.Data);
      if Item = nil then
        Break;
      DeleteItem(Item);
    end;
  finally
    FSelParentItem.ViewEndUpdate;
  end;
  { After deleting the items, select the item with the focus }
  ListItem := ListView.GetNextItem(nil, sdAll, [isFocused]);
  if Assigned(ListItem) then
    ListItem.Selected := True;
end;

procedure TTBItemEditForm.MoveItem(CurIndex, NewIndex: Integer);
var
  WasFocused: Boolean;
begin
  WasFocused := ListView.Items[CurIndex].Focused;

  FSelParentItem.Move(CurIndex, NewIndex);
  Designer.Modified;

  if WasFocused then
    ListView.Items[NewIndex].Focused := True;
  ListView.Items[NewIndex].Selected := True;
end;

procedure TTBItemEditForm.TreeViewChange(Sender: TObject; Node: TTreeNode);
var
  NewSelectedParentItem: TTBCustomItem;
begin
  if (FRebuildingTree > 0) or (FSettingSel > 0) then Exit;
  if Node = nil then
    NewSelectedParentItem := nil
  else
    NewSelectedParentItem := TTBCustomItem(Node.Data);
  SetSelParentItem(NewSelectedParentItem);
end;

procedure TTBItemEditForm.TreeViewEnter(Sender: TObject);
{ When the tree view gets the focus, act as if the currently selected item
  was clicked. }
begin
  ListView.Selected := nil;
  SetSelParentItem(FSelParentItem);
end;

procedure TTBItemEditForm.CMDeferUpdate(var Message: TMessage);
begin
  SetSelParentItem(FSelParentItem);
end;

procedure TTBItemEditForm.ListViewChange(Sender: TObject; Item: TListItem;
  Change: TItemChange);
var
  Msg: TMsg;
begin
  if (FRebuildingList > 0) or (FSettingSel > 0) or (Change <> ctState) or
     (csDestroying in ListView.ComponentState) then
    Exit;
  if not PeekMessage(Msg, Handle, CM_DEFERUPDATE, CM_DEFERUPDATE,
     PM_NOREMOVE or PM_NOYIELD) then
    PostMessage(Handle, CM_DEFERUPDATE, 0, 0);
end;

procedure TTBItemEditForm.ListViewEnter(Sender: TObject);
begin
  { When list view gets the focus, update the toolbar }
  SetSelParentItem(FSelParentItem);
end;

procedure TTBItemEditForm.ListViewDblClick(Sender: TObject);
var
  SelItem: TTBCustomItem;
  PropCount, I: Integer;
  Props: {$IFNDEF CLR} PPropList {$ELSE} TPropList {$ENDIF};
  PropInfo: {$IFNDEF CLR} PPropInfo {$ELSE} TPropInfo {$ENDIF};
  MethodName: String;
  Method: TMethod;
begin
  SelItem := nil;
  if Assigned(ListView.Selected) then
    SelItem := TTBCustomItem(ListView.Selected.Data);
  if SelItem = nil then Exit;
  if IsSubmenuItem(SelItem) then begin
    SetSelParentItem(SelItem);
    Exit;
  end;
  {$IFNDEF CLR}
  PropCount := GetPropList(SelItem.ClassInfo, [tkMethod], nil);
  GetMem(Props, PropCount * SizeOf(PPropInfo));
  try
    GetPropList(SelItem.ClassInfo, [tkMethod], Props);
  {$ELSE}
    Props := GetPropList(SelItem.ClassInfo, [tkMethod]);
    PropCount := Length(Props);
  {$ENDIF}
    for I := PropCount-1 downto 0 do begin
      PropInfo := Props[I];
      {$IFNDEF CLR}
      if CompareText(String(PropInfo.Name), 'OnClick') = 0 then begin
      {$ELSE}
      if SameText(PropInfo.Name, 'OnClick', loInvariantLocale) then begin
      {$ENDIF}
        Method := GetMethodProp(SelItem, PropInfo);
        MethodName := Designer.GetMethodName(Method);
        if MethodName = '' then begin
          MethodName := SelItem.Name + 'Click';
          Method := Designer.CreateMethod(MethodName, GetTypeData(
            {$IFNDEF CLR} PropInfo.PropType^ {$ELSE} PropInfo.TypeInfo {$ENDIF}));
          SetMethodProp(SelItem, PropInfo, Method);
          Designer.Modified;
        end;
        if Designer.MethodExists(MethodName) then
          Designer.ShowMethod(MethodName);
        Break;
      end;
    end;
  {$IFNDEF CLR}
  finally
    FreeMem(Props);
  end;
  {$ENDIF}
end;

procedure TTBItemEditForm.ListViewKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
  case Key of
    VK_RETURN: begin
        Key := 0;
        ActivateInspector(#0);
      end;
    VK_INSERT: begin
        Key := 0;
        if ssCtrl in Shift then
          NewSubmenuButtonClick(Sender)
        else
          NewItemButtonClick(Sender);
      end;
    VK_DELETE: begin
        Key := 0;
        Delete;
      end;
  end;
end;

procedure TTBItemEditForm.TreeViewKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
  case Key of
    VK_RETURN: begin
        Key := 0;
        ActivateInspector(#0);
      end;
    VK_DELETE: begin
        Key := 0;
        DeleteButtonClick(Sender);
      end;
  end;
end;

procedure TTBItemEditForm.TreeViewKeyPress(Sender: TObject; var Key: Char);
begin
  if (Key >= #33) and (Key <= #126) then begin
    ActivateInspector(Key);
    Key := #0;
  end
  else if Key = #13 then
    Key := #0;  { suppress beep }
end;

procedure TTBItemEditForm.ListViewKeyPress(Sender: TObject; var Key: Char);
begin
  if Key = '-' then begin
    NewSepButtonClick(Sender);
    Key := #0;
  end
  else if (Key >= #33) and (Key <= #126) then begin
    ActivateInspector(Key);
    Key := #0;
  end;
end;

procedure TTBItemEditForm.ListViewDragOver(Sender, Source: TObject; X,
  Y: Integer; State: TDragState; var Accept: Boolean);
{ List item dragged over the list view }
var
  Item: TListItem;
begin
  Accept := False;
  if (Sender = ListView) and (Source = ListView) and (ListView.SelCount = 1) then begin
    Item := ListView.GetItemAt(X, Y);
    if Assigned(Item) and (Item <> ListView.Selected) then
      Accept := True;
  end;
end;

procedure TTBItemEditForm.ListViewDragDrop(Sender, Source: TObject; X,
  Y: Integer);
{ List item dropped onto another list item }
var
  ListItem: TListItem;
  Item: TTBCustomItem;
  NewIndex: Integer;
begin
  if (Sender = ListView) and (Source = ListView) and (ListView.SelCount = 1) then begin
    ListItem := ListView.GetItemAt(X, Y);
    if Assigned(ListItem) and (ListItem <> ListView.Selected) and Assigned(FSelParentItem) then begin
      NewIndex := FSelParentItem.IndexOf(TTBCustomItem(ListItem.Data));
      if NewIndex <> -1 then begin
        ListView.Items.BeginUpdate;
        { For good performance and to prevent Object Inspector flicker, increment
          FSettingSel to prevent calls to SetSelParentItem while moving items }
        Inc(FSettingSel);
        try
          Item := TTBCustomItem(ListView.Selected.Data);
          MoveItem(FSelParentItem.IndexOf(Item), NewIndex);
        finally
          Dec(FSettingSel);
          ListView.Items.EndUpdate;
        end;
        { After decrementing FSettingSel, now call SetSelParentItem, to update
          the toolbar buttons }
        SetSelParentItem(FSelParentItem);
      end;
    end;
  end;
end;

function TTBItemEditForm.TreeViewDragHandler(Sender, Source: TObject;
  X, Y: Integer; Drop: Boolean): Boolean;
var
  Node: TTreeNode;
  ListItem: TListItem;
  Item, NewParentItem: TTBCustomItem;
  ItemList: TList;
  I: Integer;
  NeedRebuildTree: Boolean;
begin
  Result := False;
  if (Sender = TreeView) and (Source = ListView) then begin
    Node := TreeView.GetNodeAt(X, Y);
    if Assigned(Node) and (Node <> TreeView.Selected) then begin
      NewParentItem := TTBCustomItem(Node.Data);
      ItemList := TList.Create;
      try
        ListItem := nil;
        while True do begin
          ListItem := ListView.GetNextItem(ListItem, sdAll, [isSelected]);
          if ListItem = nil then
            Break;
          Item := TTBCustomItem(ListItem.Data);
          if Assigned(Item) and (Item <> NewParentItem) and
             not Item.ContainsItem(NewParentItem) and
             not(Item is TTBControlItem) then begin
            Result := True;
            ItemList.Add(Item);
          end;
        end;
        if Drop then begin
          NeedRebuildTree := False;
          for I := 0 to ItemList.Count-1 do begin
            Item := TTBCustomItem(ItemList[I]);
            Item.Parent.Remove(Item);
            NewParentItem.Add(Item);
            Designer.Modified;
            if IsSubmenuItem(Item) then
              NeedRebuildTree := True;
          end;
          if NeedRebuildTree then
            RebuildTree;
        end;
      finally
        ItemList.Free;
      end;
    end;
  end;
end;

procedure TTBItemEditForm.TreeViewDragOver(Sender, Source: TObject; X,
  Y: Integer; State: TDragState; var Accept: Boolean);
{ List item dragged over the tree view }
begin
  Accept := TreeViewDragHandler(Sender, Source, X, Y, False);
end;

procedure TTBItemEditForm.TreeViewDragDrop(Sender, Source: TObject; X,
  Y: Integer);
{ List item dropped onto the tree view }
begin
  TreeViewDragHandler(Sender, Source, X, Y, True);
end;

procedure TTBItemEditForm.CreateNewItem(const AClass: TTBCustomItemClass);
var
  NewIndex: Integer;
  NewItem: TTBCustomItem;
  ListItem: TListItem;
begin
  if FSelParentItem = nil then Exit;
  NewIndex := -1;
  if (GetKeyState(VK_SHIFT) >= 0) and Assigned(ListView.Selected) then
    NewIndex := FSelParentItem.IndexOf(TTBCustomItem(ListView.Selected.Data));
  if NewIndex = -1 then
    NewIndex := FSelParentItem.Count;
  NewItem := AClass.Create(FParentComponent.Owner{Designer.Form});
  try
    NewItem.Name := Designer.UniqueName(NewItem.ClassName);
    FSelParentItem.Insert(NewIndex, NewItem);
  except
    NewItem.Free;
    raise;
  end;
  Designer.Modified;
  ListView.Selected := nil;
  ListItem := ListView.FindData(0, NewItem, True, False);
  if Assigned(ListItem) then begin
    ListItem.Selected := True;
    ListItem.Focused := True;
    ListItem.MakeVisible(False);
    ListView.SetFocus;
  end;
end;

procedure TTBItemEditForm.NewSubmenuButtonClick(Sender: TObject);
begin
  CreateNewItem(TTBSubmenuItem);
end;

procedure TTBItemEditForm.NewItemButtonClick(Sender: TObject);
begin
  CreateNewItem(TTBItem);
end;

procedure TTBItemEditForm.NewSepButtonClick(Sender: TObject);
begin
  CreateNewItem(TTBSeparatorItem);
end;

procedure TTBItemEditForm.MoreItemClick(Sender: TObject);
begin
  CreateNewItem(TTBCustomItemClass((Sender as TTBItem).Tag));
end;

procedure TTBItemEditForm.CutButtonClick(Sender: TObject);
begin
  Cut;
end;

procedure TTBItemEditForm.CopyButtonClick(Sender: TObject);
begin
  Copy;
end;

procedure TTBItemEditForm.PasteButtonClick(Sender: TObject);
begin
  Paste;
end;

procedure TTBItemEditForm.DeleteButtonClick(Sender: TObject);
begin
  if ActiveControl = ListView then
    Delete
  else if (ActiveControl = TreeView) and (FSelParentItem <> FRootItem) then
    DeleteItem(FSelParentItem);
end;

procedure TTBItemEditForm.MoveUpButtonClick(Sender: TObject);
var
  SelList: TList;
  I, J: Integer;
  Item: TTBCustomItem;
  ListItem: TListItem;
begin
  if FSelParentItem = nil then Exit;
  SelList := TList.Create;
  try
    GetSelItemList(SelList);
    if SelList.Count = 0 then Exit;

    ListView.Items.BeginUpdate;
    FSelParentItem.ViewBeginUpdate;
    { For good performance and to prevent Object Inspector flicker, increment
      FSettingSel to prevent calls to SetSelParentItem while moving items }
    Inc(FSettingSel);
    try
      for I := 0 to SelList.Count-1 do begin
        Item := TTBCustomItem(SelList[I]);
        J := FSelParentItem.IndexOf(Item);
        if J <> -1 then
          MoveItem(J, J-1);
      end;
      ListItem := ListView.FindData(0, SelList[0], True, False);
      if Assigned(ListItem) then
        ListItem.MakeVisible(False);
    finally
      Dec(FSettingSel);
      FSelParentItem.ViewEndUpdate;
      ListView.Items.EndUpdate;
    end;
    { After decrementing FSettingSel, now call SetSelParentItem, to update
      the toolbar buttons }
    SetSelParentItem(FSelParentItem);
  finally
    SelList.Free;
  end;
end;

procedure TTBItemEditForm.MoveDownButtonClick(Sender: TObject);
var
  SelList: TList;
  I, J: Integer;
  Item: TTBCustomItem;
  ListItem: TListItem;
begin
  if FSelParentItem = nil then Exit;
  SelList := TList.Create;
  try
    GetSelItemList(SelList);
    if SelList.Count = 0 then Exit;

    ListView.Items.BeginUpdate;
    FSelParentItem.ViewBeginUpdate;
    { For good performance and to prevent Object Inspector flicker, increment
      FSettingSel to prevent calls to SetSelParentItem while moving items }
    Inc(FSettingSel);
    try
      for I := SelList.Count-1 downto 0 do begin
        Item := TTBCustomItem(SelList[I]);
        J := FSelParentItem.IndexOf(Item);
        if J <> -1 then
          MoveItem(J, J+1);
      end;
      ListItem := ListView.FindData(0, SelList[SelList.Count-1], True, False);
      if Assigned(ListItem) then
        ListItem.MakeVisible(False);
    finally
      Dec(FSettingSel);
      FSelParentItem.ViewEndUpdate;
      ListView.Items.EndUpdate;
    end;
    { After decrementing FSettingSel, now call SetSelParentItem, to update
      the toolbar buttons }
    SetSelParentItem(FSelParentItem);
  finally
    SelList.Free;
  end;
end;

procedure TTBItemEditForm.TConvertMenuClick(Sender: TObject);
begin
  if FSelParentItem = nil then Exit;
  DoConvert(FSelParentItem, FParentComponent.Owner);
end;


{ TTBItemsEditor }

procedure TTBItemsEditor.Edit;
var
  Item: TTBCustomItem;
begin
  if Assigned(Component) then begin
    Item := TBGetItems(Component);
    if Assigned(Item) then
      ShowEditForm(Component, Item, Designer);
  end;
end;

procedure TTBItemsEditor.ExecuteVerb(Index: Integer);
begin
  case Index of
    0: Edit;
    1: ShowVersion;
  end;
end;

function TTBItemsEditor.GetVerbCount: Integer;
begin
  Result := 2;
end;

function TTBItemsEditor.GetVerb(Index: Integer): String;
begin
  case Index of
    0: Result := 'Edit...';
    1: Result := 'Version...';
  else
    Result := '';
  end;
end;


{ TTBItemsPropertyEditor }

procedure TTBItemsPropertyEditor.Edit;
var
  Editor: {$IFDEF JR_D6} IComponentEditor {$ELSE} TComponentEditor {$ENDIF};
begin
  if PropCount <> 1 then Exit;
  Editor := GetComponentEditor(GetComponent(0) as TComponent, Designer);
  try
    Editor.Edit;
  finally
    {$IFNDEF JR_D6}
    Editor.Free;
    {$ENDIF}
  end;
end;

function TTBItemsPropertyEditor.GetAttributes: TPropertyAttributes;
begin
  Result := inherited GetAttributes + [paDialog, paReadOnly];
end;

function TTBItemsPropertyEditor.GetValue: String;
begin
  Result := '(TB2000 Items)';
end;

initialization
  ItemImageList := TImageList.Create(nil);
  {$IFNDEF CLR}
  ItemImageList.Handle := ImageList_LoadImage(HInstance, 'TB2_DSGNEDITORIMAGES',
    16, 0, clFuchsia, IMAGE_BITMAP, 0);
  {$ELSE}
  { Initialize the image list as plain ILC_COLOR (4-bit), because on Windows
    2000, at color depths > 16 (what TImageList's ILC_COLORDDB would give us
    when running on a true-color display), selected images are drawn with an
    ugly dithering effect }
  ItemImageList.Handle := ImageList_Create(16, 16, ILC_COLOR or ILC_MASK, 4, 4);
  LoadItemImage(Assembly.GetExecutingAssembly, 'TB2DsgnEditorImages.bmp');
  {$ENDIF}
  ItemClasses := TList.Create;
  {$IFNDEF CLR}
  AddModuleUnloadProc(UnregisterModuleItemClasses);
  {$ENDIF}
finalization
  {$IFNDEF CLR}
  RemoveModuleUnloadProc(UnregisterModuleItemClasses);
  {$ENDIF}
  FreeItemClasses;
  FreeAndNil(ItemImageList);
end.
