Eddie Plugin Developer Kit

rev 1.0
©1998 P.Císler

Introduction

The Eddie Plugin Developer Kit contains documentation, API definitions and sample code that allows you to write plugins for Eddie. The plugin API that Eddie provides is fairly rich and allows a great level of control and customization over the application.

The plugin API allows you to, among other things:

The plugin API has a versioning mechanism that allows backward and forward compatibility of plugins and Eddie.
The plugin API is fully defined in a single header file, Plugin.h. This header file is included in the Plugin Developer Kit you have.

Missing API?

If you are missing a call that would allow you to implement some plugin functionality or if you have any other feedback, contact me at pavel@be.com.

API Version

This document describes plugin API as defined in Eddie 1.2.1. This or later versions of Eddie are available at ftp://ftp.be.com/pub/contrib/editors/Eddie1.2.1_ppc.zip and ftp://ftp.be.com/pub/contrib/editors/Eddie1.2.1_x86.zip.

About the plugin samples

Currently the Plugin Developer Kit includes three sample plugins.
To build the plugins:
Go into the Eddie Worksheet
cd .../PluginDeveloperKit to get into the PluginDeveloperKit directory
type make
copy the resulting plugins into the Eddie plugins directory
re-launch Eddie

Commenter

This is an actual usable plugin, a C/C++ commenter. The code sample illustrates how to scan and modify document text, add a button, publish editor primitives, run a popup menu. The source for this plugin is in the file Commenter.cpp.

keyPressSample

This plugin is a torso of what could be Emacs-like Electric-C editing mode - a plugin that formats your source code as you type. It doesn't even come close to doing anything useful, however it illustrates how to intercept key down events, plus it shows how to add a preference panel to the Application Settings. The source for this plugin is in the file KeyPressHandling.cpp

DoNothingSample

This sample plugin is intended to be a source for cutting and pasting code into your plugins. It illustrates how to handle a mouse click, how to swap button icons, display a clipboard panel, handle a drag&drop of a bitmap by pasting the bits as a hex dump into the document. The source for this plugin is in the file CutNPasteSource.cpp.


Anatomy of a plugin

Let's look at how to build a plugin, starting from the end, which is:

Linking a plugin

Eddie plugins are shared libraries. To build an Eddie plugin you need to include the -G linker option in the makefile. The resulting type of the plugin needs to be "application/x-be-executable" on PPC and "application/x-vnd.Be-peexecutable" on Intel. For example, a typical makefile link sequence for Intel will look like:

LIBRARIES = \
	$(BELIBRARIES)/glue-noinit.a \
	$(BELIBRARIES)/init_term_dyn.o \
	$(BELIBRARIES)/start_dyn.o \
	$(BELIBRARIES)/libroot.so.LIB \
	$(BELIBRARIES)/libbe.so.LIB \
#

somePlugin: $(pluginObjectFiles)
	$(LD) -o $@ $(pluginObjectFiles) -G $(LIBRARIES)
	settype -t "application/x-vnd.Be-peexecutable" $@
	chmod +x $@
	mimeset -f $@
where pluginObjectFiles are .o files compiled from your plugin source.

Writing a plugin

Every plugin has to support two calls in order to get recognized by Eddie.

PluginInterface::PluginResult PluginMain(PluginInterface::PluginCallSelector selector, 
	PluginInterface *pluginInterface);

const char *PluginHelp(void);

These calls have to have the exact signature as shown here and as declared in Plugin.h. If the plugin doesn't have either of these two calls, it fails to load. Note that the plugin does not need a main function.

PluginMain

PluginMain is the main entry point of a plugin. It gets called periodically during the lifetime of a plugin. The selector parameter indicates the reason PluginMain was called. The pluginInterface parameter is the structure used to communicate between a plugin and Eddie. Here are some of the instances when PluginMain gets always called:

The selector parameter always contains the reason for which the plugin was called.

PluginMain returns a PluginInterface::PluginResult value, which may be one of:

kIgnored A plugin selector call was ignored by the plugin.
kAccepted A plugin selector call was handled fine by the plugin.
kExhausted A plugin selector call was handled fully by the plugin. The subject of the selector call (such as a message) does not need any further handling from Eddie or any other plugin.
kFailed There was an error during a plugin selector call. The plugin can no longer operate. If a plugin returns this result, it will no longer be called.

PluginHelp

PluginHelp returns a short description of the plugin. This description is for instance used when showing a tool tip for a plugin button. It should include short instructions on using the plugin, such as modifier keys used to access the different features, etc.

The PluginMain selectors

The PluginMain gets called repeatedly, the selector parameter describes the reason a plugin got called. Most of the calls to the plugin are implicit, the plugin receives them whether it is interrested in them or not. A few calls need to be explicitly requested by the plugin.


kGetPluginVersion First call a plugin receives, determines if a plugin can be used with Eddie and vice versa. A plugin is required to respond to this call in order to get further selector calls.
example
kInit Called when Eddie starts up. A plugin may initialize it's state, register itself as a handler for specific settings, keyboard primitives, etc.
example
kUnload Called when plugins are unloaded. Plugins should release any resources they acquired during their lifetime, such as dynamically allocated buffers.
kQuit Called when Eddie quits.
kWindowOpened Called when a new document window is opened - this is the time to install buttons if your plugin has any.
example
kWindowClosed Called when document window is being closed.
kWindowActivated Called when document window brought to front.
kWindowDeactivated Called when document window sent to the background.
kDocumentOpened Called when a new document opened. In multiple document windows called for every document. The DocumentUniqueID call is used to determine which document it is being called on.
kDocumentClosed Called when document closed.
kDocumentActivated Called when a document was activated. in single document windows this is pretty much like kWindowActivated, if a window has multiple documents this gets called when one of the documents is made the current focus. The DocumentUniqueID call is used to determine which document it is being called on.
kDocumentTextChanged Called when document text in window changed - when text was inserted and/or deleted. Calls ChangeStart, RemovedLength and InsertedLength provide the details about the document change. Plugin must explicitly request this selector call.
example1, example2
kDocumentStateChanged Called when document file renamed, saved, made read-only, writable, etc.
kArgvPassed Called when shell command handled by this plugin is invoked, currently used for handling plugin settings and UserStartup entries. Eddie saves settings as argv-style commands in the settings file. When reading a settings file, Eddie passes any settings that the plugin registered to handle in argv-like format, accessible with the CurrentArgv call.
example
kButtonClick Called when plugin button clicked.
example
kButtonPress Called when plugin button pressed and held. This call can be used for opeining a popup menu, etc.
kPrimitiveInvoked Called after a keyboard shortcut associated with a primitive function handled by this plugin was invoked.
example1
kButtonDraw Called after a plugin button gets redrawn. Plugin must explicitly request this selector call.
kPulse Called during a pulse event of the text view. Plugin must explicitly request this selector call.
kDraw Called right after a draw call to the text view. Plugin must explicitly request this selector call.
kMessage Called from the beginning of the MessageReceived call of the current text view. Plugin can for instance use this selector call to handle messages dropped onto a document. Plugin must explicitly request this selector call. Return kExhausted from plugin if the message is completely consumed in the call.
kMouseDown Called when a text view got a MouseDown event. The Point call can be used to determine where. Plugin must explicitly request this selector call.
kKeyDown Called from the beginning of the KeyDown call of the current text view. The plugin can use KeyBytes to figure out which key was pressed. Plugin must explicitly request this selector call.
kGlobalSettingsSaved Called when settings for the whole application are being saved, plugin may save it's own settings using the WriteSettings call.
example
kDocumentSettingsSaved Called when settings are being saved for the whole document, plugin may save it's own settings as attributes in the documents node.
kGlobalPrefsShowing Called when the Application Settings window is being shown, plugin may add it's own settings panel.
example
kDocumentPrefsShowing Called when the document settings window is being shown. This selector not implemented yet.

The PluginInterface class

The PluginInterface parameter of PluginMain is the main communication structure between a plugin and Eddie. PluginInterface is a proxy class that contains data structures, virtual and non-virtual functions allowing the plugin to interact with Eddie. When calling plugins, Eddie sets up the PluginInterface structure to contain information about the current document, etc. Different selector calls will get specific data passed in the PluginInterface structure. For instance when the kDocumentTextChanged selector is called, the offset, size of removed and size of inserted text is available in the PluginInterface. Most of the data in PluginInterface is accessible through simple getter calls.

The PluginInterface member call overview

We will start with calls that a plugin can use to obtain more details about a selector call in progress.


ReturnSupportedPluginVersion, ReturnRequiredPluginVersion

	void ReturnSupportedPluginVersion(uint32 version
		= kEddieToPluginInterfaceVersion);
	void ReturnRequiredPluginVersion(uint32 version
		= kPluginToEddieInterfaceVersion);

During kGetPluginVersion use these to obtain the plugin API version your plugin supports and the call set version it requires from Eddie.
example


Window

	BWindow *Window() const;

Returns the window in which a plugin is being called. Valid during every selector call except for kInit, kQuit, kGlobalSettingsSaved and kGlobalPrefsShowing


DocumentUniqueID

	uint32 DocumentUniqueID() const;

Returns a unique ID of the active document. Future versions of Eddie will support multiple documents in a single window. Unlike window pointers that may get reused when a window gets closed and a new one opened, document IDs are unique for each document. If you are storing some data in your plugin that needs to be matched up with a document (for instance a table of structures supporting a syntax highliting plugin), a document ID is what you use. The document unique ID is valid during these selector calls: kDocumentOpened, kDocumentClosed, kDocumentTextChanged, kDocumentStateChanged, kDocumentActivated, kWindowActivated, kMouseDown, kKeyDown, kPulse, kDraw, kMessage, kButtonDraw, kButtonPress, kButtonClick


ChangeStart, RemovedLength, InsertedLength

	uint32 ChangeStart() const;
	uint32 RemovedLength() const;
	uint32 InsertedLength() const;

Used during kDocumentTextChanged selector call. Returns the offset in the text at which a change occurred, the length of text removed and the length of text inserted. Not guaranteed to return any valid value outside the kDocumentTextChanged selector call.
example1, example2


ClickSelector

	uint32 ClickSelector() const;

Used during the kButtonClick or kButtonPress selector calls, if you have a button that is split vertically or horizontally to determine which part of the button was pressed.
example


ButtonRedrawRect, Inverted

	BRect ButtonRedrawRect() const;
	bool Inverted() const;

In a kButtonDraw selector call these can be used to determine the rectangle of the plugin button being drawn and if it is being drawn inverted.


KeyBytes, NumKeyBytes

	const char *KeyBytes() const;
	uint32 NumKeyBytes() const;

In a kKeyDown selector call these can be used to determine which key was pressed.


Point

	BPoint Point() const;

During a kMouseDown selector call the point of the mouse click can be determined using this call.


CurrentMessage

	BMessage *CurrentMessage() const;

During a kMessage selector call the current message can be examined and handled.


CurrentDrawRect

	BRect CurrentDrawRect() const;

During a kDraw selector call after the document text is redrawn, the rectangle of the draw rect can be obtained


PrefsViewRect

	BRect PrefsViewRect() const;

During a kGlobalPrefsShowing selector call the rectangle of the preference panel can be obtained. The panel can then be allocated as a view and installed using the AddAppPrefsPanel and subclass of PrefsPanel proxy.
example


RequireSelectors

	virtual void RequireSelectors(uint32 selectorMask);

Used during button initialization to request non-default selector calls. Non-default selectors are called fairly often and in a system with a large number of plugins installed and a number of windows open could take a non negligible amount of time to execute, possibly slowing down the response time of the editor. Therefore only the plugins that actually need these selector calls ask for them using the RequireSelector calls and get the corresponding selector calls as a result. This is the list of the non-default selector calls that your plugin must specifically request, along with the mask values used to request the selector.

kButtonDraw kRequestButtonDrawMask
kPulse kRequestPulseMask
kDraw kRequestDrawMask
kMessage kRequestMessageMask
kMessage kRequestMessageMask
kMouseDown kRequestMouseDownMask
kKeyDown kRequestKeyDownMask
kDocumentTextChanged kRequestDocumentChangedMask

example


SetupPluginButton, ChangeButtonBitmaps, AnimateButtonClick

	virtual void SetupPluginButton(const unsigned char *buttonBits, 
		const unsigned char *pressedButtonBits);
	virtual void SetupPluginButton(const unsigned char *buttonBits, 
		const unsigned char *pressedButtonBits, uint32 splitMode);
	virtual void ChangeButtonBitmaps(const unsigned char *buttonBits, 
		const unsigned char *pressedButtonBits, const BWindow *
			thisWindowOnly = 0);
	virtual void ChangeButtonBitmaps(const unsigned char *buttonBits, 
		const unsigned char *pressedButtonBits, uint32 splitMode,
		const BWindow *thisWindowOnly = 0);
	virtual void AnimateButtonClick(int32 selector = 0);

SetupPluginButton can be used to install buttons in a new window, should be therefore called during the kWindowOpened selector. If you are using split buttons, use the second version which allows you to specify the splitting mode. ChangeButtonBitmaps can be used to change a button bitmap to indicate a plugin state change. The Magic Prototyper uses this call to change the button when a prototyper clipboard gets loaded. The thisWindowOnly is currently unused. AnimateButtonClick can be used to give a positive feedback when invoking a plugin function using a keyboard shortcut.
example1, example2, example3


GetSelection, Select

	virtual void GetSelection(int32 *start, int32 *end) const;
	virtual void Select(int32 start, int32 end);

Lets you get/set the selection in the current document.
example1, example2, example3


Insert, Clear

	virtual void Insert(const char *text, int32 length, const char *undoName = 0);
	virtual void Clear(const char *undoName = 0);

Insert lets you insert text into the current selection in the current document. Clear lets you delete the current selection in the current document. You may specify an undo name under which the edit will appear in the undo menu.
example1, example2, example3


CurrentUndoInsert, CurrentUndoClear, StartUndo, SealUndo

	virtual void CurrentUndoInsert(const char *text, int32 length);
	virtual void CurrentUndoClear();
	virtual void StartUndo(int32 restoreSelStartTo, int32 restoreSelEndTo,
		const char *undoName = 0);
	virtual void SealUndo();

CurrentUndoInsert and CurrentUndoClear are the same as Insert and Clear, except they allow you to group several edits into a single composite undo. StartUndo opens the undo and gives it a name. SealUndo closes the undo, any subsequent edit will start a new undo record. restoreSelStartTo and restoreSelEndTo is the position of the selection after the user invokes undo. In most cases you should pass the values of the selection right before the first edit of the composite undo.
example


CharAt, GetText

	virtual char CharAt(int32 offset) const;
	virtual void GetText(char *buffer, int32 offset, int32 length) const;

CharAt returns the character at the specified offset in the current document. GetText makes a copy of a specified ammount of characters form offset into a buffer you supply.
example1, example2, example3


CountLines, CurrentLine, GoToLine

	virtual int32 CountLines() const;
	virtual int32 CurrentLine() const;
	virtual void GoToLine(int32 lineNum);

Return the number of lines, the number of the line the selection starts on. GoToLine selects a specified line. Lines are numbered starting at zero.


StartOfLine, EndOfLine

	virtual int32 StartOfLine(int32 position) const;
	virtual int32 EndOfLine(int32 position) const;

Given an offset in text, these calls return the offset of the start and end of the line at the offset, respectively.


ScrollToOffset, ScrollToSelection

	virtual void ScrollToOffset(int32 inOffset);
	virtual void ScrollToSelection();

Scroll to a specified offset or to the current selection.
example


TextColor, BackgroundColor

	virtual rgb_color TextColor() const;
	virtual rgb_color BackgroundColor() const;

Returns the text and background colors in the current document.


SetColor, StartSettingColorOnRange, SetOneColorOnRange, EndSettingColorOnRange

	virtual void SetColor(rgb_color color);
	virtual void StartSettingColorOnRange();
	virtual void SetOneColorOnRange(rgb_color color, int32 start, int32 end);
	virtual void EndSettingColorOnRange();

Color setting calls used by syntax coloring plugins, etc. SetColor sets the specified color on the current selection. StartSettingColorOnRange, SetOneColorOnRange and EndSettingColorOnRange are used for setting color on multiple parts of a document at once for maximum performance. The updates for each color change are postponed till the EndSettingColorOnRange call.


SyntaxColoring

	virtual bool SyntaxColoring() const;

Returns true if SyntaxColoring is on for a document.


GetMouse

	virtual void GetMouse(BPoint *where, uint32 *buttons, 
		bool checkMessageQueue = true) const;

Returns the mouse position and button states.


PointToOffset

	virtual int32 PointToOffset(BPoint) const;

Converts a point to an offset in text.


TextViewBounds, WindowBounds

	virtual BRect TextViewBounds() const;
	virtual BRect WindowBounds() const;

Returns bounds of a document text and a document window.


GetButtonRect

	virtual BRect GetButtonRect() const;

Returns the window relative location of the plugin button. May be used for instance for positioning a popup menu over the button during a button press.


PluginName

	virtual const char *PluginName() const;

Returns the name of this plugin.


DocumentNode, DocumentRef

	virtual const BFile *DocumentNode() const;
	virtual const entry_ref *DocumentRef() const;

DocumentNode returns the BFile used to save the document or NULL if document wasn't saved yet. DocumentRef returns the entry_ref under which the document is being saved, NULL if document wasn't saved yet.


DocumentDirty, ReadOnly, IsShellWindow, IsWorksheet

	virtual bool DocumentDirty() const;
	virtual bool ReadOnly() const;
	virtual bool IsShellWindow() const;
	virtual bool IsWorksheet() const;

Getter calls for obtaining different parts of the document state. DocumentDirty returns true if document needs saving. ReadOnly is true if you cannot save the document (you can still edit it but you will be required to do a SaveAs). IsShellWindow and IsWorksheet return true if a document is a shell and a worksheet. When one of these values changes, the kDocumentStateChanged selector gets called.


DocumentLanguageType

	virtual DocumentLanguageType LanguageType() const;

Returns the type of the language associated with a document. Currently supported are:

kUnknown unknown document types
kShell currently any file without a suffix
kCC .c files
kCH .h files
kCCPlus C++ files (.cc, .cpp, .cp, etc.)
kCHPlus C++ header files (.h++, .hpp, etc.)
kJava Java files
kHTML HTML files
kPerl Perl files
kYacc yacc or Bison files
kLex lex, flex files
kAsm assembly files
kPascal Pascal files
kModulaDef Modula 2 definition files
kModulaOberonImpl Modula 2 or Oberon implementation files
kFortran Fortran files
kRez MPW res files
kCLikeAsm .S assembly files with C-like syntax
kMakefile makefile, Makefile, make*, etc.

When the type of document changes, the kDocumentStateChanged selector gets called. More document language types are likely to be added in future versions (send me your favorite ones with file suffix conventions).


RegisterHandledShellCommand, CurrentArgv, WriteSettings

	virtual void RegisterHandledShellCommand(const char *);
	const char ***CurrentArgv() const;
	virtual void WriteSettings(const char *format, ...);

RegisterHandledShellCommand called during plugin initialization. Currently used for handling settings that are stored in Eddie's settings file in a text format. When Eddie sees the setting the plugin registered, it calls it with the kArgvPassed selector to read the setting. During the kArgvPassed selector call you can use the CurrentArgv call to access the argv-like formatted line with the setting. WriteSettings is used to save a given plugin settings into the settings file during the kGlobalSettingsSaved selector. You just use it the same way as if you were using printf to print the settings.
example1, example2


AddAppPrefsPanel

	virtual void AddAppPrefsPanel(const char *name, PrefsPanel *prefsPanelProxy);

Called during the kGlobalPrefsShowing selector, used to install a panel into the Application Settings dialog. name is the name under which your panel will show up in the dialog. prefsPanelProxy is an instance of a subclass of PrefsPanel, a proxy class that controls applying, reverting and setting the preference values to defaults.
example


RegisterHandledPrimitiveCommand, CurrentPrimitive

	virtual void RegisterHandledPrimitiveCommand(const char *command,
		const char *description);
	const char *CurrentPrimitive() const;

RegisterHandledPrimitiveCommand called during plugin initialization. The plugin can register editor primitives that the user can bind to keyboard shortcuts using SetKey. The primitives will also show up when the Primitives command is invoked from the shell. During the kPrimitiveInvoked selector call you can use the CurrentPrimitive to figure out which keyboard primitive was invoked. This is necessary if your plugin publishes more than one primitive.
example


ShowClipboardPanel

	virtual void ShowClipboardPanel(const char *text);

Open a clipboard panel under the plugin button, displaying the passed text. The panel is closed when the mouse button is released. This is used for instance by the Magic Prototyper to show the contents of the Prototyper clipboard.


Making sure plugin and Eddie versions are compatible

A versioning mechanism is included, making sure that a given plugin and a given version of Eddie are capable of running with each other. As more API gets added to Eddie over time, older plugins may continue to run with newer versions of Eddie without recompilation. This way a plugin can be sure that the copy of Eddie it is running in supports all the callbacks that it needs and the Eddie application only calls the selector calls that the plugin understands.

The kGetPluginVersion is the first selector call a plugin will receive. It needs set the pluginInterface structure with the two values, the plugin supported version and the plugin required version. The PluginInterface provides two calls to set these two values as well as default values that are defined by the current version of Plugin.h that you are building your plugin against. Your plugin has to return kAccepted as a result value:

	switch (selector) {
	...
	case PluginInterface::kGetPluginVersion:
		pluginInterface->ReturnSupportedPluginVersion();
		pluginInterface->ReturnRequiredPluginVersion();
		return PluginInterface::kAccepted;
	...

Initializing a plugin

The kInit selector is the next selector your plugin needs to deal with. During this selector a plugin can allocate any data structures it needs for it's operation, request non default selector calls if it needs them, register editor primitives for keyboard shortcuts, register settings handling. If your plugin fails to initialize, return kFailed from PluginMain and it will never get called again.

	switch (selector) {
	...
	case PluginInterface::kInit:
		myPluginState = new MyPluginState();
		pluginInterface->RequireSelectors(PluginInterface::kRequestKeyDownMask 
			| PluginInterface::kRequestPulseMask);
		...
		return PluginInterface::kAccepted;

Installing plugin buttons

Your plugin can optionally install buttons into the button bar of a window. This is done in the kWindowOpened selector, which gets called whenever a new document gets opened. The plugin uses the SetupPluginButton call, passing in bits for the button bitmap.

	switch (selector) {
	...
	case PluginInterface::kWindowOpened:
		pluginInterface->SetupPluginButton(kButtonBits, kPressedButtonBits);			
		return PluginInterface::kAccepted;
	...

The bitmap for the plugin must be an array of bytes representing 8 bit color values 16 x 16 bytes in size. A great way to obtain a button bitmap is to draw it with the bitmap editor in the FileTypes app, as if it was a file icon and then use the Dump icons to emit the bitmap array code. The 32x32 portion of the dump is discarded. Make sure the bitmap colors are consistent with those of the other buttons. A template of a button is provided in CutNPasteSource.cpp for convenience.

A button can be split horizontally, vertically or in both directions. For instance the Function popup uses the following code to install a horizontally split button:

	...
	pluginInterface->SetupPluginButton(kFPButtonBits, 
		kPressedFPButtonBits, kHorizontalySplitButton);
	...

When a plugin button is pressed the plugin receives the kButtonClick or kButtonPress selector call, depending on how long the mouse is held down. If you have a split button, you may determine which portion of the button is being clicked by using the ClickSelector call:

	switch (selector) {
	...
	case PluginInterface::kButtonClick:
		if (pluginInterface->ClickSelector() == kTopButtonPart)
			GoUp();
		else
			GoDown();
		...
		return PluginInterface::kAccepted;

You may change the button bitmap any time by calling the ChangeButtonBitmaps:

		if (CopyMachineWarmedUp())
			pluginInterface->ChangeButtonBitmaps(kCopyButton, 
				kCopyPressedButton);

Scanning and modifying document text

You may read text from the current document using the GetText and CharAt calls.
The Insert call can be used to insert text at the current selection, in our example at the beginning of the current document.

	pluginInterface->Select(0, 0);
	pluginInterface->Insert("some text",  9, "Insert Some Text");

The third parameter "Insert Some Text" will be used as the name of the Undo for this edit. (In a real plugin you should use a shorted undo name than that).

Clear is used to delete the current selection. To delete the entire text of your current document, you could call:

	pluginInterface->Select(0, pluginInterface->TextLength());
	pluginInterface->Clear("Delete Everything");

The parameter "Delete Everything" will be used as the name of the Undo for this edit.

You may set up your plugin to get notified whenever text changes by using the kDocumentTextChanged selector. During this selector call ChangeStart, InsertedLength and RemovedLength returns the offset in the text at which a change occurred, the length of text removed and the length of text inserted respectively. If for instance the user pressed a single key, say 'A', the InsertedLength will return 1, the ChangeStart will return the offset at which the letter 'a' gets inserted. RemovedLength will be zero, unless the selection was non-empty at the time of the keypress, in which case it got erased by the keypress and the length of the erased text is returned. If a Delete key is pressed, RemovedLength will return the number of deleted characters, InsertedLength will return zero. During a paste RemovedLength will again be the size of the selection that gets erased by the paste, InsertedLength will be the size of the new inserted text.

Let's show an example where a plugin monitors the text that is being inserted, detecting a case where a specific word gets inserted into the text.

	const char *kMagicWord = "pasteThis";
	int32 magicWordLength = strlen(kMagicWord);
	...
	switch (selector) {
	...
	case PluginInterface::kInit:
	...
		pluginInterface->RequireSelectors(
			PluginInterface::kRequestDocumentChangedMask);
		return PluginInterface::kAccepted;
	...
	case PluginInterface::kDocumentTextChanged:
		...
		if (pluginInterface->InsertedLength() == kMagicWordLength
			&& pluginInterface->TextLength() - pluginInterface->ChangeStart() 
				> magicWordLength) {
			char buffer[20];
			pluginInterface->GetText(buffer, pluginInterface->ChangeStart(),
				magicWordLength);
			if (strcmp(buffer, kMagicWord) == 0)
				AMagicWordWasJustPasted();
			
		}

The example above obviously does not handle a case where the magic word was typed, character by character because the plugin will get a notification about a single character insertion after every character typed. We can modify our example to handle this case. The modified example will detect every time insertion of a magic word is completed:

	case PluginInterface::kInit:
	...
		pluginInterface->RequireSelectors(
			PluginInterface::kRequestDocumentChangedMask);
		return PluginInterface::kAccepted;
	...
	case PluginInterface::kDocumentTextChanged:
		...
		if (pluginInterface->InsertedLength() > 0) {
			int32 wordEnd = pluginInterface->ChangeStart() +
				pluginInterface->InsertedLength();
			if (wordEnd >= magicWordLength) {
				int32 index = magicWordLength - 1;
				for (;index >= 0; index--)
					if (pluginInterface->CharAt(wordEnd--) 
						!= kMagicWord[index])
						break;
				if (index == -1)
					AMagicWordWasJustInserted();
			}
		}

Supporting Undo

The Insert and Clear calls support a simple Undo scheme where every edit is undone individually with the Undo command. You may want to group a series of editing commands into a single undo record that can be undone as a whole. To do this, you can use the composite Undo commands, StartUndo, CurrentUndoInsert, CurrentUndoClear and SealUndo.

	pluginInterface->StartUndo(selStart, selEnd, "SwapWords");
	char word1[256];
	int32 firstWordLength = firstWordEnd - firstWordStart;
	pluginInterface->GetText(word1, firstWordStart, firstWordLength);
	char word2[256];
	int32 secondWordLength = secondWordEnd - secondWordStart;
	pluginInterface->GetText(word2, secondWordStart, secondWordLength);
	pluginInterface->Select(secondWordStart, secondWordEnd);
	pluginInterface->CurrentUndoClear();
	pluginInterface->Select(firstWordStart, firstWordEnd);
	pluginInterface->CurrentUndoClear();
	pluginInterface->CurrentUndoInsert(word2, secondWordLength);
	int32 newFirstWordOffset = secondWordStart + secondWordLength 
		- firstWordLength;
	pluginInterface->Select(newFirstWordOffset, newFirstWordOffset);
	pluginInterface->CurrentUndoInsert(word1, firstWordLength);
	pluginInterface->SealUndo();
	pluginInterface->Select(newFirstWordOffset, newFirstWordOffset 
		+ firstWordLength);
	pluginInterface->ScrollToSelection();

You are obviously aware that the example above assumes the offset of the first word is less than that of the second word and that the words may be no longer than the size of the two static buffers. The second assumption would probably be no good in a real plugin.


Reentrancy issues you need to be aware of

You need to realize that plugin can have two selector calls invoked simultaneously. Consider for instance the following code fragment:

	switch (selector) {
	...
	case PluginInterface::kInit:
	...
		pluginInterface->RequireSelectors(
			PluginInterface::kRequestDocumentChangedMask);
		return PluginInterface::kAccepted;
	...
	case PluginInterface::kButtonClick:
		...
		pluginInterface->Select(0, 0);
		pluginInterface->Insert("text from a plugin", 19);
		return PluginInterface::kAccepted;
		...
	case PluginInterface::kDocumentTextChanged:
		...
		char buffer[256];
		pluginInterface->GetText(buffer, pluginInterface->ChangeStart(),
			someLength);
		return PluginInterface::kAccepted;

During call to Insert in the kButtonClick selector the plugin will be called again, this time with the kDocumentTextChanged selector. After all the text insertion is treated as any other text edit. You need to take the usual precautions when dealing with reentrancy like this, for instance you need to be careful about accessing any global state your plugin has.

Furthermore, calling text modifying calls, such as Insert and Clear from within the kDocumentTextChanged selector will have undefined results. You can call SetColor though, because the call does not insert or delete any text.


Handling plugin settings

Eddie allows plugins to use the same settings saving mechanism the editor uses itself. The application settings are stored in a single file in the ~/config/settings/Eddie/settings file in a text form, formatted one setting per line. You can actually edit the settings file with a text editor if you are so inclined. The order of the individual setting items is arbitrary. The settings file can contain comments (even though they will get stripped the next time the settings are saved). The white space formatting is also arbitrary, tabs and spaces can be used freely. A setting must however be on a single line. You can even copy settings lines and paste them into the UserStartup file. They will be recognized just fine, the only problem would be that if you tweaked any of the settings say using the Application Settings dialog, the changes would get saved back into the settings file, however since the UserStartup gets read after the settings file, the changes would be overridden by the corresponding settings in the UserStartup that you pasted there. This could be confusing so it's not as usefull as it might have seemed at first. What is however usefull about this is the fact that setting and UserStartup items can be handled using the same mechanism and same code, saving you effort.
To keep things simple, settings that can be changed using the Application Settings preference panels, the checkboxes in the Find panel, etc. are saved in the ~/config/settings/Eddie/settings file and are not usually edited by the user in the text form. The ones that would be edited in text form would be found in the UserStartup file. Here is a little example:

	switch (selector) {
	...
	case PluginInterface::kInit:
	...
		pluginInterface->RegisterHandledShellCommand
			("UltraPluginPluggingSpeed");
		pluginInterface->RegisterHandledShellCommand
			("UltraPluginPluggingColor");
		return PluginInterface::kAccepted;
	...
	case PluginInterface::kArgvPassed:
		{
			const char **argv = *pluginInterface->CurrentArgv();
			if (strcmp(*argv, "UltraPluginPluggingSpeed") == 0)
				pluggingSpeed = atoi(*++argv);
			else if (strcmp(*argv, "UltraPluginPluggingColor") == 0)
				pluggingColor = MyConvertRGBColor(++argv);
		}
		return PluginInterface::kAccepted;
	...

Your plugin will get called with the kArgvPassed selector whenever Eddie detects a line with one of the two UltraPluginPluggingSpeed and UltraPluginPluggingColor keywords in the settings file or in UserStartup. It will format the line for your plugin into argv form and will let you get it using the CurrentArgv call and parse it. In future versions of Eddie a plugin will be able to intercept shell commands typed into a shell window and executed, all with the same registering and processing mechanism.

If the setting belongs to the ~/config/settings/Eddie/settings, you need to make sure it get's saved at the right time. Doing so is pretty easy:

	case PluginInterface::kGlobalSettingsSaved:
	...
		if (pluggingSpeed != kDefaultPluggingSpeed)
			pluginInterface->WriteSettings("%s %d\n",
				"UltraPluginPluggingSpeed", pluggingSpeed);
			pluginInterface->WriteSettings("%s %d %d %d\n",
				"UltraPluginPluggingColor", (int)pluggingColor.red,
					(int)pluggingColor.blue, (int)pluggingColor.blue);
	...
		return PluginInterface::kAccepted;
	...
Just use the printf-like WriteSettings call to write the setting into the Eddie settings file. Note that you don't have any control over the order in which your settings get written with respect to those of other plugins. Note that by convention Eddie does not write out settings that have values identical to the defaults. This keeps the settings file smaller. Also note that for reasons mentioned above you don't wan't to write out values that are to be read from UserStartup. These should be tweaked by the user by editing the UserStartup file.

Supporting preference panels

A plugin can add it's page to the Application Settings dialog. To do this, the plugin creates the prefence panel duiring the kGlobalPrefsShowing selector call, wraps it into a subclass of PrefsPanel and hands it off to Eddie using the AddAppPrefsPanel call. PrefsPanel is a proxy class with nine virtuals that control the behavior of applying, reverting and setting the preference panel to default values.

	virtual bool ShowRevert() const;
	virtual bool ShowDefault() const;
	virtual bool ShowApply() const;

These three calls are used by the settings dialog to determine whether or not to show the "Revert", "Default" and "Apply" buttons respectively when showing the plugins preference panel. They return true which is the desired behavior for most preference panels.

	virtual bool CanRevert() const;
	virtual bool CanDefault() const;
	virtual bool CanApply() const;

These three calls are used by the settings dialog to determine if the data entered into the prefence panel's controls differs from the settings at the time the dialog was opened, differs from the default values and differs from the current settings respectively. The call is used for instance to enable and disable the three buttons based on the data values. Your plugin will generally override these calls in the subclass of PrefsPanel to suit the specific setup of your panel.

	virtual void Revert();
	virtual void Defaults();
	virtual void Apply();

Your PrefsPanel subclass has to override these three calls bacause they are pure virtuals. Revert applies the revert values - the values of settings at the time the Settings dialog was opened and sets the preference panel controls to these values. Defaults applies default settings to and sets them to the preference panel controls. Apply applies the values from the controls.

The preference panel your plugin installs is a BView. Use the call PrefsViewRect to determine the size needed. Place all the settings controls onto the preference panel view using AddChild. When your panel is layed out, allocate an instance of your subclass of PrefsPanel, passing the preference panel view into the constructor. Add the instance of the PrefsPanel subclass to the dialog by calling the AddAppPrefsPanel call.

	
	case PluginInterface::kGlobalPrefsShowing:
		BRect frame = pluginInterface->PrefsViewRect();
		frame.InsetBy(10, 10);
		frame.top += 3;
		BView *preferencePanel = new MyPanelBackgroundView(frame, ...
		
		// ... add controls to preferencePanel
		
		PrefsPanel *panel = new MyPrefsPanel(preferencePanel);
		panel->SetTarget(this);
		pluginInterface->AddAppPrefsPanel("Plugging settings", panel);
		return PluginInterface::kAccepted;
	

For a more complete example check out the KeyPressHandling.cpp sample.


Supporting keyboard shortcuts

Plugin commands can be bound to keyboard shortcuts using SetKey just like editor primitives in the editor itself. To do this, a plugin publishes one or more editor primitives. The user can then attach any keyboard shortcut to the primitive using the SetKey command in UserStartup. When the shortcut is pressed, the plugin will get called:

	switch (selector) {
	...
	case PluginInterface::kInit:
		pluginInterface->RegisterHandledPrimitiveCommand(
			"PPPluginInsertPi",
			"Inserts the constant pi into the current selection");
		pluginInterface->RegisterHandledPrimitiveCommand(
			"PPPluginInsertPhoneNumber",
			"Inserts your phone number into the current selection");
	...
		return PluginInterface::kAccepted;
	...

	case PluginInterface::kPrimitiveInvoked:
		const char *primitive = pluginInterface->CurrentPrimitive();
		
		if (strcmp(primitive, "PPPluginInsertPi") == 0)
			pluginInterface->Insert("3.1415", 6, "Insert pi");
		else if (strcmp(primitive, "PPPluginInsertPhoneNumber") == 0)
			pluginInterface->Insert("(650) 462-4100", 14, "Insert Phone#");
			
		pluginInterface->AnimateButtonClick();
		result = PluginInterface::kAccepted;
		break;

The invoked primitive can be determined by calling the CurrentPrimitive call. If your plugin has a button and the invoked primitive matches the function called during the button click, call the AnimateButtonClick to give positive feedback to the user, as if the button was clicked.
Now the user can go and add SetKey lines to the UserStartup:

SetKey Alt-Control-P 		PPPluginInsertPi
SetKey Alt-Shift-P 		PPPluginInsertPhoneNumber

Legal suff, disclaimers:

This version of the Eddie Plugin Developer Kit is shareware. You may use it, copy it and redistribute it freely. You may use the included sample code or parts of it in your own Eddie plugins freely. It most certainly contains bugs, some of the features are not fully functional. Use at your own risk, you have been warned.

PAVEL CISLER PROVIDES THIS SOFTWARE AND DOCUMENTATION "AS IS", WITH NO WARRANTY OF ANY KIND EITHER EXPRESS, IMPLIED OR STATUTORY, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.

IN NO EVENT IS PAVEL CISLER LIABLE FOR ANY INDIRECT DAMAGES OR OTHER RELIEF ARISING OUT OF YOUR USE OR INABILITY TO USE THE PROGRAM INCLUDING, BY WAY OF ILLUSTRATION AND NOT LIMITATION, LOST PROFITS, LOST BUSINESS OR LOST OPPORTUNITY, OR ANY SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF SUCH USE OR INABILITY TO USE THE PROGRAM, EVEN IF PAVEL CISLER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES, OR FOR ANY CLAIM BY ANY OTHER PART.

Be Incorporated is no way responsible for Eddie or the Eddie Plugin Developer Kit, Eddie is not a product of Be Incorporated.