Writing Well-Behaved Plugins

This page contains my ideas about how a well-behaved plugin should act. It also includes sample code that shows how to implement these features on both MacOS and Windows systems. If enough third-party developers implement these features, then maybe Coda will, too.

The basic tenets are

In all the code examples, I am careful to use #if...#elif statements for each operating system. Using #if...#else may be dangerous because it assumes that the only two possibilities will always be MacOS & Windows.

Remember Window Positions

When a Finale plug-in calls the FX_Dialog() API, Finale ignores the positioning information in the resource file and centers the dialog. While this may be acceptable behavior for many users, some users, especially those with large or multiple monitors, will want to control where the dialog goes. A well-behaved plug-in dialog should remember where the user last placed it and always re-display itself there.

There are two schools of thought about how to handle subsidiary dialogs. The question is whether subsidiary dialogs should maintain their positions relative to the parent dialog. I do not feel strongly one way or the other. I do think that a subsidiary dialog should display relative its parent the first time it is opened. Otherwise, if the parent were moved before the child had ever been opened, it might be in an entirely different place on the screen before the user had ever seen it.

The following code examples illustrate how to do this. It shows how to move the window and how to retrieve the window's current position. The code checks for the possibility that the screen configuration may have changed since the last time the plug-in was run. If the dialog would display in a non-existent area of the screen, this code reverts to Finale's default positioning. (This check is most important. On Macs a user can reconfigure a multiple monitor setup without rebooting.)

The examples make use of a pointer to an undefined class called yourData. This is where you would keep any data that you save into a config file. Typically you would typically pass a pointer to this struct into FX_Dialog() so that your callback function has access to your data.The examples assume this is your architecture, and that your struct contains the following variables.

twobyte m_xPos;
twobyte m_yPos;

Somewhere you need to define:

#define BOGUS_WIN_POS_VALUE -32768

The first time you run the plugin, when no configuration data have yet been saved, initialize your position variables as

yourData->m_xPos = BOGUS_WIN_POS_VALUE;
yourData->m_yPos = BOGUS_WIN_POS_VALUE;

The following code is added to the WM_INITDIALOG event in your dialog handling routine. It checks to see if the current point is visible and moves the dialog if so. On Macs, at least with Fin97, there is a brief flicker, because Finale draws the dialog frame before the WM_INITDIALOG event fires. Unfortunately, this is the earliest point at which you have the opportunity to move the dialog, unless you do all the Mac dialog processing yourself. (Highly discouraged!) With Windows this is not an issue.

#if OPERATING_SYSTEM == MAC_OS
if ( (yourData->m_xPos != BOGUS_WIN_POS_VALUE) && (yourData->m_yPos != BOGUS_WIN_POS_VALUE) )
{
// first make sure the window will be on screen. In a multi-monitor setup
// one of the monitors could be offline, or the user could have moved it in
// the Monitors and Sound control panel. Thus, our stored position may be bogus.
 
Point thePt = {yourData->m_yPos, yourData->m_xPos};
if ( PtInRgn (thePt, GetGrayRgn()) )
MoveWindow (hDlg, thePt.h, thePt.v, NO);
}
#elif OPERATING_SYSTEM == WINDOWS
if ( (yourData->m_xPos != BOGUS_WIN_POS_VALUE) && (yourData->m_yPos != BOGUS_WIN_POS_VALUE) )
{
// windows systems can have multiple screens, too, but I don't know a better
// way to check if the point is visible than the one that follows:
twobyte scrCX = GetSystemMetrics (SM_CXSCREEN);
twobyte scrCY = GetSystemMetrics (SM_CYSCREEN);
if (
(yourData->m_xPos <= scrCX) && (yourData->m_yPos <= scrCY)
&&
(yourData->m_xPos >= 0) && (yourData->m_yPos >= 0)
)
{
RECT theRect;
GetWindowRect (hDlg, &theRect);
theRect.right = theRect.right - theRect.left; //chg to offset for MoveWindow
theRect.bottom = theRect.bottom - theRect.top; //chg to offset for MoveWindow
theRect.left = yourData->m_xPos;
theRect.top = yourData->m_yPos;
MoveWindow (hDlg, theRect.left, theRect.top, theRect.right, theRect.bottom, YES);
}
}
#endif

The following code executes whenever the user hits OK. It collects the current position and stuffs it away inside yourData. Unfortunately, FX_Dialog() does not appear to send your dialog handler the WM_CLOSE event, so you have to include this code as part of the WM_COMMAND event. You will be sure to get the current coordinates by executing it in *every* WM_COMMAND event. This has the advantage of not requiring the routine to assume how the dialog code is going to react to a specific command. However, the code will work equally well if executed only for the OK and Cancel commands.

// Finale seems to be hiding the WM_CLOSE event from us, so the only way
// to update the position coordinates seems to be to do it on commands. The cost
// of doing it on every command is small, and it makes sure we get them.
#if OPERATING_SYSTEM == MAC_OS
{
GrafPtr gCurrPtr;
GetPort (&gCurrPtr); // saving the current GrafPort may not be necessary, but why risk it?
SetPort (hDlg); // even if hDlg is already the current GrafPort, setting it again doesn't hurt.
Point pt = * (Point *) &hDlg->portRect;
LocalToGlobal (&pt);
yourData->m_xPos = pt.h;
yourData->m_yPos = pt.v;
SetPort (gCurrPtr);
}
#elif OPERATING_SYSTEM == WINDOWS
{
RECT theRect;
GetWindowRect (hDlg, &theRect);
yourData->m_xPos = theRect.left;
yourData->m_yPos = theRect.top;
}
#endif

Maintain Persistent Settings

All configuration dialogs should redisplay with their settings from the last time the user hit OK on the dialog. These settings should be persistent even between Finale sessions. Some plugins have windows that are not configuration dialogs but rather perform the actual work of the plugin. For these types of windows, maintaining persistent settings may be less important than being sensitive to the user's current situation. A good example of such a plugin is Tobias Giesen's Staff List Manager. For most plug-ins, however, the dialog is simply a preamble to the actual work, and these windows should always remember their previous settings.

Dialog settings should be stored in standard locations.

MacOS: System Folder:Preferences:Finale Plug-Ins
Win32: Registry Key: HKEY_CURRENT_USER\Software

MacOS plugins should used the FindFolder() API to find the Preferences folder rather than hardcoding its path. The sample code here show how to do this. The routines are

This code checks the size you passed in against the size that is currently stored. If the size you passed in is less than the size that is stored, the prefs will only be read or written up to the size you pass in. If the size you passed in is greater than the size that is stored, GetSettings initializes the remaining portion of your buffer with zero. PutSettings increases the stored length to the size you passed in.

This behavior naturally and gracefully handles different versions of your code. As long as you never rearrange the order of your prefs struct, and as long as you only add to rather than subtracting from it in new versions, then this code automatically handles updating your prefs struct and even allows an older version to read/write a prefs struct from a newer version. All a newer version needs to do is recognize and initialize zero values that may be returned if an older (shorter) version of the prefs struct was read.

The MacOS version of the routines finds and stores the prefs using the resource ID, whereas the Win32 version uses the name. However, the Mac version attaches the name to the resource when it creates it, so the routines should be called in the same way, regardless of platform.

The routines are generic. Even if your plugin file contains multiple plug-ins, the routines allow you to create separate resources for each, all within the same prefs file (Mac) or registry key (Win). You simply pass in different resource ID's and plugin names for each plugin. (My own practice is to use the same resource ID as that for my dialog box.)

#include <string.h>
#include "finextnd.h"
 
#if OPERATING_SYSTEM == MAC_OS
 
#include <MacMemory.h>
#include <Files.h>
#include <Resources.h>
#include <Folders.h>
#include <Script.h>
#define FIN_PI_PREFS_RESTYPE <any 4-characters in single quotes, e.g. 'MYPR'--avoid std MacOS restypes if you can>
#define FIN_PI_PREFS_NAME <your Mac prefs file name here, e.g. "My Prefs">
 
#elif OPERATING_SYSTEM == WINDOWS
 
#include <winreg.h>
#define FIN_PI_PREFS_REGKEY "Software\\"##<your Windows registry name here,. e.g., "My Prefs\\This Plugin">
 
#endif
 
tbool FinPI::GetSettings (
twobyte MACCODE(prefResourceID), //resource ID for prefs resource
void * data, //pointer to data struct to return prefs in
fourbyte dataSize, //size of data struct
char * WINCODE(plugInName) ) //name of your plugin (or other resource)
{
tbool gotSettings = NO;
#if OPERATING_SYSTEM == MAC_OS
twobyte currResFile;
FSSpec prefFileSpec;
twobyte prefResFile;
Handle hPrefResource;
 
if ( GetPrefsFile (&prefFileSpec, NO) )
{
// Switch to our prefs Filespec
currResFile = CurResFile();
prefResFile = FSpOpenResFile (&prefFileSpec, fsWrPerm);
// Get our config resource if we find the res file
if ( prefResFile >= 0 )
{
UseResFile (prefResFile);
if ( ( hPrefResource = GetResource (FIN_PI_PREFS_RESTYPE, prefResourceID) ) != NULL )
{
Size sizePrefResource = GetHandleSize(hPrefResource);
if ( dataSize <= sizePrefResource )
memcpy (data, *hPrefResource, dataSize);
else
{
memcpy (data, *hPrefResource, sizePrefResource);
memset ((void *) ((onebyte *)data+sizePrefResource), 0, dataSize-sizePrefResource);
}
gotSettings = YES;
ReleaseResource (hPrefResource);
}
}
UseResFile (currResFile);
}
 
#elif OPERATING_SYSTEM == WINDOWS
HKEY hPrefKey;
DWORD dwType;
DWORD dwLength = (DWORD) dataSize;
 
if ( RegOpenKeyEx (HKEY_CURRENT_USER, FIN_PI_PREFS_REGKEY, 0, KEY_READ, &hPrefKey) == ERROR_SUCCESS )
{
if ( RegQueryValueEx (hPrefKey, plugInName, 0, &dwType, (BYTE *) data, &dwLength) == ERROR_SUCCESS )
{
if ( dwLength < dataSize )
memset ((void *) ((onebyte *)data+dwLength), 0, dataSize-dwLength);
gotSettings = TRUE;
}
RegCloseKey (hPrefKey);
}
#endif
 
return gotSettings;
}

void FinPI::PutSettings (
twobyte MACCODE(prefResourceID), //resource ID for prefs resource
void * data, //pointer to data struct that will be stored
fourbyte dataSize, //size of data struct
char * plugInName ) //name of your plugin (or other resource)
{
#if OPERATING_SYSTEM == MAC_OS
twobyte currResFile;
FSSpec prefFileSpec;
twobyte prefResFile;
Handle hPrefResource;
 
if ( GetPrefsFile (&prefFileSpec, YES) )
{
// Switch to our prefs Filespec
currResFile = CurResFile();
prefResFile = FSpOpenResFile (&prefFileSpec, fsWrPerm);
// If the res file does not exist, create it
if ( prefResFile < 0 )
{
OSErr resError = ResError();
if ( resError == nsvErr || resError == fnfErr || resError == dirNFErr )
{
FSpCreateResFile (&prefFileSpec, FIN_PI_PREFS_RESTYPE, 'pref', 0);
prefResFile = FSpOpenResFile (&prefFileSpec, fsWrPerm);
}
}
if ( prefResFile >= 0 )
{
UseResFile (prefResFile);
tbool resourceExists = YES;
if ( ( hPrefResource = GetResource (FIN_PI_PREFS_RESTYPE, prefResourceID) ) == NULL )
{
hPrefResource = NewHandle (dataSize);
resourceExists = NO;
}
if ( hPrefResource != NULL )
{
OSErr memErrorWhenExpanding = noErr;
Size sizePrefResource = GetHandleSize(hPrefResource);
if ( dataSize > sizePrefResource )
{
SetHandleSize (hPrefResource, dataSize);
memErrorWhenExpanding = MemError();
}
if ( memErrorWhenExpanding == noErr )
{
memcpy (*hPrefResource, data, dataSize);
if ( resourceExists )
ChangedResource (hPrefResource);
else
{
Str255 resName;
memcpy (&resName[1], plugInName, MIN(sizeof(Str255)-1, strlen(plugInName)));
resName[0] = MIN(sizeof(Str255)-1, strlen(plugInName));
AddResource (hPrefResource, FIN_PI_PREFS_RESTYPE, prefResourceID, resName);
}
if ( ResError() == noErr )
WriteResource (hPrefResource);
}
ReleaseResource (hPrefResource);
}
}
UseResFile (currResFile);
}
#endif
 
#if OPERATING_SYSTEM == WINDOWS
HKEY hPrefKey;
DWORD dwDisposition;
 
if ( RegCreateKeyEx ( HKEY_CURRENT_USER, FIN_PI_PREFS_REGKEY, 0, "",
REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS,
NULL, &hPrefKey, &dwDisposition ) == ERROR_SUCCESS )
{
RegSetValueEx (hPrefKey, plugInName, 0, REG_BINARY, (BYTE *) data, (DWORD) dataSize);
RegCloseKey (hPrefKey);
}
#endif
}

#if OPERATING_SYSTEM == MAC_OS
tbool FinPI::GetPrefsFile (
FSSpec * theSpec, //when return value is YES, this contains the FSSpec for the prefs file
CONST tbool makeIt) //if YES, create the prefs directory if it isn't there
{
fourbyte prefID;
twobyte prefVol;
if ( FindFolder (kOnSystemDisk, kPreferencesFolderType,
makeIt ? kCreateFolder : kDontCreateFolder,
&prefVol, &prefID) != noErr )
return NO;
 
// Set up Pascal folder name.
memcpy ((onebyte *)&theSpec->name[1], FIN_PI_FOLDER_NAME, sizeof(FIN_PI_FOLDER_NAME));
theSpec->name[0] = sizeof(FIN_PI_FOLDER_NAME);
 
CInfoPBRec finpiPBRec;
memset (&finpiPBRec, 0, sizeof(CInfoPBRec));
finpiPBRec.dirInfo.ioNamePtr = theSpec->name;
finpiPBRec.dirInfo.ioVRefNum = prefVol;
finpiPBRec.dirInfo.ioDrDirID = prefID;
if ( PBGetCatInfoSync(&finpiPBRec) != noErr )
{
theSpec->vRefNum = finpiPBRec.dirInfo.ioVRefNum;
theSpec->parID = finpiPBRec.dirInfo.ioDrDirID;
OSErr err = FSpDirCreate (theSpec, smSystemScript, &theSpec->parID);
if ( err != noErr && err != fnfErr)
return NO;
}
else
{
theSpec->vRefNum = finpiPBRec.dirInfo.ioVRefNum;
theSpec->parID = finpiPBRec.dirInfo.ioDrDirID;
}
 
// Set up Pascal file name
memcpy ((onebyte *)&theSpec->name[1], FIN_PI_PREFS_NAME, sizeof(FIN_PI_PREFS_NAME));
theSpec->name[0] = sizeof(FIN_PI_PREFS_NAME);
// Move from original location if there.
MovePrefsFile (prefVol, prefID, theSpec->parID, theSpec->name);
return YES;
}
#endif

#if OPERATING_SYSTEM == MAC_OS
tbool FinPI::MovePrefsFile (
twobyte vRefNum, //volume ref num for both directories
fourbyte fromDir, //directory ID where the file is now
fourbyte toDir, //directory ID where the file needs to go
ConstStr255Param pName) //pointer to file name in Pascal format
{
FSSpec from;
FSSpec to;
tbool moved = NO;

if (
( FSMakeFSSpec (vRefNum, fromDir, pName, &from) == noErr )
&&
( FSMakeFSSpec (vRefNum, toDir, (ConstStr255Param) "", &to) == noErr )
&&
( FSpCatMove (&from, &to) == noErr )
)
moved = YES;

return moved;
}
#endif

Call these functions as follows:

struct YOUR_CONFIG_OPTIONS
{
twobyte opt1;
tbool opt2;
//
// etc.
} config_options;
 
GetSettings (MY_RESOURCE_ID, &config_options, sizeof(config_optios), "My Plugin Name");
//
// do a bunch of work
//
PutSettings (MY_RESOURCE_ID, &config_options), sizeof(config_options), "My Plugin Name");

Skip Dialogs with Modifier Key

The discussion on maintaining persistent settings drew a distinction between dialog boxes that are the point of the plugin and configuration dialogs that are simply the prelude to the plugin and determine how the plugin should act. In this latter, more common case, users should be able to skip the dialog by holding down a modifer] key when selecting it from Finale's Plugin menu. The plugin should then use its currently stored settings

For Windows, the Plugin should recognize [SHIFT] for this purpose. For MacOS, the plugin should recognize [OPTION].

The following code example shows how to detect each modifier ey.

#if OPERATING_SYSTEM == MAC_OS
#include <Events.h>
#elif OPERATING_SYSTEM == WINDOWS
#include <winuser.h>
#endif
 
// In FinaleExtensionInvoke or somewhere soon after, put:
 
tbool skipOptions = NO;
 
#if OPERATING_SYSTEM == WINDOWS // use [SHIFT]
 
skipOptions = ( GetKeyState(VK_SHIFT) < 0 );
 
#elif OPERATING_SYSTEM == MAC_OS // use [OPTION]
 
KeyMap theKeys;
GetKeys (theKeys);
skipOptions = theKeys[1] & 0x00000004L; //theKeys is an array of fourbytes
 
#endif
 
if ( ! skipOptions )
{
// Do the dialog box
}