{ Copyright (C) 2004 This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. implementing ActionList Editor author: Radek Cervinka, radek.cervinka@centrum.cz contributors: Mattias Gaertner version: 0.1 - 26-27.2.2004 - write all from scratch 0.2 - 3.3.2004 - speed up filling listboxes some ergonomic fixes (like stay in category after ADD) fixed possible language problems 0.3 - 27.3.2004 - rename action > actualise editor 0.4 - 29.3.2004 - dblclick generate xxx.OnExecute code to editor TODO:- after changing action category in Object Inspector need sort category to listbox - sometimes click in listbox causes selecting last item (it's an strange gtk error. The LCL and the gtk intf do not send any change to the gtk. Either it is a bug in the gtk1 or we are doing something wrong in the handlers.) } unit ActionsEditor; {$mode objfpc}{$H+} interface uses Classes, SysUtils, LCLProc, Forms, StdCtrls, Buttons, ActnList, ExtCtrls, Controls, Dialogs, ObjInspStrConsts, ComponentEditors, PropEdits; type { TActionListEditor } TActionListEditor = class(TForm) btnAdd: TButton; btnDelete: TButton; lblCategory: TLabel; lblName: TLabel; lstActionName: TListBox; lstCategory: TListBox; Panel: TPanel; procedure btnAddClick(Sender: TObject); procedure btnDeleteClick(Sender: TObject); procedure lstActionNameClick(Sender: TObject); procedure lstActionNameDblClick(Sender: TObject); procedure lstCategoryClick(Sender: TObject); private FActionList: TActionList; FDesigner: TComponentEditorDesigner; protected procedure OnPersistentDeleting(APersistent: TPersistent); procedure OnPersistentAdded(APersistent: TPersistent; Select: boolean); procedure OnComponentRenamed(AComponent: TComponent); procedure CreateActionListEditor; // create form function GetSelectedAction: TContainedAction; public constructor Create(aOwner: TComponent); override; destructor Destroy; override; procedure SetActionList(AActionList: TActionList); procedure FillCategories; procedure FillActionByCategory(iIndex: Integer); property Designer:TComponentEditorDesigner read FDesigner write FDesigner; end; { TActionListComponentEditor } TActionListComponentEditor = class(TComponentEditor) private FActionList: TActionList; FDesigner: TComponentEditorDesigner; protected public constructor Create(AComponent: TComponent; ADesigner: TComponentEditorDesigner); override; destructor Destroy; override; procedure Edit; override; property ActionList: TActionList read FActionList write FActionList; function GetVerbCount: Integer; override; function GetVerb(Index: Integer): string; override; procedure ExecuteVerb(Index: Integer); override; end; { Action Registration } type TRegisteredAction = class private FActionClass: TBasicActionClass; FGroupId: Integer; public constructor Create(TheActionClass: TBasicActionClass; TheGroupID: integer); property ActionClass: TBasicActionClass read FActionClass; property GroupId: Integer read FGroupId; end; PRegisteredAction = ^TRegisteredAction; TRegisteredActionCategory = class private FCount: integer; FName: string; FItems: PRegisteredAction; FResource: TComponentClass; function GetItems(Index: integer): TRegisteredAction; public constructor Create(const CategoryName: string; AResource: TComponentClass); procedure Add(const AClasses: array of TBasicActionClass); destructor Destroy; override; function IndexOfClass(AClass: TBasicActionClass): integer; procedure EnumActions(Proc: TEnumActionProc; Info: Pointer); property Count: integer read FCount; property Name: string read FName; property Items[Index: integer]: TRegisteredAction read GetItems; property Resource: TComponentClass read FResource; end; TRegisteredActionCategories = class private FItems: TList; function GetItems(Index: integer): TRegisteredActionCategory; public procedure Add(const CategoryName: string; const AClasses: array of TBasicActionClass; AResource: TComponentClass); destructor Destroy; override; function IndexOfCategory(const CategoryName: string): integer; procedure EnumActions(Proc: TEnumActionProc; Info: Pointer); function FindResource(AClass: TBasicActionClass): TComponentClass; function Count: integer; property Items[Index: integer]: TRegisteredActionCategory read GetItems; end; var RegisteredActions: TRegisteredActionCategories = nil; type TNotifyActionListChange = procedure; var NotifyActionListChange: TNotifyActionListChange = nil; procedure RegisterActions(const ACategory: string; const AClasses: array of TBasicActionClass; AResource: TComponentClass); procedure UnRegisterActions(const Classes: array of TBasicActionClass); procedure EnumActions(Proc: TEnumActionProc; Info: Pointer); function CreateAction(TheOwner: TComponent; ActionClass: TBasicActionClass): TBasicAction; { ActionListEditorForm } var ActionListEditorForm: TActionListEditor; // global since there is normally // only one needed procedure ShowActionListEditor(AActionList: TActionList; ADesigner: TComponentEditorDesigner); implementation procedure RegisterActions(const ACategory: string; const AClasses: array of TBasicActionClass; AResource: TComponentClass); begin RegisteredActions.Add(ACategory,AClasses,AResource); end; procedure UnRegisterActions(const Classes: array of TBasicActionClass); begin end; procedure EnumActions(Proc: TEnumActionProc; Info: Pointer); begin RegisteredActions.EnumActions(Proc,Info); end; function CreateAction(TheOwner: TComponent; ActionClass: TBasicActionClass): TBasicAction; var ResourceClass: TComponentClass; ResInstance: TComponent; i: Integer; Component: TComponent; Action: TBasicAction; Src: TCustomAction; Dest: TCustomAction; begin Result := ActionClass.Create(TheOwner); // find a Resource component registered for this ActionClass ResourceClass:=RegisteredActions.FindResource(ActionClass); if ResourceClass=nil then exit; ResInstance:=ResourceClass.Create(nil); try // find an action owned by the Resource component Action:=nil; for i:=0 to ResInstance.ComponentCount-1 do begin Component:=ResInstance.Components[i]; if (CompareText(Component.ClassName,ActionClass.ClassName)=0) and (Component is TBasicAction) then begin Action:=TBasicAction(Component); break; end; end; if Action=nil then exit; // copy TCustomAction properties if (Action is TCustomAction) and (Result is TCustomAction) then begin Src:=TCustomAction(Action); Dest:=TCustomAction(Result); Dest.Caption:=Src.Caption; Dest.Checked:=Src.Checked; Dest.Enabled:=Src.Enabled; Dest.HelpContext:=Src.HelpContext; Dest.Hint:=Src.Hint; Dest.ImageIndex:=Src.ImageIndex; Dest.ShortCut:=Src.ShortCut; Dest.Visible:=Src.Visible; if (Dest is TContainedAction) and (Dest.ImageIndex>=0) then begin end; end; finally ResInstance.Free; end; end; procedure ShowActionListEditor(AActionList: TActionList; ADesigner:TComponentEditorDesigner); begin if AActionList=nil then Raise Exception.Create('ShowActionListEditor AActionList=nil'); if ActionListEditorForm=nil then begin ActionListEditorForm:=TActionListEditor.Create(Application); end; ActionListEditorForm.Designer:=ADesigner; ActionListEditorForm.SetActionList(AActionList); ActionListEditorForm.ShowOnTop; end; procedure TActionListEditor.btnAddClick(Sender: TObject); var NewAction:TContainedAction; begin NewAction:=TAction.Create(FActionList.Owner); { writeln('Add entry'); writeln(NewAction.ClassName); writeln(FDesigner.CreateUniqueComponentName(NewAction.ClassName));} NewAction.Name:=FDesigner.CreateUniqueComponentName(NewAction.ClassName); writeln(NewAction.Name); if lstCategory.ItemIndex>1 then // ignore first two items (virtual categories) NewAction.Category:=lstCategory.Items[lstCategory.ItemIndex] else NewAction.Category:=''; // writeln('Category',NewAction.Category); NewAction.ActionList:=FActionList; FDesigner.PropertyEditorHook.PersistentAdded(NewAction,true); FDesigner.Modified; // writeln('Add done'); end; procedure TActionListEditor.btnDeleteClick(Sender: TObject); var iNameIndex:Integer; OldName: string; OldAction: TContainedAction; begin // writeln('Delete Enter'); iNameIndex:=lstActionName.ItemIndex; if iNameIndex<0 then Exit; OldName:=lstActionName.Items[iNameIndex]; writeln('',OldName); lstActionName.Items.Delete(iNameIndex); OldAction:=FActionList.ActionByName(OldName); // be gone if assigned(OldAction) then begin try FDesigner.PropertyEditorHook.PersistentDeleting(OldAction); OldAction.Free; except on E: Exception do begin MessageDlg('Error deleting action', 'Error while deleting action:'#13 +E.Message,mtError,[mbOk],0); end; end; end; if lstActionName.Items.Count=0 then // last act in category > rebuild FillCategories else begin if iNameIndex>=lstActionName.Items.Count then lstActionName.ItemIndex:=lstActionName.Items.Count -1 else lstActionName.ItemIndex:=iNameIndex; FDesigner.SelectOnlyThisComponent( FActionList.ActionByName(lstActionName.Items[lstActionName.ItemIndex])); end; end; procedure TActionListEditor.lstActionNameDblClick(Sender: TObject); var CurAction: TContainedAction; begin CurAction:=GetSelectedAction; if CurAction=nil then exit; // Add OnExecute for this action CreateComponentEvent(CurAction,'OnExecute'); end; procedure TActionListEditor.lstCategoryClick(Sender: TObject); begin if lstCategory.ItemIndex<0 then Exit; FillActionByCategory(lstCategory.ItemIndex); end; procedure TActionListEditor.lstActionNameClick(Sender: TObject); var CurAction: TContainedAction; begin if lstActionName.ItemIndex<0 then Exit; CurAction:=GetSelectedAction; if CurAction=nil then exit; FDesigner.SelectOnlyThisComponent(CurAction); end; procedure TActionListEditor.OnPersistentDeleting(APersistent: TPersistent); var xIndex:Integer; begin if (APersistent is TAction) then begin xIndex:=lstActionName.Items.IndexOf(TAction(APersistent).Name); if xIndex<0 then Exit; // action not showed in listbox (other category) lstActionName.Items.Delete(xIndex); if lstActionName.Items.Count=0 then FillCategories //last action in category is deleted, rebuild category list else if xIndex>=lstActionName.Items.Count then lstActionName.ItemIndex:=lstActionName.Items.Count-1 else lstActionName.ItemIndex:=xIndex; end; end; procedure TActionListEditor.OnPersistentAdded(APersistent: TPersistent; Select: boolean); begin if (APersistent is TAction) then // ToDo: only set update flag and do not rebuild everything on every change FillCategories; end; procedure TActionListEditor.OnComponentRenamed(AComponent: TComponent); var iIndex:Integer; begin if not (AComponent is TAction) then Exit; FillActionByCategory(lstCategory.ItemIndex); iIndex:= lstActionName.Items.IndexOf(AComponent.Name);// is new action showed? if iIndex<0 then Exit; lstActionName.ItemIndex:=iIndex; // yes, select is end; constructor TActionListEditor.Create(aOwner: TComponent); begin inherited Create(aOwner); CreateActionListEditor; end; destructor TActionListEditor.Destroy; begin if GlobalDesignHook<>nil then GlobalDesignHook.RemoveAllHandlersForObject(Self); inherited Destroy; end; procedure TActionListEditor.CreateActionListEditor; begin Caption:=oisActionListEditor; BorderStyle:=bsDialog; SetInitialBounds(0,0,400,300); Position :=poScreenCenter; Panel:=TPanel.Create(Self); with Panel do begin Parent:=Self; Align:=alTop; Height:=42; end; btnAdd:=TButton.Create(Panel); with btnAdd do begin Parent:=Panel; OnClick:=@btnAddClick; Caption:=oisAdd; Top:=8; Left:=2; Width:=75; end; btnDelete:=TButton.Create(Panel); with btnDelete do begin Parent:=Panel; OnClick:=@btnDeleteClick; Caption:=sccsLvEdtBtnDel; Top:=8; Width:=75; Left:=128; end; lstCategory:=TListBox.Create(Self); with lstCategory do begin Parent:=Self; SetBounds(8,72,112, 224); OnClick:=@lstCategoryClick; end; lstActionName:=TListBox.Create(Self); with lstActionName do begin Parent:=Self; SetBounds(130,72, 160 ,224); OnClick:=@lstActionNameClick; OnDblClick:=@lstActionNameDblClick; end; lblCategory:=TLabel.Create(Self); with lblCategory do begin Parent:=Self; Caption:=oisCategory; SetBounds(8,48, 65 ,17); end; lblName:=TLabel.Create(Self); with lblName do begin Parent:=Self; Caption:=oisAction; SetBounds(130,48, 65 ,17); end; GlobalDesignHook.AddHandlerPersistentDeleting(@OnPersistentDeleting); GlobalDesignHook.AddHandlerPersistentAdded(@OnPersistentAdded); GlobalDesignHook.AddHandlerComponentRenamed(@OnComponentRenamed); end; function TActionListEditor.GetSelectedAction: TContainedAction; begin if lstActionName.ItemIndex>=0 then Result:= FActionList.ActionByName(lstActionName.Items[lstActionName.ItemIndex]) else Result:=nil; end; procedure TActionListEditor.SetActionList(AActionList: TActionList); begin FActionList:=AActionList; FillCategories; end; procedure TActionListEditor.FillCategories; var i:Integer; sCategory:String; xIndex:Integer; sOldCategory:String; begin // try remember old category sOldCategory:=''; if (lstCategory.Items.Count>0) and (lstCategory.ItemIndex>-1) then sOldCategory:=lstCategory.Items[lstCategory.ItemIndex]; lstCategory.Items.BeginUpdate; try lstCategory.Clear; lstCategory.Items.Add(cActionListEditorUnknownCategory); lstCategory.Items.Add(cActionListEditorAllCategory); for i:=0 to FActionList.ActionCount-1 do begin sCategory:=FActionList.Actions[i].Category; if Trim(sCategory)='' then Continue; xIndex:=lstCategory.Items.IndexOf(sCategory); if xIndex<0 then lstCategory.Items.Add(sCategory); end; finally lstCategory.Items.EndUpdate; end; xIndex:=lstCategory.Items.IndexOf(sOldCategory); if xIndex<0 then xIndex:=0; lstCategory.ItemIndex:=xIndex; FillActionByCategory(xIndex); end; procedure TActionListEditor.FillActionByCategory(iIndex:Integer); var i:Integer; sCategory:String; begin lstActionName.Items.BeginUpdate; if iIndex<0 then iIndex:=1;// all try lstActionName.Clear; // handle all if iIndex = 1 then begin for i:=0 to FActionList.ActionCount-1 do lstActionName.Items.Add(FActionList.Actions[i].Name); Exit; //throught finally end; // handle unknown if iIndex = 0 then begin for i:=0 to FActionList.ActionCount-1 do begin if Trim(FActionList.Actions[i].Category)='' then lstActionName.Items.Add(FActionList.Actions[i].Name); end; Exit; //throught finally end; // else sort to categories sCategory:=lstCategory.Items[iIndex]; for i:=0 to FActionList.ActionCount-1 do begin if FActionList.Actions[i].Category = sCategory then lstActionName.Items.Add(FActionList.Actions[i].Name); end; finally lstActionName.Items.EndUpdate; end; end; { TActionListComponentEditor } constructor TActionListComponentEditor.Create(AComponent: TComponent; ADesigner: TComponentEditorDesigner); begin inherited Create(AComponent, ADesigner); FDesigner:=ADesigner; end; destructor TActionListComponentEditor.Destroy; begin if assigned(ActionListEditorForm) and (ActionListEditorForm.FActionList=GetComponent) then FreeThenNil(ActionListEditorForm); inherited Destroy; end; procedure TActionListComponentEditor.Edit; begin writeln('TActionListComponentEditor.Edit ',GetComponent.Name); ShowActionListEditor(GetComponent as TActionList, FDesigner); end; function TActionListComponentEditor.GetVerbCount: Integer; begin Result:=1; end; function TActionListComponentEditor.GetVerb(Index: Integer): string; begin Result:=oisEditActionList; end; procedure TActionListComponentEditor.ExecuteVerb(Index: Integer); begin Edit; end; //------------------------------------------------------------------------------ { TRegisteredActionCategory } function TRegisteredActionCategory.GetItems(Index: integer): TRegisteredAction; begin Result:=FItems[Index]; end; constructor TRegisteredActionCategory.Create(const CategoryName: string; AResource: TComponentClass); begin FName:=CategoryName; FResource:=AResource; end; procedure TRegisteredActionCategory.Add( const AClasses: array of TBasicActionClass); var i: integer; CurCount: Integer; IsDouble: Boolean; j: Integer; AClass: TBasicActionClass; begin if length(AClasses)=0 then exit; CurCount:=FCount; inc(FCount,length(AClasses)); // add all classes (ignoring doubles) ReAllocMem(FItems,SizeOf(TBasicActionClass)*FCount); for i:=Low(AClasses) to High(AClasses) do begin AClass:=AClasses[i]; // check if already exists IsDouble:=false; for j:=0 to CurCount-1 do begin if FItems[j].ActionClass=AClass then begin IsDouble:=true; break; end; end; // add if not IsDouble then begin // TODO use current designer group instead of -1 FItems[CurCount]:=TRegisteredAction.Create(AClass,-1); inc(CurCount); RegisterNoIcon([AClass]); Classes.RegisterClass(AClass); end; end; // resize FItems if CurCount=0) and (FItems[Result].ActionClass<>AClass) do dec(Result); end; procedure TRegisteredActionCategory.EnumActions(Proc: TEnumActionProc; Info: Pointer); var i: Integer; begin for i:=0 to Count-1 do Proc(Name,FItems[i].ActionClass,Info); end; { TRegisteredAction } constructor TRegisteredAction.Create(TheActionClass: TBasicActionClass; TheGroupID: integer); begin FActionClass:=TheActionClass; FGroupId:=TheGroupID; end; { TRegisteredActionCategories } function TRegisteredActionCategories.GetItems(Index: integer ): TRegisteredActionCategory; begin Result:=TRegisteredActionCategory(FItems[Index]); end; procedure TRegisteredActionCategories.Add(const CategoryName: string; const AClasses: array of TBasicActionClass; AResource: TComponentClass); var i: LongInt; Category: TRegisteredActionCategory; begin i:=IndexOfCategory(CategoryName); if i>=0 then begin Category:=Items[i]; if Category.Resource<>AResource then raise Exception.Create('TRegisteredActionCategories.Add Resource<>OldResource'); end else begin Category:=TRegisteredActionCategory.Create(CategoryName,AResource); if FItems=nil then FItems:=TList.Create; FItems.Add(Category); end; Category.Add(AClasses); if Assigned(NotifyActionListChange) then NotifyActionListChange; end; destructor TRegisteredActionCategories.Destroy; var i: Integer; begin for i:=Count-1 downto 0 do Items[i].Free; FItems.Free; inherited Destroy; end; function TRegisteredActionCategories.IndexOfCategory(const CategoryName: string ): integer; begin Result:=Count-1; while (Result>=0) and (CompareText(Items[Result].Name,CategoryName)<>0) do dec(Result); end; procedure TRegisteredActionCategories.EnumActions(Proc: TEnumActionProc; Info: Pointer); var i: Integer; begin for i:=0 to Count-1 do Items[i].EnumActions(Proc,Info); end; function TRegisteredActionCategories.FindResource(AClass: TBasicActionClass ): TComponentClass; var Category: TRegisteredActionCategory; i: Integer; begin for i:=0 to Count-1 do begin Category:=Items[i]; if Category.IndexOfClass(AClass)>=0 then begin Result:=Category.Resource; exit; end; end; Result:=nil; end; function TRegisteredActionCategories.Count: integer; begin if FItems=nil then Result:=0 else Result:=FItems.Count; end; initialization RegisteredActions:=TRegisteredActionCategories.Create; RegisterActionsProc := @RegisterActions; UnRegisterActionsProc := @UnregisterActions; EnumRegisteredActionsProc := @EnumActions; CreateActionProc := @CreateAction; RegisterComponentEditor(TActionList,TActionListComponentEditor); finalization RegisteredActions.Free; RegisteredActions:=nil; end.