Difference between revisions of "Extending the IDE"

From Free Pascal wiki
Jump to navigationJump to search
(→‎Original contributors and changes: wiki already tracks contributors and history)
Line 476: Line 476:
 
end;
 
end;
 
</Delphi>
 
</Delphi>
 
==Original contributors and changes==
 
 
This page has been converted from the epikwiki [http://lazarus-ccr.sourceforge.net/index.php?wiki=ExtendingTheIde version].
 
 
This document was authored by Mattias Gaertner
 
Initial import and formatted for Lazarus-CCR - [[User:Tom | VlxAdmin]] 9/26/2003
 

Revision as of 17:22, 10 January 2010

Deutsch (de) English (en) español (es) français (fr) 日本語 (ja) русский (ru) slovenčina (sk) 中文(中国大陆)‎ (zh_CN)

Extending the IDE

Overview

The IDE supports several types of plugins:

Components
These are the items in the component palette. For a example TButton can be used to create Buttons.
Component Editors
Component editors are used when you double click on a component in the designer or to add some extra items to the popup menu of the designer, when you right click on a component.
Property Editors
These are used by the rows in the object inspector.
Experts
These are all other types.


There are two possibilities to add your own plugins to Lazarus:

  1. Write a package, install it and register your plugins in the 'Register' procedure of a unit.
  2. Extend the lazarus code, and send your svn diff to the lazarus mailing list.

Load/Save settings

All configuration files of the IDE are stored in one directory as xml files. Packages can add their own files there too. The primary config directory can be read with:

<Delphi> uses LazIDEIntf; ...

 Directory:=LazarusIDE.GetPrimaryConfigPath;

</Delphi>

Packages can create their own xml files with:

<Delphi> uses

 ..., LCLProc, BaseIDEIntf, LazConfigStorage;

const

 Version = 1;

var

 Config: TConfigStorage;
 SomeValue: String;
 SomeInteger: Integer;
 SomeBoolean: Boolean;
 FileVersion: LongInt;

begin

 SomeValue:='Default';
 SomeInteger:=3;
 SomeBoolean:=true;
 // save settings
 try
   Config:=GetIDEConfigStorage('mysettings.xml',false);
   try
     // store the version number so future extensions can handle old config files
     Config.SetDeleteValue('Path/To/The/Version',Version,0);
     // store string variable "SomeValue"
     // if SomeValue has the default value the entry is not stored,
     // so only the differences to the default are stored.
     // This way the xml is kept short and defaults may change in future.
     Config.SetDeleteValue('Path/To/Some/Value',SomeValue,'Default');
     Config.SetDeleteValue('Path/To/Some/Integer',SomeInteger,3);
     Config.SetDeleteValue('Path/To/Some/Boolean',SomeBoolean,true);
     // there are more overloads for SetDeleteValue, find out with Ctrl+Space
   finally
     Config.Free;
   end;
 except
   on E: Exception do begin
     DebugLn(['Saving mysettings.xml failed: ',E.Message]);
   end;
 end;
 // load settings
 try
   Config:=GetIDEConfigStorage('mysettings.xml',true);
   try
     // read the version of the config
     FileVersion:=Config.GetValue('Path/To/The/Version',0);
     // read a string variable "SomeValue". If the entry does not exist, use
     // the Default
     SomeValue:=Config.GetValue('Path/To/Some/Value','Default');
     SomeInteger:=Config.GetValue('Path/To/Some/Integer',3);
     SomeBoolean:=Config.GetValue('Path/To/Some/Boolean',true);
   finally
     Config.Free;
   end;
 except
   on E: Exception do begin
     DebugLn(['Loading mysettings.xml failed: ',E.Message]);
   end;
 end;

end; </Delphi>

Writing components

You can create new components via the package editor. For example: Create or open a package, click on add, click on New Component, fill in the items: Ancestor Type = TButton, New class name = TMyButton, palette page = Misc, Unit file name = mybutton.pas (this file will be created), unit name MyButton and click ok.

Giving a new component an icon for the component palette

For example give TMyButton an icon. Create an image file of the format .bmp, .xpm or .png with the same name as the component class. For example tmybutton.png and save it in the package source directory. The image can be created by any graphic program (e.g. gimp) and should be no bigger than 24x24 pixel. Then convert the image to a .lrs file with the lazres tool, which can be found in the lazarus/tools directory:

 ~/lazarus/tools/lazres mybutton.lrs tmybutton.png

This creates an pascal include file, which is used in the initialization section of mybutton.pas:

 initialization
   {$I mybutton.lrs}

Install the package.

Hiding a component in the component palette

Package IDEIntf, unit componentreg:

IDEComponentPalette.FindComponent('TButton').Visible:=false;

Writing component editors

A component editor handles things like double clicking on a component in a designer or adding menu items when right clicking on a component. Writing a component editor is easy. See the unit componenteditors.pas of the package IDEIntf for lots of examples. For instance to show an editor when double clicking see TCheckListBoxComponentEditor.

Writing property editors

Each type of property (integer, string, TFont, ...) in the Object Inspector needs a property editor. If your component has a property Password of type string, then you can define a property editor for your specific component class, with a property named Password of type string. The unit propedits.pp of the package IDEIntf contains many of the standard property editors used by the IDE itself. For example the TStringPropertyEditor is the default property editor for all strings, while TComponentNamePropertyEditor is more specific and handles only TComponent.Name.

Writing a designer mediator

The standard designer allows to visually edit LCL controls, while all others are shown as icons. To visually edit non LCL control, you can create a designer mediator. This can be used to design webpages, UML diagrams or other widgetsets like fpGUI. There is a complete example in examples/designnonlcl/.

  • Install the example package examples/designnonlcl/notlcldesigner.lpk and restart the IDE. This will register the designer mediator for TMyWidget components and add the new components TMyButton, TMyGroupBox to the component palette.
  • Open the the example project examples/designnonlcl/project/NonLCL1.lpi.
  • Open the unit1.pas and show the designer form (F12). You should see the components as red rectangles, which can be selected, moved and resized like LCL controls.

Select a component in the designer/object inspector

<Delphi> uses propedits; .. GlobalDesignHook.SelectOnlyThis(AComponent); </Delphi>

Register event handlers

There are several events in the IDE, for which plugins can add their own handlers.

Designer events

In propedits.pp there is a "GlobalDesignHook" object, which maintains several events for designing. Each event calls a list of handlers. The default handlers are added by the IDE. You can add your own handlers with the AddHandlerXXX and RemoveHandlerXXX methods. They will be called before the default handlers.

Examples:

 Adding your handler (this is normally done in the constructor of your object):
   GlobalDesignHook.AddHandlerComponentAdded(@YourOnComponentAdded);
Removing your handler: GlobalDesignHook.RemoveHandlerComponentAdded(@YourOnComponentAdded);
You can remove all handlers at once. For example, it is a good idea to add this line in the destructor of object: GlobalDesignHook.RemoveAllHandlersForObject(Self);

The handlers of GlobalDesignHook:

 // lookup root
 ChangeLookupRoot
   Called when the "LookupRoot" changed.
   The "LookupRoot" is the owner object of the currently selected components.
   Normally this is a TForm.
// methods CreateMethod GetMethodName GetMethods MethodExists RenameMethod ShowMethod Called MethodFromAncestor ChainCall
// components GetComponent GetComponentName GetComponentNames GetRootClassName ComponentRenamed Called when a component was renamed ComponentAdded Called when a new component was added to the LookupRoot ComponentDeleting Called before a component is freed. DeleteComponent Called by the IDE to delete a component. GetSelectedComponents Get the current selection of components.
// persistent objects GetObject GetObjectName GetObjectNames
// modifing Modified Revert RefreshPropertyValues // Selection SetSelection GetSelection

Getting notified when a designer form is modified

Every designed LCL form has a Designer of type TIDesigner. The IDE creates designers of type TComponentEditorDesigner defined in the IDEIntf unit componenteditors. For example:

<Delphi> procedure TYourAddOn.OnDesignerModified(Sender: TObject); var

 IDEDesigner: TComponentEditorDesigner;

begin

 IDEDesigner:=TComponentEditorDesigner(Sender);
 ...

end;

procedure TYourAddOn.ConnectDesignerForm(Form1: TCustomForm); var

 IDEDesigner: TComponentEditorDesigner;

begin

 IDEDesigner:=TComponentEditorDesigner(Form1.Designer);
 IDEDesigner.AddHandlerModified(@OnDesignerModified);

end; </Delphi>

Project events

These events are defined in unit LazIDEIntf.

 LazarusIDE.AddHandlerOnProjectClose
 LazarusIDE.AddHandlerOnProjectOpened
 LazarusIDE.AddHandlerOnSavedAll
 LazarusIDE.AddHandlerOnSavingAll

Current Project

The current main project can be obtained by LazarusIDE.ActiveProject. (unit LazIDEIntf)

All units of current project

To iterate through all pascal units of the current main project of the IDE you can use for example:

<Delphi> uses LCLProc, FileUtil, LazIDEIntf, ProjectIntf;

procedure ListProjectUnits; var

 LazProject: TLazProject;
 i: Integer;
 LazFile: TLazProjectFile;

begin

 LazProject:=LazarusIDE.ActiveProject;
 if LazProject<>nil then
   for i:=0 to LazProject.FileCount-1 do
   begin
     LazFile:=LazProject.Files[i];
     if LazFile.IsPartOfProject
     and FilenameIsPascalUnit(LazFile.Filename)
     then
       debugln(LazFile.Filename);
   end;

end; </Delphi>

The .lpr, .lpi and .lps file of a project

<Delphi> uses LCLProc, FileUtil, ProjectIntf, LazIDEIntf; var

 LazProject: TLazProject;

begin

 LazProject:=LazarusIDE.ActiveProject;
 // every project has a .lpi file:
 DebugLn(['Project lpi file: ',LazProject.ProjectInfoFile]);
 // if the project session information is stored in a separate .lps file:
 if LazProject.SessionStorage<>pssNone then
   DebugLn(['Project lps file: ',LazProject.ProjectSessionFile]);
 // If the project has a .lpr file it is the main source file:
 if (LazProject.MainFile<>nil)
 and (CompareFileExt(LazProject.MainFile.Filename,'lpr')=0) then
   DebugLn(['Project has lpr file: ',LazProject.MainFile.Filename]);

end; </Delphi>

The executable / target file name of a project

There is a macro $(TargetFile), which can be used in paths and external tools. You can query the macro in code:

<Delphi> uses MacroIntf;

function MyGetProjectTargetFile: string; begin

 Result:='$(TargetFile)';
 if not IDEMacros.SubstituteMacros(Result) then
   raise Exception.Create('unable to retrieve target file of project');

end; </Delphi>

Add your own file type

You can add items to the 'New ...' dialog:

  • See the unit ProjectIntf of the package IDEIntf.
  • Choose a base class TFileDescPascalUnit for normal units or TFileDescPascalUnitWithResource for a new form/datamodule.

Add a new file type

<Delphi> uses ProjectIntf; ...

 { TFileDescText }
 TFileDescMyText = class(TProjectFileDescriptor)
 public
   constructor Create; override;
   function GetLocalizedName: string; override;
   function GetLocalizedDescription: string; override;
 end;

...

procedure Register;

implementation

procedure Register; begin

 RegisterProjectFileDescriptor(TFileDescMyText.Create,FileDescGroupName);

end;

{ TFileDescMyText }

constructor TFileDescMyText.Create; begin

 inherited Create;
 Name:='MyText'; // do not translate this
 DefaultFilename:='text.txt';
 AddToProject:=false;

end;

function TFileDescText.GetLocalizedName: string; begin

 Result:='My Text'; // replace this with a resourcestring

end;

function TFileDescText.GetLocalizedDescription: string; begin

 Result:='An empty text file';

end; </Delphi>

Add a new form type

<Delphi> uses ProjectIntf;

...

 TFileDescPascalUnitWithMyForm = class(TFileDescPascalUnitWithResource)
 public
   constructor Create; override;
   function GetInterfaceUsesSection: string; override;
   function GetLocalizedName: string; override;
   function GetLocalizedDescription: string; override;
 end;

...

procedure Register;

implementation

procedure Register; begin

 RegisterProjectFileDescriptor(TFileDescPascalUnitWithMyForm.Create,FileDescGroupName);

end;

{ TFileDescPascalUnitWithMyForm }

constructor TFileDescPascalUnitWithMyForm.Create; begin

 inherited Create;
 Name:='MyForm'; // do not translate this
 ResourceClass:=TMyForm;
 UseCreateFormStatements:=true;

end;

function TFileDescPascalUnitWithMyForm.GetInterfaceUsesSection: string; begin

 Result:='Classes, SysUtils, MyWidgetSet';

end;

function TFileDescPascalUnitWithForm.GetLocalizedName: string; begin

 Result:='MyForm'; // replace this with a resourcestring

end;

function TFileDescPascalUnitWithForm.GetLocalizedDescription: string; begin

 Result:='Create a new MyForm from example package NotLCLDesigner';

end; </Delphi>

CodeTools in the IDE

Before using the codetools you should commit the current changes of the source editor to the codetool buffers:

<Delphi> uses LazIDEIntf; ...

 // save changes in source editor to codetools
 LazarusIDE.SaveSourceEditorChangesToCodeCache(-1); // -1: commit all source editors

</Delphi>

Adding a resource directive to a file

This adds a {$R example.res} to a pascal unit:

<Delphi> procedure AddResourceDirectiveToPascalSource(const Filename: string); var

 ExpandedFilename: String;
 CodeBuf: TCodeBuffer;

begin

 // make sure the filename is trimmed and contains a full path
 ExpandedFilename:=CleanAndExpandFilename(Filename);
 
 // save changes in source editor to codetools
 LazarusIDE.SaveSourceEditorChangesToCodeCache(-1);
 // load the file
 CodeBuf:=CodeToolBoss.LoadFile(ExpandedFilename,true,false);
 // add the resource directive
 if not CodeToolBoss.AddResourceDirective(CodeBuf,'example.res') then
   LazarusIDE.DoJumpToCodeToolBossError;

end; </Delphi>

The codetools provides also functions like FindResourceDirective and RemoveDirective.

Adding help for sources

First create a THelpDatabase:

 HelpDB:=TFPDocHTMLHelpDatabase(
    HelpDatabases.CreateHelpDatabase('ANameOfYourChoiceForTheDatabase',
                                            TFPDocHTMLHelpDatabase,true));
 HelpDB.DefaultBaseURL:='http://your.help.org/';
 FPDocNode:=THelpNode.CreateURL(HelpDB,
                  'Package1 - A new package',
                  'file://index.html');
 HelpDB.TOCNode:=THelpNode.Create(HelpDB,FPDocNode);// once as TOC
 DirectoryItem:=THelpDBISourceDirectory.Create(FPDocNode,'$(PkgDir)/lcl',
                                 '*.pp;*.pas',false);// and once as normal page
 HelpDB.RegisterItem(DirectoryItem);

Adding lines to the messages window

<Delphi> unit IDEMsgIntf; ... var Dir: String; begin

 Dir:=GetCurrentDir;
 IDEMessagesWindow.BeginBlock;
 IDEMessagesWindow.AddMsg('unit1.pas(30,4) Error: Identifier not found "a"',Dir,0);
 IDEMessagesWindow.EndBlock;

end; </Delphi>