MultiWindow DA
Volume Number: 7
Issue Number: 7
Column Tag: Pascal Procedures
Multi-Window/Menu DA 
By Lincoln Stein, MD, Boston, MA
Note: Source code files accompanying article are located on MacTech CD-ROM orsource code disks.
[Lincoln Stein, MD, is a Harvard Medical School Graduate. He is currently
developing medical education software with the Decision Systems Group at Brigham and
Women’s Hospital in Boston.]
Once Upon a Desktop
A long long time ago (well, only about two years, really) Macintosh applications
and desk accessories were quite easy to tell apart. Applications supported multiple
windows, owned all the menus in the menu bar, and ran one at a time. Desk accessories,
in contrast, only had one window and a single menu if any. In partial compensation for
these limitations, however, multiple desk accessories could run simultaneously with
each other and the underlying application.
Things changed considerably with the advent of Multi finder. Now multiple
applications can run simultaneously, nullifying desk accessories’ major advantage. The
distinction will become even more murky with the advent of System 7, which promises
to allow applications to be placed in the apple menu and to let desk accessories be
launched from the desktop. The widespread predictions of the DA’s demise turned out to
be premature, however. Indeed, desk accessories are proliferating as rapidly as ever
and are reaching heights of sophistication that rival full-blown applications. At the
same time that Multifinder has made applications more desk accessory-like, savvy
developers have been making their desk accessories more and more application-like.
You’ve probably noticed that some commercial and public domain desk
accessories are not content with the single window that imprisoned the classic desk
accessories. These applications let you spawn new windows wantonly, a distinctly
application-like thing to do. This article will show you how to repeat this
multi-window trick. It will also show you how desk accessories can take over the entire
menu bar in order to display Multiple menus, something that I haven’t seen done by any
desk accessory, commercial or otherwise!
This article will not discuss the basics of how to write a desk accessory, which
has been covered well in past articles (see MacTutor volumes II no. 4, II no. 5, II no. 6,
and III no. 10).
A DA With a Difference
The example desk accessory, MultiDA, serves no useful function. It was written
like this in order to keep the code simple. When the DA is invoked it takes over the
menu bar and replaces the application’s menu bar with its own Apple, File, Edit and
Windows menus. The DA allows you to open up an unlimited number of windows, type
into a text edit field, cut and paste between windows, stack the windows up neatly, and
close the windows. When the last window is closed, or when you choose “Quit” from the
File menu, the desk accessory closes itself. If you select MultiDA from the apple menu
when it is already open, it brings all its windows to the front. The desk accessory’s
menus are switched in and out as appropriate so that there’s never any conflict between
MultiDA’s menus and the application’s. MultiDA works under Unifinder and,
surprisingly, under Multifinder as well (with a small caveat discussed later). Figure
1 shows MultiDA in action: notice that the Apple, File, Edit and Window menus all belong
to the DA.
The code is written in Think Pascal. Unlike other development environments,
Think lets you write desk accessories that use global variables and multiple segments.
If you use a different development environment, you will have to make some changes to
overcome these differences.
Figure 1. MultiDA.
Multiplying Menus
In principle it is as easy for a desk accessory to put up multiple menus as it is to
put up a single menu. All the work of putting up and taking down a single menu occurs
during activate and deactivate events. Whenever its window is activated, the desk
accessory calls the Menu Manager routine InsertMenu to add its menu to the menu bar.
Similarly, whenever the desk accessory window is deactivated, the DA calls DeleteMenu
to remove its menu. In order to let the system know that the DA has put a menu up, the
DA must store its menu’s ID in the dCtlMenu field of the driver’s driver control entry
(this is a relocatable data structure created for each desk accessory when the DA is
opened; see previous MacTutor articles for further information). Once this is done, the
system refers all selections from that menu to the DA’s code.
Similarly, all the work of putting up multiple menus occurs during
activate/deactivate events. In the source code, most of the fancy stuff occurs in the
routines DoActivate, which handles both activate and deactivate events. The required
steps are described in Inside Macintosh volume 1, pages 446-447. First the DA must
save a copy of the application’s menu bar in its globals using the Menu Manager
procedure GetMenuBar. Then it must clear the menu bar and add its own menu using
ClearMenuBar followed by calls to InsertMenu. As before, it stores the ID of one of its
menus into the dCtlMenu field of the driver control entry (any of the menu IDs will do
for this purpose). Finally, and this is the key part, the desk accessory must save this
same menu ID into the low memory global mBarEnable ($A20). This tells the system
that the desk accessory now owns the entire menu bar directs the system to send all
menu choices to the desk accessory rather than to the underlying application. Whenever
the desk accessory window is deactivated, this process should be reversed. The menu
bar should be cleared and the application’s menus put back using SetMenuBar with the
previously stored copy. The mBarEnable global should then be cleared.
While the desk accessory’s menus are in the menu bar, the system handles all
the tedious work of pulling down the menu and tracking the user’s selections. All the
DA ever sees is an accMenu control call. When this call is received, the DA should
examine the I/O parameter block (another desk accessory data structure: see Inside
Macintosh for details) to determine the menu and item chosen: param[0] will contain
the ID number of the menu chose, and param[1] will contain the item number of the
selected menu item.
This is essentially all there is to giving a desk accessory multiple menus, and
just a few details remain to be ironed out. The first detail is determining the IDs of the
desk accessory’s menus. Usually, a menu is stored as a ‘MENU’ resource; its menu ID
is usually identical to the resource ID number. The problem with desk accessories is
that their resource numbers get shifted around by Font/DA Mover and by such utilities
as Suitcase in order to avoid numbering conflicts with other desk accessories. In order
to get around the problem of shifty resource ID’s, each resource used by the desk
accessory is numbered as an “owned resource” (as described in the resource manager
chapter of Inside Macintosh); The desk accessory’s own resource ID is mathematically
combined with each owned resource’s “sub-ID” to form the true resource ID. While
the ID number of the resources change each time the desk accessory’s ID changes, the
sub-ID number never changes. The runtime resource ID can be derived from the
driver’s reference number and the (constant) resource sub-ID using a “magic
formula” which is given in the source code within the function GetResID.
When MultiDA initializes during its Open routine, it calculates its menu
resource numbers using GetResID. It then reads the menus into memory one at a time
(using GetMenu), and stores the returned menuHandles into a global array. It then
stores the run-time ID into the MenuID field of the MenuHandle so that Menu Manager
routines find the correct menu when required to. For convenience, the example DA
stores the list of menu IDs in a global array so that the run-time IDs do not have to be
calculated more than once.
A second bit of ironing concerns handling the command keys in DA-owned menus.
Unfortunately the system does NOT handle menu command key selections for DA-owned
menus. When the desk accessory receives a keydown event, it must check if the
command key was held down, and if so, determine whether this corresponds to a menu
selection. The natural thing to do would be to call the toolbox routine MenuKey in order
to determine the corresponding menu and item number. Unfortunately, this call fails
from within a desk accessory. The problem here is that MenuKey returns 0 if the
associated menu item belongs to a desk accessory. Since a DA owns all the menus,
MenuKey always returns 0. To circumvent this problem, you can use the routine
DAMenuKey, given in the example program, to accomplish the same thing as MenuKey.
A third detail concerns the Apple menu. It’s nice for the desk accessory to put up
an Apple menu so that the user can view the about box, use Multi finder, or choose other
desk accessories. The problem here is that it isn’t very nice for an essentially
parasitic desk accessory to open up other desk accessories without asking the
underlying application’s permission. As far as I have been able to determine, only one
popular application objects to this behavior; unfortunately that application happens to
be Multi finder’s DA Handler. Apparently DA Handler counts the number of desk
accessories opened while it is active. Whenever that count drops down to 0, DA Handler
closes up its Multifinder partitition and quits. If several desk accessories are opened
from within MultiDA, however, DA Handler can get confused and miscount. This can
result in DA Handler exiting even though there are still desk accessories active. To
avoid this problem, MultiDA checks to see if DA Handler is active, and if so, greys out
the desk accessories items in the apple menu. It does not do this if Multifinder is not
active or if the option key was held down when the DA was opened, forcing the desk
accessory into the application’s heap.
The final detail is a merely cosmetic one. In a desk accessory with a single
window and multiple menus, it is appropriate for the desk accessory to put up its menus
when its window becomes active, and take down its menus when the window deactivates.
However, when the DA has multiple windows, it isn’t necessary to mess with the menu
bar on a deactivate event when all that’s happening is another one of the DA’s windows
is becoming active. In order to avoid unnecessary menu bar flickering, MultiDA checks
the event queue every time it receives a deactivate event. If it finds a pending activate
event pertaining to one of its other windows, it leaves the menu bar as is.
Spawning Windows
Multiple desk accessory windows are slightly easier to implement than multiple
menus. Opening desk accessory windows is easy: all you do is call NewWindow (or
GetNewWindow) and store the DA’s driver reference number in the windowKind field of
the newly- created window. This tells the system that the newly-opened window
belongs to the desk accessory. This is demonstrated in the routine OpenAWindow in the
example code.
The dCtlWindow field of the desk accessory’s driver control entry is supposed to
point to the desk accessory’s window. When the DA has multiple windows, this field
should point to the topmost DA window. A good time to update this field is when a
window becomes active. In the example code, the routine DoActivate, in addition to
swapping in the DA’s menus, places the activating window’s windowPtr into the
dCtlWindow field.
Closing a window in a multi-window desk accessory is somewhat trickier. When
the user clicks in any of the DA’s windows’ close boxes, the desk accessory receives a
Close call. Ordinarily this means that the desk accessory should bank its fires and quit.
However this is NOT what the user wants to happen when he’s only closing the topmost
of several DA windows. The correct thing to do when a Close call is received is to
determine how many DA windows are open. If more than one window is open, then the
DA closes the topmost window (the pointer to which can be obtained from the
dCtlWindow field) and informs the system that the DA didn’t close by returning a
CloseErr (-24). If only one of the desk accessory’s windows is open, then the DA
should go ahead and close up (it can, if it wishes, put up an “Are you sure?” alert and
allow the user to change his mind).
There is one circumstance, however, in which the DA has no choice about
whether to close. This occurs when the underlying application is quitting or when the
Mac is shutting down. To handle this case, MultiDA has the dNeedGoodbye bit set in its
header. When the heap is about to be reinitialized, the system sends MultiDA a
“goodbye kiss” call. This gives the DA a chance to close all its windows and clean up its
data.
The other thing to be careful about when one of the desk accessory’s multiple
windows is closed is to make sure that the dCtlWindow field of the device control entry
always points to a valid window. The CloseAWindow routine in the example source code
contains a subroutine called UpdateDCE. This routine runs through the DA’s windows
and updates the device control entry to point to the currently topmost DA window
immediately after a window is closed. Never leave a pointer to a disposed window in the
dCtlWindow field! This field must always remain valid. It is no use waiting for an
activate event to fix the dCtlWindow field, since the Mac may well crash long before an
activate event has had a chance to occur.
What should a multi-window desk accessories do when the user chooses the
already-open desk accessory from the Apple menu? When this happens, the desk
accessory receives a second Open call. One option is to activate just the topmost window
(the one stored in the dCtlWindow field of the driver control entry). I chose the
alternative behavior of bringing all the desk accessory’s windows to the front using the
ModifyWindows routine. This behavior, simulating as it does the Multifinder “layers”
effect, seemed more in keeping with what people expect.
Most Multi-window desk accessories will want to store a list of their currently
open windows in some sort of global array. This would let them, for example, keep a
parallel array of associated data structures, or create and maintain a Windows menu
that lists the open windows and lets the user bring them to the front selectively. To
keep the code simple, I didn’t do this in the example DA. Instead, whenever I want to
perform a repetitive action on all the windows, such as bringing them all forward,
stacking them up nicely, or closing them all, I call a routine called ModifyWindows.
This routine takes a procedure pointer as one of its parameters. It searches through the
linked window list for each of the DA’s windows (which it recognizes by the
windowKind field), and then calls the procedure pointer for each window in turn. While
playing with this way of doing things, I realized that some operations, such as stacking
the windows up, would look much nicer if the operation were performed starting at the
furthest back window and working forwards. The elegant way to do this was to make
ModifyWindows a recursive procedure; see the code for details.
Compiling the Example Code
To compile the desk accessory in from the listing that follows you need Think
Pascal and access to MPW rez or ResEdit. You must first use MPW rez (or ResEdit) to
create a resource file. Then set up a Think Pascal project with the build order shown
in Figure 2. You must use the drvrRuntime library rather than the standard runtime
library or you will get link errors! Set Think’s runtime options to use the resource
file you created. Then set up the project type dialog as shown in Figure 3.
Figure 2. Build Order.
Figure 3. Project Type.
Compile the project as a desk accessory. This will create a suitcase-type file
that can be installed with Font/DA mover, or used with Suitcase or Font/DA juggler.
Concluding Remarks
MultiDA demonstrates how to implement multiple windows and menus from
within a desk accessory. With these techniques, and with Think Pascal’s support for
global data and multiple segments in DA’s, who needs applications?
{File Multi DA.p}
{Example of a desk accessory with multiple windows and menus.}
{© 1990, Lincoln D. Stein}
unit MenuDA;
interface
function Main (DCtlE: DCtlPtr;
IOPB: ParmBlkPtr;
driveCall: Integer): OSErr;
implementation
const
{Define all of the possible driver calls}
DriverOpen = 0;
DriverPrime = 1;
DriverControl = 2;
DriverStatus = 3;
DriverClose = 4;
OpenErr = -23;
CloseErr = -24;
{Offsets to resource IDs for main dialog and alert}
DlogID = 0;
AboutID = 0;
{Subitem resource numbers of our menus}
AppleMenu = 0;
AboutItem = 1;
FileMenu = 1;
NewWindowItem = 1;
CloseItem = 2;
QuitItem = 4;
EditMenu = 2;
undoItem = 1;
cutItem = 3;
copyItem = 4;
pasteItem = 5;
clearItem = 6;
WindowMenu = 3;
CleanupItem = 1;
BeepItem = 2;
{Dialog itemlist}
NewWindowButton = 1;
EraseButton = 2;
EditText = 3;
MenuBar = (DAMenus, AppMenus);
var
done: boolean;
SavedMenuList: handle;
MenuIDs: array[0..WindowMenu] of integer;
OurMenus: array[0..WindowMenu] of MenuHandle;
OurMenuBar: boolean;
DCE: DCtlPtr;
OurName: str255;
NumWindows: integer;
WindowCounter: integer;
{ ********************************************** }
{ ************Global Utility Functions********** }
{ ********************************************** }
{The actual resource IDs of our resources depends on our desk
accessory's run-time device control reference number (this gets
switched around by Font/DA Mover and Suitcase. The "owned resource
sub-ID never changes, however. Use our dCtlRefNum to get the
run-time ID of our resources}
function GetResID (SubID: integer): integer;
begin
GetResID := BOR($C000, SubID + (BSL((Abs(DCE^.dCtlRefNum) - 1),
5)));
end;
{----------------------DAMenuKey--------------}
{MenuKey does not work from within a desk accessory because the
system does not respond correctly to the meta-keys used in DA menus.
This procedure is a substitute for MenuKey that performs the function
correctly. Given a character, this function will determine if it is a
menu meta-character and returns the menu ID in the high order word
and the item in the low order word just as MenuKey does. Note that
this code makes use of information stored in our globals, and is NOT
directly transferable to other desk accessories.}
function DAMenuKey (cmd: char): longint;
var
i, item: integer;
key: char;
begin
{Return a '0' as default}
DAMenuKey := 0;
{Capitalize lowercase letters}
if (cmd >= 'a') & (cmd <= 'z') then
cmd := Chr(Ord(cmd) - (Ord('a') - Ord('A')));
{Loop through each menu, looking for matches}
for i := 0 to WindowMenu do
{If we find an enabled menu then examine each enabled item in turn
until we find a matching command key.}
if BTST(OurMenus[i]^^.enableFlags, 0) then
for item := 1 to CountMItems(OurMenus[i]) do
if BTST(OurMenus[i]^^.enableFlags,item) then
begin
GetItemCmd(OurMenus[i], item, key);
if key <> Cmd then
cycle
else
DAMenuKey := BOR(item,BSL(MenuIDs[i],16));
HiliteMenu(MenuIDs[i]);
Exit(DAMenuKey);
end
end; {of FUNCTION MyMenuKey}
{**********************************************}
{************ Menu Handling Routines **********}
{**********************************************}
{------------------InitMenus--------------}
{Fetch and install our menus, remembering the application's menu bar
for later switching. We return the menu ID of any of our menus for
installation into the dCtlMenu field of the device control entry. It
doesn't matter exactly which of our menus we return.}
function InitMenus: integer;
const
CurApName = $910; {Low memory global}
{Hard code the name of the desk accessory layer in multi finder.
Note that there is a non-breaking space between DA and Handler.}
DALayer = 'DAHandler';
var
i: integer;
name: str255;
begin
for i := 0 to WindowMenu do
begin
MenuIDs[i] := GetResID(i);
OurMenus[i] := GetMenu(MenuIDs[i]);
OurMenus[i]^^.MenuID := MenuIDs[i];
end;
AddResMenu(OurMenus[AppleMenu], 'DRVR');
InitMenus := MenuIDs[AppleMenu];
{If we've been loaded into DA Handler, then we dim out our desk
accessories. This is done because DA Handler does not like desk
accessories opening other desk accessories!}
if StringPtr(CurApName)^ = DALayer then
for i:=1 to CountMItems(OurMenus[AppleMenu]) do
begin
GetItem(OurMenus[AppleMenu], i, name);
if name[1] = char($00) then
DisableItem(OurMenus[AppleMenu], i);
end;
end;
{----------------SaveMenuBar------------------}
{This procedure is called in order to make a copy of, and save, the
current menu bar data structure. It is conceivable that it may be
called twice in a row, so dispose of any previously saved menubars.}
procedure SaveMenuBar;
begin
if SavedMenuList <> nil then
Dispos handle(SavedMenuList);
SavedMenuList := GetMenuBar;
end;
{------------------InsertDAMenus----------------}
procedure InsertDAMenus;
var
i: integer;
begin
ClearMenuBar;
for i := 0 to WindowMenu do
InsertMenu(OurMenus[i], 0);
end;
{------------------AdjustMenus------------------}
{This procedure adjusts the menus periodically to allow for changes
It would typically be used enabling and disabling menu items, changing
item names, etc. as appropriate for the DA's state.}
procedure AdjustMenus;
begin
if NumWindows > 1 then
enableItem(OurMenus[Filemenu], CloseItem)
else
disableItem(OurMenus[FileMenu], CloseItem)
end;
{--------------------SetMenu--------------------}
{This procedure installs our menubar when one of our windows becomes
active. Pass it "DAMenus" to install our DA's menubar. Pass it
"AppMenus" to restore the application's menubar.}
procedure SetMenu ( which: MenuBar);
var
mBarEnable: ^integer;
{----}
{FUNCTION OursIsActivating is used to determine if one of our
windows is about to come to the top. If this is going to happen, then
there is no use switching menus when one of our windows is
deactivated. It calls EventAvail, looking for an activate event in
one of our own windows.}
function OursIsActivating: boolean;
var
Evt: eventRecord;
kind: integer;
begin
OursIsActivating := false;
if EventAvail(activMask, Evt) &
(BitAnd(Evt.modifiers,activeFlag)<>0) then
begin
kind := WindowPeek(Evt.message)^. windowkind;
OursIsActivating := (kind = DCE^.dctlRefNum)
end
end;
{----}
begin {PROCEDURE SetMenu}
mBarEnable := Pointer($A20);
{Install DA's menubar if requested to and our menu bar isn't there
already.}
if (which = DAMenus) then
begin
if not OurMenuBar then
begin
SaveMenuBar;
InsertDAMenus;
MBarEnable^ := DCE^.dctlMenu;
OurMenuBar := true;
end
end
else if not OursIsActivating then
begin
SetMenuBar(SavedMenuList);
MBarEnable^ := 0;
OurMenuBar := False;
end
end;
{**********************************************}
{ ********* Window Handling Routines ********* }
{********************************************* }
{----------------ModifyWindows---------------- }
{PROCEDURE ModifyWindows is passed a procedure parameter. It loops
through our windows performing the passed procedure on each of our
windows, starting with the window passed in Start. By calling itself
recursively, it performs the action on the bottom-most window first
and the top-most window last. A neat trick! }
procedure ModifyWindows (Start: UNIV WindowPeek;
procedure DoSomething (aW: Windowptr));
begin
if Start = nil then
Exit(ModifyWindows);
WindowCounter := 1;
ModifyWindows(Start^.NextWindow, DoSomething);
if Start^. windowKind = DCE^.dctlRefNum then
begin
DoSomething(WindowPtr(Start));
WindowCounter := succ(WindowCounter)
end;
end;
{------------------------------}
{The following procedures are used in calls to the ModifyWindows
procedure to do the same task to each of our windows in turn...}
{PROCEDURE CleanupProc stacks the windows in place one by one, using
the WindowCounter global to keep track of which window we're working
on.}
procedure CleanupProc (theWind: windowptr);
const
spacing = 10;
vStart = 40;
hStart = 5;
begin
HideWindow(theWind);
MoveWindow(theWind, WindowCounter * spacing + hStart, WindowCounter
* spacing + vStart, true);
ShowWindow(theWind)
end;
{------------------------------}
procedure BringWindowForward (aW: Windowptr);
begin
BringToFront(aW)
end;
{--------------------OpenAWindow----------------}
{FUNCTION OpenAWindow opens up a new window, stores our DA's
dCtlRefNum into its windowKind field so that the system knows it's a
DA's window, and sets the title and initial location in an
appropriate manner. We use the run-time name of our desk accessory to
form the window title. We also bump up our window counter to keep
track of how many windows are open. This would be a good place to
store the windowptr into an array or linked list, in order to keep
track of the windows more carefully.}
function OpenAWindow: Windowptr;
var
aW: windowptr;
WindowNo: str255;
theID:integer;
begin
theID:=GetResID(DlogID);
aW:= GetNewDialog(theID, nil, pointer(-1));
if aW <> nil then
begin
windowpeek(aW)^. windowKind := DCE^.dCtlRefNum;
NumToString(NumWindows, WindowNo);
SetWTitle(aW, concat(OurName, ' ', WindowNo));
WindowCounter := NumWindows;
CleanUpProc(aW);
NumWindows := NumWindows + 1;
end;
OpenAWindow := aW
end;
{----------------CloseAWindow------------------}
{PROCEDURE CloseAWindow closes down one of our windows and
decrements the NumWindows counter, in a more sophisticated desk
accessory, it would handle disposing of the various data structures,
files, etc. associated with the window. Note that if the window we're
closing is the same as that stored in the dCtlWindow field, we must
update the field to point to a current valid window. Local procedure
UpdateDCE handles this task.}
procedure CloseAWindow (aWindow: WindowPtr);
{----}
{Procedure UpdateDCE sets the DCE^.dCtlWindow field to point to our
topmost open window. The dCtlWindow field should always point to a
valid window or the desk accessory will die horribly in a matter of
ticks.}
procedure UpdateDCE;
var
aWindow: WindowPeek;
begin
DCE^.dCtlWindow := nil ;
aWindow := WindowPeek(FrontWindow);
while aWindow <> nil do
if aWindow^.WindowKind = DCE^.dCtlRefNum then
begin
DCE^.dCtlWindow := pointer(aWindow);
Exit(UpdateDCE);
end
else
aWindow := aWindow^.NextWindow;
end; {PROCEDURE UpdateDCE}
{----}
begin {PROCEDURE CloseAWindow}
DisposDialog(aWindow);
NumWindows := NumWindows - 1;
if DCE^.dCtlWindow = aWindow then
UpdateDCE;
SetMenu(AppMenus);
end;
{ ********************************************** }
{ ***************** Menu Handlers*************** }
{ ********************************************** }
{------------------DoApple----------------------}
{PROCEDURE DoApple handles the apple menu}
procedure DoApple (itemNo: integer);
var
dummy: integer;
name: str255;
begin
if itemNo = AboutItem then
dummy := Alert(GetResId(AboutID), nil )
else
begin
GetItem(OurMenus[ applemenu], itemNo, name);
dummy := OpenDeskAcc(name);
end
end;
{--------------------DoFile--------------------}
{PROCEDURE DoFile handles the file menu}
procedure DoFile (ItemNo: integer);
var
dummy: windowptr;
begin
case ItemNo of
NewWindowItem:
dummy := OpenAWindow;
CloseItem:
CloseAWindow(FrontWindow);
QuitItem:
done := true;
end;
end;
{------------------DoEdit----------------------}
{PROCEDURE DoEdit handles the Edit Menu}
procedure DoEdit (ItemNo: integer);
var
OurDlog: DialogPtr;
dummy: integer;
begin
OurDlog := FrontWindow;
if OurDlog <> nil then
begin
{Move the public scrap to the TE scrap for the dialog manager's
use.}
dummy := TEFromScrap;
case ItemNo of
undoItem:
sysbeep(5);
cutItem:
DlgCut(OurDlog);
copyItem:
DlgCopy(OurDlog);
pasteItem:
DlgPaste(OurDlog);
clearItem:
DlgDelete(OurDlog);
otherwise
end;
{Move the TE scrap to the public scrap.}
dummy := ZeroScrap;
dummy := TEToScrap
end
else
sysbeep(5);
end;
{----------------DoWindowMenu------------------}
{PROCEDURE DoWindowMenu handles the Windows menu}
procedure DoWindowMenu (itemNo: integer);
begin
case itemNo of
CleanupItem:
modifyWindows(FrontWindow, CleanupProc);
BeepItem:
sysbeep(20);
end;
end;
{--------------------DoMenus----------------}
{PROCEDURE DoMenus is the main dispatch for all menu selections.
Since the menu IDs are determined onlyat run time, we cannot use a
CASE constant structure here. Instead, we use a series of IF ELSE
statements to determine which of our menus was chosen.}
procedure DoMenus (MenuNo, ItemNo: integer);
var
Str1, Str2: str255;
dummy: integer;
OurDlog: dialogptr;
begin
if MenuNo = MenuIDs[AppleMenu] then
DoApple(ItemNo)
else if MenuNo = MenuIDs[FileMenu] then
DoFile(ItemNo)
else if MenuNo = MenuIDs[EditMenu] then
DoEdit(ItemNo)
else if MenuNo = MenuIDs[WindowMenu] then
DoWindowMenu(ItemNo);
Hilitemenu(0);
end;
{***********************************************}
{*********** Event Handling Routines ***********}
{***********************************************}
{------------------DoActivate------------------}
{PROCEDURE DoActivate handles activate/deactivate events in our DA's
windows. On an activate event we install the DA's menu bar and update
DCE^.dCtlWindow field to point to the active window. This assures
that if our DA is later selected from the application's apple menu,
our current active window will be brought to the foreground.}
procedure DoActivate (var Evt: event record);
var
active: boolean;
theWindow: Windowptr;
kind: integer;
begin
active := BitAnd(Evt.modifiers, activeFlag) <> 0;
if active then
begin
theWindow := pointer(Evt.message);
kind := windowPeek(theWindow)^. windowkind;
if kind <> DCE^.dctlRefNum then
Exit(DoActivate);
DCE^.dCtlWindow := theWindow;
SetMenu(DAMenus);
end
else
SetMenu(AppMenus);
end;
{------------------MetaKey------------------}
{FUNCTION MetaKey checks whether the cloverleaf key was pressed
during keypress. If so, it gets the menu and item from DAMenuKey and
passes the menu selection on to DoMenus. If the cloverleaf was not
depressed, then MetaKey returns FALSE so that the caller knows to
handle the keypress normally.}
function MetaKey(var Evt: event record): boolean;
var
aChr: char;
Tangled: longint;
begin
if BitAnd(cmdKey, Evt.modifiers) <> 0 then
begin
aChr:= Char(BitAnd(Evt.message, charCodeMask));
Tangled:= DAMenuKey(aChr);
DoMenus(HiWord(Tangled), LoWord(Tangled));
MetaKey:= true
end
else
MetaKey:= false;
end;
{--------------------DoIdle----------------------}
{PROCEDURE DoIdle gets called for null events. It does two things:
{ 1. It checks the cursor and changes it into an I-beam when the
cursor is over the window's text edit field. }
{ 2. It manufacturer's a "dummy" null event and calls DialogSelect
so that the text edit cursor gets flashed.}
{This is the place to do any other background processing.}
procedure DoIdle;
var
theDlog: DialogPtr;
itemHit: integer;
event: EventRecord;
dummy: boolean;
aPt: point;
begin
theDlog := DCE^.dctlWindow;
{First make the mouse into an I-Beam if we're above the text field}
GetMouse(aPt);
with dialogPeek(theDlog)^ do
begin
if textH <> nil then
if PtInRect(aPt, textH^^.viewRect) then
SetCursor(GetCursor(IBeamCursor)^^)
else
initCursor
end;
{Next call DialogSelect with a null event in order to blink the
cursor}
event.what := NullEvent;
dummy := dialogSelect( event, theDlog, Itemhit)
end;
{------------------HandleEvents----------------}
{PROCEDURE HandleEvents is the main dispatcher for all events
appertaining to our DA.}
procedure HandleEvents (var Evt: EventRecord);
var
dummy: windowptr;
itemHit: integer;
WhichDialog: DialogPtr;
begin
AdjustMenus;
{We do some pre-processing before calling DialogSelect}
case Evt.what of
ActivateEvt:
DoActivate(Evt);
KeyDown, AutoKey:
if MetaKey(Evt) then
Exit(HandleEvents);
otherwise
end;
{Here we call DialogSelect to do most of the housekeeping window
tasks}
if DialogSelect(Evt, whichDialog, Itemhit) then
case itemHit of
NewWindowButton:
dummy := OpenAWindow;
EraseButton:
begin
SelIText( whichDialog, EditText, 0, 10000);
DlgDelete(WhichDialog);
end;
otherwise
end;
end; {procedure HandleEvents}
{************************************************}
{**************** Main DA Routines **************}
{************************************************}
{ ================== CLOSE ======================}
function CLOSE: OSErr;
var
i: integer;
begin
Close := NoErr;
{If we don't have any window, then we haven't been opened and Close
is being called}
{inappropriately.}
if DCE^.dCtlWindow = nil then
Exit(Close);
{Clean up after ourselves}
with DCE^ do
begin
ModifyWindows(FrontWindow, CloseAWindow);
dctlWindow := nil ;
for i := 0 to WindowMenu do
ReleaseResource( handle(OurMenus[i]));
Dispos handle(SavedMenuList);
dCtlMenu := 0;
end;
end; {of function CLOSE}
{ ================ OPEN =====================}
function OPEN (DCTlE: DCtlPtr;
IOPB: ParmBlkPtr): OsErr;
var
aWind: WindowPeek;
begin
open := NoErr;
OurName := IOPB^.ioNamePtr^;
with DCE^ do
begin
if dctlWindow = nil then
{The window is nil, so initialize and allocate everything!}
begin
if dCtlStorage = nil then
begin
Sysbeep(20);
Open := OpenErr;
end
else
begin
dctlWindow := OpenAWindow;
dCtlMenu := InitMenus;
end
end
else
begin
{If we get here, then we are already open. We must bring all our
windows forward. First bring forward all windows below our topmost DA
window. This has the effect of bringing forward ALL our windows when
the DA is selected without changing their relative order. Next select
our topmost window to bring it to the front. The topmost window is
already stored in the DCE^.dctlWindow field.}
aWind:= WindowPeek(dctlWindow)^.nextWindow;
ModifyWindows(aWind, BringWindowForward);
{Now select our topmost window}
SelectWindow(dctlWindow);
end;
end; {of WITH clause}
end; {of OPEN}
{=================== CONTROL =================== }
function CONTROL (IOPB: ParmBlkPtr): OsErr;
const
accGoodBye = -1;
EventPtr = ^EventRecord;
var
APort: GrafPtr;
begin {functon CONTROL}
Control := NoErr;
GetPort(aPort);
SetPort(DCE^.dctlWindow);
{Dispatch for all the different control calls}
case IOPB^.csCode of
accEvent:
HandleEvents(EventPtr(IOPB^.ioMisc)^);
accCursor:
DoIdle;
accMenu:
DoMenus(IOPB^.csParam[0], IOPB^.csParam[1]);
accGoodbye:
Control := Close;
otherwise
end; {of case statement}
SetPort(APort);
end; {function CONTROL}
{******************************************}
{************MAIN FUNCTION*****************}
{******************************************}
{FUNCTION Main is the entry point for the DA. THINK Pascal will
intercept the driver (OPEN/CLOSE/CONTROL) call and funnel it through
this routine, along with a pointer to the device control entry, the
parameter block, and a selector indicating which driver call was
made. Pascal very nicely sets up a global storage block}
function MAIN (DCtlE: DCtlPtr;
IOPB: ParmBlkPtr;
driveCall: Integer): OSErr;
begin
{Need to call RememberA4 up front if our dialogs have user items or
if we get called at interrupt time. This code doesn't use these
features, but call it just in case.}
RememberA4;
Done := false;
DCE := DCtlE;
case driveCall of
DriverOpen:
Main := Open(DCtlE, IOPB);
DriverControl:
with DCE^ do
begin
{Turn off further control calls while servicing this one in order to
avoid re-entrancy issues}
dCtlFlags := BitAnd(dCtlFlags, $FBFF);
Main := Control(IOPB);
dCtlFlags := BitOr(dCtlFlags, $0400);
end;
DriverClose:
if NumWindows > 1 then
{We have more than one window open, so just close topmost}
begin
CloseAWindow(FrontWindow);
Main := CloseErr
end
else
Main := Close;
DriverStatus, DriverPrime:
Main := NoErr;
end; {case statement}
{This happens when the user selects "quit" from the menus or there
is a fatal error.}
if Done then
begin
Main := Close;
CloseDeskAcc(DCE^.dctlRefnum);
end
end; {of MAIN function}
end. {of the whole unit}