Clean up Mac OS GUI code

Include paths are updated to include the directory. It seems that
CodeWarrior will search for include files recursively, which means that
"error.h" may resolve to the wrong file (there are two). I am unsure of
the rules CodeWarrior uses to find header files.

Support for older versions of Universal Interfaces has been added.

The project code has been reworked after thoroughly reviewing Inside
Macintosh: Files. It is not complete, but it compiles, and the behavior
of the Save / Save As commands have been thought out more carefully.
This commit is contained in:
Dietrich Epp 2022-11-16 18:19:05 -05:00
parent 8e04863cda
commit d5bfaa510a
10 changed files with 321 additions and 253 deletions

View File

@ -23,6 +23,13 @@
// Classic Mac OS. Header is part of Universal Interfaces & Carbon.
#include <ConditionalMacros.h>
// The target API macros do not exist until later versions of Universal
// Interfaces. For example, they do not exist in Universal Interfaces 3.1, which
// ships with CodeWarrior Pro 4.
#ifndef TARGET_API_MAC_OS8
#define TARGET_API_MAC_OS8 1
#endif
#elif __APPLE__
// Newer apple systems, including macOS >= 10. Header is in /usr/include, or

View File

@ -24,7 +24,7 @@ void Failf(const char *msg, ...) __attribute__((format(printf, 1, 2)));
// invalid.
const char *ErrorDescriptionOrDie(ErrorCode err);
// Print information about completed tests and return the status code.
// Print information about completed tests and return the status code.
int TestsDone(void);
#endif

View File

@ -1,11 +1,11 @@
// Copyright 2022 Dietrich Epp.
// This file is part of SyncFiles. SyncFiles is licensed under the terms of the
// Mozilla Public License, version 2.0. See LICENSE.txt for details.
#include "choose_directory.h"
#include "macos/choose_directory.h"
#include "error.h"
#include "resources.h"
#include "strutil.h"
#include "macos/error.h"
#include "macos/resources.h"
#include "macos/strutil.h"
#include <string.h>

View File

@ -1,11 +1,11 @@
// Copyright 2022 Dietrich Epp.
// This file is part of SyncFiles. SyncFiles is licensed under the terms of the
// Mozilla Public License, version 2.0. See LICENSE.txt for details.
#include "error.h"
#include "macos/error.h"
#include "main.h"
#include "resources.h"
#include "strutil.h"
#include "macos/main.h"
#include "macos/resources.h"
#include "macos/strutil.h"
#include <Dialogs.h>
#include <TextUtils.h>

View File

@ -4,7 +4,8 @@
#ifndef MACOS_ERROR_H
#define MACOS_ERROR_H
// Error codes, corresponding to messages in a STR# resource.
// Error codes, corresponding to messages in a STR# resource. This should be
// kept in sync with STR# rSTRS_Errors in resources.r.
typedef enum {
kErrUnknown = 1,
kErrInternal,
@ -12,11 +13,6 @@ typedef enum {
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);
@ -41,7 +37,7 @@ void ExitAssert(const unsigned char *file, int line,
struct ErrorParams {
ErrorCode err;
OSErr osErr;
short osErr;
const unsigned char *strParam;
};

View File

@ -1,11 +1,11 @@
// Copyright 2022 Dietrich Epp.
// This file is part of SyncFiles. SyncFiles is licensed under the terms of the
// Mozilla Public License, version 2.0. See LICENSE.txt for details.
#include "main.h"
#include "macos/main.h"
#include "error.h"
#include "project.h"
#include "resources.h"
#include "macos/error.h"
#include "macos/project.h"
#include "macos/resources.h"
#ifndef __MWERKS__
// Link error if defined with CodeWarrior.

View File

@ -1,9 +1,9 @@
// Copyright 2022 Dietrich Epp.
// This file is part of SyncFiles. SyncFiles is licensed under the terms of the
// Mozilla Public License, version 2.0. See LICENSE.txt for details.
#include "path.h"
#include "macos/path.h"
#include "error.h"
#include "macos/error.h"
#include <string.h>

View File

@ -1,17 +1,17 @@
// Copyright 2022 Dietrich Epp.
// This file is part of SyncFiles. SyncFiles is licensed under the terms of the
// Mozilla Public License, version 2.0. See LICENSE.txt for details.
#include "project.h"
#include "macos/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 "lib/crc32.h"
#include "macos/choose_directory.h"
#include "macos/error.h"
#include "macos/main.h"
#include "macos/path.h"
#include "macos/resources.h"
#include "macos/strutil.h"
#include "macos/tempfile.h"
#include <Files.h>
#include <MacMemory.h>
#include <MacWindows.h>
@ -23,45 +23,43 @@ Project format:
Header:
byte[32] magic
uint32 version
uint32 file size
uint32 data crc32
uint32 header length
uint32 header crc32
uint32 chunk count
ckinfo[] chunk info
byte[] chunk data
Chunk info:
byte[8] chunk name
uint32 chunk ID
uint32 byte offset
uint32 byte length
uint32 chunk crc32
Chunks: (names zero-padded)
"s.alias" source directory alias
"d.alias" destination directory alias
"LOCA" local directory alias
"REMA" 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.
*/
#define kVersion 0x10000
// clang-format off
// Magic cookie that identifies project files.
static const kMagic[32] = {
static const unsigned char kMagic[32] = {
// Identify the file type.
'S', 'y', 'n', 'c', 'F', 'i', 'l', 'e', 's', ' ',
'P', 'r', 'o', 'j', 'e', 'c', 't',
'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,
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;
@ -71,9 +69,10 @@ struct ProjectHeader {
};
struct ProjectChunk {
UInt8 name[8];
UInt32 id;
UInt32 offset;
UInt32 size;
UInt32 crc32;
};
// Dimensions of controls.
@ -83,11 +82,6 @@ enum {
kDirVSize = 62,
};
enum {
// Base ID for aliases.
rAliasBase = 128,
};
enum {
kControlChooseDirLocal,
kControlChooseDirRemote,
@ -110,9 +104,15 @@ struct Project {
int windowWidth;
int isActive;
// File reference to the project file, or fileRef == 0 if file not saved.
// File reference to the project file, or fileRef == 0 otherwise.
short fileRef;
// Location of the project file, if vRefNum != 0. In a possible edge case,
// the fileSpec may be set even when fileRef is 0. This may happen if you
// save the project to a volume which does not support FSpExchangeFiles(),
// and the operation fails.
FSSpec fileSpec;
// Script code for the filename.
ScriptCode fileScript;
struct ProjectDir dirs[2];
};
@ -174,9 +174,6 @@ static void ProjectClose(WindowRef window, ProjectHandle project)
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);
@ -185,146 +182,294 @@ static void ProjectClose(WindowRef window, ProjectHandle project)
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)
static OSErr ProjectWriteHeader(short refNum, int nchunks,
struct ProjectChunk *ckInfo)
{
struct ProjectChunk ckInfo[kChunkCount];
Handle ckData[kChunkCount];
UInt32 size, pos, ckOff, ckSize;
Handle h;
int i;
struct ProjectHeader *hdr;
Ptr ptr;
struct ProjectHeader *head;
UInt32 size;
long count;
OSErr err;
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) {
size = sizeof(struct ProjectHeader) + sizeof(struct ProjectChunk) * nchunks;
ptr = NewPtr(size);
if (ptr == 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);
head = (struct ProjectHeader *)ptr;
memcpy(head->magic, kMagic, sizeof(head->magic));
head->version = kVersion;
head->size = size;
head->crc32 = 0;
head->chunkCount = nchunks;
memcpy(ptr + sizeof(struct ProjectHeader), ckInfo,
nchunks * sizeof(struct ProjectChunk));
head->crc32 = CRC32Update(0, ptr, size);
count = size;
err = FSWrite(refNum, &count, ptr);
DisposePtr(ptr);
return err;
}
static const UInt32 kProjectAliasChunks[2] = {'LOCA', 'REMA'};
static OSErr ProjectFlush(short refNum)
{
ParamBlockRec pb;
memset(&pb, 0, sizeof(pb));
pb.ioParam.ioRefNum = refNum;
return PBFlushFileSync(&pb);
}
// ProjectWriteContent writes the project data to the given file.
static OSErr ProjectWriteContent(ProjectHandle project, short refNum)
{
UInt32 pad[4];
OSErr err;
Handle ckData[kChunkCount];
struct ProjectChunk ckInfo[kChunkCount];
UInt32 off;
Size size;
Handle h;
int i, nchunks;
long count;
// Gather all chunks.
nchunks = 0;
for (i = 0; i < 2; i++) {
h = (Handle)(*project)->dirs[i].alias;
if (h != NULL) {
ckData[nchunks] = h;
ckInfo[nchunks].id = kProjectAliasChunks[i];
nchunks++;
}
}
// Calculate chunk offsets.
off = sizeof(struct ProjectHeader) + sizeof(struct ProjectChunk) * nchunks;
for (i = 0; i < nchunks; i++) {
size = GetHandleSize(ckData[i]);
ckInfo[i].offset = off;
ckInfo[i].size = size;
ckInfo[i].crc32 = CRC32Update(0, *ckData[i], size);
off = ALIGN(off + size);
}
// Write file. Refer to IM: Files listing 1-9, page 1-24.
err = SetEOF(refNum, off);
if (err != noErr) {
return err;
}
err = ProjectWriteHeader(refNum, nchunks, ckInfo);
if (err != noErr) {
return err;
}
for (i = 0; i < 4; i++) {
pad[i] = 0;
}
for (i = 0; i < nchunks; i++) {
size = ckInfo[i].size;
count = size;
err = FSWrite(refNum, &count, *ckData[i]);
if (err != noErr) {
return err;
}
count = (-size) & 15;
if (count > 0) {
err = FSWrite(refNum, &count, pad);
if (err != noErr) {
return err;
}
}
}
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)
// A SaveMode describes the exact type of save operation.
typedef enum {
// kSaveNew indicates that the file is new, it does not exist yet.
kSaveNew,
// kSaveReplace indicates that the file is being saved over an existing
// file.
kSaveReplace,
// kSaveUpdate indicates that the file is being updated in-place.
kSaveUpdate
} SaveMode;
// ProjectCloseFile closes any open files and sets the file ref to 0.
static void ProjectCloseFile(ProjectHandle project)
{
// See listing 1-12 in IM: Files. This has some differences.
struct Project *projectp;
projectp = *project;
if (projectp->fileRef != 0) {
FSClose(projectp->fileRef);
}
}
// ProjectSetFile closes any open files and replaces them with the given file.
static void ProjectSetFile(ProjectHandle project, short fileRef, FSSpec *spec)
{
struct Project *projectp;
projectp = *project;
if (projectp->fileRef != 0) {
FSClose(projectp->fileRef);
}
projectp->fileRef = fileRef;
memcpy(&projectp->fileSpec, spec, sizeof(FSSpec));
}
// ProjectWrite writes the project to a file.
static OSErr ProjectWrite(ProjectHandle project, FSSpec *spec,
ScriptCode script, SaveMode mode)
{
HParamBlockRec hb;
CMovePBRec cm;
FSSpec temp;
OSErr err;
short refNum;
err = MakeTempFile(spec->vRefNum, &temp);
if (err != noErr) {
return err;
}
err = FSpCreate(&temp, kCreator, kTypeProject, script);
if (err != noErr) {
return err;
}
err = FSpOpenDF(&temp, fsRdWrPerm, &refNum);
if (err != noErr) {
goto error1;
}
err = ProjectWriteContent(project, refNum);
if (err != noErr) {
goto error2;
}
// For ordinary "Save", swap the file contents.
if (mode == kSaveUpdate) {
err = FSpExchangeFiles(&temp, spec);
if (err == noErr) {
err = FlushVol(NULL, spec->vRefNum);
if (err != noErr) {
FSClose(refNum);
return err;
}
ProjectSetFile(project, refNum, spec);
// FIXME: What error handling, here?
FSpDelete(&temp);
return noErr;
}
// paramErr: function not supported by volume.
if (err != paramErr) {
goto error2;
}
}
ProjectCloseFile(project);
if (mode != kSaveNew) {
// For replace or update, the file already exists.
err = FSpDelete(spec);
if (err != noErr && err != fnfErr) {
goto error2;
}
}
// Try move & rename.
memset(&hb, 0, sizeof(hb));
hb.copyParam.ioNamePtr = temp.name;
hb.copyParam.ioVRefNum = temp.vRefNum;
hb.copyParam.ioNewName = spec->name;
hb.copyParam.ioNewDirID = spec->parID;
hb.copyParam.ioDirID = temp.parID;
err = PBHMoveRenameSync(&hb);
if (err == noErr) {
goto done;
}
// paramErr: function not supported by volume.
if (err != paramErr) {
goto error2;
}
// Try move, then rename.
memset(&cm, 0, sizeof(cm));
cm.ioNamePtr = temp.name;
cm.ioVRefNum = temp.vRefNum;
cm.ioNewDirID = spec->parID;
cm.ioDirID = temp.parID;
err = PBCatMoveSync(&cm);
if (err != noErr) {
goto error2;
}
temp.parID = spec->parID;
err = FSpRename(&temp, spec->name);
if (err != noErr) {
goto error2;
}
done:
err = FlushVol(NULL, spec->vRefNum);
if (err != noErr) {
FSClose(refNum);
return err;
}
ProjectSetFile(project, refNum, spec);
return noErr;
error2:
FSClose(refNum);
error1:
FSpDelete(&temp);
return err;
}
// A SaveCommand distinguishes between Save and Save As.
typedef enum { kSave, kSaveAs } SaveCommand;
// ProjectSave handles the Save and SaveAs menu commands.
static void ProjectSave(WindowRef window, ProjectHandle project,
SaveCommand cmd)
{
// See listing 1-12 in IM: Files.
Str255 name;
StandardFileReply reply;
short refNum;
struct ProjectData data;
struct Project *projectp;
OSErr err;
Boolean hasfile;
ParamBlockRec pb;
struct ErrorParams errp;
long size;
FSSpec spec;
ScriptCode script;
data.data = NULL;
refNum = 0;
hasfile = FALSE;
if (cmd == kSaveAs || (*project)->fileSpec.vRefNum == 0) {
GetWTitle(window, name);
StandardPutFile("\pSave project as:", name, &reply);
if (!reply.sfGood) {
return;
}
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);
err = ProjectWrite(project, &reply.sfFile, reply.sfScript,
reply.sfReplacing ? kSaveReplace : kSaveNew);
if (err != noErr) {
goto error;
}
SetWTitle(window, reply.sfFile.name);
} else {
memcpy(&spec, &(*project)->fileSpec, sizeof(FSSpec));
script = (*project)->fileScript;
err = ProjectWrite(project, &spec, script, kSaveUpdate);
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;
@ -332,86 +477,6 @@ error:
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)
{
@ -422,10 +487,10 @@ void ProjectCommand(WindowRef window, ProjectHandle project, int menuID,
ProjectClose(window, project);
break;
case iFile_Save:
ProjectSave(window, project);
ProjectSave(window, project, kSave);
break;
case iFile_SaveAs:
ProjectSaveAs(window, project);
ProjectSave(window, project, kSaveAs);
break;
}
break;

View File

@ -1,9 +1,9 @@
// Copyright 2022 Dietrich Epp.
// This file is part of SyncFiles. SyncFiles is licensed under the terms of the
// Mozilla Public License, version 2.0. See LICENSE.txt for details.
#include "strutil.h"
#include "macos/strutil.h"
#include "error.h"
#include "macos/error.h"
#include <NumberFormatting.h>

View File

@ -1,7 +1,7 @@
// Copyright 2022 Dietrich Epp.
// This file is part of SyncFiles. SyncFiles is licensed under the terms of the
// Mozilla Public License, version 2.0. See LICENSE.txt for details.
#include "tempfile.h"
#include "macos/tempfile.h"
#include <string.h>