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.