{ /*************************************************************************** ChangeClassDialog.pas - Lazarus IDE unit ---------------------------------------- ***************************************************************************/ *************************************************************************** * * * This source 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 code 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 * * General Public License for more details. * * * * A copy of the GNU General Public License is available on the World * * Wide Web at . You can also * * obtain it by writing to the Free Software Foundation, * * Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1335, USA. * * * *************************************************************************** Author: Mattias Gaertner Abstract: Functions and Dialog to change the class of a designer component. } unit ChangeClassDialog; {$mode objfpc}{$H+} interface uses Classes, SysUtils, AvgLvlTree, // LCL LCLType, Forms, Controls, Dialogs, StdCtrls, ButtonPanel, // LazUtils LazLoggerBase, // Codetools LFMTrees, CodeCache, CodeToolManager, // BuildIntf ComponentReg, // IdeIntf PropEdits, ComponentEditors, FormEditingIntf, SrcEditorIntf, IDEDialogs, // IDE LazarusIDEStrConsts, CheckLFMDlg, Project, MainIntf, EnvironmentOpts; type { TChangeClassDlg } TChangeClassDlg = class(TForm) BtnPanel: TButtonPanel; NewClassComboBox: TComboBox; NewAncestorsListBox: TListBox; OldAncestorsListBox: TListBox; OldClassLabel: TLabel; NewGroupBox: TGroupBox; OldGroupBox: TGroupBox; procedure ChangeClassDlgCreate(Sender: TObject); procedure NewClassComboBoxEditingDone(Sender: TObject); procedure NewClassComboBoxKeyUp(Sender: TObject; var Key: Word; {%H-}Shift: TShiftState); private FClasses: TAvgLvlTree; FNewClass: TClass; FThePersistent: TPersistent; FClassAmbiguous: TPointerToPointerTree; procedure SetNewClass(const AValue: TClass); procedure SetThePersistent(const AValue: TPersistent); procedure UpdateInfo; procedure UpdateOldInfo; procedure UpdateNewInfo; procedure FillAncestorListBox(AClass: TClass; AListBox: TListBox); procedure AddClass(const AClass: TPersistentClass); procedure AddComponentClass(const AClass: TComponentClass); function CompareClasses({%H-}Tree: TAvgLvlTree; Data1, Data2: Pointer): integer; function ClassToCaption(aComp: TClass): string; public constructor Create(TheOwner: TComponent); override; destructor Destroy; override; procedure FillNewClassComboBox; property ThePersistent: TPersistent read FThePersistent write SetThePersistent; property NewClass: TClass read FNewClass write SetNewClass; end; function ShowChangeClassDialog(ADesigner: TIDesigner; APersistent: TPersistent): TModalResult; function ChangePersistentClass(ADesigner: TIDesigner; APersistent: TPersistent; NewClass: TClass): TModalResult; implementation {$R *.lfm} function ShowChangeClassDialog(ADesigner: TIDesigner; APersistent: TPersistent): TModalResult; var ChangeClassDlg: TChangeClassDlg; begin Result:=mrCancel; ChangeClassDlg:=TChangeClassDlg.Create(nil); try ChangeClassDlg.ThePersistent:=APersistent; ChangeClassDlg.FillNewClassComboBox; if ChangeClassDlg.ShowModal=mrOk then begin Result:=ChangePersistentClass(ADesigner,APersistent,ChangeClassDlg.NewClass); end; finally ChangeClassDlg.Free; end; end; function ChangePersistentClass(ADesigner: TIDesigner; APersistent: TPersistent; NewClass: TClass): TModalResult; var ComponentStream: TMemoryStream; PersistentName: String; UnitCode: TCodeBuffer; LFMBuffer: TCodeBuffer; LFMTree: TLFMTree; UnitInfo: TUnitInfo; procedure ShowAbortMessage(const Msg: string); begin IDEMessageDialog('Error', Format(lisUnableToChangeClassOfTo, [Msg, LineEnding, PersistentName, NewClass.UnitName+'.'+NewClass.ClassName]), mtError,[mbCancel]); end; function StreamSelection: boolean; begin Result:=false; // select only this persistent GlobalDesignHook.SelectOnlyThis(APersistent); // stream selection ComponentStream:=TMemoryStream.Create; if (not FormEditingHook.SaveSelectionToStream(ComponentStream)) or (ComponentStream.Size=0) then begin ShowAbortMessage(lisUnableToStreamSelectedComponents2); exit; end; Result:=true; end; function ParseLFMStream: boolean; var SrcEdit: TSourceEditorInterface; Msg: String; begin Result:=false; if not CodeToolBoss.GatherExternalChanges then begin ShowAbortMessage(lisUnableToGatherEditorChanges); exit; end; MainIDEInterface.GetUnitInfoForDesigner(ADesigner,SrcEdit,UnitInfo); if UnitInfo=nil then begin ShowAbortMessage(lisUnableToGetSourceForDesigner); exit; end; UnitCode:=UnitInfo.Source; LFMBuffer:=CodeToolBoss.CreateTempFile('lazaruschangeclass.lfm'); if (LFMBuffer=nil) or (ComponentStream.Size=0) then begin ShowAbortMessage(lisUnableToCreateTemporaryLfmBuffer); exit; end; ComponentStream.Position:=0; LFMBuffer.LoadFromStream(ComponentStream); //debugln('ChangePersistentClass-Before-Checking--------------------------------------------'); //debugln(LFMBuffer.Source); //debugln('ChangePersistentClass-Before-Checking-------------------------------------------'); if not CodeToolBoss.ParseLFM(LFMBuffer,LFMTree) then begin debugln('ChangePersistentClass-Before--------------------------------------------'); debugln(LFMBuffer.Source); debugln('ChangePersistentClass-Before--------------------------------------------'); if CodeToolBoss.ErrorMessage<>'' then MainIDEInterface.DoJumpToCodeToolBossError else begin Msg:=lisErrorParsingLfmComponentStream; if LFMTree<>nil then Msg:=Msg+LineEnding+LineEnding+LFMTree.FirstErrorAsString+LineEnding; ShowAbortMessage(Msg); end; exit; end; Result:=true; end; function ChangeClassName: boolean; var CurNode: TLFMTreeNode; ObjectNode: TLFMObjectNode; begin Result:=false; // find classname position CurNode:=LFMTree.Root; while CurNode<>nil do begin if (CurNode is TLFMObjectNode) then begin ObjectNode:=TLFMObjectNode(CurNode); if (CompareText(ObjectNode.Name,(APersistent as TComponent).Name)=0) and (CompareText(ObjectNode.TypeName,APersistent.ClassName)=0) then begin // replace classname LFMBuffer.Replace(ObjectNode.TypeNamePosition,length(ObjectNode.TypeName), NewClass.ClassName); Result:=true; exit; end; end; CurNode:=CurNode.NextSibling; end; ShowAbortMessage(Format(lisUnableToFindInLFMStream, [PersistentName])); end; function CheckProperties: boolean; begin Result:=RepairLFMBuffer(UnitCode,LFMBuffer,false,false,false)=mrOk; if not Result and (CodeToolBoss.ErrorMessage<>'') then MainIDEInterface.DoJumpToCodeToolBossError; end; function InsertStreamedSelection: boolean; var MemStream: TMemoryStream; NewParent: TWinControl; begin Result:=false; if LFMBuffer.SourceLength=0 then exit; MemStream:=TMemoryStream.Create; try debugln('ChangePersistentClass-After--------------------------------------------'); debugln(LFMBuffer.Source); debugln('ChangePersistentClass-After--------------------------------------------'); LFMBuffer.SaveToStream(MemStream); MemStream.Position:=0; NewParent:=nil; if APersistent is TControl then NewParent:=TControl(APersistent).Parent; Result:=FormEditingHook.InsertFromStream(MemStream,NewParent, [cpsfReplace]); if not Result then ShowAbortMessage(lisReplacingSelectionFailed); finally MemStream.Free; end; end; begin Result:=mrCancel; if NewClass = nil then exit; if APersistent.ClassType=NewClass then begin Result:=mrOk; exit; end; PersistentName:=APersistent.ClassName; if APersistent is TComponent then begin PersistentName:=TComponent(APersistent).Name+': '+PersistentName; end else begin ShowAbortMessage(lisCanOnlyChangeTheClassOfTComponents); exit; end; ComponentStream:=nil; LFMTree:=nil; try if not StreamSelection then exit; if not ParseLFMStream then exit; if not ChangeClassName then exit; if not CheckProperties then exit; if not InsertStreamedSelection then exit; finally ComponentStream.Free; // Note: do not free LFMTree, it is cached by the codetools end; Result:=mrOk; end; { TChangeClassDlg } procedure TChangeClassDlg.ChangeClassDlgCreate(Sender: TObject); begin OldGroupBox.Caption:=lisOldClass; NewGroupBox.Caption:=lisNewClass; NewClassComboBox.DropDownCount:=EnvironmentOptions.DropDownCount; end; procedure TChangeClassDlg.NewClassComboBoxEditingDone(Sender: TObject); begin UpdateNewInfo; end; procedure TChangeClassDlg.NewClassComboBoxKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState); begin if Key = VK_RETURN then UpdateNewInfo; end; procedure TChangeClassDlg.SetThePersistent(const AValue: TPersistent); begin if FThePersistent=AValue then exit; FThePersistent:=AValue; UpdateInfo; end; procedure TChangeClassDlg.SetNewClass(const AValue: TClass); begin if FNewClass=AValue then exit; FNewClass:=AValue; UpdateNewInfo; end; procedure TChangeClassDlg.UpdateInfo; begin UpdateNewInfo; UpdateOldInfo; end; procedure TChangeClassDlg.UpdateOldInfo; begin FillAncestorListBox(ThePersistent.ClassType,OldAncestorsListBox); if ThePersistent<>nil then begin if ThePersistent is TComponent then OldClassLabel.Caption:=TComponent(ThePersistent).Name+': '+ClassToCaption(ThePersistent.ClassType) else OldClassLabel.Caption:=ClassToCaption(ThePersistent.ClassType); Caption:=Format(lisCCDChangeClassOf, [OldClassLabel.Caption]); end else begin OldClassLabel.Caption:=lisCCDNoClass; Caption:=lisChangeClass; end; end; procedure TChangeClassDlg.UpdateNewInfo; var ANode: TAvgLvlTreeNode; begin FNewClass:=nil; if FClasses<>nil then begin ANode:=FClasses.FindLowest; while (ANode<>nil) do begin FNewClass:=TClass(ANode.Data); if (CompareText(ClassToCaption(NewClass),NewClassComboBox.Text)=0) then break else FNewClass:=nil; ANode:=FClasses.FindSuccessor(ANode); end; end; FillAncestorListBox(NewClass,NewAncestorsListBox); if NewClass<>nil then begin NewClassComboBox.Text:=ClassToCaption(NewClass); BtnPanel.OKButton.Enabled:=true; end else begin NewClassComboBox.Text:=''; BtnPanel.OKButton.Enabled:=false; end; end; procedure TChangeClassDlg.FillAncestorListBox(AClass: TClass; AListBox: TListBox); var List: TStringList; procedure AddAncestor(CurClass: TClass); begin if CurClass=nil then exit; List.Add(ClassToCaption(CurClass)); AddAncestor(CurClass.ClassParent); end; begin List:=TStringList.Create; AddAncestor(AClass); AListBox.Items.Assign(List); List.Free; end; procedure TChangeClassDlg.AddClass(const AClass: TPersistentClass); begin if FClasses.FindPointer(AClass)<>nil then exit; FClasses.Add(AClass); end; procedure TChangeClassDlg.AddComponentClass(const AClass: TComponentClass); begin AddClass(AClass); end; function TChangeClassDlg.CompareClasses(Tree: TAvgLvlTree; Data1, Data2: Pointer ): integer; // sort: // transforming ThePersistent to descending classes is easy // transforming ThePersistent to ascending classes is medium // // count distance between, that means: find nearest shared ancestor, then // give two points for every step from ThePersistent to ancestor and one point // for every step from ancestor to class // // otherwise sort for classnames var Class1: TClass absolute Data1; Class2: TClass absolute Data2; function AncestorDistance(ChildClass, AncestorClass: TClass): integer; begin Result:=0; while (ChildClass<>nil) and (ChildClass<>AncestorClass) do begin ChildClass:=ChildClass.ClassParent; inc(Result); end; end; function RelationDistance(SrcClass, DestClass: TClass): integer; var Ancestor: TClass; begin // find shared ancestor of Ancestor:=SrcClass; while (Ancestor<>nil) and (not DestClass.InheritsFrom(Ancestor)) do Ancestor:=Ancestor.ClassParent; // going to the ancestor is normally more difficult than going away Result:=2*AncestorDistance(SrcClass,Ancestor) +AncestorDistance(DestClass,Ancestor); end; var Dist1: LongInt; Dist2: LongInt; begin Result:=0; if (ThePersistent<>nil) then begin Dist1:=RelationDistance(ThePersistent.ClassType,Class1); Dist2:=RelationDistance(ThePersistent.ClassType,Class2); Result:=Dist1-Dist2; if Result<>0 then exit; end; Result:=CompareText(Class1.ClassName,Class2.ClassName); if Result<>0 then exit; Result:=CompareText(Class1.UnitName,Class2.UnitName); end; function TChangeClassDlg.ClassToCaption(aComp: TClass): string; begin Result:=aComp.ClassName; if FClassAmbiguous[aComp]<>nil then Result:=Result+'('+aComp.UnitName+')'; end; constructor TChangeClassDlg.Create(TheOwner: TComponent); var i: Integer; RegComp: TRegisteredComponent; aComp, OldClass: TComponentClass; FAllClasses: TStringToPointerTree; // lowercase classname to TClass LowerClassName: String; begin inherited Create(TheOwner); FClassAmbiguous:=TPointerToPointerTree.Create; FAllClasses:=TStringToPointerTree.Create(false); try for i:=0 to IDEComponentPalette.Comps.Count-1 do begin RegComp:=IDEComponentPalette.Comps[i]; aComp:=RegComp.ComponentClass; while aComp<>TComponent do begin LowerClassName:=lowercase(aComp.ClassName); OldClass:=TComponentClass(FAllClasses[LowerClassName]); if OldClass=nil then // new class FAllClasses[LowerClassName]:=aComp else if OldClass=aComp then // already added break else if FClassAmbiguous[aComp]=nil then // new ambiguous class FClassAmbiguous[aComp]:=OldClass; aComp:=TComponentClass(aComp.ClassParent); end; end; finally FAllClasses.Free; end; end; destructor TChangeClassDlg.Destroy; begin FreeAndNil(FClassAmbiguous); FreeAndNil(FClasses); inherited Destroy; end; procedure TChangeClassDlg.FillNewClassComboBox; var ANode: TAvgLvlTreeNode; List: TStringList; begin // create/clear tree if FClasses=nil then FClasses:=TAvgLvlTree.CreateObjectCompare(@CompareClasses) else FClasses.Clear; // add class of ThePersistent if ThePersistent<>nil then AddClass(TPersistentClass(ThePersistent.ClassType)); // add all registered component classes if (IDEComponentPalette<>nil) then IDEComponentPalette.IterateRegisteredClasses(@AddComponentClass); // add list of classnames List:=TStringList.Create; try ANode:=FClasses.FindLowest; while ANode<>nil do begin List.Add(ClassToCaption(TClass(ANode.Data))); ANode:=FClasses.FindSuccessor(ANode); end; // assign to combobox NewClassComboBox.Items.Assign(List); if (NewClassComboBox.Items.IndexOf(NewClassComboBox.Text)<0) and (NewClassComboBox.Items.Count>0) then NewClassComboBox.Text:=NewClassComboBox.Items[0]; UpdateNewInfo; finally List.Free; end; end; end.