Wednesday, January 21, 2004

Menu items, Toolbars, glyphs... etc..

One area that has always been a common source of questions with the Open Tools API is "how do I add a menu item and tool button to the IDE." Well... starting with C#Builder new methods have been added to INTAServices that make adding items such as these much simpler. If you look in ToolsAPI.pas at INTAServices:



INTAServices = interface(INTAServices70)
['{89160C3A-8EF4-4D2E-8FD5-D8492F61DB3E}']
{! AddImages takes all the images from the given image list and adds them to the
main application imagelist. It also creates an internal mapping array from the
original image indices to the new indices in the main imagelist. This
mapping is used by AddActionItem to remap the ImageIndex property of the
action object to the new ImageIndex. This should be the first method
called when adding actions and menu items to the main application window.
The return value is the first index in the main application image list of
the first image in the source list. Call this function with an nil
image list to clear the internal mapping array. }

function AddImages(AImages: TCustomImageList): Integer;
{! AddActionMenu takes an action item, a menu item, and a menu item component name
and inserts the action in to the main action list and the menu item into the
menu either preceding or following the named menu component. If the action
component has an ImageIndex > -1, then the mapping table created by the
previous call to AddImages above is used to determine the new value for
ImageIndex. NewAction can be nil, in which case only the menu item is
added. Likewise, NewMenu can be nil, in which case the Name param is
ignored and only the action is added to the main action list. If Name
cannot be found, an exception is raised. If the ImageIndex of NewAction
is out of range, then it is set to -1. }

procedure AddActionMenu(const Name: string; NewAction: TCustomAction;
NewItem: TMenuItem; InsertAfter: Boolean = True; InsertAsChild: Boolean = False);
{! NewToolBar creates a new toolbar with the given name and caption.
If the ReferenceToolBar parameter is specified, it is used as a reference point
for insertion based on the InsertBefore parameter. If InsertBefore is True, then
the new toolbar is inserted physically before the reference, else it is after.
if ReferenceToolBar is not specified, then the toolbar is inserted into a
position determined by the IDE. }

function NewToolbar(const Name, Caption: string;
const ReferenceToolBar: string = '';
InsertBefore: Boolean = False): TToolbar;
{! AddToolButton creates a new toolbutton on the named toolbar using the given
Action component. In order for the user to be able to add and remove this
toolbutton, an Action *must* be specified. otherwise the user may remove
the button, never to return until the toolbar config entries in the registry
are deleted and the toolbars are reset to the original configuration. If
IsDivider is True, then Action is ignored since a divider toolbutton is
created. If you wish the toolbutton to have a dropdown menu, then owner-
ship of that menu *must* be transferred to the owner of the toolbutton. }

function AddToolButton(const ToolBarName, ButtonName: string;
AAction: TCustomAction; const IsDivider: Boolean = False;
const ReferenceButton: string = ''; InsertBefore: Boolean = False): TControl;
{! UpdateMenuAccelerators causes the IDE to reset all the assigned accelerator
keys to the associated menu items. This is the accelerators as defined in
the current keymap }

procedure UpdateMenuAccelerators(Menu: TMenu);
{! ReadToolbar reads the configuration of the given toolbar from the
registry and recreates it if necessary. If SubKey is specified, it will
attempt to obtain a stream from that key first and if not found, then
will get the stream from the main toolbar key. Use Subkey to read
view-specific version of a toolbar. You can also optionally pass in a
TStream object if you wish to control the actual storage of the stream
itself. Set DefaultToolbar to true in order to read one of the global
toolbars from the "toolbar reset" storage. This is the default
configuration of the toolbar. }

procedure ReadToolbar(AOwner: TComponent; AParent: TWinControl; const AName: string;
var AToolBar: TWinControl; const ASubKey: string = ''; AStream: TStream = nil;
DefaultToolbar: Boolean = False);
{! WriteToolbar will take the given toolbar and write it out to the registry
under the subkey name if specified. Use SubKey to write view-specific
versions of a toolbar. }

procedure WriteToolbar(AToolbar: TWinControl; const AName: string = '';
const ASubkey: string = ''; AStream: TStream = nil);
{! CustomizeToolbars will open the toolbar customize dialog and set the given
toolbars into customize mode. The INTACustomizeToolbarNotifier interface
is used to handler certain events during the customizing process. If
ActionList is specified, then only the actions in that action list can
be used to customize the toolbar. If not specified, =nil, then the IDE's
global action list is used. The return value is the customize dialog
component. You can add a FreeNotification in order to know when the user
closes the dialog and customization is complete. }

function CustomizeToolbar(const AToolbars: array of TWinControl;
const ANotifier: INTACustomizeToolbarNotifier; AButtonOwner: TComponent = nil;
AActionList: TCustomActionList = nil; AButtonsOnly: Boolean = True): TComponent;
{! Call CloseCustomize when it is needed to forcibly terminate the customize
mode. For instance if the view being customized is destroyed or hidden,
this procedure may be called to terminate customization. }

procedure CloseCustomize;
{! Call ToolbarModified if you wish to notify all interested parties that
a toolbar was modified by means other than calling CustomizeToolbar. For
instance, when the toolbar is on a TControlBar and the toolbar band was
repositioned. This will cause all registered INTACustomizeToolbarNotifier.ToolbarModified
events to be called. }

procedure ToolbarModified(AToolbar: TWinControl);
{! RegisterToolbarNotifier registers the given INTACustomizeToolbarNotifier
in order to receive certain events related to toolbars and customizing
them. The most often used event will probably be the ToolbarModified
event since that is how other views can know when a particular named
toolbar is customized. }

function RegisterToolbarNotifier(const ANotifier: IOTANotifier): Integer;
procedure UnregisterToolbarNotifier(Index: Integer);
{! MenuBegin/EndUpdate allows the caller to control how often the main
menu will be updated. If many changes to the main menu are made at
one time performance can be improved by using these methods. }

procedure MenuBeginUpdate;
procedure MenuEndUpdate;
end;



...

This interface is designed to be used when initializing an IDE addin. What many of the internally developed IDE addins do is to create Data Module. On that data module, drop a TImageList, TActionList, and TPopupMenu. Add the images you want to use with the toolbar buttons and menu items to the image list. Add the action items, reference the image list and the index of the specific images (these will be fixed up later). Now add a menu item to the popup menu and assign the specific action item to the Action property of the menu item. This is all done via the designer (Delphi 7 can be used to do this). Now, when you want to initialize your add-in, you need to create an instance of the data module then call in this order:



var
NTAServices: INTAServices;
ToolBar: TToolBar;
...
begin
...
NTAServices := BorlandIDEServices as INTAServices;
...
NTAServices.AddImages(ImageList1);
// Inserts the new menu item just before the File|Exit... menu item
NTAServices.AddActionMenu('FileExitItem', ActionItem1, MenuItem1, False);
NTAServices.AddActionMenu(...);// add any other menu items.
...
ToolBar := NTAServices.NewToolBar('NewCustomToolbar', 'Allen''s Custom Toolbar');
NTAServices.AddToolButton('NewCustomToolbar', 'ActionButton1', ActionItem1);
...
end;



...

One item of note is that currently, if you attempt to query BorlandIDEServices from within IDERegister, it will fail. This is due to the fact that IDERegister is call very early in the initialization cycle. One workaround is to set a timer and try it again periodically. Another, more accurate technique is to create a hidden window (See AllocateHWnd and DeallocateHWnd in Classes.pas), and post a message with PostMessage to that window handle. This will ensure that you know the initialization is complete because the message loop has started (TApplication.Run).

The above technique is how all internally developed IDE personalities and extensions add their items to the main menu. The benefit of following these procedures is that your newly added action items will now participate in toolbar customization and persistence. Some of the more advanced uses of this interface and the attending INTAToolbarNotifier interface you can implement, is the ability to have local-view toolbars that can be persisted and designed just like the main toolbars (see the toolbar on the WinForm Designer view).

1 comment:

Please keep your comments related to the post on which you are commenting. No spam, personal attacks, or general nastiness. I will be watching and will delete comments I find irrelevant, offensive and unnecessary.