Showing posts with label Think Pascal. Show all posts
Showing posts with label Think Pascal. Show all posts

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.

Sunday, 5 March 2023

MiniMorseMac

Introduction

After porting Mini Morse from the ZX81 to the ZX80, it occurred to me that Mini Morse would be a good candidate for converting to a classic Macintosh. I used my original introduction to classic Macintosh programming: Inside The Toolbox with Think Pascal.

I have that book, but used it to develop programs using Think C 5.0.4, because I had a proper copy of that; it works in a similar way and I'm more comfortable with C instead of Pascal. It turned out that it was fairly straightforward and satisfying to code, with the final application coming in at 3020 bytes.

Digital Archeology

Unfortunately, although Think C is readily available, I'm not sure of its legal status with regard to copying, even though it's abandonware. However, I am aware that Think Pascal, certainly up to version 4.5.4 at the end of its development is available for downloads for personal use.

I had had a version of Think Pascal 4.5.4 downloaded well over a decade ago, and had used it for some minor projects. Again, unfortunately, it's not easy to download. The web page still exists, but the Symantec link it uses at the time is now broken.

So, initially I ported my Mini Morse program to my Think Pascal 4.5.4, which I copied indirectly from my PowerBook 1400c (via a SCSI Zip drive and USB Zip drive). Then I found that the application it created, was about 16kB long. It seemed very unlikely to me that a straightforward Toolbox program, converted from Think C 5 for a Mac 7 Toolbox geared for Pascal would be that much bigger. And this lead me to the conclusion that Think Pascal 4.5.4 is not such a great IDE for early era Mac application development.

Then began the quest to see if it was any better under an earlier version. It turns out that Think Pascal 4.0.2 is available from a number of sources including Macintosh Garden, but the source I would trust the most is the link from MacGui.com. This downloads a Stuffit .sit file. You'll also need to download ResEdit, which you can from the same site here as a disk image.

Now, Stuffit still exists as a format for compressing Mac and Windows files, but the .sit file in question can't be expanded by the version of Stuffit Lite I had on my miniVMac emulator (as this Stuffit Lite was too early), and it couldn't be expanded by a later version of Stuffit Lite (because the compression format is no longer supported). After several approaches, I found that my iMac G3 running Mac OS 9 had a version of Stuffit that could expand that file, but I couldn't then use it to compress it so that it could work with Stuffit Lite on miniVMac.

In the end, I took a different approach. I used Disk Copy on the iMac G3 to create a .img disk image from the Think Pascal folder; which I could then open as a disk on miniVMac. From here I could create a new project in Think Pascal 4.0.2, which, when compiled as an application, took up only 2803 bytes.

This illustrates an increasing issue with the maintenance of retro software on modern platforms, via emulation or other means: the number of bridging technologies is becoming more convoluted as modern systems leave behind archaic technology. It's why I think I have to, or ought to maintain a number of Macs between my oldest (a 68000 Mac Plus from the 1986 era) to my MacBook Pro from 2018 via a Performa 400 (68030 Colour Mac) PowerBook 1400C (an early PowerPC Mac which runs System 7.5.3 and Mac OS 8.1 reasonable); iMac G3 (which runs Mac OS 9.2 and Mac OS X 10.3 OK); Mac Book Core Duo (which runs up to Mac OS X 10.6.8). Using these machines gives me a continuity of technology.

Running MiniMorse For 68K Mac

The purpose of this blog is to show how compact a 1kB ZX81 program can be on an early GUI computer. The ZX81 was a simple text-based 8-bit home computer which could officially support up to 16kB of RAM, but came with 1kB of RAM as default, along with its programming language, in an 8kB ROM.

The early Macintosh was a major conceptual leap in computing, supporting a sophisticated (for the time) Graphical User Interface with Windows, Icons, Menus and directly controllable gadgets. The earliest versions used a 32-bit, 68000 CPU and provided 128x the memory of a ZX81, with 8x the ROM. I never had the opportunity to use one of these machines directly, but at the University Of East Anglia, where I did my Computer Science BSc, they had the next model, with 512kB of RAM. These Macs could run simple development environments such as the interpreted Mac Pascal; or ETH Modula-2.

So, it's a natural question to ask how much more bloated equivalent applications were, on computers with that level of sophistication and about 8x to 32x the resources. And the answer, as far as Macs go, is that they are proportionally more sophisticated (a 3kB program / 400 bytes is about 7.5x larger).

What is really different is the length of the program itself. The ZX81 program is tokenised, so the source code for the version that takes up just under 400 bytes, is also 400 bytes, because the source is the final program. On the other hand, the Mac Pascal version takes up nearly 7kB of source code, around 17x larger, even though it compiles into a program only 7.5x larger.

The MiniMorse application can be found here. Download it onto your computer. You'll need a means of converting from .hqx to the application itself, but in https://system7.app that's easy. First you need to copy the file, by opening "The Outside World" server then dragging the MiniMorse app onto the Downloads folder. Drag the .hqx file to The Infinite Hard Disk. Then you open Infinite HD:Utilities:Compact Pro; Open Compact Pro and select Misc:Convert FROM Bin Hex. Navigate to the .hqx file in The Infinite Hard Disk and then save the Application in the same location. After a few seconds, the application appears in The Infinite Hard Disk.

Double-click the application to run it. You'll see a simple Mac Application:


As with the ZX81 version, typing a letter or digit generates the Morse code and typing a morse code generates the letter or digit. In addition to the ZX81 program, like a good Macintosh application MiniMorse sets the cursor to an arrow (as you can see), supports an about dialog:

And a Quit option with the command Q short-cut.

You'll find that MiniMorseMac handles window updates properly, can move to the back or front and can launch a desk accessory. It should work with Multifinder under System 6 or earlier or simply the Finder. You can shrink the memory allocation down to about 16kB and it will work fine.

Typing In The Program

MiniMorse Mac is short enough to type in. So, here's how to create the application from scratch. Firstly, create a folder called DevProjects and within it, create a folder called MiniMorse. This is where the Pascal project will go.

Next, run Think Pascal. It might complain that it needs 4MB to run, but it doesn't. Change the memory allocation to just 1MB and it'll be fine (From the Finder, choose File:Get Info on the application and change the box on the bottom right).

When you run Think Pascal, it will want to start with a project. Navigate to the MiniMorse folder and click on [New]. It will ask for a project name, type MiniMorse.π ('π' is option+p). Don't create an instant project, click on Done and the project window will be created. Save and then Quit.

The next step is to create the resources. Find ResEdit 2.1 and open the application. Create a new file and call it MiniMorse.π.rsrc. It's fairly tricky to describe how create resources interactively, but the following table should condense the information. You may find that the WIND, DITL and ALRT resources have fields for Width and Height instead of Right and Bottom, in which case the e.g. WIND menu will give you an option to switch the fields. Finally, [¹] means choose Resource:Create New Resource... (Command)K; [²] means choose Resource:Get Resource Info... (Command)I and in menus, [ENTER] means you literally need to press the [ENTER] key.:


Re-source¹ Fields.. [Close Window] Info².. [Close, Close]
WIND [OK] TOP=40, Bottom=240, Left=40, Right=280, Initially Visible, Close Box, Default Color ID=400, Name="MiniMorse", Purgeable (only)
MENU [OK] [X] Enabled, Title=• Apple Menu[ENTER], [X] Enabled, Title="About MiniMorse..."[ENTER] [ ]Enabled, • Separator line ID=128, No attributes.
MENU [OK] [X] Enabled, Title="File"[ENTER], [X] Enabled, Title="Quit", Cmd-Key:Q[ENTER] ID=129, No attributes.
DITL [OK] Drag [Static Text] to DITL and double-click it. [X] Enabled. Text="MiniMorse ©2023 Julian Skidmore[ENTER]Press a letter or digit to convert to Morse code, or press a sequence of '-' and '.' to convert a letter/digit", Top:4, Bottom:73, Left:64, Right:311. Close, Drag Button below Text. Double-click it. [X] Enabled. Text="OK" Top:90, Bottom:110, Left:159, Right:217. Close. ID=400, Only attribute=Purgeable.
ALRT [OK] Color:Default, DITL ID=400, Top:40, Bottom:240, Left:40, Right:280 ID=400, Only attribute=Purgeable.

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

program MiniMorse;
begin
end.

Choose File:Save to save it as MiniMorse.p. Choose Project:Add "MiniMorse.p" to add the file to the project. Next, add the resource file by choosing Run:Run Options... Tick the [ ] Use Resource File field and then choose the MiniMorse.π.rsrc file you created. Finally, Click [OK]

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

program MiniMorse;
  const
    kWindIdBase = 400;
    kMenuBarId = kWindIdBase;
    kMenuIdApple = 128;
    kMenuAbout = 1;
    kMenuIdFile = 129;
    kMenuQuit = 1;
    kAboutAlertId = 400;
    kWneTrapNum = $60;
    kUnimplTrapNum = $9f;
    { We want to support a 1s event timeout. }
    kSleep = $60;
    kOriginX = 10;
    kOriginY = 10;
    kMaxMorseLen = 5;
    kTapMax = 15;
    kMorseLetterCodes = 26;
    { 0..9=0..9, 10..16 :;<=>?@}
    {17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30}
    {A B C D E F G H I J K L M N}
    gLettersToMorse = '6AE92D;@4N=B75?FK:83<H&gt>IMC';
    kMorseDigitCodes = 10;
    gDigitsToMorse = '_^\XP@ACGO';
    kMorseCodes = 64;
    gMorseToAlpha = '0.ETIANMSURWDKGOHVF.L.PJBXCYZQ3.54.3...24....:.16.5....:7...8.90';
    cMorseSyms = '.-';
  var
    gMorseWin: WindowPtr;
    gDone, gWNEImplemented: boolean;
    gTheEvent: EventRecord;
    gHelloStr, gNumStr: Str255;
    gMorseStr: string[6];
    gKeyCode, gMorseSyms: string[2];
    gUpdates, gMorseTap: integer;
    gTapTimeout: longint;
    { 0.5s timeout }

  function Later (aTimeout: LONGINT): LONGINT;
  begin
    Later := TickCount + aTimeout;
  end;

  function After (aTimeout: LONGINT): boolean;
    var
      tick: longint;
      timedOut: boolean;
  begin
    tick := TickCount;
    timedOut := aTimeout - tick < 0;
    After := timedOut;
  end;

  function DoMenuBarInit: boolean;
    var
      menu: MenuHandle;
  begin
    menu := nil;
    menu := GetMenu(128);
    AddResMenu(menu, 'DRVR');
    InsertMenu(menu, 0); { add after all.}
    menu := NewMenu(129, 'File');
    AppendMenu(menu, 'Quit/Q');
    InsertMenu(menu, 0);
    DrawMenuBar;
  end;

  function Init: boolean;
    var
      fontNum: integer;
      ok: boolean;
  begin
    gDone := false;
    gUpdates := 0;
    gKeyCode := ' ';
    gMorseStr := ' ';
    gMorseTap := 1;
    gMorseSyms := '.-';
    gTapTimeout := Later(kTapMax);
    gWNEImplemented := NGetTrapAddress(kWindIdBase, ToolTrap) <> NGetTrapAddress(kUnimplTrapNum, ToolTrap);
    gMorseWin := GetNewWindow(kWindIdBase, nil, WindowPtr(-1));
    ok := gMorseWin <> nil;
    if ok then
      begin
        SetWTitle(gMorseWin, 'MiniMorse');
        SetPort(gMorseWin);
        GetFNum('monaco', fontNum);
        TextFont(fontNum);
        TextSize(9); { just standard size.}
        ShowWindow(gMorseWin);
        ok := DoMenuBarInit;
        if ok then
          InitCursor;
      end;
    Init := ok;
  end;

  procedure DoMenuApple (aItem: integer);
    var
      accName: Str255;
      accNumber, itemNumber, dummy: integer;
      appleMenu: MenuHandle;
  begin
    case aItem of
      kMenuAbout:
        dummy := NoteAlert(kAboutAlertId, nil);
      otherwise
        begin
          appleMenu := GetMHandle(kMenuIdApple);
          GetItem(appleMenu, aItem, accName);
          accNumber := OpenDeskAcc(accName);
        end;
    end;
  end;

  procedure DoMenuFile (aItem: integer);
  begin
    case aItem of
      kMenuQuit:
        gDone := true;

    end;
  end;

  procedure DoMenuChoice (aMenuChoice: LONGINT);
    var
      menu, item: integer;
  begin
    if aMenuChoice <> 0 then
      begin
        menu := HiWord(aMenuChoice);
        item := LoWord(aMenuChoice);
        case menu of
          kMenuIdApple:
            DoMenuApple(item);

          kMenuIdFile:
            DoMenuFile(item);

        end;
        HiliteMenu(0);
      end;
  end;


  procedure DoMouseDown;
    var
      whichWindow: WindowPtr;
      thePart: integer;
      windSize, menuChoice: LONGINT;
      oldPort: GrafPtr;
  begin
    thePart := FindWindow(gTheEvent.where, whichWindow);
    case thePart of
      inMenuBar:
        begin
          menuChoice := MenuSelect(gTheEvent.where);
          DoMenuChoice(menuChoice);
        end;
      inSysWindow:
        SystemClick(gTheEvent, whichWindow);
      inDrag:
        DragWindow(whichWindow, gTheEvent.where, screenBits.bounds);
      inContent:
        if whichWindow <> FrontWindow then
          begin
            SelectWindow(whichWindow);
          end;

      inGrow:
        ; { don't support.}
      inGoAway:
        gDone := true;
      inZoomIn:
        ;
      inZoomOut:
        ; { don't support.}
    end;
  end;

  procedure Repaint (aWindow: WindowPtr);
    var
      oldPort: GrafPtr;
  begin
    GetPort(oldPort);
    SetPort(aWindow);
  { tempRgn=NewRgn;}
  { GetClip(tempRgn);}
    EraseRect(aWindow^.portRect);
    MoveTo(kOriginX, kOriginY);
  { DrawString('Updates=');}
  { NumToString(++gUpdates, gNumStr);}
  { DrawString(gNumStr);}
    DrawString('Key=');
    DrawString(gKeyCode);
  { MoveTo(kOriginX, kOriginY+10);}
    DrawString(' Morse=');
    DrawString(gMorseStr);
    SetPort(oldPort);
  end;

  procedure DoUpdate (aWindow: WindowPtr);
    var
      myRect, drawingClipRect: Rect;
      oldPort: GrafPtr;
      tempRgn: RgnHandle;
  begin
    BeginUpdate(aWindow);
    if aWindow = gMorseWin then
      begin
    { DrawMyStuff}
        Repaint(aWindow);
      end;
    EndUpdate(aWindow);
  end;

 { @ brief , handle Key Event , assume Later bottom byte .}
 {Keys aren 't sent to it now.}
  procedure DoKey;
    var
      ch: char;
      len, morseCode: integer;
  begin
    ch := char(BitAnd(gTheEvent.message, 255));
    if BitAnd(gTheEvent.modifiers, cmdKey) = cmdKey then
      begin
        DoMenuChoice(MenuKey(ch));
      end
    else
      begin
        morseCode := 1;
        len := 0;
        if (ch >= '0') and (ch <= '9') then
          begin
            morseCode := ord(gDigitsToMorse[ord(ch) - ord('0') + 1]) - 32;
          end
        else if (ch >= 'a') and (ch <> 'z') or (ch >= 'A') and (ch <= 'Z') then
          begin
            morseCode := ord(gLettersToMorse[BitAnd(ord(ch), $1f)]) - 48;
          end;
        if (ch = '.') or (ch = '-') then
          begin
            gMorseTap := (gMorseTap * 2);
            if ch = '-' then
              gMorseTap := gMorseTap + 1;
            gTapTimeout := Later(kTapMax);
          end
        else
          begin
            gMorseStr := ' ';
            while morseCode > 1 do
              begin
                len := len + 1;
                gMorseStr[len] := gMorseSyms[1 + BitAnd(morseCode, 1)];
                morseCode := morseCode div 2;
              end;
            Repaint(gMorseWin);
          end;
      end;
  end;

  procedure DoNull;
    var
      front: WindowPtr;
  begin
    if (gMorseTap > 1) and After(gTapTimeout) then
      begin
        front := FrontWindow;
        if front = gMorseWin then
          begin
      { We have a valid Morse pattern!}
            gKeyCode[1] := gMorseToAlpha[BitAnd(gMorseTap, 63) + 1];
            Repaint(gMorseWin);
            gMorseTap := 1;
          end;
      end;
  end;

  procedure DoEvent;
    var
      gotOne: boolean;
  begin
    gotOne := false;
    if gWNEImplemented then
      begin
        gotOne := WaitNextEvent(everyEvent, gTheEvent, kTapMax, nil);
      end
    else
      begin
        SystemTask;
        gotOne := GetNextEvent(everyEvent, gTheEvent);
      end;
    if gotOne or not (gotOne) and (gTheEvent.what = nullEvent) then
      begin
        case gTheEvent.what of
          nullEvent: { Handle end; Morse tapping.}
            DoNull;
          mouseDown: { handle mousedown.}
            DoMouseDown;
          mouseUp:
            ; { handle mouse.}
          keyDown:
            DoKey;
          updateEvt:
            DoUpdate(WindowPtr(gTheEvent.message));
          activateEvt:
            ; { Handle DoActivate;}
          diskEvt:
            ; { don't handle.}
          keyUp:
            ; { do nothing.}
          autoKey:
            ;
        end;
      end;
  end;

begin
  if Init then
    begin
      while not (gDone) do
        begin
          DoEvent;
        end;
    end;
end.
Finally, to create the application, choose Project:Build Application... Type a name for the application (e.g. MiniMorse), click [OK] and it should complete. Look for the compiled application and open it!

Conclusion

It's really simple to write a version of MiniMorse for a ZX81, but a lot more involved trying to create a version of the application for an early GUI environment on a Mac. It would almost certainly be much more complex still to do the same thing for Windows 3.1 or GEM (Atari ST). Nevertheless, it's possible to create a small pascal program at just under 3kB, because the Classic Mac's operating system interface is straightforward and compact.