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
- GetSettings()
Looks for your settings and retrieves them if found.
- PutSettings().
Stores your settings.
- GetPrefsFile() (MacOS Only)
Finds the correct location for your prefs file.
- MovePrefsFile() (MacOS Only)
Move a prefs file from one location to another. This is useful if you are currently storing your prefs in another location and would like to move them to the Finale Plug-Ins prefs folder.
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
- }
-