Download paper, slides and code (2753kB)
Plug-in, Switch On, Fall Over:
Adventures with the Open Tools API
by Paul Spain,
The Excellent Programming Company, Melbourne, Australia.
Contact: paul@xpro.com.au Internet: http://www.xpro.com.au/
Introduction
The Open Tools API, a.k.a. Open Tools, or simply
OTA, is the third-party plug-in architecture provided by Borland
for its Rapid Application Development (RAD) tools. There is a
common API for the Delphi, Kylix and C++Builder tools, and a distinct,
yet similarly named, API for the JBuilder product. Open Tools
for JBuilder is not discussed here.
Using the OTA, you can write modules that extend
and customise the Integrated Development Environment (IDE) of
Borland’s RAD Tools. These modules take the form of dynamic link
libraries (DLLs) or design-time packages for the Win32 products,
and shared objects under Linux.
As DLLs are a standard binary format, you can
write Win32 plug-ins in any language that supports function pointers,
and associated tools that, obviously, can produce DLLs. However,
there are significant advantages in producing your plug-in as
a proprietary Borland design-time package - more on this later…
Unless noted otherwise, all subsequent references
to the OTA indicate the version shipped with Delphi 5.
I use the term “plug-in” here, whereas Borland
has used “expert” and more recently “wizard” to describe these
modules. To my mind, “wizard” implies a particular cookbook type
of plug-in, which automates a manual process; but the OTA has
evolved into something much broader. Also, the term “plug-in”
has much wider currency in the developer community.
Code examples
There is an associated zip archive containing
all the examples referenced in this paper. After unzipping the
archive, refer to the file BorCon2001\OpenTools\README.TXT
for instructions on loading and running the examples.
Most of the examples are licensed under the Lesser
GPL Open Source licence, available for your perusal at http://www.gnu.org/copyleft/lesser.html.
…Approximating in plain English: You are free
to use, modify and distribute this software in open source and
proprietary applications, provided you make this source code, including
any modifications you make, freely available under the same Lesser
GPL licence.
History
Delphi 1 shipped with a simple Expert API, manifested
as four files within the Visual Component Library (VCL) source
directory. It facilitated the creation of custom forms and project
types, and other Experts that appeared as new items on the IDE
Help submenu. There was also an interface for a Version Control
System, which simply created a new submenu on the IDE main menu.
Experts gained access to IDE facilities through a global ToolServices
object.
Delphi 2 saw the birth of the OTA, in its own
ToolsAPI source directory. Four new files augmented Delphi 1’s
Expert API, with support for form designers, property editors,
new menu items and virtual file systems. It also provided access
to the contents of the file buffers within the IDE, and provided
notifications for IDE events, such as files and projects opening
and closing.
Delphi 3 introduced packages, some new methods,
and some reorganisation.
Delphi 4 saw a move from an object-based API,
to interfaces, the new language feature. The interface names and
methods closely followed the classes of the earlier OTA. There
was a jargon switch from Experts to Wizards, and the monolithic
ToolServices object was split into a collection of smaller Services
interfaces. These are accessed by querying the global interface
variable, BorlandIDEServices. New features included debugger integration
with access to the CallStack, breakpoints, and processes and threads
on local and remote hosts. The MessageView was surfaced, and new
Notifier interfaces to deal with IDE events, and closure of transient
IDE artefacts. The new interfaces were wholly contained in the
new file ToolsAPI.pas. Apart from DsgnIntf.pas, which contained
the property editor and form designer code, all the files for
the class-based OTA were deprecated.
Delphi 5 saw new interfaces introduced for keyboard
bindings, search and replace, editor form access, selected text
blocks, IDE automation, extended breakpoint support and ToDo lists.
Kylix introduced CLX (cross-platform component
library) support.
I have had little time to explore the Delphi
6 OTA, but the first thing I noticed was the removal of the deprecated
class-based OTA.
Future
Coding with the OTA will give you a different
perspective on your Borland RAD tool. These polished performers
can look pretty flaky from a plug-in developers perspective. Open
Tools is an under-resourced area of Borland R&D. It appears
to be a very part-time project of one (albeit very talented) engineer,
Allen Bauer, kept otherwise busy as the Delphi/C++Builder IDE
R&D Manager. Developer relations toes the very reasonable
line that they will devote resources in accordance with customer
requests. This leaves the OTA in a Catch-22 situation: the unimplemented
features, long-standing bugs and dearth of current documentation
make it difficult to generate the critical mass of third-party
developers necessary to properly resource OTA R&D. Furthermore,
the OTA is essentially unused within the RAD tools. Compare this
with the robust JBuilder Open Tools API, which is the backbone
of that very cool product.
However, information available via the Borland
Community web site (http://community.borland.com),
surrounding the release of Kylix and Delphi 6 indicates that there
is a progressive conversion to interfaces within these products.
We can hope that these interfaces will closely align with the
OTA, or eventually be surfaced as a replacement. I have also heard
at BorCon Asia Pacific 2001 that Open Tools will receive more
attention in the Delphi 7 release, so we shall have to wait and
see.
DLLs or Packages?
In common with most plug-in architectures, OTA
plug-ins are dynamically linked binary modules. As previously
mentioned, these are either common DLLs or Borland design-time
packages, as introduced in Delphi 3.
The advantages of DLLs:
Use non-Borland languages and tools to
produce a plug-in DLL.
Legacy support for Delphi 2 through to
Delphi 5 from a single code base using the deprecated class-based
OTA.
Port plug-ins written in C++Builder to
Delphi.
Separate language namespace. Packages
share namespace with the VCL and all other installed packages.
Religiously using a unique identifier prefix and/or extracting
common code to a run-time package can help to solve this problem.
The advantages of design-time packages:
They can be loaded and unloaded repeatedly from
the IDE, which makes the edit-compile-run cycle considerably shorter.
Conversely, DLL plug-ins are fixed in memory for the lifetime
of the RAD host application.
Access to the same instances of the VCL and run-time
libraries as your RAD host – you can directly manipulate global
objects and data, such as the Application object. Such direct
manipulation is not officially encouraged… of course!
Access to NTA (Native Tools API) interfaces for
directly manipulating IDE VCL objects
…the nice way!
Access to data members exposed by other packages,
whereas common DLLs only export functions/procedures.
Which tool for development?
Delphi, C++Builder and Kylix support the OTA,
and all products use packages. However, as I understand it, packages
created in Delphi will run without modification in C++Builder,
whereas packages created in C++Builder won’t work in Delphi. Kylix
is another kettle of fish. The process involves creating your
plug-in in Delphi, and recompiling in Kylix, but you should read
Ray Lischner’s article on the Borland Community web site. See
the Resources section at the end of this paper.
Bottom line… if you want to easily write plug-ins
that will work in Delphi, C++Builder and Kylix, develop them in
Delphi.
All the examples in this paper are design-time
packages for Delphi 5 and 6. Usually, wherever I refer to Delphi
in the text, you can substitute your RAD tool of choice.
Structural outline
The following diagram represents the relationship
between design-time package plug-ins and the RAD tool IDE. Design-time
packages share the libraries linked to the IDE, but can only access
the non-visual artefacts of the IDE via the Open Tools API.
Plugging-in without the OTA
As mentioned earlier, a feature of design-time
packages is sharing your RAD host’s VCL and run-time libraries.
This feature allows us to plunder all the global objects and variables
in the libraries, such as Application, Screen, and Database, without
writing a jot of OTA code. This is something of a double-edged
sword, as there is no guarantee of the continuity of the IDE implementation
between versions, or even point releases within a version.
The entry-point for all packages is a global
procedure, Register, (case-sensitive). You could also use an initialisation section,
but the IDE is guaranteed to be ready for you by the time it calls
your Register
procedure. Any cleanup required for the package that isn’t handled
in destructors should occur in a finalization section.
Example: NoOTA.dpk
|
…
interface
//
Standard Delphi package entry point
procedure Register;
implementation
uses
Graphics, // TColor
SysUtils, // Win32BuildNumber
Forms; // Application
…
procedure Register;
begin
// Preserve original values
…
//
// Let's give Delphi
a facial and upgrade Windows!
//
// Manipulate global VCL global Application
object shared with Delphi
Application.MainForm.Color := clLime;
// Manipulate global VCL global Screen object
shared with Delphi
Screen.MenuFont.Name := 'Showcard Gothic';
Screen.MenuFont.Height
:= 24;
// Manipulate global RTL global variable
shared with Delphi
SysUtils.Win32BuildNumber := 3000;
end;
initialization
finalization
//
This code will execute when the package is unloaded
// Restore old values and free resources
…
end.
|
Playing safe …and smart
An interface is a contract between the implementer
and the user. The contents of an interface should be immutable
once it has been published. But… the OTA is no exception to the
list of exceptions! Regardless, using the OTA interfaces is your
best bet for minimising the amount of work required to port your
plug-in across versions and tools.
Having said that, it’s time to explore the landscape
that is the Open Tools API!
OTA Architecture
The OTA is a collection of source files, stored
in the ToolsAPI subdirectory of the Source
directory of Delphi or C++Builder. For Delphi 4&5, the only
relevant files are ToolsAPI.pas
and DsgnIntf.pas.
All the other files are the previous class-based OTA, now deprecated,
and finally expunged in Delphi 6.
ToolsAPI.pas
is your first port of call. It is a bulging swag of interfaces,
a helper base class, and a few global variables and functions.
Of primary concern are the interfaces. Some are implemented by
your RAD tool and accessed via the Services interfaces. Others,
including Wizards and Notifiers, are implemented by you, the plug-in
author.
Let’s start with a simple example OTA plug-in…
Example: HelloWorld.dpk …like
there’s a choice here!
This plug-in is as simple as it gets,
but demonstrates the essential elements for integrating with the
Borland IDE. It installs a new menu item on Delphi’s Help submenu.
Choosing that menu item will add a ‘Hello World!’ message to Delphi’s
MessageView window.
I will use the term “wizard” to describe
the plug-in class herein, as it ties closely to the OTA naming
scheme. These code fragments are taken from Simple.pas.
|
procedure Register;
implementation
|
All the Open Tools interfaces are declared in
one file, $(DELPHI)\Source\Toolsapi\ToolsAPI.pas.
The path to this file is not included in the factory-default search
or library paths. You’ll need to add it to the Search Path in
Project Options for each plug-in you write, or you can add it
to the Library Path in your Environment Options, and forget about
it thereafter. If you plan to distribute your source to other
developers, add it to the Search Path in Project Options and distribute
your packages’s CFG and DOF files.
Note that our wizard, TSimpleWizard, is declared within the
implementation section of the unit. It is never used outside the
scope of this unit, so it can be declared here to maximise encapsulation.
It also reduces the package size, as there are fewer exported
symbols. This is not a necessary requirement for a wizard,
just good style, IMHO!
TSimpleWizard
inherits from TNotifierObject,
a helper class defined in ToolsAPI.pas,
which inherits an
IUnknown implementation and provides a stubbed (empty
methods) implementation of
IOTANotifier. This is a very helpful helper class,
as most wizards only need to meaningfully implement a subset of IOTANotifier.
Our wizard explicitly implements IOTAWizard and IOTAMenuWizard, as we want the wizard
to appear as a menu item (on the Help submenu)
|
Type
TSimpleWizard = class(TNotifierObject,
IOTAWizard, IOTAMenuWizard)
Private
|
As a rule of thumb preventive measure, I implement
interface methods as private or protected methods within the implementing
class. Visibility doesn’t matter for interface implementation
methods, so we are only blocking direct public usage of
the object methods. Allowing access to the implementing
object within the same scope as the interface will often lead
to invalid reference bugs.
|
// IOTAWizard implementation
function GetIDString: string;
function
GetName: string;
function
GetState: TWizardState;
procedure
Execute;
// IOTAMenuWizard implementation
function GetMenuText: string;
end;
|
As previously noted, Register is the preferred entry point
for the IDE. Here we are calling the global procedure, ToolsAPI.RegisterPackageWizard,
passing an IOTAWizard
interface to the IDE. The compiler retrieves the interface from
the object (TSimpleWizard
instance). As there is no local reference to the interface, the
IDE controls the lifetime of the wizard.
Where possible, avoid holding a reference to
an interface also held by the IDE, as you could interfere with
the operation of the IDE by stopping destruction of the underlying
objects. There are solutions for these situations, but they are
sometimes difficult to detect, and why introduce unnecessary complexity?
|
procedure Register;
begin
ToolsAPI.RegisterPackageWizard(TSimpleWizard.Create);
end;
|
GetIDString
must return a unique identifier. By convention,
the identifier has two parts: organization & a wizard name.
|
function TSimpleWizard.GetIDString:
string;
begin
Result := 'BorCon
2001.Simple Wizard';
end;
|
GetName
returns a user-friendly name for the wizard. Delphi displays the
name in error messages, and for repository wizards, in the Object
Repository.
|
function TSimpleWizard.GetName: string;
begin
Result := 'Simple
Wizard';
end;
|
Execute
only fires for menu, form, and project wizards.
It is invoked by selection of the menu item or Object Repository
entry respectively. Here, we write a message in Delphi’s MessageView.
First, we reference BorlandIDEServices,
which is the universal entry point for the OTA. This is an interface
of type IBorlandIDEServices,
an empty descendant of
IUnknown, so we query it for the interface we are
interested in, IOTAMessageServices,
and invoke the interface method, AddTitleMessage. Supports is a convenience function, which combines the
querying with assignment of the resultant interface (the third
(out) parameter, MessageView), and returns a boolean.
|
procedure TSimpleWizard.Execute;
var
MessageView: IOTAMessageServices;
begin
if SysUtils.Supports(BorlandIDEServices,
IOTAMessageServices, MessageView) then
MessageView.AddTitleMessage('Hello
World!');
end;
|
GetState
only fires for menu wizards. It describes the state of the menu
item, enabled/disabled and/or checked.
|
function TSimpleWizard.GetState: TWizardState;
begin
Result := [wsEnabled];
end;
|
GetMenuText
is the sole method of the IOTAMenuWizard
interface, and returns the label for the associated menu item.
|
function
TSimpleWizard.GetMenuText:
string;
begin
Result := 'Greetings
from BorCon 2001!';
end;
|
Add any cleanup code for wizard unloading to
the finalization
section, or, better yet, handle it in the wizard destructor. There
is no Unregister equivalent
for the Register
procedure. Remember that the IDE frees the wizard object we created
in Register!
|
initialization
finalization
end.
|
Notifiers
Many IDE artefacts, CodeEditor modules and breakpoints
for example, are transient in nature. It is therefore hazardous
to hold references to these things, as the reference may be invalid
when you attempt to use it. The OTA has provided a solution to
this problem through a hierarchy of notifiers, an implementation
of the Observer pattern. These are interfaces, which you, the
plug-in author, must implement. You register a notifier with the
IDE, which will subsequently call the methods of your interface.
Each notifier is associated with another
OTA interface. For example IOTAModuleNotifier
is associated with IOTAModule40.
When an IOTAModule
is about to be destroyed, all its observers (who have registered
notifiers) will be notified.
This notification mechanism has been
leveraged throughout the notifier hierarchy to inform plug-ins
of other IDE events, such as editor windows receiving focus, or
files being renamed. The base interface of the notifier hierarchy
is IOTANotifier.
|
IOTANotifier = interface(IUnknown)
['{F17A7BCF-E07D-11D1-AB0B-00C04FB16FB3}']
procedure
AfterSave;
procedure
BeforeSave;
procedure
Destroyed;
procedure
Modified;
end;
|
The method called by the IDE for most implementations
of this interface is Destroyed.
This occurs when the notifying or observed IDE artefact is being
destroyed, oddly enough! Any interfaces you are holding for that
artefact must be released in this method. This practice is fundamental
to the health of your plug-in and its host application.
The other methods of IOTANotifier are only called for some
notifiers. This lack of optimisation is apparent in several OTA
interfaces. I suspect it is largely a legacy issue - to remain
backwardly compatible with older plug-ins as the OTA evolves and
grows.
Note that from Delphi 5 onwards, there is a helper
class, TNotifierObject,
which inherits from TInterfacedObject,
and provides a stubbed implementation of IOTANotifier. This is a good base class
for plug-ins which need to implement IOTANotifier. Just remember that it may
be necessary to reimplement IOTANotifier.Destroyed if you are holding IDE interfaces.
You will need to declare IOTANotifier in your plug-in’s interface
implementation list in that case.
Wizards
Wizard plug-ins date back to Delphi 1, when they
were known as Experts. There are three flavours of wizard: menu
(a.k.a standard), form and project.
As of Delphi 4, to write a wizard, you must write
a class which implements several wizard interfaces, and register
your wizard in your package’s Register
implementation by calling ToolsAPI.RegisterPackageWizard.
Stubbed implementations will suffice for some
wizard interface methods. Refer to the comments for the relevant
interfaces in ToolsAPI.pas.
Menu Wizards
Menu Wizards appear as new menu items. Unfortunately,
they are always added to the Help menu, so they are not generally
useful for much besides demo-ware and testing. In your wizard
class, you implement IOTANotifier,
IOTAWizard and
IOTAMenuWizard.
Example: EPCErrorTextWizard.dpk - error codes as text
This wizard creates a Help menu item “ErrorCode As Text…”. Choosing this item displays
a modal dialogue that will convert numeric (decimal or hex) Win32
and BDE error codes to their textual equivalents - much easier
to comprehend for mortals!
To install a new menu item elsewhere, manipulate
the host application’s main menu, surfaced on the INTAServices interface. The next example
uses this technique.
Form and Project Wizards
Form Wizards and Project Wizards both produce
new items in the Object Repository. Both wizard types must implement
IOTANotifier,
IOTAWizard and
IOTARepositoryWizard.
Furthermore, Form Wizards must implement IOTAFormWizard, and Project Wizards IOTAProjectWizard.
By default, Form Wizards will appear as a new
item on the Forms page of the repository (File|New…|Forms).
Selecting this item will create a new form in the IDE, in accordance
with your implementation.
Likewise, the default location for a Project
Wizard is on File|New…|Projects. This will
create a new IDE project in accordance with your wizard.
Example: EPCSourceTemplatesWizard.dpk - generate new forms and
units from your template(s).
This is a Form Wizard that also creates a new
menu item on the File menu. You can use, modify add or delete
new templates for units and forms, editing their source in the
IDE. The provided templates generate licence and copyright information,
class and interface skeletons, and specific mark-up for CVS version
control.
This wizard uses IOTAModuleServices.CreateModule to create
the new units and forms. CreateModule expects an IOTAModuleCreator argument.
|
procedure TSTBaseWizard.WizardFormCreateClick(Sender:
TObject);
begin
(BorlandIDEServices
as IOTAModuleServices).CreateModule(self);
end;
|
TSTBaseWizard,
passed in as “self”
above, implements IOTAModuleCreator
and its ancestor interface, IOTACreator. The content of the files is returned
via the following method implementations for IOTAModuleCreator. Note that the file
content returned is expected as an IOTAFile implementation. Here, I have
delegated that responsibility to other classes, TSTFormFile and TSTUnitFile for
form and unit content respectively.
|
function TSTBaseWizard.NewFormFile(
const FormIdent,
AncestorIdent: string): IOTAFile;
begin
Result := TSTFormFile.Create(WizardForm);
end;
function TSTBaseWizard.NewImplSource(
const ModuleIdent,
FormIdent, AncestorIdent: string): IOTAFile;
begin
Result := TSTUnitFile.Create(WizardForm);
end;
|
The template files are edited within the IDE
editor by calling on IOTAActionServices.OpenFile.
|
procedure TSTBaseWizard.WizardEditTemplateClick(Sender:
TObject);
begin
(BorlandIDEServices
as IOTAActionServices).OpenFile(WizardForm.EditTemplate);
end;
|
Services
As previously seen, the entry point for OTA services
is the global interface variable,
BorlandIDEServices.
This variable is of type IBorlandIDEServices,
an empty descendant of IUnknown.
To do anything useful, you must query BorlandIDEServices for one of the following
interfaces:
Interface
|
Facilities
|
|
INTAServices
|
|
|
IOTAActionServices
|
Open, close, save and reload files.
Open projects.
|
|
IOTADebuggerServices
|
Enumerate, create and attach to processes
on local and/or remote hosts.
Create and access address and source breakpoints.
Create module load breakpoints.
Add and remove notifiers.
|
|
|
Get and set Editor Options.
Retrieve iterator to access edit buffers.
Get active editor buffer and view.
|
|
|
Add custom IDE behaviour.
Bind new or existing IDE behaviour to keyboard shortcuts.
|
|
IOTAKeyboardServices
|
Add and remove keyboard bindings.
Record, play and delete editor macros.
Push and pop keyboards (collections of keybindings).
Iterate, lookup and execute keybindings.
|
|
IOTAMessageServices
|
Add messages to the Message View of type:
Custom: customised behaviour and/or drawing.
Tool: standardised display and context behaviour.
Title: bold text with no associated context behaviour.
|
|
IOTAModuleServices
|
Close and save all modules.
Create and manipulate modules.
Add, retrieve and delete virtual file systems.
|
|
IOTAPackageServices
|
Retrieve names of loaded packages and names
of components contained therein.
|
|
IOTAServices
|
Get Environment options.
Get base Registry key and product ID.
Get Application handle (HWND).
Add and remove notifier for general IDE events.
|
|
IOTAToDoServices
|
Retrieve ToDo items by index.
Add and remove collections (Managers) of ToDo items.
Add and remove notifiers.
|
|
IOTAWizardServices
|
Add and remove IOTAWizards and descendants.
|
Debugging plug-ins
Debugging is the dark side of plug-in development
- it puts the “F” into “Fall
Over”. However, with some care and thought, you can save yourself
a lot of grief.
As previously mentioned, your package shares
its instance of the VCL and RTL with your RAD tool’s IDE, and
also with any other installed design-time packages. This is the
key to much of the power of the OTA, but one slip-up by any of
these players is the difference between soufflé and omelette,
between a Jumbo Jet and a flying brick, between… you get the picture!
So, obviously, you want to isolate yourself from
the effects of other players. Initially, uninstall as many third-party
components and third-party design-time packages as is practicable.
The host application for your package is also
your debugging tool, ie you’ll be debugging Delphi with Delphi.
Running an untested package into your development/debugging environment
is something you do only once …hopefully. After you’ve mopped
up the blood and brains from the Delphi explosion, you’ll realise
there has to be another way. The solution involves running two
concurrent copies of your RAD tool.
If you have a well-behaved package and Delphi
installation, you can proceed as for any package or DLL:
Ensure that your package is not
checked in the design packages list in the Project Options dialog.
(Project|Options..|Packages|Design packages).
Save your project to persist this setting.
As mentioned, you debug your package or DLL in
the context of its host application. To specify the host application,
go to the Parameters dialog off the Run menu (Run|Parameters…|Local|Host
Application). For Delphi, specify delphi32.exe,
found in the Bin
subdirectory of your Delphi installation.
I have usually found it necessary to restart
Delphi to properly persist the above settings.
Compile your package with debug information on
and optimisation off. Add debug DCUs, stack frames, range checking,
overflow checking etc. to taste. All these options are on the
Compile tab of the Project Options dialog (Select
Options.. off the Project menu). My preferences are shown
in the above screenshot.
Having two copies of Delphi running can get a
bit confusing, so I check “Minimize on run” in the Environment
Options (Tools|Environment Options..|Preferences|Compiling
and running|Minimize on run). Seeing the debugger copy
shrink down and pop up keeps me sane for a bit longer.
The Register
global procedure in your package is the official entry point for
your plug-in, so I tend to put a breakpoint on the first line
of Register.
Now run the host application (Run|Parameters…|Load, F9 key, toolbar button, whatever…).
When the second copy of the IDE appears, add your package to the
list of design packages, if not already present, and enable it
(Project|Options..|Packages|Design packages)
or (Component|Install
Packages..).
All going well, the first (debugger) copy of
the IDE should now pop up, with execution halted on your breakpoint
in Register. You can now step through code, set breakpoints, set
watches, or whatever your heart desires. Note that enabling “debug
DCUs” in your Compiler settings links a debug version of the VCL.
Much of the code you will be executing is within the Delphi binary
and associated packages, which you won’t be able to step through.
If you have an ill-behaved package, using
the integrated debugger can be difficult or impossible.
In that situation, it is wise to run two copies
of the IDE, one for coding, the other for testing. You will need
to resort to old-fashioned debugging techniques such as log messages
to determine execution flow and parameter values.
If you have a really badly behaved package,
which is enabled on the design-time package list (Component|Install
Packages…), it is possible to crash the IDE during startup.
The solution is to remove or disable the offending
package in the design-time packages list via some Registry hacking,
using the Windows utility, regedit.exe.
Using Delphi 6 as our example, the design-time packages loaded
at start-up are listed under the key HKEY_CURRENT_USER\Software\Borland\Delphi\6.0\Known
Packages, as seen below:
Deleting an entry from this key will remove
the associated package from the list. To only disable the
package, add a similar entry for the package to HKEY_CURRENT_USER\Software\Borland\Delphi\6.0\Disabled Packages,
visible towards the middle of the above figure.
Example: EPCCodeJumpWizard.dpk – IDE editor enhancement module.
We will now look at a more extensive example
that adds new behaviour to the IDE editor. This plug-in makes
use of the keyboard facilities introduced in Delphi 5. The keyboard
architecture can be conceptualised as a collection of key-mapping
modules, and a list of enhancement modules, each module
containing a collection of behaviours and associated keystroke
bindings.
This is presented in the IDE in Tools|Editor Properties…|Key Mappings
Key-mapping modules must provide a handler for
all keystrokes, a full keyboard implementation. The current key-mapping
module can be changed via the dialog mentioned above.
Enhancement modules, by definition, provide a
partial keyboard implementation. Multiple enhancement modules
can be installed and ordered, overlying the current key-mapping
module.
Keystrokes entered into the IDE editor filter
down the list of enhancement modules, till a matching keystroke
binding is encountered. The key-mapping module, at the bottom,
supplies the default behaviour.
This example implements an enhancement module.
It provides block-end matching for all Object Pascal block statement
constructs up to and including Delphi 6 (excluding repeat/until) -
toggling the cursor position to either end of the block, or highlighting
the enclosing block. It also provides cyclic jumps between all
uses clauses and
the current position, and cyclic jumps to the interface, implementation,
initialisation,
finalization
sections and the current position.
Keybinding
|
Description
|
|
Alt+Shift+Home
|
|
|
Alt+Shift+End
|
Acts as a toggle. The first press will
move the cursor from current position to the end of the
minimally enclosing block statement. The second press restores
the previous cursor position.
|
|
Alt+Shift+B
|
Acts as a toggle. The first press will
select all the minimally enclosing block statement for the
current cursor position. The second press unselects the
block and restores the previous cursor position.
|
|
Alt+Shift+Left
|
Moves the cursor back to the previous position
after one of the following jumps:
|
|
Alt+Shift+I
|
Repetitive presses cycles through: INTERFACE,
IMPLEMENTATION, INITIALIZATION, FINALIZATION sections, current
position
|
|
Alt+Shift+U
|
Repetitive presses cycles through: INTERFACE
USES clause, IMPLEMENTATION USES clause, current position
|
The OpenTools-related code all resides in the
XPCodeJumpWizard.pas
unit.
This plug-in works by scanning the currently
focussed IDE editor buffer, when it either changes or is modified.
One token event handler for the scanner, TCodeJumpWizard.BlocksParser,
parses for block syntax and creates a mapping of all language
block constructs and their endpoints in the current buffer. Another, TCodeJumpWizard.JumpsParser,
parses for uses clause and section heading syntax, also creating
a mapping to buffer positions. The keybinding handlers perform
look-ups against these mappings.
We need to determine when the current IDE editor
buffer changes, or is closed. For this we implement IOTAModuleNotifier, and register it with
the current module. There is also an IOTAEditorNotifier interface, but these
are never called by the IDE. We determine the current module,
and all other context-related properties, through the IOTAKeyContext variable passed
in to all keybinding handlers. The signature for those handlers
looks like:
|
type TKeyBindingProc = procedure
(const Context: IOTAKeyContext; KeyCode:
TShortcut; var
BindingResult: TKeyBindingResult) of object;
|
An enhancement or key-mapping module must implement
the IOTAKeyboardBinding
interface.
|
TCodeJumpWizard
= class(TNotifierObject, IOTAKeyboardBinding,
IOTANotifier,
IOTAModuleNotifier)
…
|
Return
btPartial or
btComplete to indicate an enhancement module or
key-mapping module respectively.
|
function GetBindingType: TBindingType;
|
Return
user-friendly and unique name (typically Organisation.BindingName)
respectively.
|
function GetDisplayName: string;
function
GetName: string;
|
Bind locally defined handler functions to keystroke
combinations. Look over the examples in Demos\ToolsAPI\Editor Keybinding in your
product installation or CD. All keystrokes are not
the same! Also, avoid any keystrokes with only an Alt modifier,
eg Alt+X. These may cause access violations when your plug-in
is loaded or unloaded. Other combinations, eg Alt+Shift+X are
safe. Unused key combinations are a scarce commodity, so you may
end up overriding some other IDE behaviour, or in turn, get clobbered
by another enhancement module. Do a bit of research before you
choose.
|
procedure BindKeyboard(const
BindingServices: IOTAKeyBindingServices);
|
IOTAKeyBindingServices,
passed as an argument above, is the one Services
interface not accessible through querying the BorlandIDEServices global interface variable.
The plug-in also handles syntactically incomplete
units, adding context-jumpable messages to the IDE’s MessageView,
similar to compiler messages. See TCodeJumpWizard.CheckBlocks.
There is another class declared in XPCodeJumpWizard.pas.This
is TCJOwnerWizard which acts as a container for a TCodeJumpWizard
instance. The reasons are explained in the code and relate to
reference counting and unloading of the plug-in mid-session. Using
a container interface is often a good solution for reference count
problems.
Example: EPCBufferList.dpk – dockable IDE Buffers ListView.
This example is a work in progress. It implements
a dockable list-view of all the open buffers in the IDE. Stemming
from a dockable IDE example published on the Borland Community
web site (see Opening Doors – Article 3 in Resources section below),
the desired outcome was a better UI than the tabbed notebook of
the IDE editor. That interface is fine for a handful of open files,
but useless for managing lots of open files.
The list view is sortable in both directions
via file name and location. Select or multi-select entries to
focus or close buffers. It uses the INTAServices
to install the menu item, and IOTAServices to receive notifications of files opening
or closing and synchronise the display. Unfortunately, this doesn’t
provide individual file notifications when the encompassing project
or project group is closed, so an IOTAModuleNotifier
approach will be required for a complete solution.
I would advise you to read the article mentioned
above, as there is a lot of dummy code to get the undocumented
IDE package to work with the plug-in in the form designer. The
active code for this plug-in is contained in the file XPBufferListToolbarForm.pas.
Resources
Borland Community site (community.borland.com)
"Opening
doors (Getting inside the IDE)"
by Allen Bauer, Staff Engineer/Delphi&C++Builder R&D
Manager
Article 1: http://community.borland.com/article/0,1410,20360,00.html
Article 2: http://community.borland.com/article/0,1410,20419,00.html
Article 3: http://community.borland.com/article/0,1410,21114,00.html
Interview with
Allen Bauer
Talking about OTA enhancements for Delphi 5. This download
appears corrupted nowadays.
http://community.borland.com/article/0,1410,21046,00.html
Open Tools API
"Live Chat" transcript (Nov 18, 1999)
http://community.borland.com/article/1,1410,20099,00.html
"Using the
Open Tools API in Kylix"
by Ray Lischner, Tempest Software
(Author of "Hidden Paths of Delphi 3", and others)
http://community.borland.com/article/0,1410,27205,00.html
Open Tools API
by Ray Lischner, Tempest Software.
Covers Delphi 3 & 4, with good bug coverage:
http://www.tempest-sw.com/opentools
GExperts Open Tools API FAQ
Lots of howtos
and bug documentation:
http://www.gexperts.org/opentools
Newsgroups
The OpenTools API newsgroup. An excellent
resource for answers to specific questions. Ray Lischner (Hidden
Paths) and Erik Berry (GExperts) are regular posters here…
news://newsgroups.borland.com:119/borland.public.delphi.opentoolsapi
Formerly DejaNews, acquired by Google. A searchable
archive of the borland.public.delphi.opentoolsapi newsgroup
http://groups.google.com/groups?hl=en&lr=lang_en&safe=off&group=borland.public.delphi.opentoolsapi
Books
“Hidden Paths of Delphi 3” by Ray Lischner,
Informant Press 1997
ISBN 0-9657366-0-1
Software
GExperts
An open source collection of Delphi wizards:http://www.gexperts.org
Ray Lischner's Expert building components
Unsupported freeware with source
Delphi 3:
http://www.tempest-sw.com/freeware/Delphi/etk10.exe
Delphi 4:
http://www.tempest-sw.com/freeware/Delphi/etk40.exe
Other shareware and freeware
Delphi Super Pages (Australian mirror):
http://mirror.aarnet.edu.au/pub/delphi
Torry’s Delphi Pages:
http://www.torry.net