diff --git a/macos/choose_directory.c b/macos/choose_directory.c new file mode 100644 index 0000000..54ff82f --- /dev/null +++ b/macos/choose_directory.c @@ -0,0 +1,105 @@ +#include "choose_directory.h" + +#include "error.h" +#include "resources.h" +#include "strutil.h" + +#include + +typedef enum { + kCDNone, + kCDChild, + kCDCurrent, +} ChooseDirStatus; + +struct ChooseDirReply { + ChooseDirStatus status; + FSSpec *directory; +}; + +// FIXME: Don't use pascal on PowerMac. +static pascal Boolean ChooseDirectoryFileFilter(CInfoPBPtr pb, + void *yourDataPtr) +{ + (void)yourDataPtr; + return (pb->hFileInfo.ioFlAttrib & ioDirMask) == 0; +} + +enum { + kCDItemChoose = 10, + kCDItemChooseCurrent = 11, +}; + +static void SelectCurrentDirectory(struct ChooseDirReply *reply) +{ + CInfoPBRec ci; + OSStatus err; + Str63 name; + FSSpec *directory; + + memset(&ci, 0, sizeof(ci)); + ci.dirInfo.ioNamePtr = name; + ci.dirInfo.ioVRefNum = -LMGetSFSaveDisk(); + ci.dirInfo.ioFDirIndex = -1; + ci.dirInfo.ioDrDirID = LMGetCurDirStore(); + err = PBGetCatInfoSync(&ci); + if (err != 0) { + ExitErrorOS(kErrUnknown, err); + } + directory = reply->directory; + directory->vRefNum = ci.dirInfo.ioVRefNum; + directory->parID = ci.dirInfo.ioDrParID; + StrCopy(directory->name, sizeof(directory->name), name); + reply->status = kCDCurrent; +} + +static pascal short ChooseDirectoryDialogHook(short item, DialogRef theDialog, + void *yourDataPtr) +{ + struct ChooseDirReply *reply; + if (GetWRefCon((WindowRef)theDialog) != sfMainDialogRefCon) { + return item; + } + reply = yourDataPtr; + switch (item) { + case kCDItemChoose: + reply->status = kCDChild; + return sfItemCancelButton; + case kCDItemChooseCurrent: + SelectCurrentDirectory(yourDataPtr); + return sfItemCancelButton; + default: + return item; + } +} + +Boolean ChooseDirectory(FSSpec *directory) +{ + struct ChooseDirReply cdreply; + FileFilterYDUPP fileFilter; + DlgHookYDUPP dlgHook; + StandardFileReply sfreply; + Point where; + + cdreply.status = kCDNone; + cdreply.directory = directory; + fileFilter = NewFileFilterYDProc(ChooseDirectoryFileFilter); + ASSERT(fileFilter != NULL); + dlgHook = NewDlgHookYDProc(ChooseDirectoryDialogHook); + ASSERT(dlgHook != NULL); + where.v = -1; + where.h = -1; + CustomGetFile(fileFilter, -1, NULL, &sfreply, rDLOG_OpenFolder, where, + dlgHook, NULL /* filterProc */, 0, NULL, &cdreply); + // DisposeFileFilterYDUPP(fileFilter); + // DisposeDlgHookYDUPP(dlgHook); + switch (cdreply.status) { + case kCDChild: + BlockMoveData(&sfreply.sfFile, directory, sizeof(FSSpec)); + return 1; + case kCDCurrent: + return 1; + default: + return 0; + } +} diff --git a/macos/choose_directory.h b/macos/choose_directory.h new file mode 100644 index 0000000..b5da516 --- /dev/null +++ b/macos/choose_directory.h @@ -0,0 +1,8 @@ +#ifndef MACOS_CHOOSE_DIRECTORY_H +#define MACOS_CHOOSE_DIRECTORY_H + +// Present a dialog box to choose a directory. Returns true and initializes +// directory if a directory is selected. +Boolean ChooseDirectory(FSSpec *directory); + +#endif diff --git a/macos/derez.sh b/macos/derez.sh new file mode 100644 index 0000000..fb52c9f --- /dev/null +++ b/macos/derez.sh @@ -0,0 +1 @@ +DeRez icons.rsrc | iconv -f MACINTOSH -t UTF-8 | sed 's# */.*##' > icons.r diff --git a/macos/error.c b/macos/error.c new file mode 100644 index 0000000..4a3590b --- /dev/null +++ b/macos/error.c @@ -0,0 +1,72 @@ +#include "error.h" + +#include "main.h" +#include "resources.h" +#include "strutil.h" + +#include +#include + +void ExitError(ErrorCode errCode) +{ + Str255 message; + + GetIndString(message, rSTRS_Errors, errCode); + if (message[0] == 0) { + GetIndString(message, rSTRS_Errors, kErrUnknown); + } + ParamText(message, NULL, NULL, NULL); + Alert(rAlrtError, NULL); + QuitApp(); +} + +void ExitErrorOS(ErrorCode errCode, short osErr) +{ + Str255 message; + + GetIndString(message, rSTRS_Errors, errCode); + if (message[0] == 0) { + GetIndString(message, rSTRS_Errors, kErrUnknown); + } + StrAppendFormat(message, "\p\rError code: %d", osErr); + ParamText(message, NULL, NULL, NULL); + Alert(rAlrtError, NULL); + QuitApp(); +} + +void ExitMemError(void) +{ + ExitErrorOS(kErrOutOfMemory, MemError()); +} + +void ExitAssert(const unsigned char *file, int line, + const unsigned char *message) +{ + Str255 dmessage; + + GetIndString(dmessage, rSTRS_Errors, kErrInternal); + StrAppendFormat(dmessage, "\p\rError at: %S:%d", file, line); + if (message != NULL) { + StrAppendFormat(dmessage, "\p: %S", message); + } + ParamText(dmessage, NULL, NULL, NULL); + Alert(rAlrtError, NULL); + QuitApp(); +} + +void ShowError(const struct ErrorParams *p) +{ + Str255 message; + + GetIndString(message, rSTRS_Errors, p->err); + if (message[0] == 0) { + GetIndString(message, rSTRS_Errors, kErrUnknown); + } else if (p->strParam != NULL) { + StrSubstitute(message, p->strParam); + } + if (p->osErr != NULL) { + StrAppendFormat(message, "\p\rError code: %d", p->osErr); + } + ParamText(message, NULL, NULL, NULL); + Alert(rAlrtError, NULL); +} diff --git a/macos/error.h b/macos/error.h new file mode 100644 index 0000000..a70badc --- /dev/null +++ b/macos/error.h @@ -0,0 +1,48 @@ +// error.h - Error handling. +#ifndef MACOS_ERROR_H +#define MACOS_ERROR_H + +// Error codes, corresponding to messages in a STR# resource. +typedef enum { + kErrUnknown = 1, + kErrInternal, + kErrOutOfMemory, + kErrCouldNotSaveProject, +} ErrorCode; + +struct Error { + ErrorCode code; + short osErr; +}; + +// Show an error dialog with the given error message, then quit the program. +void ExitError(ErrorCode errCode); + +// Show an error dialog with the given error message and an OS error code, then +// quit the program. +void ExitErrorOS(ErrorCode errCode, short osErr); + +// Show an out of memory error and quit the program. +void ExitMemError(void); + +// Show an assertion error and quit the program. +void ExitAssert(const unsigned char *file, int line, + const unsigned char *message); + +#define EXIT_ASSERT(str) ExitAssert("\p" __FILE__, __LINE__, str) + +#define ASSERT(p) \ + do { \ + if (!(p)) \ + ExitAssert("\p" __FILE__, __LINE__, "\p" #p); \ + } while (0) + +struct ErrorParams { + ErrorCode err; + OSErr osErr; + const unsigned char *strParam; +}; + +void ShowError(const struct ErrorParams *p); + +#endif diff --git a/macos/icons.r b/macos/icons.r new file mode 100644 index 0000000..9275e70 --- /dev/null +++ b/macos/icons.r @@ -0,0 +1,316 @@ +data 'icl8' (128) { + $"0000 0000 0000 0000 0000 0000 0000 00FF" + $"0000 0000 0000 00FF 0000 0000 0000 0000" + $"0000 0000 0000 0000 0000 0000 0000 FF9F" + $"FF00 0000 0000 FFC0 FF00 0000 0000 0000" + $"0000 0000 0000 0000 0000 0000 00FF 9F92" + $"9FFF 0000 00FF C0C0 C0FF 0000 0000 0000" + $"0000 0000 0000 0000 0000 0000 FF9F 929F" + $"929F FF00 FFC0 C0C0 C0C0 FF00 0000 0000" + $"0000 0000 0000 00FF FFFF FFFF FFFF FFFF" + $"9F92 9FFF C0C0 C0C0 C0C0 C0FF 0000 0000" + $"0000 0000 0000 FFFF F5F5 F5F5 F5F5 F5FF" + $"929F FFFF FFFF C0C0 C0FF FFFF FF00 0000" + $"0000 0000 00FF 2BFF F5F5 F5FF FFFF FFFF" + $"FFFF FFFF 9FFF C0C0 C0FF 0000 0000 0000" + $"0000 0000 FFFF FFFF F5F5 FFFF F5F5 F5F5" + $"F5F5 F5FF 92FF C0C0 C0FF 0000 0000 0000" + $"0000 0000 FFF5 F5F5 F5FF 2BFF F5F5 F5F5" + $"F5F5 F5FF 9FFF C0C0 C0FF 0000 0000 0000" + $"0000 0000 FFF5 F5F5 FFFF FFFF F5F5 F5F5" + $"F5F5 F5FF 92FF C0C0 C0FF 0000 0000 0000" + $"0000 0000 FFF5 F5F5 FFF5 F5F5 F5F5 F5F5" + $"F5F5 F5FF 9FFF C0C0 C0FF 0000 0000 0000" + $"0000 0000 FFF5 F5F5 FFF5 F5F5 F5F5 FFFF" + $"FFFF F5FF 92FF C0C0 C0FF FF00 0000 0000" + $"0000 00FF FFF5 F5F5 FFF5 F5F5 F5FF 2A2A" + $"2A2A FFFF 9FFF C0C0 C0FF 9FFF 0000 0000" + $"0000 FF9F FFF5 F5F5 FFF5 F5F5 FF2A 2A2A" + $"2A2A 2AFF FFFF FFFF FFFF FF9F FF00 0000" + $"00FF 9F92 FFF5 F5F5 FFF5 F5F5 FF00 2A00" + $"2A00 2A00 2A00 2A00 2A00 54FF 9FFF 0000" + $"FF9F 929F FFF5 F5F5 FFF5 F5F5 FF2A 002A" + $"002A 002A 002A 002A 002A 54FF 929F FF00" + $"00FF 9F92 FFF5 F5F5 FFF5 F5F5 FF00 2A00" + $"2A00 2A00 2A00 2A00 2A00 54FF 9F92 9FFF" + $"0000 FF9F FFF5 F5F5 FFF5 F5F5 FF2A 002A" + $"002A 002A 002A 002A 002A 54FF 929F FF00" + $"0000 00FF FFF5 F5F5 FFF5 F5F5 FF00 2A00" + $"2A00 2A00 2A00 2A00 2A00 54FF 9FFF 0000" + $"0000 0000 FFFF FFFF FFF5 F5F5 FF2A 002A" + $"002A 002A 002A 002A 002A 54FF FF00 0000" + $"0000 0000 00FF FF69 FFF5 F5F5 FF00 2A00" + $"2A00 2A00 2A00 2A00 2A00 54FF 0000 0000" + $"0000 0000 0000 FF69 FFFF FFFF FF2A 002A" + $"002A 002A 002A 002A 002A 54FF 0000 0000" + $"0000 0000 0000 FF69 6969 FF92 FF00 2A00" + $"2A00 2A00 2A00 2A00 2A00 54FF 0000 0000" + $"0000 0000 0000 FF69 6969 FF9F FFFF FFFF" + $"FFFF FFFF FFFF FFFF FFFF FFFF 0000 0000" + $"0000 0000 0000 FF69 6969 FF92 9F92 9F92" + $"9F92 9F92 9F92 9FFF 0000 0000 0000 0000" + $"0000 0000 0000 FF69 6969 FF9F 929F 929F" + $"929F 929F 929F FF00 0000 0000 0000 0000" + $"0000 00FF FFFF FF69 6969 FFFF FFFF 9F92" + $"9F92 9F92 9FFF 0000 0000 0000 0000 0000" + $"0000 0000 FF69 6969 6969 6969 FF9F 929F" + $"929F 929F FF00 0000 0000 0000 0000 0000" + $"0000 0000 00FF 6969 6969 69FF 00FF 9F92" + $"9F92 9FFF 0000 0000 0000 0000 0000 0000" + $"0000 0000 0000 FF69 6969 FF00 0000 FF9F" + $"929F FF00 0000 0000 0000 0000 0000 0000" + $"0000 0000 0000 00FF 69FF 0000 0000 00FF" + $"9FFF 0000 0000 0000 0000 0000 0000 0000" + $"0000 0000 0000 0000 FF00 0000 0000 0000" + $"FF00 0000 0000 0000 0000 0000 0000 0000" +}; + +data 'icl8' (129) { + $"0000 00FF FFFF FFFF FFFF FFFF FFFF FFFF" + $"FFFF FFFF FFFF 0000 0000 0000 0000 0000" + $"0000 00FF F5F5 F5F5 F5F5 F5F5 F5F5 F5F5" + $"F5F5 F5F5 F5FF FF00 0000 0000 0000 0000" + $"0000 00FF F5F5 F5F5 F5F5 F5F5 F5F5 F5F5" + $"F5F5 F5F5 F5FF 2BFF 0000 0000 0000 0000" + $"0000 00FF F5F5 F5F5 F5F5 F5FF F5F5 F5F5" + $"F5F5 F5F5 F5FF 2B2B FF00 0000 0000 0000" + $"0000 00FF F5F5 F5F5 F5F5 FFC0 FFF5 F5F5" + $"F5F5 F5F5 F5FF 2B2B 2BFF 0000 0000 0000" + $"0000 00FF F5F5 F5F5 F5FF C0C0 C0FF F5F5" + $"F5F5 F5F5 F5FF 2B2B 2B2B FF00 0000 0000" + $"0000 00FF F5F5 F5F5 FFC0 C0C0 C0C0 FFF5" + $"F5F5 F5F5 F5FF FFFF FFFF FFFF 0000 0000" + $"0000 00FF F5F5 F5FF C0C0 C0C0 C0C0 C0FF" + $"F5F5 F5F5 F5F5 F5F5 F5F5 F5FF 0000 0000" + $"0000 00FF F5F5 FFFF FFFF C0C0 C0FF FFFF" + $"FFF5 F5F5 F5F5 F5F5 F5F5 F5FF 0000 0000" + $"0000 00FF F5F5 F5F5 F5FF C0C0 C0FF F5F5" + $"F5F5 F5F5 F5F5 F5F5 F5F5 F5FF 0000 0000" + $"0000 00FF F5F5 F5F5 F5FF C0C0 C0FF F5F5" + $"F5F5 F5F5 F5F5 F5F5 F5F5 F5FF 0000 0000" + $"0000 00FF F5F5 F5F5 F5FF C0C0 C0FF F5F5" + $"F5FF FFFF FFFF F5F5 F5F5 F5FF 0000 0000" + $"0000 00FF F5F5 F5F5 F5FF C0C0 C0FF F5F5" + $"F5FF 6969 69FF F5F5 F5F5 F5FF 0000 0000" + $"0000 00FF F5F5 F5F5 F5FF C0C0 C0FF F5F5" + $"F5FF 6969 69FF F5F5 F5F5 F5FF 0000 0000" + $"0000 00FF F5F5 F5F5 F5FF C0C0 C0FF F5F5" + $"F5FF 6969 69FF F5F5 F5F5 F5FF 0000 0000" + $"0000 00FF F5F5 F5F5 F5FF C0C0 C0FF F5F5" + $"F5FF 6969 69FF F5F5 F5F5 F5FF 0000 0000" + $"0000 00FF F5F5 F5F5 F5FF C0C0 C0FF F5F5" + $"F5FF 6969 69FF F5F5 F5F5 F5FF 0000 0000" + $"0000 00FF F5F5 F5F5 F5FF C0C0 C0FF F5F5" + $"F5FF 6969 69FF F5F5 F5F5 F5FF 0000 0000" + $"0000 00FF F5F5 F5F5 F5FF C0C0 C0FF F5F5" + $"F5FF 6969 69FF F5F5 F5F5 F5FF 0000 0000" + $"0000 00FF F5F5 F5F5 F5FF C0C0 C0FF F5F5" + $"F5FF 6969 69FF F5F5 F5F5 F5FF 0000 0000" + $"0000 00FF F5F5 F5F5 F5FF FFFF FFFF F5F5" + $"F5FF 6969 69FF F5F5 F5F5 F5FF 0000 0000" + $"0000 00FF F5F5 F5F5 F5F5 F5F5 F5F5 F5F5" + $"F5FF 6969 69FF F5F5 F5F5 F5FF 0000 0000" + $"0000 00FF F5F5 F5F5 F5F5 F5F5 F5F5 F5F5" + $"F5FF 6969 69FF F5F5 F5F5 F5FF 0000 0000" + $"0000 00FF F5F5 F5F5 F5F5 F5F5 F5F5 FFFF" + $"FFFF 6969 69FF FFFF FFF5 F5FF 0000 0000" + $"0000 00FF F5F5 F5F5 F5F5 F5F5 F5F5 F5FF" + $"6969 6969 6969 69FF F5F5 F5FF 0000 0000" + $"0000 00FF F5F5 F5F5 F5F5 F5F5 F5F5 F5F5" + $"FF69 6969 6969 FFF5 F5F5 F5FF 0000 0000" + $"0000 00FF F5F5 F5F5 F5F5 F5F5 F5F5 F5F5" + $"F5FF 6969 69FF F5F5 F5F5 F5FF 0000 0000" + $"0000 00FF F5F5 F5F5 F5F5 F5F5 F5F5 F5F5" + $"F5F5 FF69 FFF5 F5F5 F5F5 F5FF 0000 0000" + $"0000 00FF F5F5 F5F5 F5F5 F5F5 F5F5 F5F5" + $"F5F5 F5FF F5F5 F5F5 F5F5 F5FF 0000 0000" + $"0000 00FF F5F5 F5F5 F5F5 F5F5 F5F5 F5F5" + $"F5F5 F5F5 F5F5 F5F5 F5F5 F5FF 0000 0000" + $"0000 00FF F5F5 F5F5 F5F5 F5F5 F5F5 F5F5" + $"F5F5 F5F5 F5F5 F5F5 F5F5 F5FF 0000 0000" + $"0000 00FF FFFF FFFF FFFF FFFF FFFF FFFF" + $"FFFF FFFF FFFF FFFF FFFF FFFF 0000 0000" +}; + +data 'ICN#' (128) { + $"0001 0100 0002 8280 0004 4440 0008 2820" + $"01FF 1010 0301 3C78 051F F440 0F30 1440" + $"0850 1440 08F0 1440 0880 1440 0883 D460" + $"1884 3450 288F FFE8 4888 0014 8888 0012" + $"4888 0011 2888 0012 1888 0014 0F88 0018" + $"0688 0010 02F8 0010 0228 0010 022F FFF0" + $"0220 0100 0220 0200 1E3C 0400 0808 0800" + $"0414 1000 0222 2000 0141 4000 0080 8000" + $"0001 0100 0003 8380 0007 C7C0 000F EFE0" + $"01FF FFF0 03FF FFF8 07FF FFC0 0FFF FFC0" + $"0FFF FFC0 0FFF FFC0 0FFF FFC0 0FFF FFE0" + $"1FFF FFF0 3FFF FFF8 7FFF FFFC FFFF FFFE" + $"7FFF FFFF 3FFF FFFE 1FFF FFFC 0FFF FFF8" + $"07FF FFF0 03FF FFF0 03FF FFF0 03FF FFF0" + $"03FF FF00 03FF FE00 1FFF FC00 0FFF F800" + $"07F7 F000 03E3 E000 01C1 C000 0080 8000" +}; + +data 'ICN#' (129) { + $"1FFF FC00 1000 0600 1000 0500 1010 0480" + $"1028 0440 1044 0420 1082 07F0 1101 0010" + $"13C7 8010 1044 0010 1044 0010 1044 7C10" + $"1044 4410 1044 4410 1044 4410 1044 4410" + $"1044 4410 1044 4410 1044 4410 1044 4410" + $"107C 4410 1000 4410 1000 4410 1003 C790" + $"1001 0110 1000 8210 1000 4410 1000 2810" + $"1000 1010 1000 0010 1000 0010 1FFF FFF0" + $"1FFF FC00 1FFF FE00 1FFF FF00 1FFF FF80" + $"1FFF FFC0 1FFF FFE0 1FFF FFF0 1FFF FFF0" + $"1FFF FFF0 1FFF FFF0 1FFF FFF0 1FFF FFF0" + $"1FFF FFF0 1FFF FFF0 1FFF FFF0 1FFF FFF0" + $"1FFF FFF0 1FFF FFF0 1FFF FFF0 1FFF FFF0" + $"1FFF FFF0 1FFF FFF0 1FFF FFF0 1FFF FFF0" + $"1FFF FFF0 1FFF FFF0 1FFF FFF0 1FFF FFF0" + $"1FFF FFF0 1FFF FFF0 1FFF FFF0 1FFF FFF0" +}; + +data 'icl4' (128) { + $"0000 0000 0000 000F 0000 000F 0000 0000" + $"0000 0000 0000 00F8 F000 00F7 F000 0000" + $"0000 0000 0000 0F88 8F00 0F77 7F00 0000" + $"0000 0000 0000 F888 88F0 F777 77F0 0000" + $"0000 000F FFFF FFFF 888F 7777 777F 0000" + $"0000 00FF 0C0C 0C0F 88FF FF77 7FFF F000" + $"0000 0FCF C0CF FFFF FFFF 8F77 7F00 0000" + $"0000 FFFF 0CFF 0C0C 0C0F 8F77 7F00 0000" + $"0000 F0C0 CFCF C0C0 C0CF 8F77 7F00 0000" + $"0000 FC0C FFFF 0C0C 0C0F 8F77 7F00 0000" + $"0000 F0C0 F0C0 C0C0 C0CF 8F77 7F00 0000" + $"0000 FC0C FC0C 0CFF FF0F 8F77 7FF0 0000" + $"000F F0C0 F0C0 CFCC CCFF 8F77 7F8F 0000" + $"00F8 FC0C FC0C FCCC CCCF FFFF FFF8 F000" + $"0F88 F0C0 F0C0 F0C0 C0C0 C0C0 C0CF 8F00" + $"F888 FC0C FC0C FC0C 0C0C 0C0C 0CCF 88F0" + $"0F88 F0C0 F0C0 F0C0 C0C0 C0C0 C0CF 888F" + $"00F8 FC0C FC0C FC0C 0C0C 0C0C 0CCF 88F0" + $"000F F0C0 F0C0 F0C0 C0C0 C0C0 C0CF 8F00" + $"0000 FFFF FC0C FC0C 0C0C 0C0C 0CCF F000" + $"0000 0FF4 F0C0 F0C0 C0C0 C0C0 C0CF 0000" + $"0000 00F4 FFFF FC0C 0C0C 0C0C 0CCF 0000" + $"0000 00F4 44F8 F0C0 C0C0 C0C0 C0CF 0000" + $"0000 00F4 44F8 FFFF FFFF FFFF FFFF 0000" + $"0000 00F4 44F8 8888 8888 888F 0000 0000" + $"0000 00F4 44F8 8888 8888 88F0 0000 0000" + $"000F FFF4 44FF FF88 8888 8F00 0000 0000" + $"0000 F444 4444 F888 8888 F000 0000 0000" + $"0000 0F44 444F 0F88 888F 0000 0000 0000" + $"0000 00F4 44F0 00F8 88F0 0000 0000 0000" + $"0000 000F 4F00 000F 8F00 0000 0000 0000" + $"0000 0000 F000 0000 F000 0000 0000 0000" +}; + +data 'icl4' (129) { + $"000F FFFF FFFF FFFF FFFF FF00 0000 0000" + $"000F 0C0C 0C0C 0C0C 0C0C 0FF0 0000 0000" + $"000F C0C0 C0C0 C0C0 C0C0 CFCF 0000 0000" + $"000F 0C0C 0C0F 0C0C 0C0C 0FCC F000 0000" + $"000F C0C0 C0F7 F0C0 C0C0 CFCC CF00 0000" + $"000F 0C0C 0F77 7F0C 0C0C 0FCC CCF0 0000" + $"000F C0C0 F777 77F0 C0C0 CFFF FFFF 0000" + $"000F 0C0F 7777 777F 0C0C 0C0C 0C0F 0000" + $"000F C0FF FF77 7FFF F0C0 C0C0 C0CF 0000" + $"000F 0C0C 0F77 7F0C 0C0C 0C0C 0C0F 0000" + $"000F C0C0 CF77 7FC0 C0C0 C0C0 C0CF 0000" + $"000F 0C0C 0F77 7F0C 0FFF FF0C 0C0F 0000" + $"000F C0C0 CF77 7FC0 CF44 4FC0 C0CF 0000" + $"000F 0C0C 0F77 7F0C 0F44 4F0C 0C0F 0000" + $"000F C0C0 CF77 7FC0 CF44 4FC0 C0CF 0000" + $"000F 0C0C 0F77 7F0C 0F44 4F0C 0C0F 0000" + $"000F C0C0 CF77 7FC0 CF44 4FC0 C0CF 0000" + $"000F 0C0C 0F77 7F0C 0F44 4F0C 0C0F 0000" + $"000F C0C0 CF77 7FC0 CF44 4FC0 C0CF 0000" + $"000F 0C0C 0F77 7F0C 0F44 4F0C 0C0F 0000" + $"000F C0C0 CFFF FFC0 CF44 4FC0 C0CF 0000" + $"000F 0C0C 0C0C 0C0C 0F44 4F0C 0C0F 0000" + $"000F C0C0 C0C0 C0C0 CF44 4FC0 C0CF 0000" + $"000F 0C0C 0C0C 0CFF FF44 4FFF FC0F 0000" + $"000F C0C0 C0C0 C0CF 4444 444F C0CF 0000" + $"000F 0C0C 0C0C 0C0C F444 44FC 0C0F 0000" + $"000F C0C0 C0C0 C0C0 CF44 4FC0 C0CF 0000" + $"000F 0C0C 0C0C 0C0C 0CF4 FC0C 0C0F 0000" + $"000F C0C0 C0C0 C0C0 C0CF C0C0 C0CF 0000" + $"000F 0C0C 0C0C 0C0C 0C0C 0C0C 0C0F 0000" + $"000F C0C0 C0C0 C0C0 C0C0 C0C0 C0CF 0000" + $"000F FFFF FFFF FFFF FFFF FFFF FFFF 0000" +}; + +data 'ics#' (128) { + $"0110 02A8 0444 08EE 1028 2028 5C2C 942A" + $"5429 343A 1404 1408 7710 2220 1540 0880" + $"0110 03B8 07FC 0FFE 1FF8 3FF8 7FFC FFFE" + $"7FFF 3FFE 1FFC 1FF8 7FF0 3FE0 1DC0 0880" +}; + +data 'ics#' (129) { + $"7FE0 4430 4A28 513C 7B84 4A04 4AE4 4AA4" + $"4AA4 4EA4 40A4 43BC 4114 40A4 4044 7FFC" + $"7FE0 7FF0 7FF8 7FFC 7FFC 7FFC 7FFC 7FFC" + $"7FFC 7FFC 7FFC 7FFC 7FFC 7FFC 7FFC 7FFC" +}; + +data 'ics8' (128) { + $"0000 0000 0000 00FF 0000 00FF 0000 0000" + $"0000 0000 0000 FF9F FF00 FFC0 FF00 0000" + $"0000 0000 00FF 9F92 9FFF C0C0 C0FF 0000" + $"0000 0000 FF9F 929F FFFF FFC0 FFFF FF00" + $"0000 00FF 9F92 9F92 9F92 FFC0 FF00 0000" + $"0000 FF9F 929F 929F 929F FFC0 FF00 0000" + $"00FF 9FFF FFFF 9F92 9F92 FFC0 FFFF 0000" + $"FF9F 92FF 69FF 929F 929F FFC0 FF9F FF00" + $"00FF 9FFF 69FF 9F92 9F92 FFC0 FF92 9FFF" + $"0000 FFFF 69FF 929F 929F FFFF FF9F FF00" + $"0000 00FF 69FF 9F92 9F92 9F92 9FFF 0000" + $"0000 00FF 69FF 929F 929F 929F FF00 0000" + $"00FF FFFF 69FF FFFF 9F92 9FFF 0000 0000" + $"0000 FF69 6969 FF9F 929F FF00 0000 0000" + $"0000 00FF 69FF 00FF 9FFF 0000 0000 0000" + $"0000 0000 FF00 0000 FF00 0000 0000 0000" +}; + +data 'ics8' (129) { + $"00FF FFFF FFFF FFFF FFFF FF00 0000 0000" + $"00FF F5F5 F5FF F5F5 F5F5 FFFF 0000 0000" + $"00FF F5F5 FFC0 FFF5 F5F5 FF2B FF00 0000" + $"00FF F5FF C0C0 C0FF F5F5 FFFF FFFF 0000" + $"00FF FFFF FFC0 FFFF FFF5 F5F5 F5FF 0000" + $"00FF F5F5 FFC0 FFF5 F5F5 F5F5 F5FF 0000" + $"00FF F5F5 FFC0 FFF5 FFFF FFF5 F5FF 0000" + $"00FF F5F5 FFC0 FFF5 FF69 FFF5 F5FF 0000" + $"00FF F5F5 FFC0 FFF5 FF69 FFF5 F5FF 0000" + $"00FF F5F5 FFFF FFF5 FF69 FFF5 F5FF 0000" + $"00FF F5F5 F5F5 F5F5 FF69 FFF5 F5FF 0000" + $"00FF F5F5 F5F5 FFFF FF69 FFFF FFFF 0000" + $"00FF F5F5 F5F5 F5FF 6969 69FF F5FF 0000" + $"00FF F5F5 F5F5 F5F5 FF69 FFF5 F5FF 0000" + $"00FF F5F5 F5F5 F5F5 F5FF F5F5 F5FF 0000" + $"00FF FFFF FFFF FFFF FFFF FFFF FFFF 0000" +}; + +data 'ics4' (128) { + $"0000 000F 000F 0000 0000 00F8 F0F7 F000" + $"0000 0F88 8F77 7F00 0000 F888 FFF7 FFF0" + $"000F 8888 88F7 F000 00F8 8888 88F7 F000" + $"0F8F FF88 88F7 FF00 F88F 4F88 88F7 F8F0" + $"0F8F 4F88 88F7 F88F 00FF 4F88 88FF F8F0" + $"000F 4F88 8888 8F00 000F 4F88 8888 F000" + $"0FFF 4FFF 888F 0000 00F4 44F8 88F0 0000" + $"000F 4F0F 8F00 0000 0000 F000 F000 0000" +}; + +data 'ics4' (129) { + $"0FFF FFFF FFF0 0000 0F0C 0F0C 0CFF 0000" + $"0FC0 F7F0 C0FC F000 0F0F 777F 0CFF FF00" + $"0FFF F7FF F0C0 CF00 0F0C F7FC 0C0C 0F00" + $"0FC0 F7F0 FFF0 CF00 0F0C F7FC F4FC 0F00" + $"0FC0 F7F0 F4F0 CF00 0F0C FFFC F4FC 0F00" + $"0FC0 C0C0 F4F0 CF00 0F0C 0CFF F4FF FF00" + $"0FC0 C0CF 444F CF00 0F0C 0C0C F4FC 0F00" + $"0FC0 C0C0 CFC0 CF00 0FFF FFFF FFFF FF00" +}; + diff --git a/macos/main.c b/macos/main.c new file mode 100644 index 0000000..e92c2b0 --- /dev/null +++ b/macos/main.c @@ -0,0 +1,278 @@ +#include "main.h" + +#include "error.h" +#include "project.h" +#include "resources.h" + +#ifndef __MWERKS__ +// Link error if defined with CodeWarrior. +QDGlobals qd; +#endif + +void QuitApp(void) +{ + ExitToShell(); +} + +static void HandleClose(WindowRef window) +{ + ProjectHandle project; + + if (window == NULL) { + return; + } + switch (GetWindowKind(window)) { + case kApplicationWindowKind: + project = (ProjectHandle)GetWRefCon(window); + ASSERT(project != NULL); + ProjectCommand(window, project, rMENU_File, iFile_Close); + break; + } +} + +static void AdjustMenus(void) +{ + WindowRef window; + MenuHandle fileMenu; + ProjectHandle project; + Boolean hasDocument = FALSE; + + fileMenu = GetMenuHandle(rMENU_File); + window = FrontWindow(); + if (window != NULL) { + switch (GetWindowKind(window)) { + case kApplicationWindowKind: + project = (ProjectHandle)GetWRefCon(window); + ASSERT(project != NULL); + hasDocument = TRUE; + ProjectAdjustMenus(project, fileMenu); + break; + } + } + + if (fileMenu != NULL) { + EnableItem(fileMenu, iFile_New); + DisableItem(fileMenu, iFile_Open); + if (!hasDocument) { + DisableItem(fileMenu, iFile_Close); + DisableItem(fileMenu, iFile_Save); + DisableItem(fileMenu, iFile_SaveAs); + DisableItem(fileMenu, iFile_Revert); + } + EnableItem(fileMenu, iFile_Quit); + } +} + +static void HandleMenuCommand(long command) +{ + int menuID = command >> 16; + int item = command & 0xffff; + WindowRef window; + ProjectHandle project; + + switch (menuID) { + case rMENU_Apple: + switch (item) { + case iApple_Info: + return; + default: { + Str255 itemName; + GetMenuItemText(GetMenuHandle(rMENU_Apple), item, itemName); + OpenDeskAcc(itemName); + } + return; + } + return; + case rMENU_File: + switch (item) { + case iFile_New: + ProjectNew(); + return; + case iFile_Quit: + QuitApp(); + return; + } + break; + case rMENU_Edit: + break; + } + + window = FrontWindow(); + if (window != NULL) { + switch (GetWindowKind(window)) { + case kApplicationWindowKind: + project = (ProjectHandle)GetWRefCon(window); + ASSERT(project != NULL); + ProjectCommand(window, project, menuID, item); + return; + } + } + + SysBeep(1); +} + +// Handle a mouse down event. +static void HandleMouseDown(EventRecord *event) +{ + short clickPart; + WindowPtr window; + ProjectHandle project; + + clickPart = FindWindow(event->where, &window); + switch (clickPart) { + // case inDesk: + + case inMenuBar: + AdjustMenus(); + HandleMenuCommand(MenuSelect(event->where)); + HiliteMenu(0); + break; + + case inSysWindow: + SystemClick(event, window); + break; + + case inContent: + if (window != FrontWindow()) { + SelectWindow(window); + } else { + switch (GetWindowKind(window)) { + case kApplicationWindowKind: + project = (ProjectHandle)GetWRefCon(window); + ASSERT(project != NULL); + ProjectMouseDown(window, project, event); + break; + } + } + break; + + case inDrag: + if ((event->modifiers & cmdKey) != 0 && window != FrontWindow()) { + SelectWindow(window); + } + DragWindow(window, event->where, &(*GetGrayRgn())->rgnBBox); + break; + + // case inGrow: + + case inGoAway: + if (TrackGoAway(window, event->where)) { + HandleClose(window); + } + break; + + // case inZoomIn: + // case inZoomOut: + // case inCollapseBox: + // case inProxyIcon: + } +} + +static void HandleKeyDown(EventRecord *event) +{ + long command; + + if ((event->modifiers & cmdKey) != 0) { + AdjustMenus(); + command = MenuKey(event->message & charCodeMask); + HandleMenuCommand(command); + HiliteMenu(0); + } +} + +static void HandleActivate(EventRecord *event) +{ + ProjectHandle project; + WindowRef window; + GrafPtr savedPort; + int isActive; + + window = (WindowRef)event->message; + GetPort(&savedPort); + SetPort(window); + isActive = (event->modifiers & activeFlag) != 0; + switch (GetWindowKind(window)) { + case kApplicationWindowKind: + project = (ProjectHandle)GetWRefCon(window); + ASSERT(project != NULL); + ProjectActivate(window, project, isActive); + break; + } + SetPort(savedPort); +} + +static void HandleUpdate(EventRecord *event) +{ + ProjectHandle project; + WindowPtr window; + GrafPtr savedPort; + + window = (WindowPtr)event->message; + GetPort(&savedPort); + SetPort(window); + BeginUpdate(window); + switch (GetWindowKind(window)) { + case kApplicationWindowKind: + project = (ProjectHandle)GetWRefCon(window); + ASSERT(project != NULL); + ProjectUpdate(window, project); + break; + } + EndUpdate(window); + SetPort(savedPort); +} + +void main(void) +{ + MaxApplZone(); + InitGraf(&qd.thePort); + InitFonts(); + InitWindows(); + InitMenus(); + TEInit(); + InitDialogs(NULL); + InitCursor(); + + // Set up menu bar. + { + Handle menuBar; + menuBar = GetNewMBar(rMBAR_Main); + if (menuBar == NULL) { + EXIT_ASSERT(NULL); + } + SetMenuBar(menuBar); + DisposeHandle(menuBar); + AppendResMenu(GetMenuHandle(rMENU_Apple), 'DRVR'); + DrawMenuBar(); + } + + for (;;) { + EventRecord event; + WaitNextEvent(everyEvent, &event, 1, NULL); + switch (event.what) { + case mouseDown: + HandleMouseDown(&event); + break; + + // case mouseUp: + + case keyDown: + HandleKeyDown(&event); + break; + + // case autoKey: + + case activateEvt: + HandleActivate(&event); + break; + + case updateEvt: + HandleUpdate(&event); + break; + + // case diskEvt: + // osEvt: + // case kHighLevelEvent: + } + } +} diff --git a/macos/main.h b/macos/main.h new file mode 100644 index 0000000..c5b0e1e --- /dev/null +++ b/macos/main.h @@ -0,0 +1,21 @@ +// main.h - Main entry point. +#ifndef MACOS_MAIN_H +#define MACOS_MAIN_H + +// Menu item IDs. +enum { + iApple_Info = 1, + + iFile_New = 1, + iFile_Open = 2, + iFile_Close = 4, + iFile_Save = 5, + iFile_SaveAs = 6, + iFile_Revert = 7, + iFile_Quit = 9, +}; + +// Quit the application. +void QuitApp(void); + +#endif diff --git a/macos/menu.r b/macos/menu.r new file mode 100644 index 0000000..e737a6a --- /dev/null +++ b/macos/menu.r @@ -0,0 +1,56 @@ +#include "Menus.r" + +#include "resources.h" + +resource 'MBAR' (rMBAR_Main) {{ + rMENU_Apple, + rMENU_File, + rMENU_Edit, +}}; + +resource 'MENU' (rMENU_Apple, preload) { + rMENU_Apple, + textMenuProc, + 0b01111111111111111111111111111101, + enabled, + apple, + { + "About " APPNAME "…", noIcon, noKey, noMark, plain, + "-", noIcon, noKey, noMark, plain, + } +}; + +resource 'MENU' (rMENU_File, preload) { + rMENU_File, + textMenuProc, + 0b01111111111111111111111101111011, + enabled, + "File", + { + "New", noIcon, "N", noMark, plain, + "Open…", noIcon, "O", noMark, plain, + "-", noIcon, noKey, noMark, plain, + "Close", noIcon, "W", noMark, plain, + "Save", noIcon, "S", noMark, plain, + "Save As…", noIcon, noKey, noMark, plain, + "Revert", noIcon, noKey, noMark, plain, + "-", noIcon, noKey, noMark, plain, + "Quit", noIcon, "Q", noMark, plain, + } +}; + +resource 'MENU' (rMENU_Edit, preload) { + rMENU_Edit, + textMenuProc, + 0b01111111111111111111111111111101, + enabled, + "Edit", + { + "Undo", noIcon, "Z", noMark, plain, + "-", noIcon, noKey, noMark, plain, + "Cut", noIcon, "X", noMark, plain, + "Copy", noIcon, "C", noMark, plain, + "Paste", noIcon, "V", noMark, plain, + "Clear", noIcon, noKey, noMark, plain, + } +}; diff --git a/macos/path.c b/macos/path.c new file mode 100644 index 0000000..0d6b6a4 --- /dev/null +++ b/macos/path.c @@ -0,0 +1,90 @@ +#include "path.h" + +#include "error.h" + +#include + +enum { + kMaxPath = 512, +}; + +OSErr GetDirPath(FSSpec *spec, int *pathLength, Handle *path) +{ + Handle segments; // Packed sequence of segments, Pascal strings. + int textLen; // Total length of segments, including length byte. + Handle result; + unsigned char *name; + CInfoPBRec ci; + OSErr err; + unsigned char *op; + const unsigned char *ip, *ie; + int length; + + // Query target. + memset(&ci, 0, sizeof(ci)); + ci.dirInfo.ioNamePtr = spec->name; + ci.dirInfo.ioVRefNum = spec->vRefNum; + ci.dirInfo.ioDrDirID = spec->parID; + err = PBGetCatInfoSync(&ci); + if (err != noErr) { + return err; + } + // FIXME: kioFlAttribDirMask + if ((ci.hFileInfo.ioFlAttrib & ioDirMask) == 0) { + return fnfErr; + } + segments = NULL; + textLen = spec->name[0] + 1; + PtrToHand(spec->name, &segments, textLen); + if (segments == NULL) { + return MemError(); + } + + // Add name of parent. + while (ci.dirInfo.ioDrParID != fsRtParID) { + ASSERT(textLen < kMaxPath); + SetHandleSize(segments, textLen + 256); + err = MemError(); + if (err != noErr) { + goto done; + } + HLock(segments); + name = (unsigned char *)*segments + textLen; + ci.dirInfo.ioNamePtr = name; + ci.dirInfo.ioDrDirID = ci.dirInfo.ioDrParID; + ci.dirInfo.ioFDirIndex = -1; + err = PBGetCatInfoSync(&ci); + HUnlock(segments); + if (err != 0) { + goto done; + } + textLen += name[0] + 1; + } + + // Copy segments into handle in reverse order. + result = NewHandle(textLen); + if (result == NULL) { + err = MemError(); + goto done; + } + op = (unsigned char *)*result + textLen; + ip = (const unsigned char *)*segments; + ie = ip + textLen; + while (ip < ie) { + op--; + *op = ':'; + length = *ip; + op -= length; + BlockMoveData(ip + 1, op, length); + ip += length + 1; + } + *pathLength = textLen; + *path = result; + err = 0; + +done: + if (segments != NULL) { + DisposeHandle(segments); + } + return err; +} diff --git a/macos/path.h b/macos/path.h new file mode 100644 index 0000000..ef3a18c --- /dev/null +++ b/macos/path.h @@ -0,0 +1,8 @@ +// path.h - Path manipulation utilities. +#ifndef MACOS_PATH_H +#define MACOS_PATH_H + +// Get the full path to a directory. +OSErr GetDirPath(FSSpec *spec, int *pathLength, Handle *path); + +#endif diff --git a/macos/project.c b/macos/project.c new file mode 100644 index 0000000..9e091a7 --- /dev/null +++ b/macos/project.c @@ -0,0 +1,530 @@ +#include "project.h" + +#include "choose_directory.h" +#include "crc32.h" +#include "error.h" +#include "main.h" +#include "path.h" +#include "resources.h" +#include "tempfile.h" + +#include +#include +#include + +#include + +/* +Project format: + +Header: +byte[32] magic +uint32 version +uint32 file size +uint32 data crc32 +uint32 chunk count +ckinfo[] chunk info +byte[] chunk data + +Chunk info: +byte[8] chunk name +uint32 byte offset +uint32 byte length + +Chunks: (names zero-padded) +"s.alias" source directory alias +"d.alias" destination directory alias + +Chunks are aligned to 16 byte boundaries, and the file is padded to a 16 byte +boundary. This is just to make it easier to read hexdumps. +*/ + +// clang-format off + +// Magic cookie that identifies project files. +static const kMagic[32] = { + // Identify the file type. + 'S', 'y', 'n', 'c', 'F', 'i', 'l', 'e', 's', ' ', + 'P', 'r', 'o', 'j', 'e', 'c', 't', + // Detect newline conversions. + 0, 10, 0, 13, 0, 13, 10, 0, + // Detect encoding conversions (infinity in Roman and UTF-8). + 0xb0, 0, 0xe2, 0x88, 0x9e, 0, 0, +}; + +// clang-format on + +static const kChunkAlias[2][8] = { + {'l', '.', 'a', 'l', 'i', 'a', 's', 0}, + {'r', '.', 'a', 'l', 'i', 'a', 's', 0}, +}; + +struct ProjectHeader { + UInt8 magic[32]; + UInt32 version; + UInt32 size; + UInt32 crc32; + UInt32 chunkCount; +}; + +struct ProjectChunk { + UInt8 name[8]; + UInt32 offset; + UInt32 size; +}; + +// Dimensions of controls. +enum { + kVBorder = 10, + kHBorder = 10, + kDirVSize = 62, +}; + +enum { + // Base ID for aliases. + rAliasBase = 128, +}; + +enum { + kControlChooseDirLocal, + kControlChooseDirRemote, +}; + +// FIXME: this is redundant. +enum { + kDirLocal, + kDirRemote, +}; + +struct ProjectDir { + int pathLength; + Handle path; + AliasHandle alias; +}; + +// A synchronization project. +struct Project { + int windowWidth; + int isActive; + + // File reference to the project file, or fileRef == 0 if file not saved. + short fileRef; + FSSpec fileSpec; + + struct ProjectDir dirs[2]; +}; + +void ProjectNew(void) +{ + ProjectHandle project; + WindowRef window; + ControlRef control; + struct Project *projectp; + int i, windowWidth, controlWidth; + + project = (ProjectHandle)NewHandle(sizeof(struct Project)); + if (project == NULL) { + EXIT_ASSERT(NULL); + } + window = GetNewCWindow(rWIND_Project, NULL, (WindowPtr)-1); + if (window == NULL) { + EXIT_ASSERT(NULL); + } + windowWidth = window->portRect.right - window->portRect.left; + SetWRefCon(window, (long)project); + projectp = *project; + memset(projectp, 0, sizeof(*projectp)); + projectp->windowWidth = windowWidth; + + for (i = 0; i < 2; i++) { + control = GetNewControl(rCNTL_ChooseFolder, window); + if (control == NULL) { + EXIT_ASSERT(NULL); + } + controlWidth = + (*control)->contrlRect.right - (*control)->contrlRect.left; + MoveControl(control, windowWidth - controlWidth - kHBorder, + kVBorder + kDirVSize * i + 22); + SetControlReference(control, kControlChooseDirLocal + i); + } + + ShowWindow(window); +} + +void ProjectAdjustMenus(ProjectHandle project, MenuHandle fileMenu) +{ + (void)project; + if (fileMenu != NULL) { + EnableItem(fileMenu, iFile_Close); + EnableItem(fileMenu, iFile_Save); + EnableItem(fileMenu, iFile_SaveAs); + EnableItem(fileMenu, iFile_Revert); + } +} + +static void ProjectClose(WindowRef window, ProjectHandle project) +{ + struct Project *projectp; + int i; + + KillControls(window); + DisposeWindow(window); + HLock((Handle)project); + projectp = *project; + if (projectp->fileRef != 0) { + FSClose(projectp->fileRef); + } + for (i = 0; i < 2; i++) { + if (projectp->dirs[i].path != NULL) { + DisposeHandle(projectp->dirs[i].path); + } + } + DisposeHandle((Handle)project); +} + +#define kHeaderSize 48 +#define kChunkInfoSize 16 +#define kChunkCount 2 + +#define ALIGN(x) (((x) + 15) + ~(UInt32)15) + +struct ProjectData { + Handle data; + Size size; +}; + +static OSErr ProjectMarshal(ProjectHandle project, struct ProjectData *datap) +{ + struct ProjectChunk ckInfo[kChunkCount]; + Handle ckData[kChunkCount]; + UInt32 size, pos, ckOff, ckSize; + Handle h; + int i; + struct ProjectHeader *hdr; + + size = kHeaderSize + kChunkCount * kChunkInfoSize; + for (i = 0; i < 2; i++) { + h = (Handle)(*project)->dirs[i].alias; + ckSize = GetHandleSize(h); + memcpy(ckInfo[i].name, kChunkAlias[i], 8); + ckInfo[i].offset = size; + ckInfo[i].size = ckSize; + ckData[i] = h; + size = ALIGN(size + ckSize); + } + + h = NewHandle(size); + if (h == NULL) { + return MemError(); + } + hdr = (void *)*h; + memcpy(hdr->magic, kMagic, sizeof(kMagic)); + hdr->version = 0x10000; + hdr->size = size; + hdr->chunkCount = kChunkCount; + memcpy(*h + kHeaderSize, ckInfo, sizeof(ckInfo)); + pos = kHeaderSize + kChunkCount * kChunkInfoSize; + for (i = 0; i < kChunkCount; i++) { + ckOff = ckInfo[i].offset; + ckSize = ckInfo[i].size; + memset(*h + pos, 0, ckOff - pos); + memcpy(*h + ckOff, *ckData[i], ckSize); + pos = ckOff + ckSize; + } + memcpy(*h + pos, 0, size - pos); + + datap->data = h; + datap->size = size; + return noErr; +} + +// Prompt the user for a location to save the project. +static void ProjectSaveAs(WindowRef window, ProjectHandle project) +{ + // See listing 1-12 in IM: Files. This has some differences. + Str255 name; + StandardFileReply reply; + short refNum; + struct ProjectData data; + struct Project *projectp; + OSErr err; + Boolean hasfile; + ParamBlockRec pb; + struct ErrorParams errp; + long size; + + data.data = NULL; + refNum = 0; + hasfile = FALSE; + + GetWTitle(window, name); + StandardPutFile("\pSave project as:", name, &reply); + if (!reply.sfGood) { + return; + } + + err = ProjectMarshal(project, &data); + if (err != noErr) { + goto error; + } + + // Delete file if replacing. This way, we get a new creation date (makes + // sense) and the correct creator and type codes. + if (reply.sfReplacing) { + err = FSpDelete(&reply.sfFile); + if (err != noErr) { + goto error; + } + } + + // Write out the new file. + err = FSpCreate(&reply.sfFile, kCreator, kTypeProject, reply.sfScript); + if (err != noErr) { + goto error; + } + hasfile = TRUE; + err = FSpOpenDF(&reply.sfFile, fsWrPerm, &refNum); + if (err != noErr) { + goto error; + } + size = data.size; + err = FSWrite(refNum, &size, *data.data); + if (err != noErr) { + goto error; + } + DisposeHandle(data.data); + data.data = NULL; + memset(&pb, 0, sizeof(pb)); + pb.fileParam.ioFRefNum = refNum; + PBFlushFile(&pb, FALSE); + err = pb.fileParam.ioResult; + if (err != noErr) { + goto error; + } + + // Done writing, update the window and project. + SetWTitle(window, reply.sfFile.name); + projectp = *project; + if (projectp->fileRef != 0) { + FSClose(projectp->fileRef); + } + projectp->fileRef = refNum; + projectp->fileSpec = reply.sfFile; + return; + +error: + if (data.data != NULL) { + DisposeHandle(data.data); + } + if (refNum != 0) { + FSClose(refNum); + } + if (hasfile) { + FSpDelete(&reply.sfFile); + } + memset(&errp, 0, sizeof(errp)); + errp.err = kErrCouldNotSaveProject; + errp.osErr = err; + errp.strParam = reply.sfFile.name; + ShowError(&errp); +} + +static void ProjectSaveImpl(WindowRef window, ProjectHandle project) +{ + struct ProjectData data; + short refNum; + FSSpec temp; + OSErr err; + struct ErrorParams errp; + Boolean hasfile; + long size; + + (void)window; + + data.data = NULL; + refNum = 0; + hasfile = FALSE; + + err = ProjectMarshal(project, &data); + if (err != noErr) { + goto error; + } + err = MakeTempFile((*project)->fileSpec.vRefNum, &temp); + if (err != noErr) { + goto error; + } + err = FSpCreate(&temp, 'trsh', 'trsh', smSystemScript); + if (err != noErr) { + goto error; + } + hasfile = TRUE; + err = FSpOpenDF(&temp, fsWrPerm, &refNum); + if (err != noErr) { + goto error; + } + size = data.size; + err = FSWrite(refNum, &size, *data.data); + if (err != noErr) { + goto error; + } + DisposeHandle(data.data); + data.data = NULL; + err = FSClose(refNum); + refNum = 0; + if (err != noErr) { + goto error; + } + err = FSpExchangeFiles(&temp, &(*project)->fileSpec); + if (err != noErr) { + goto error; + } + FSpDelete(&temp); + return; + +error: + if (data.data) { + DisposeHandle(data.data); + } + if (refNum != 0) { + FSClose(refNum); + } + if (hasfile) { + FSpDelete(&temp); + } + memcpy(&temp.name, &(*project)->fileSpec.name, sizeof(temp.name)); + memset(&errp, 0, sizeof(errp)); + errp.err = kErrCouldNotSaveProject; + errp.osErr = err; + errp.strParam = temp.name; + ShowError(&errp); +} + +// Save the project to disk in response to a Save command. +static void ProjectSave(WindowRef window, ProjectHandle project) +{ + if ((*project)->fileRef == 0) { + ProjectSaveAs(window, project); + } else { + ProjectSaveImpl(window, project); + } +} + +void ProjectCommand(WindowRef window, ProjectHandle project, int menuID, + int item) +{ + switch (menuID) { + case rMENU_File: + switch (item) { + case iFile_Close: + ProjectClose(window, project); + break; + case iFile_Save: + ProjectSave(window, project); + break; + case iFile_SaveAs: + ProjectSaveAs(window, project); + break; + } + break; + } +} + +void ProjectUpdate(WindowRef window, ProjectHandle project) +{ + struct Project *projectp; + Rect rect; + Handle text; + int i; + + HLock((Handle)project); + + projectp = *project; + rect = window->portRect; + EraseRect(&rect); + MoveTo(0, kVBorder + kDirVSize - 10); + LineTo(projectp->windowWidth, kVBorder + kDirVSize - 10); + + for (i = 0; i < 2; i++) { + MoveTo(kHBorder, kVBorder + kDirVSize * i + 12); + DrawString(i == 0 ? "\pLocal Folder" : "\pRemote Folder"); + text = projectp->dirs[i].path; + if (text != NULL) { + HLock(text); + MoveTo(kHBorder, kVBorder + kDirVSize * i + 36); + DrawText(*text, 0, projectp->dirs[i].pathLength); + HUnlock(text); + } + } + UpdateControls(window, window->visRgn); + + HUnlock((Handle)project); +} + +void ProjectActivate(WindowRef window, ProjectHandle project, int isActive) +{ + struct Project *projectp; + + projectp = *project; + if (projectp->isActive != isActive) { + projectp->isActive = isActive; + InvalRect(&window->portRect); + } +} + +static void ProjectChooseDir(WindowRef window, ProjectHandle project, + int whichDir) +{ + struct Project *projectp; + struct ProjectDir *dirp; + FSSpec directory; + Handle path; + int pathLength; + OSErr err; + + if (ChooseDirectory(&directory)) { + err = GetDirPath(&directory, &pathLength, &path); + if (err != noErr) { + ExitErrorOS(kErrUnknown, err); + } + projectp = *project; + dirp = &projectp->dirs[whichDir]; + if (dirp->path != NULL) { + DisposeHandle(dirp->path); + } + dirp->pathLength = pathLength; + dirp->path = path; + InvalRect(&window->portRect); + } +} + +void ProjectMouseDown(WindowRef window, ProjectHandle project, + EventRecord *event) +{ + Point mouse; + GrafPtr savedPort; + ControlRef control; + ControlPartCode part; + long reference; + + GetPort(&savedPort); + SetPort(window); + mouse = event->where; + GlobalToLocal(&mouse); + part = FindControl(mouse, window, &control); + switch (part) { + case kControlButtonPart: + reference = GetControlReference(control); + if (TrackControl(control, mouse, NULL) != 0) { + switch (reference) { + case kControlChooseDirLocal: + ProjectChooseDir(window, project, kDirLocal); + break; + case kControlChooseDirRemote: + ProjectChooseDir(window, project, kDirRemote); + break; + } + } + break; + } + SetPort(savedPort); +} diff --git a/macos/project.h b/macos/project.h new file mode 100644 index 0000000..a0503a4 --- /dev/null +++ b/macos/project.h @@ -0,0 +1,28 @@ +// project.h - Synchronization projects. +#ifndef MACOS_PROJECT_H +#define MACOS_PROJECT_H + +// Handle to a synchronization project. +typedef struct Project **ProjectHandle; + +// Create a new empty synchronization project. +void ProjectNew(void); + +// Adjust menus before selecting a menu item. +void ProjectAdjustMenus(ProjectHandle project, MenuHandle fileMenu); + +// Handle a menu command or equivalent. +void ProjectCommand(WindowRef window, ProjectHandle project, int menuID, + int item); + +// Handle an update event for a project window. +void ProjectUpdate(WindowRef window, ProjectHandle project); + +// Set whether a project window is active. +void ProjectActivate(WindowRef window, ProjectHandle project, int isActive); + +// Handle a mouse down event in a window. +void ProjectMouseDown(WindowRef window, ProjectHandle project, + EventRecord *event); + +#endif diff --git a/macos/resources.h b/macos/resources.h new file mode 100644 index 0000000..53fabd4 --- /dev/null +++ b/macos/resources.h @@ -0,0 +1,32 @@ +#ifndef MACOS_RESOURCES_H +#define MACOS_RESOURCES_H + +#define APPNAME "SyncFiles" + +#define kCreator 'SyFl' +#define kTypeProject 'SyPr' + +/* ALRT Error dialog */ +#define rAlrtError 128 + +/* CNTL Controls */ +#define rCNTL_ChooseFolder 128 + +/* DLOG Dialogs */ +#define rDLOG_OpenFolder 128 + +/* MBAR Menu bar */ +#define rMBAR_Main 128 + +/* MENU Menus */ +#define rMENU_Apple 128 +#define rMENU_File 129 +#define rMENU_Edit 130 + +/* STR# Error messages. */ +#define rSTRS_Errors 128 + +/* WIND Windows */ +#define rWIND_Project 128 + +#endif diff --git a/macos/resources.r b/macos/resources.r new file mode 100644 index 0000000..e96623d --- /dev/null +++ b/macos/resources.r @@ -0,0 +1,147 @@ +#include "Controls.r" +#include "Dialogs.r" +#include "Finder.r" +#include "MacTypes.r" +#include "Processes.r" + +#include "resources.h" + +resource 'ALRT' (rAlrtError, purgeable) { + { 40, 40, 141, 360 }, + 128, /* DITL ID */ + { + OK, visible, sound1, + OK, visible, sound1, + OK, visible, sound1, + OK, visible, sound1, + }, + centerMainScreen +}; + +/* See Macintosh Human Interface Guidelines (1992) p. 197 "Basic Dialog Box Layout" */ +/* Note that there are an extra 3px of margin for "free" on the dialog border. */ +resource 'DITL' (128, purgeable) {{ + /* Quit button: 13px margin */ + { 71, 240, 91, 310 }, + Button { enabled, "Quit" }, + /* Error text: 13px margin, 3 lines @ 16px line height */ + { 10, 75, 58, 310 }, + StaticText { enabled, "^0" }, + /* Icon: 13px vertical, 23px horizontal */ + { 10, 20, 42, 52 }, + Icon { enabled, 0 }, +}}; + +resource 'DLOG' (rDLOG_OpenFolder, purgeable) { + /* Height += 31 */ + {0, 0, 197, 344}, + dBoxProc, + invisible, + noGoAway, + 0, + 129, + "", + noAutoCenter, +}; + +/* Open Folder, based on system -6042 */ +resource 'DITL' (129, purgeable) {{ + /* Standard file (do not change, see IM: Files 3-18) */ + {135, 252, 155, 332}, + Button {enabled, "Open"}, + {104, 252, 124, 332}, + Button {enabled, "Cancel"}, + {0, 0, 0, 0}, + HelpItem {disabled, HMScanhdlg {-6042}}, + {8, 235, 24, 337}, + UserItem {enabled}, + {32, 252, 52, 332}, + Button {enabled, "Eject"}, + {60, 252, 80, 332}, + Button {enabled, "Desktop"}, + {29, 12, 159, 230}, + UserItem {enabled}, + {6, 12, 25, 230}, + UserItem {enabled}, + {91, 251, 92, 333}, + Picture {disabled, 11}, + /* Additions */ + {166, 252, 186, 332}, + Button {enabled, "Choose"}, + {166, 12, 186, 230}, + Button {enabled, "Choose Current Directory"}, +}}; + +#ifdef MPW +resource 'SIZE' (-1) { + dontSaveScreen, + acceptSuspendResumeEvents, + enableOptionSwitch, + canBackground, + multiFinderAware, + backgroundAndForeground, + dontGetFrontClicks, + ignoreChildDiedEvents, + is32BitCompatible, + notHighLevelEventAware, + onlyLocalHLEvents, + notStationeryAware, + dontUseTextEditServices, + notDisplayManagerAware, + reserved, + reserved, + 60 * 1024, + 60 * 1024 +}; +#endif + +type kCreator as 'STR '; +resource kCreator (0, purgeable) { + APPNAME " 0.1 ©2022 Dietrich Epp" +}; + +resource 'vers' (1) { + 0x00, 0x01, development, 0x00, + verUS, + "0.1", + "0.1, Copyright ©2021 Dietrich Epp" +}; + +resource 'FREF' (128, purgeable) {'APPL', 0, ""}; +resource 'FREF' (129, purgeable) { kTypeProject, 1, ""}; +resource 'BNDL' (128, purgeable) { + kCreator, + 0, + { + 'ICN#', {0, 128, 1, 129}, + 'FREF', {0, 128, 1, 129}, + }, +}; + +resource 'STR#' (rSTRS_Errors) {{ + "An unknown error occurred.", + "An internal error occurred.", + "Out of memory.", + "Could not save project “^1”.", +}}; + +resource 'WIND' (rWIND_Project, preload, purgeable) { + {32, 32, 256, 32 + 128*3}, + documentProc, + invisible, + goAway, + 0, + "Untitled Project", + staggerMainScreen, +}; + +resource 'CNTL' (rCNTL_ChooseFolder, preload, purgeable) { + {10, 10, 30, 90}, + 0, + visible, + 1, + 0, + pushButProc, + 0, + "Choose…", +}; diff --git a/macos/strutil.c b/macos/strutil.c new file mode 100644 index 0000000..c01b66e --- /dev/null +++ b/macos/strutil.c @@ -0,0 +1,135 @@ +#include "strutil.h" + +#include "error.h" + +#include + +#include +#include + +static unsigned char *StrPtrAppend(unsigned char *ptr, unsigned char *end, + const unsigned char *str) +{ + unsigned i, len, rem; + + len = str[0]; + rem = end - ptr; + if (len > rem) { + len = rem; + } + str++; + for (i = 0; i < len; i++) { + *ptr++ = *str++; + } + return ptr; +} + +static void StrAppendVFormat(unsigned char *dest, const unsigned char *msg, + va_list ap) +{ + const unsigned char *mptr = msg + 1, *mend = mptr + msg[0]; + unsigned char *ptr = dest + 1 + dest[0], *end = dest + 256; + unsigned char c, temp[16]; + const unsigned char *pstrvalue; + int ivalue; + +next: + while (mptr < mend) { + c = *mptr++; + if (c == '%' && mptr < mend) { + switch (*mptr) { + case '%': + mptr++; + break; + case 'd': + mptr++; + ivalue = va_arg(ap, int); + NumToString(ivalue, temp); + ptr = StrPtrAppend(ptr, end, temp); + goto next; + case 'S': + mptr++; + pstrvalue = va_arg(ap, const unsigned char *); + ptr = StrPtrAppend(ptr, end, pstrvalue); + goto next; + } + } + if (ptr == end) { + break; + } + *ptr++ = c; + } + + *dest = ptr - dest - 1; +} + +void StrFormat(unsigned char *dest, const unsigned char *msg, ...) +{ + va_list ap; + + dest[0] = 0; + va_start(ap, msg); + StrAppendVFormat(dest, msg, ap); + va_end(ap); +} + +void StrAppendFormat(unsigned char *dest, const unsigned char *msg, ...) +{ + va_list ap; + + va_start(ap, msg); + StrAppendVFormat(dest, msg, ap); + va_end(ap); +} + +void StrCopy(unsigned char *dest, int dest_size, unsigned char *src) +{ + int len = src[0] + 1; + if (len > dest_size) { + EXIT_ASSERT(NULL); + } + BlockMoveData(src, dest, len); +} + +struct StrRef { + const unsigned char *src; + int len; +}; + +void StrSubstitute(unsigned char *str, const unsigned char *param) +{ + int i, n, end; + struct StrRef refs[3]; + + n = str[0]; + for (i = 0; i < n - 1; i++) { + if (str[i + 1] == '^' && str[i + 2] == '1') { + refs[0].src = str + 1; + refs[0].len = i; + refs[1].src = param + 1; + refs[1].len = param[0]; + refs[2].src = str + i + 3; + refs[2].len = n - i - 2; + goto concat; + } + } + return; + +concat: + n = 0; + for (i = 0; i < 3; i++) { + n += refs[i].len; + } + str[0] = n > 255 ? 255 : n; + for (i = 3; i > 0;) { + i--; + end = n; + n -= refs[i].len; + if (end > 255) { + end = 255; + } + if (n < end) { + memmove(str + 1 + n, refs[i].src, end - n); + } + } +} diff --git a/macos/strutil.h b/macos/strutil.h new file mode 100644 index 0000000..af492ec --- /dev/null +++ b/macos/strutil.h @@ -0,0 +1,22 @@ +// strutil.h - String utilities. +#ifndef MACOS_STRUTIL_H +#define MACOS_STRUTIL_H + +// Write a formatted string. +void StrFormat(unsigned char *dest, const unsigned char *msg, ...); + +// Append a formatted string to another string. +// +// %% - literal % +// %d - int argument +// %S - Pascal string argument +void StrAppendFormat(unsigned char *dest, const unsigned char *msg, ...); + +// Copy a Pascal string. Aborts the program if the string does not fit in the +// destination buffer. +void StrCopy(unsigned char *dest, int dest_size, unsigned char *src); + +// Substitute ^1 in the string with param. +void StrSubstitute(unsigned char *str, const unsigned char *param); + +#endif diff --git a/macos/tempfile.c b/macos/tempfile.c new file mode 100644 index 0000000..eeae512 --- /dev/null +++ b/macos/tempfile.c @@ -0,0 +1,50 @@ +#include "tempfile.h" + +#include + +// Filename prefix for temporary files. +static const char kTempPrefix[] = "SyncFiles."; + +// Hexadecimal digits. +static const char kHexDigits[16] = {'0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + +static void U32ToHex(unsigned char *ptr, UInt32 x) +{ + int i; + for (i = 0; i < 8; i++) { + ptr[7 - i] = kHexDigits[x & 15]; + x >>= 4; + } +} + +OSErr MakeTempFile(short vRefNum, FSSpec *spec) +{ + short tempVRefNum; + long tempParID; + unsigned long time; + OSErr err; + int pos; + unsigned char name[32]; + + // This is inspired by IM: Files listing 1-10, page 1-25. + + // Generate hopefully unique name. + GetDateTime(&time); + memcpy(name + 1, kTempPrefix, sizeof(kTempPrefix) - 1); + pos = sizeof(kTempPrefix); + U32ToHex(name + pos, time); + pos += 8; + name[0] = pos - 1; + + err = FindFolder(vRefNum, kTemporaryFolderType, TRUE, &tempVRefNum, + &tempParID); + if (err != noErr) { + return err; + } + err = FSMakeFSSpec(tempVRefNum, tempParID, name, spec); + if (err != noErr && err != fnfErr) { + return err; + } + return noErr; +} diff --git a/macos/tempfile.h b/macos/tempfile.h new file mode 100644 index 0000000..7741095 --- /dev/null +++ b/macos/tempfile.h @@ -0,0 +1,8 @@ +#ifndef MACOS_TEMPFILE_H +#define MACOS_TEMPFILE_H + +// Create a FSSpec for a temporary file on the given volume. Does not actually +// create the file. +OSErr MakeTempFile(short vRefNum, FSSpec *spec); + +#endif