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
- }
-