This is MODocController.m in view mode; [Download] [Up]
// MODocController.m
//
// by Mike Ferris
// Part of MOKit
// Copyright 1993, all rights reserved.
// ABOUT MOKit
//
// MOKit is a collection of useful and general objects. Permission is
// granted by the author to use MOKit in your own programs in any way
// you see fit. All other rights to the kit are reserved by the author
// including the right to sell these objects as part of a LIBRARY or as
// SOURCE CODE. In plain English, I wish to retain rights to these
// objects as objects, but allow the use of the objects as pieces in a
// fully functional program. NO WARRANTY is expressed or implied. The author
// will under no circumstances be held responsible for ANY consequences to
// you from the use of these objects. Since you don't have to pay for
// them, and full source is provided, I think this is perfectly fair.
#import "MOKit/MODocController.h"
#import "MOKit/MODocType.h"
#import "MOKit/MODocManager.h"
#import "MOKit/MOString.h"
#import "MOKit/MOPathString.h"
#import "MOKit/MOClassVariable.h"
#import <objc/objc-runtime.h>
#define CLASS_VERSION 0
#define CLASS_NAME "MODocController"
#define BUNDLE_TYPE "bundle"
#define MOSTRING_CLASS_NAME "MOString"
#define MOPATHSTRING_CLASS_NAME "MOPathString"
#define MODOCMANAGER_CLASS_NAME "MODocManager"
#define MOCLASSVARIABLE_CLASS_NAME "MOClassVariable"
#define MODOCTYPE_CLASS_NAME "MODocType"
#define UNTITLED_STRING "Untitled"
#define MOKIT_BUNDLE "MOKitBundle"
#define SAVEACCESSORY_NIBNAME "MODocSaveAccessory.nib"
@interface MODocController(Private)
+ (Class)MO_loadClassBundle:(const char *)className;
+ (BOOL)MO_loadMOKitBundle;
@end
@implementation MODocController
// True class variables via hashtables
static MOClassVariable *MO_classControllerName;
static MOClassVariable *MO_classDocTypes;
static Class MOStringClass;
static Class MOPathStringClass;
static Class MODocManagerClass;
static Class MOClassVariableClass;
static Class MODocTypeClass;
static NXBundle *MOKitBundle;
+ (Class)MO_loadClassBundle:(const char *)className
// Finds the bundle of the same name as the class, grabs it and loads the
// class from it and returns the named class.
{
char pathBuff[MAXPATHLEN+1];
id classBundle = nil;
Class class = nil;
// Load the bundle
if ((class = objc_lookUpClass(className)) == nil) {
// class is not already loaded... load it.
// Look for the bundle in the main bundle first,
// else try in this class's bundle.
if (![[NXBundle mainBundle] getPath:pathBuff forResource:className
ofType:BUNDLE_TYPE]) {
if (![[NXBundle bundleForClass:[self class]] getPath:pathBuff
forResource:className ofType:BUNDLE_TYPE]) {
NXLogError("[%s loadClassBundle] failed to "
"find %s class bundle.", [self name], className);
return nil;
}
}
classBundle = [[NXBundle allocFromZone:[self zone]]
initForDirectory:pathBuff];
if (!classBundle) {
NXLogError("[%s loadClassBundle] failed to "
"create bundle for class %s.", [self name], className);
return nil;
}
if ((class = [classBundle classNamed:className]) == nil) {
NXLogError("[%s loadClassBundle] failed to "
"load %s class from bundle.", [self name], className);
return nil;
}
}
return class;
}
+ (BOOL)MO_loadMOKitBundle
// Finds the MOKit bundle (hopefully) in which a nib we need is stored.
{
char pathBuff[MAXPATHLEN+1];
// Look for the bundle in the main bundle first,
// else try in this class's bundle.
if (![[NXBundle mainBundle] getPath:pathBuff forResource:MOKIT_BUNDLE
ofType:BUNDLE_TYPE]) {
if (![[NXBundle bundleForClass:[self class]] getPath:pathBuff
forResource:MOKIT_BUNDLE ofType:BUNDLE_TYPE]) {
NXLogError("[%s MO_loadMOKitBundle] failed to "
"find MOKit bundle.", [self name]);
return NO;
}
}
MOKitBundle = [[NXBundle allocFromZone:[self zone]]
initForDirectory:pathBuff];
if (!MOKitBundle) {
NXLogError("[%s MO_loadMOKitBundle] failed to "
"create bundle for MOKit bundle.", [self name]);
return NO;
}
return YES;
}
+ initialize
// Set the version. Load classes. Load the MOKit bundle.
// Init class variables.
{
if (self == objc_lookUpClass(CLASS_NAME)) {
[self setVersion:CLASS_VERSION];
// Load all the classes we need
MOStringClass = [self MO_loadClassBundle:MOSTRING_CLASS_NAME];
MOPathStringClass = [self MO_loadClassBundle:MOPATHSTRING_CLASS_NAME];
MODocManagerClass = [self MO_loadClassBundle:MODOCMANAGER_CLASS_NAME];
MOClassVariableClass = [self MO_loadClassBundle:
MOCLASSVARIABLE_CLASS_NAME];
MODocTypeClass = [self MO_loadClassBundle:MODOCTYPE_CLASS_NAME];
// Find the MOKit bundle which contains MOKit's nibs and
// other non-code resources.
[self MO_loadMOKitBundle];
// Set up class variables
MO_classControllerName = [[MOClassVariableClass
allocFromZone:[self zone]]
initDoesFreeValues:YES];
MO_classDocTypes = [[MOClassVariableClass
allocFromZone:[self zone]]
initDoesFreeValues:YES];
}
return self;
}
+ setControllerName:(const char *)typeName
// Sets the name of this document class type. This is used in New
// and Open submenus that MODocManager creates if there is more
// than one document type being managed.
{
id newVal;
if (typeName) {
newVal = [[MOStringClass allocFromZone:[self zone]]
initStringValue:typeName];
} else {
newVal = nil;
}
[MO_classControllerName setObject:newVal forClass:self];
return self;
}
+ (MOString *)controllerName
// Returns the controller name.
{
return [MO_classControllerName getObjectForClass:self];
}
+ addDocType:(int)tag name:(const char *)name extension:(const char *)ext
openSelector:(SEL)openSel saveSelector:(SEL)saveSel
// Adds a new document type that controllers of this class support.
// The name is the full name of the doc type. Extension is the file
// extension for docs of that type. Tag is for your own use. open
// and save selectors take a single MODocType as their argument.
// Either can be NULL if this is a read-only or write-only file type.
{
List *typeList = [self docTypes];
if (!name || !*name) {
NXLogError("[%s addDocType]: Attempted to add a document type "
"with no name. Document types must have names.",
[self name]);
return nil;
}
if (!typeList) {
typeList = [[List allocFromZone:[self zone]] init];
[MO_classDocTypes setObject:typeList forClass:self];
}
[typeList addObject:[[MODocTypeClass allocFromZone:[self zone]]
initForControllerClass:self tag:tag name:name extension:ext
openSelector:openSel saveSelector:saveSel]];
return self;
}
+ (List *)docTypes
// Returns the list of document types supported by this class.
{
return (List *)[MO_classDocTypes getObjectForClass:self];
}
+ (MODocType *)docTypeForTag:(int)tag
// Returns the doc type whose tag is tag.
{
List *docTypes = [self docTypes];
int i, c = [docTypes count];
MODocType *type = nil;
for (i=0; i<c; i++) {
type = [docTypes objectAt:i];
if ([type typeTag] == tag) {
return type;
}
}
return nil;
}
+ (MODocType *)docTypeForName:(const char *)name
// Returns the doc type whose name is name.
{
List *docTypes = [self docTypes];
int i, c = [docTypes count];
MODocType *type = nil;
for (i=0; i<c; i++) {
type = [docTypes objectAt:i];
if (([type typeName]) && (name) &&
(strcmp([type typeName], name) == 0)) {
return type;
}
}
return nil;
}
+ (MODocType *)docTypeForExtension:(const char *)extension
// Returns the doc type whose extension is extension.
// If we find a match, return it. If we don't find a match, and we have
// an extension-less doc type, return that.
{
List *docTypes = [self docTypes];
int i, c = [docTypes count];
MODocType *type = nil;
const char *curExt = NULL;
for (i=0; i<c; i++) {
type = [docTypes objectAt:i];
curExt = [type typeExtension];
if ((!extension || !*extension) && (!curExt || !*curExt)) {
return type;
}
if ((curExt) && (extension) &&
(strcmp(curExt, extension) == 0)) {
return type;
}
}
if (extension && *extension) {
return [self docTypeForExtension:NULL];
}
return nil;
}
+ (List *)getOpenTypesList:(List *)list
// Fills list with the types which support reading from our docTypes list.
// list is returned. If list is nil, a List object is allocated for it.
{
List *docTypes = [self docTypes];
int i, c = [docTypes count];
MODocType *type = nil;
if (!list) {
list = [[List allocFromZone:[self zone]] init];
}
for (i=0; i<c; i++) {
type = [docTypes objectAt:i];
if ([type canOpenType]) {
[list addObject:type];
}
}
return list;
}
+ (List *)getSaveTypesList:(List *)list
// Fills list with the types which support saving from our docTypes list.
// list is returned. If list is nil, a List object is allocated for it.
{
List *docTypes = [self docTypes];
int i, c = [docTypes count];
MODocType *type = nil;
if (!list) {
list = [[List allocFromZone:[self zone]] init];
}
for (i=0; i<c; i++) {
type = [docTypes objectAt:i];
if ([type canSaveType]) {
[list addObject:type];
}
}
return list;
}
+ startUnloading
// We have to deallocate the class variables.
{
[MO_classControllerName free];
[MO_classDocTypes free];
return self;
}
- initFromFile:(const char *)path manager:aManager
// This is the initializer that will be used normally. It just calls the DI.
{
return [self initWithFrameName:NULL fromFile:path manager:aManager];
}
- initWithFrameName:(const char *)theFrameName
// Just call the DI.
{
return [self initWithFrameName:theFrameName fromFile:NULL manager:nil];
}
- initWithFrameName:(const char *)theFrameName
fromFile:(const char *)path manager:aManager
// Designated Initializer. A null file means a new untitled doc.
{
[super initWithFrameName:theFrameName];
[self setManager:aManager];
docName = [[MOPathStringClass allocFromZone:[self zone]] init];
untitledString = [[MOPathStringClass allocFromZone:[self zone]] init];
docType = nil;
isDirty = NO;
saveAccessoryPanel = nil;
saveAccessoryBox = nil;
saveAccessoryPopupButton = nil;
[self setFile:path];
return self;
}
- free
// free our ivars
{
[docName free];
[untitledString free];
if (manager) [manager removeDocument:self];
return [super free];
}
- nibDidLoad
// Set the window title, move the window according to the frameCycle.
{
[super nibDidLoad];
if (window) {
[self resetWindowTitle];
if (manager) {
NXPoint pt;
[manager getNextWindowLocation:&pt];
[window moveTopLeftTo:pt.x :pt.y];
}
}
return self;
}
- (BOOL)doClear
// override this to totally clear this doc into a blank one.
{
return YES;
}
- (BOOL)doPrint
// override this to print the document (including running print panel, etc...).
{
return YES;
}
- saveAccessoryPopupAction:sender
// The action of the save accessory document type popup. It sets
// the Save panels required file extension appropriately.
{
id savePanel = [SavePanel new];
List *saveTypes = [[self class] getSaveTypesList:nil];
[savePanel setRequiredFileType:[[saveTypes objectAt:
[sender selectedRow]] typeExtension]];
[saveTypes free];
return self;
}
- getSaveAccessoryForSaveTypes:(List *)saveTypes
currentType:(MODocType *)currentType
// This will prepare and return the saveAccessory view for the given sender
// document. If necessary it will load the accessory nib first. It returns
// nil if there is only a single save type since no accessory is needed then.
// Subclasses may override this to use a different nib file. If a subclass
// does override, it is important that the new nib file NOT use the existing
// saveAccessory... outlets for MODocController. Declare all new outlets for
// your save accessory nib in your subclass and use those. The saveAccessory
// outlets in this class are not used outside this method, but other subclasses
// may not override, and they should get the old nib.
{
id popup = nil;
int i, c;
MOString *scratch = nil;
MODocType *type = nil;
c = [saveTypes count];
if (c <= 1) {
return nil;
}
if (!saveAccessoryBox) {
if (MOKitBundle) {
char pathBuff[MAXPATHLEN+1];
if (![MOKitBundle getPath:pathBuff
forResource:SAVEACCESSORY_NIBNAME ofType:NULL]) {
NXLogError("%s: failed to find nib file %s in MOKit bundle.",
[[self class] name], SAVEACCESSORY_NIBNAME);
return nil;
}
if (![NXApp loadNibFile:pathBuff owner:self]) {
NXLogError("%s: failed to load nib file %s.",
[[self class] name], pathBuff);
return nil;
}
} else {
if (![NXApp loadNibSection:SAVEACCESSORY_NIBNAME owner:self]) {
NXLogError("%s: failed to load nib file %s.",
[[self class] name], SAVEACCESSORY_NIBNAME);
return nil;
}
}
[saveAccessoryBox removeFromSuperview];
[saveAccessoryPanel free];
saveAccessoryPanel = nil;
}
popup = [saveAccessoryPopupButton target];
// remove all but the first item in the list
for (i=[popup count]-1; i>0; i--) {
[[popup removeItemAt:i] free];
}
// set the first item's title to ""
[[[popup itemList] cellAt:0:0] setTitle:""];
scratch = [[MOString allocFromZone:[self zone]] init];
// add the new items
for (i=0; i<c; i++) {
type = [saveTypes objectAt:i];
[scratch setFromFormat:"%s (%s)",
[type typeName],
([type typeExtension]?[type typeExtension]:"<none>")];
[popup addItem:[scratch stringValue]];
// If this is the current type...
if (type == currentType) {
[saveAccessoryPopupButton setTitle:[scratch stringValue]];
}
}
// If there is no current type, select the first one.
if (currentType == nil) {
type = [saveTypes objectAt:0];
[scratch setFromFormat:"%s (%s)",
[type typeName],
([type typeExtension]?[type typeExtension]:"<none>")];
[saveAccessoryPopupButton setTitle:[scratch stringValue]];
}
// remove the first item (which was still left from the old.
[[popup removeItemAt:0] free];
// set the target/action
[popup setTarget:self];
[popup setAction:@selector(saveAccessoryPopupAction:)];
[popup sizeToFit];
return saveAccessoryBox;
}
- save:sender
// Called from various places when we are supposed to save.
{
BOOL ret = YES;
if ([docName isEmpty]) {
return [self saveAs:self];
}
ret = ([self perform:[docType saveSelector] with:docType] != nil);
if (ret) {
[self setDirty:NO];
}
return ret?self:nil;
}
- saveAs:sender
// Called by DocManager in response to Save As command.
{
id savePanel = [SavePanel new];
MOPathString *dir, *file, *extension;
List *saveTypes = [[self class] getSaveTypesList:nil];
id saveAccessory = nil;
if ([saveTypes count] == 0) {
[saveTypes free];
return nil;
}
saveAccessory = [self getSaveAccessoryForSaveTypes:saveTypes
currentType:docType];
[savePanel setAccessoryView:saveAccessory];
[savePanel setDelegate:self];
[savePanel setTreatsFilePackagesAsDirectories:NO];
if (!docType) {
[savePanel setRequiredFileType:[[saveTypes objectAt:0] typeExtension]];
} else {
[savePanel setRequiredFileType:[docType typeExtension]];
}
if ([docName isEmpty]) {
dir = [[MOPathStringClass allocFromZone:[self zone]]
initPath:NXHomeDirectory()];
file = [untitledString copy];
} else {
file = [docName file];
dir = [docName directory];
}
if (![savePanel runModalForDirectory:[dir stringValue]
file:[file stringValue]]) {
[file free];
[dir free];
[saveTypes free];
return nil;
}
[docName setPath:[savePanel filename]];
extension = [docName fileExtension];
docType = [[self class] docTypeForExtension:[extension stringValue]];
[extension free];
[self resetWindowTitle];
[self save:self];
[file free];
[dir free];
[saveTypes free];
return self;
}
- saveTo:sender
// Called by DocManager in response to Save To command. The bit about
// the flushWindow is so the title bar doesn't flash the save to path and
// then reset to the old path visibly.
{
id copyString = [docName copy];
BOOL copyDirty = [self isDirty];
if (window) {
[window disableFlushWindow];
}
[self saveAs:self];
[self setDirty:copyDirty];
[docName free];
docName = copyString;
[self resetWindowTitle];
if (window) {
[window reenableFlushWindow];
[window flushWindow];
}
return self;
}
- revert:sender
// Called when a file is opened or reverted.
{
BOOL ret = YES;
if ([docName isEmpty]) {
ret = [self doClear];
} else {
ret = ([self perform:[docType openSelector] with:docType] != nil);
}
if (ret) {
[self resetWindowTitle];
[self setDirty:NO];
}
return ret?self:nil;
}
- close:sender
// Closes the document's window.
{
[window performClose:self];
return self;
}
- print:sender
// Prints the doc.
{
BOOL ret = [self doPrint];
return ret?self:nil;
}
- saveIfNeeded
// Save ourself only if we are dirty.
{
if ([self isDirty]) [self save:self];
return self;
}
- setManager:aManager
// Set the docManager that manages us.
{
if (manager) [manager removeDocument:self];
manager = aManager;
if (manager) [manager addDocument:self];
return self;
}
- manager
// Return our manager.
{
return manager;
}
- setFile:(const char *)fName
// Name our file. If fName is NULL, set up an untitled name. The file
// should be of a type we can read. Also, if the filename is not of a
// type we can save, make it untitled after loading.
{
id extension = nil;
[docName setStringValue:fName];
if ([docName isEmpty]) {
docType = nil;
if (manager) {
[untitledString setFromFormat:"%s-%d", UNTITLED_STRING,
[manager nextUntitledNum]];
} else {
[untitledString setFromFormat:"%s", UNTITLED_STRING];
}
}
[self window:YES];
if (window) {
[window disableFlushWindow];
}
if (![docName isEmpty]) {
extension = [docName fileExtension];
docType = [[self class] docTypeForExtension:[extension stringValue]];
}
if (![self revert:self]) {
NXRunAlertPanel("Error", "Failed to revert document %s.", "OK",
NULL, NULL, [docName stringValue]);
}
// If we can't save files of whatever type this is, set us to untitled.
// This must be done after the revert.
if (![docName isEmpty]) {
if (!docType || (![docType canSaveType])) {
[docName setNull];
docType = nil;
if (manager) {
[untitledString setFromFormat:"%s-%d", UNTITLED_STRING,
[manager nextUntitledNum]];
} else {
[untitledString setFromFormat:"%s", UNTITLED_STRING];
}
[self resetWindowTitle];
}
}
if (extension) [extension free];
if (window) {
[window reenableFlushWindow];
[window flushWindow];
}
[self showWindow:self];
return self;
}
- (BOOL)isUntitled
// Returns YES if this doc has no file.
{
return [docName isEmpty];
}
- (const char *)file
// Return our filename (or the untitled name if we have no file).
{
if (![docName isEmpty]) {
return [docName stringValue];
} else {
return [untitledString stringValue];
}
}
- docName
// Return the actual MOPathString used to store our doc name. This method
// can be useful if you want to use the MOPathString features. Otherwise,
// use the -file method.
{
return docName;
}
- resetWindowTitle
// Make sure that our window title is correct.
{
if (window) [window setTitleAsFilename:[self file]];
return self;
}
- setDirty:(BOOL)flag
// Mark ourselves as edited.
{
isDirty = flag;
if (window) [window setDocEdited:flag];
return self;
}
- (BOOL)isDirty
// Returns whether or not the doc is dirty.
{
return isDirty;
}
- windowDidBecomeMain:sender
// Inform the manager that we should be the current document.
{
if (manager) [manager setCurrentDocument:self];
return self;
}
- windowDidResignMain:sender
// Inform the manager that we should no longer be the current document.
{
if (manager) [manager setCurrentDocument:nil];
return self;
}
- windowWillClose:sender
// If we are dirty, ask if we should save before closing and give the chance
// to cancel.
{
int ret;
if ([self isDirty]) {
ret = NXRunAlertPanel("Close", "Document %s has unsaved changes. "
"Do you wish to save, close anyway, or cancel?",
"Save", "Close Anyway", "Cancel", [self file]);
if (ret==NX_ALERTDEFAULT) {
// save
if (![self save:self]) {
return nil;
}
} else if (ret==NX_ALERTALTERNATE) {
// close anyway
// do nothing, just let it close
} else {
// cancel
return nil;
}
}
// if we get to here, we are closing
[NXApp delayedFree:self];
return self;
}
- awake
// Initialize stuff we don't archive.
{
saveAccessoryPanel = nil;
saveAccessoryBox = nil;
saveAccessoryPopupButton = nil;
return self;
}
- read:(NXTypedStream *)strm
// This method is probably not useful. But here it is anyway.
{
int classVersion;
[super read:strm];
classVersion = NXTypedStreamClassVersion(strm, CLASS_NAME);
switch (classVersion) {
case 0: // First version.
docName = NXReadObject(strm);
untitledString = NXReadObject(strm);
manager = NXReadObject(strm);
NXReadType(strm, "C", &isDirty);
docType = nil;
[self setFile:[docName stringValue]];
break;
default:
NXLogError("[%s read:] class version %d cannot read "
"instances archived with version %d",
CLASS_NAME, CLASS_VERSION, classVersion);
docName = [[MOPathStringClass allocFromZone:[self zone]] init];
untitledString = [[MOPathStringClass allocFromZone:
[self zone]] init];
manager = nil;
isDirty = NO;
docType = nil;
[self setFile:NULL];
break;
}
return self;
}
- write:(NXTypedStream *)strm
// This method is probably not useful. But here it is anyway.
{
[super write:strm];
NXWriteObject(strm, docName);
NXWriteObject(strm, untitledString);
NXWriteObjectReference(strm, manager);
NXWriteType(strm, "C", &isDirty);
return self;
}
@end
These are the contents of the former NiCE NeXT User Group NeXTSTEP/OpenStep software archive, currently hosted by Netfuture.ch.