Thursday 28 December 2023

Dialog Disillusion - The Mac Programming Primer Let Me Down

 Introduction

We did a bit of Macintosh programming at UEA, my undergraduate University between 1986 and 1989. Here we mostly used the interpreted MacPascal and a couple of sheets of the list of ToolBox APIs. We had infrequent access to MPW Pascal on the Mac IIs in the 3rd year, but the vast majority of development was done on Mac 512Ks and Mac Plusses.

This meant that we didn't really learn Macintosh programming properly. That's partly because MacPascal didn't support it properly (it used wacko inline functions to access the ToolBox), partly because we didn't get enough time on the Macs and partly because we just didn't have enough usable documentation.

So, when I found a copy of The Macintosh Pascal Programming Primer in about 1993 when I finally had a Mac (a Performa 400), I was overjoyed! I followed the entire set of examples from beginning to end and found them really educational: a whole bunch of well-written example applications that covered most of the needs of Toolbox API-based programs. The only difference was that I was using THINK C 5.0.4 instead of THINK Pascal, but it was easy to translate.

I used this knowledge to write a 16-bit RISC CPU simulator that gained me access to an MPhil degree in Computer Architecture at the University of Manchester between 1996 and 1998.

The Problem

Recently I've wanted to write a simple simulation framework for the classic Mac OS that consists of a dialog box to enter parameters and a main window to run the simulation. I probably want to integrate the dialog box with the main window and allow it to be updated live, but to start with I thought it would be easier to use a modal dialog, so that the user interaction would be:
  1. Edit the parameters
  2. Start/Restart the simulation
  3. Maybe stop the simulation before it ends
  4. Go back to steps 1 or 2 or Quit.
I started by taking the demo THINK C program: oopsBullseye and then adding the Dialog handling code from the Macintosh Pascal Programming Primer. But it didn't work - it just crashed the Mac every time (actually just a miniVMac emulator, but it's still a crash).

I wondered what kind of mistake I'd made, so I went back to the original Dialog chapter (chapter 6) and followed it through. Lo-and-behold, it worked. I still couldn't see where I'd gone wrong, but I thought it was because on my version, I could see that the dialog box appeared behind the main window, and that seemed to hang it. So I modified the Dialog demo to make it more like my application: the window would be open all the time (not just when the countdown was happening) and countdowns could be interactively started or restarted. I had to support update events.

And then I found out that this modified dialog application didn't work any more either! It had the same problem, the dialog box appeared behind the main window and crashed when ModalDialog was called. I scoured my copy of Inside Macintosh and Macintosh Toolbox essentials (I have a paper copy of both) and found some example Dialog box code for modal dialogs, but it still wasn't obvious what the difference was.

It turns out that the Macintosh Pascal Programming Primer is doing Dialog boxes really badly! I was gutted! Their example uses a couple of poor idioms which would mislead other programmers and it makes all the difference.

Analysis

TMPPP does two basic things that are wrong.

Firstly, it creates a global dialog box in DialogInit() (by using GetNewDialog(..) to read it in from the application's resource) which then sits there in memory all the time as a window you can't see. This means that when any other windows are created, the dialog box will pop up behind them when ShowWindow(dialogPtr) is called and then ModalDialog(..) will crash (the Mac!).

What it should do is create the dialog box when needed using GetNewDialog(..), i.e. in the Dialog Box handler, and when the user has finished with it, dispose of the dialog box (DisposeDialog()). Then the operation of the modal dialog is handled all in one place, and the Mac can deallocate the dialog box memory when it isn't needed, which is what we want.

Secondly, it violates the standard model / view / controller paradigm. Here, essentially, the model is the parameters used the dialog box. But in their example, they store the actual parameters in the dialog box items themselves; then when the dialog handler is called, they're saved to an internal data structure; and only if the user presses cancel, the internal data structure is used to restore back to the dialog box itself.

It should be done the other way around, the model is the internal data structure. When calling the Dialog box handler, the parameters should get copied to the dialog box items (which is equivalent to RestoreSettings(..)); and when the user quits by clicking [Save], the new items' values are copied back to the internal data structure, which is the SaveSettings(..) option ([Cancel] doesn't do anything, it just quits dialog box operations without updating the internal data structure).

The New Dialog Box Demo

So, my new Dialog Box demo is included here. It's significantly shorter, <500 lines; doesn't use the Notification manager, but importantly does use the modal dialog box the way it's supposed to. I avoid most of the repeated copying of lines of code by factoring the code that sets and gets controls: I think that this is going to be just as easy for new programmers to understand, because they won't have to scan a whole set of very similar lines of code to understand what each set of lines is doing: they can just go back to the lower-level getter/setter code and when they do their own dialog boxes, they'll be more likely to factor it too.

The resources are all almost exactly the same. I removed a menu option, because it no longer applied. You don't need the SICN icon.


Re-source¹ Fields.. [Close Window] Info².. [Close, Close]
DITL [OK]  
(see image below. Start with the Save, then Cancel buttons, then the other fields)

ID=400, Name="Alarm", Purgeable (only)
DITL [OK]
(see image below. Start with the OK button, then the text field)

ID=401, Name="About", Purgeable (only)
ALRT [OK] TOP=40, Bottom=142, Left=40, Right=332, DITL=401, Default Color ID=401, Name="About", Purgeable (only)
DLOG [OK] TOP=40, Bottom=200, Left=60, Right=320, DITL=400, Default Color, Standard double-border Dialog style, Not Initially visible, No close box ID=400, Name="Alarm", Purgeable (only)
MENU [OK] [X] Enabled, Title=• Apple Menu[ENTER], [X] Enabled, Title="About..."[ENTER] [ ]Enabled, • Separator line ID=400, No attributes.
MENU [OK] [X] Enabled, Title="File"[ENTER], [X] Enabled, Title="Settings..", Cmd-Key:S[ENTER], Title="Run", Cmd-Key:R[ENTER], Title="Quit", Cmd-Key:Q[ENTER] ID=401, No attributes.
MENU [OK] [X] Enabled, Title="Edit"[ENTER], no options [ ] Enabled: Title="Undo", Cmd-Key:Z[ENTER], Separator Line[Enter], Title="Cut", Cmd-Key:X[ENTER], Title="Copy", Cmd-Key:C[ENTER], Title="Paste", Cmd-Key:V[ENTER], Title="Clear", Cmd-Key:none[ENTER] ID=402, No attributes.
MBAR [OK] Each time, click in '****', choose Resource:Insert New Field(s) for Menu Res IDs 400, 401, 402. Top should say "# of menus 3 at the end." ID=400, No attributes.
WIND [OK] Close, then choose Resource:Open Using Template [WIND] [OK]. Bounds Rect= 70, 36, 106, 156 [Set], Proc ID=0, Visible=false, GoAway=false, RefCon=0, Title="Countdown", Auto Position=$0000. ID=400, Name="Countdown", Purgeable (only)

Parameters Ditl


About Ditl




When you've finished, close the .rsrc file. ResEdit will ask you to save it - save it. Then open up the Dlog.π project.  Choose File:New and create a stub of a C program:

int main(void)
{
    return 0;
}

Choose File:Save to save it as Dlog.c. Choose Project:Add "Dlog.c" to add the file to the project. You don't need to do anything clever to add the rsrc file to the project, THINK C will automatically associate the .rsrc with the same prefix as your application. 

Now you want to replace the dummy program with the rest of file. When you've finished...

Dlog.h

/**
* @file: Reminder.h
*/

#ifndef Reminder_h
#define Reminder_h

#define kBaseResId 400
#define kAboutAlert 401
#define kBadSysAlert 402

#define kSleep 60

#define kSaveButton 1
#define kCancelButton 2
#define kTimeField 4
#define kSOrMField 5
#define kSoundOnBox 6
#define kIconOnBox 7
#define kAlertOnBox 8
#define kSecsRadio 10
#define kMinsRadio 11

#define kDefaultSecsId 401
#define kDefaultMinsId 402

#define kOff 0
#define kOn 1

#define kSecondsPerMinute 60

#define kTop 25
#define kLeft 12

#define kMarkApplication 1
#define kAppleMenuId (kBaseResId)
#define kFileMenuId (kBaseResId+1)
#define kAboutItem 1

#define kChangeItem 1
#define kStartStopItem 2
#define kQuitItem 3

#define kSysVersion 2

typedef enum{
  kBoolFalse=0,
  kBoolTrue=1
}tBool;

typedef enum {
  kTimeUnitSeconds=0,
  kTimeUnitMinutes=1
}tTimeUnit;

typedef struct {
  long iTime;
  int iSound, iIcon, iAlert;
  tTimeUnit iUnit;
}tSettings;



extern Handle DlogItemGet(DialogPtr aDialog, int aItem);
extern void CtlSet(DialogPtr aDialog, int aItem, int aValue);
extern int CtlGet(DialogPtr aDialog, int aItem);
extern void CtlFlip(DialogPtr aDialog, int aItem);
extern void ITextSet(DialogPtr aDialog, int aItem, Str255 *aStr);

extern void StartCountDown(long aNumSecs);
extern void HandleCountDown(void);
extern void UpdateCountDown(void);

extern void RestoreSettings(DialogPtr aSettingsDialog);
extern void SaveSettings(DialogPtr aSettingsDialog);
extern void HandleDialog(void);
extern void HandleFileChoice(int aTheItem);
extern void HandleAppleChoice(int aTheItem);
extern void HandleMenuChoice(long aMenuChoice);
extern void HandleMouseDown(void);
extern void HandleEvent(void);
extern void MainLoop(void);
extern void MenuBarInit(void);
extern void DialogInit(void);
extern void WinInit(void);
extern tBool Sys6OrLater(void);
extern void ToolboxInit(void);
extern int main(void);

#endif // Reminder_h

Dlog.c

/**
 * Dlog.c
 */

#include "Dlog.h"

tBool gDone;

EventRecord gTheEvent;
tSettings gSavedSettings;


WindowPtr gCountDownWindow;
long gTimeout, gOldTime;
tBool gIsCounting;

Handle DlogItemGet(DialogPtr aDialog, int aItem)
{
  int itemType;
  Rect itemRect;
  Handle itemHandle;
  GetDItem(aDialog, aItem, &itemType, &itemHandle, &itemRect);
  return itemHandle;
}

void CtlSet(DialogPtr aDialog, int aItem, int aValue)
{
  Handle itemHandle=DlogItemGet(aDialog, aItem);
  SetCtlValue((ControlHandle)itemHandle, aValue);
}

int CtlGet(DialogPtr aDialog, int aItem)
{
  Handle itemHandle=DlogItemGet(aDialog, aItem);
  return GetCtlValue((ControlHandle)itemHandle);
}

/*
void ITextSet(DialogPtr aDialog, int aItem, Str255 *aStr)
{
  Handle itemHandle=DlogItemGet(aDialog, aItem);
  SetIText(itemHandle, aStr);
}
*/
void CtlFlip(DialogPtr aDialog, int aItem)
{
  Handle itemHandle=DlogItemGet(aDialog, aItem);
  SetCtlValue((ControlHandle)itemHandle,
    (GetCtlValue((ControlHandle)itemHandle)==kOn)? kOff:kOn);
}

void StartCountDown(long aNumSecs)
{
  GetDateTime(&gOldTime);
  if(gSavedSettings.iUnit==kTimeUnitMinutes) {
    aNumSecs*=kSecondsPerMinute;
  }
  gTimeout=gOldTime+aNumSecs; // this is the timeout.
  gIsCounting=kBoolTrue;
}

// Called on Null event.
void HandleCountDown(void)
{
  if(gIsCounting==kBoolTrue) {
    long myTime;
    GetDateTime(&myTime);
    if(myTime!=gOldTime) {
      GrafPtr oldPort;
      gOldTime=myTime; // gTimeout-gOldTime==remaining seconds.
      // gen update, but how?
      GetPort(&oldPort);
      SetPort((GrafPtr)gCountDownWindow);
      InvalRect(&gCountDownWindow->portRect);
      SetPort(oldPort);
    }
  }
}

void UpdateCountDown(void)
{
  //
  WindowPtr win=(WindowPtr)gTheEvent.message;
  if(win==gCountDownWindow) {
    long remaining=gTimeout-gOldTime;
    Str255 myTimeString;
    BeginUpdate(win);
    MoveTo(kLeft, kTop);
    if(remaining<=0 || gIsCounting==kBoolFalse) {
      remaining=0;
      gIsCounting=kBoolFalse;
    }
    NumToString(remaining, myTimeString);
    EraseRect(&(gCountDownWindow->portRect));
    DrawString(myTimeString);
    EndUpdate(win);
  }
}

void RestoreSettings(DialogPtr aSettingsDialog)
{
  Handle itemHandle;
  Str255 timeString;
  tBool isInSeconds=(gSavedSettings.iUnit==kTimeUnitSeconds)?
      kBoolTrue:kBoolFalse;
 
  itemHandle=DlogItemGet(aSettingsDialog, kTimeField);
  NumToString(gSavedSettings.iTime, &timeString);
  SetIText(itemHandle, timeString);
 
  CtlSet(aSettingsDialog, kSoundOnBox, gSavedSettings.iSound);
  CtlSet(aSettingsDialog, kIconOnBox, gSavedSettings.iIcon);
  CtlSet(aSettingsDialog, kAlertOnBox, gSavedSettings.iAlert);
  CtlSet(aSettingsDialog, kSecsRadio, (isInSeconds==kBoolTrue)?kOn:kOff);
  CtlSet(aSettingsDialog, kMinsRadio, (isInSeconds==kBoolFalse)?kOn:kOff);

  itemHandle=DlogItemGet(aSettingsDialog, kSOrMField);
  SetIText(itemHandle,(gSavedSettings.iUnit==kTimeUnitSeconds)?
      "\pseconds":"\pminutes");
}

void SaveSettings(DialogPtr aSettingsDialog)
{
  Handle itemHandle;
  Str255 timeString;

  itemHandle=DlogItemGet(aSettingsDialog, kTimeField);
  GetIText(itemHandle, &timeString);
  StringToNum(timeString, &gSavedSettings.iTime);
 
  gSavedSettings.iSound=CtlGet(aSettingsDialog, kSoundOnBox);
  gSavedSettings.iIcon=CtlGet(aSettingsDialog, kIconOnBox);
  gSavedSettings.iAlert=CtlGet(aSettingsDialog, kAlertOnBox);
  gSavedSettings.iUnit=(CtlGet(aSettingsDialog, kSecsRadio)==kOn)?
        kTimeUnitSeconds:kTimeUnitMinutes;
}

void HandleDialog(void)
{
  tBool dialogDone;
  int itemHit;
  long alarmDelay;
  Handle itemHandle;
  DialogPtr settingsDialog;
 
  settingsDialog=GetNewDialog(kBaseResId, NULL, (WindowPtr)-1);

  ShowWindow(settingsDialog);
  RestoreSettings(settingsDialog);
 
  dialogDone=kBoolFalse;
  while(dialogDone==kBoolFalse) {
    ModalDialog(NULL, &itemHit);
    switch(itemHit) {
    case kSaveButton:
      SaveSettings(settingsDialog); // update them.
      dialogDone=kBoolTrue;
      break;
    case kCancelButton:
      dialogDone=kBoolTrue;
      break;
    case kSoundOnBox:
    case kIconOnBox:
    case kAlertOnBox:
      CtlFlip(settingsDialog, itemHit);
      break;
    case kSecsRadio:
      CtlSet(settingsDialog, kSecsRadio, kOn);
      CtlSet(settingsDialog, kMinsRadio, kOff);

      itemHandle=DlogItemGet(settingsDialog, kSOrMField);
      SetIText(itemHandle, "\pseconds");
      break;
    case kMinsRadio:
      CtlSet(settingsDialog, kSecsRadio, kOff);
      CtlSet(settingsDialog, kMinsRadio, kOn);

      itemHandle=DlogItemGet(settingsDialog, kSOrMField);
      SetIText(itemHandle, "\pminutes");
      break;
    }
  }
  DisposeDialog(settingsDialog);
}

void HandleFileChoice(int aTheItem)
{
  switch(aTheItem) {
  case kChangeItem:
    HandleDialog();
    break;
  case kStartStopItem:
    HiliteMenu(0);
    StartCountDown(gSavedSettings.iTime);
    break;
  case kQuitItem:
    gDone=true;
    break;
  }
}

void HandleAppleChoice(int aTheItem)
{
  Str255 accName;
  int accNumber, itemNumber, dummy;
  MenuHandle appleMenu;
  switch(aTheItem) {
  case kAboutItem:
    NoteAlert(kAboutAlert, NULL);
    break;
  default:
    appleMenu=GetMHandle(kAppleMenuId);
    GetItem(appleMenu, aTheItem, &accName);
    OpenDeskAcc(accName);
    break;
  }
}

void HandleMenuChoice(long aMenuChoice)
{
  int theMenu, theItem;
  if(aMenuChoice!=0) {
    theMenu=HiWord(aMenuChoice);
    theItem=LoWord(aMenuChoice);
    switch(theMenu) {
    case kAppleMenuId:
      HandleAppleChoice(theItem);
      break;
    case kFileMenuId:
      HandleFileChoice(theItem);
      break;
    }
    HiliteMenu(0);
  }
}

void HandleMouseDown(void)
{
  WindowPtr whichWindow;
  int thePart;
  long menuChoice, windSize;
  thePart=FindWindow(gTheEvent.where, &whichWindow);
  switch(thePart) {
  case inMenuBar:
    menuChoice=MenuSelect(gTheEvent.where);
    HandleMenuChoice(menuChoice);
    break;
  case inSysWindow:
    SystemClick(&gTheEvent, whichWindow);
    break;
  case inDrag:
    DragWindow(whichWindow, gTheEvent.where, &screenBits.bounds);
    break;
  case inGoAway:
    gDone=kBoolTrue;
    break;
  }
}

void HandleEvent(void)
{
  char theChar;
  tBool dummy;
  WaitNextEvent(everyEvent, &gTheEvent, kSleep, NULL);
  switch(gTheEvent.what){
  case mouseDown:
    HandleMouseDown();
    break;
  case keyDown: case autoKey:
    theChar=(char)(gTheEvent.message & charCodeMask);
    if((gTheEvent.modifiers & cmdKey)!=0) {
      HandleMenuChoice(MenuKey(theChar));
    }
    break;
  case nullEvent:
    HandleCountDown();
    break;
  case updateEvt:
    UpdateCountDown();
    break;
  }
}

void MainLoop(void)
{
  gDone=kBoolFalse;
  while(gDone==kBoolFalse) {
    HandleEvent();
  }
}

void MenuBarInit(void)
{
  Handle myMenuBar;
  MenuHandle aMenu;
  myMenuBar=GetNewMBar(kBaseResId);
  SetMenuBar(myMenuBar);
  DisposHandle(myMenuBar);
  aMenu=GetMHandle(kAppleMenuId);
  AddResMenu(aMenu, 'DRVR');
  DrawMenuBar();
}

void WinInit(void)
{
  gCountDownWindow=GetNewWindow(kBaseResId, NULL, (WindowPtr)-1);
  gIsCounting=kBoolFalse;
  SetPort(gCountDownWindow);
  TextFace(bold); // it's the same in THINK C.
  TextSize(24);
  ShowWindow(gCountDownWindow);
}

void DialogInit(void)
{
  gSavedSettings.iTime=12;
 
  gSavedSettings.iSound=kOn;
  gSavedSettings.iIcon=kOn;
  gSavedSettings.iAlert=kOn;
 
  gSavedSettings.iUnit=kTimeUnitSeconds;
}

tBool Sys6OrLater(void)
{
  OSErr status;
  SysEnvRec SysEnvData;
  int dummy;
  tBool result=kBoolTrue;
  status=SysEnvirons(kSysVersion, &SysEnvData);
  if(status!=noErr || SysEnvData.systemVersion<0x600) {
    StopAlert(kBadSysAlert, NULL);
    result=kBoolFalse;
  }
  return result;
}

void ToolboxInit(void)
{
  InitGraf(&thePort);
  InitFonts();
  InitWindows();
  InitMenus();
  TEInit();
  InitDialogs(NULL);
  MaxApplZone();
}

int main(void)
{
  ToolboxInit();
  if(Sys6OrLater()) {
    DialogInit();
    MenuBarInit();
    WinInit();
    InitCursor();
    MainLoop();
  }
  return 0;
}

Conclusion

As a whole, The Macintosh Pascal (and C) Programming Primer is a brilliantly simple introduction to traditional (non-OO) Macintosh programming. However, the Dialog box chapter ("Working With Dialogs") is a major exception. After a bit of sleuthing (which I should never have needed to do), I worked out the problem and rewrote a better demo (and made sure the cursor is an arrow instead of the watch icon). This means I'm closer to my goal of writing a simple simulation framework.

For the chronically lazy amongst you, feel free to download the full project from here.

No comments: