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.
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) [1] . 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
This package changes some of the global objects and variables in the VCL and RTL that are shared with the host application, Delphi. In the
Register
procedure in
main.pas
, we change the colour of Delphi’s main form, the menu bar font, and the Windows build number as reported in the Delphi About… box. These changes take effect as the package is loaded by Delphi. The changes are reverted by the
finalization
section, which executes when the package is unloaded.
// 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
.
interface
…
The entry point for any and all Borland packages,
Register
…
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.
uses
ToolsAPI;
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?
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
[2] is a convenience function, which combines the querying with assignment of the resultant interface (the third (out) parameter,
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
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.
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
Access the host application’s main menu, action list, image list and toolbars.
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.
IOTAEditorServices
Get and set Editor Options.
Retrieve iterator to access edit buffers.
Get active editor buffer and view.
IOTAKeyBindingServices
[3]
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
, 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
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
Acts as a toggle. The first press will move the cursor from current position to the start of the minimally enclosing block statement. The second press restores the previous cursor position.
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
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
Delphi Super Pages (Australian mirror): http://mirror.aarnet.edu.au/pub/delphi
Torry’s Delphi Pages: http://www.torry.net
[1] There is much urban myth about the reason for the case-sensitivity of
Register
in the otherwise case-insensitive Delphi, but Allen Bauer sets the record straight in his third “Opening Doors” article on the Borland Community website:
”The actual reason is that with Win32, all exports from a DLL are case-sensitive. When Delphi loads a design-time package it looks in a special compiler generated resource in the package that describes its contents. This resource lists the unit names within the package. The IDE then scans through this list creating a string containing the unit name and the
Register
procedure then does a
GetProcAddress
call on that string. If it locates that entry-point it is called. Sure, we could try all possible combinations, but that would be 256 combinations checked for each unit in that package!”
[2] Supports can also tolerate a nil first argument, whereas QueryInterface, the function it wraps, will cause an exception when applied to an unassigned interface.
[3] IOTAKeyBindingServices is not accessible directly through the BorlandIDEServices global variable. Instead, it is passed as an argument to the IOTAKeyboardBinding.BindKeyboard method. Keyboard bindings are manipulated via the IOTAKeyboardServices interface, which is accessible via BorlandIDEServices.