I am currently in the process of trying to teach myself Cocoa development. Toward this end, I purchased a mostly-excellent book, Cocoa Recipes for Mac OS X: Vermont Recipes, which walks through creating a sample application. It's pretty good, but it's written against XCode 3.2 rather than XCode 4. So far, I've been able to bridge this myself, but I've run up against an issue where I can't figure out how to follow the instruction.
我目前正在尝试自学可可开发。为此,我买了一本非常棒的书,是关于Mac OS X的Cocoa食谱:佛蒙特州的食谱,通过创建一个样本应用程序。这很好,但是它是针对XCode 3.2而不是XCode 4编写的。到目前为止,我已经能够自己解决这个问题了,但是我遇到了一个问题,我不知道该怎么做。
Essentially, the book goes through a sample case of subclassing NSDocumentController
so that the application can handle two (eventually maybe an arbitrary number) different types of documents, and so it opens the appropriate window for each type. So, I've created a custom subclass of NSDocumentController
(which the book calls VRDocumentController
), and now I need to make it such that an instance of this controller loads relatively early in the application launch process. Basically, the class is a singleton, and so I have to instantiate mine before the application instantiates the standard class, and this has to be done early in the process. Fair enough.
从本质上讲,本书通过了一个子类化NSDocumentController的示例案例,以便应用程序能够处理两种(最终可能是任意数量的)不同类型的文档,因此它为每种类型打开了合适的窗口。因此,我已经创建了一个NSDocumentController的自定义子类(本书称为VRDocumentController),现在我需要使它在应用程序启动过程中较早地加载该控制器的实例。基本上,类是一个单例,所以在应用程序实例化标准类之前,我必须实例化我的类,这必须在流程的早期完成。很好。
The book cites the Apple documentation for subclassing NSDocumentController, which states that there are two ways to attack the problem: to instantiate the class in your MainMenu.xib
file or to instantiate one in your -applicationWillFinishLaunching:
delegate method. The Apple documentation doesm't give clear instruction on how to do either of these (more on that in a moment) and the book covers only the first version, which I think is probably my preferred method.
这本书引用了苹果文档的子类化NSDocumentController,说明有两种方法可以解决这个问题:在主菜单中实例化类。xib文件或实例化一个在你的-applicationWillFinishLaunching: delegate方法。苹果文档并没有明确说明如何处理这些(稍后会详细说明),而本书只涵盖了第一个版本,我认为这可能是我的首选方法。
My problem: I cannot for the life of me pull this off in XCode 4. The instructions the book provides for XCode 3.2 are no longer accurate, because Interface Builder has been shuffled into XCode itself now and the new version of the "classes tab" no longer shows my project's classes. I found this question on Stack Overflow asking a similar question, so I tried following the accepted answer there. However, when I open the Identity Inspector and try to type VRDocumentController
, it just beeps at me and doesn't take it. None of the other controller classes I've written seem to be acceptable inputs either.
我的问题是:我无法在XCode 4中实现我的生活。书中提供的XCode 3.2的说明不再准确,因为接口构建器现在已经被拖进XCode本身,而新版本的“classes标签”不再显示我的项目的类。我在Stack Overflow上发现了这个问题,问了一个类似的问题,所以我试着遵循了这个被接受的答案。但是,当我打开身份检查器并尝试键入VRDocumentController时,它只会向我发出哔哔的声音,并且不接受它。我所编写的其他控制器类都不是可接受的输入。
I'd also be happy to go the other route; instantiating a copy in the -applicationWillFinishLaunching
method. But, I have no earthly idea in which class that method actually belongs, or what its return type is. I've done a non-trivial amount of searching for that, too, with no luck.
我也很乐意走另一条路;在-applicationWillFinishLaunching方法中实例化一个副本。但是,我不知道哪种方法真正属于哪个类,或者它的返回类型是什么。我也做了一些非平凡的搜索,没有运气。
7 个解决方案
#1
13
In your application delegate:
在您的应用程序委托:
// LukeAppDelegate.h
#import "LukeAppDelegate.h"
#import "VRDocumentController"
- (void)applicationWillFinishLaunching:(NSNotification *)notification {
VRDocumentController *dc = [[VRDocumentController alloc] init];
}
This will make sure that an instance of VRDocumentController
is created and registered as the shared document controller, preventing Cocoa from using the default NSDocumentController
.
这将确保创建一个VRDocumentController实例并注册为共享文档控制器,防止Cocoa使用默认的NSDocumentController。
As to why you haven’t been able to use a custom object in your nib file, make sure that that you select Object (blue cube) instead of Object Controller (blue cube inside a green sphere) when dragging a new object into the nib file.
至于为什么不能在nib文件中使用自定义对象,请确保在将新对象拖进nib文件时,选择对象(蓝色立方体)而不是对象控制器(绿色球体内部的蓝色立方体)。
Edit: If you’re targeting an OS X version that supports restoration, -applicationWillFinishLaunching:
may be too late to register a custom document controller. If the application delegate is placed inside MainMenu.xib, it should be instantiated by the nib loading process before any documents are restored, hence you can move the NSDocumentController
subclass initialisation to the application delegate’s init method:
编辑:如果你的目标是支持恢复的OS X版本,那么-applicationWillFinishLaunching:可能已经来不及注册一个自定义文档控制器了。如果应用程序委托被放置在主菜单中。xib,在任何文档恢复之前,应该由nib加载过程实例化,因此您可以将NSDocumentController子类初始化到应用程序委托的init方法:
// LukeAppDelegate.h
#import "LukeAppDelegate.h"
#import "VRDocumentController"
- (id)init {
self = [super init];
VRDocumentController *dc = [[VRDocumentController alloc] init];
return self;
}
#2
5
The answer marked correct (@Bavarious'answer) fails to work on document-based apps because documents often load before any applicationWillFinishLaunching:
is called. After reading the helpful clues in @Graham Perks answer, I tried a few different approaches and this seems to work reliably:
答案是正确的(@Bavarious的答案),由于文档经常在任何applicationWillFinishLaunching之前加载,所以不能使用基于文档的应用程序。在阅读了@Graham津贴的有用提示后,我尝试了几种不同的方法,这似乎是可靠的:
@implementation AppDelegate
- (id)init
{
self = [super init];
if (self) {
MyDocumentController *dc = [[MyDocumentController alloc] init];
if (dc) {};
}
return self;
}
Note Obviously (if not already created) you'll need to create an AppDelegate yourself first and link it in your MainMenu.XIB.
注意,显然(如果还没有创建),首先需要创建一个AppDelegate,并将其链接到MainMenu.XIB。
#3
2
Here's a solution:
这里有一个解决方案:
// In MyDocumentController.h
@interface MyDocumentController : NSDocumentController
@end
// In MyDocumentController.m
@implementation MyDocumentController
// ... your custom code here
@end
// In MyAppDelegate.h
@interface AppDelegate : NSObject <NSApplicationDelegate>
@property (nonatomic, strong) IBOutlet MyDocumentController *myController;
@end
Now, go into MainMenu.xib and add a custom object to your nib. Be sure to use the inspector on this object, and in the third pane of the inspector, set the custom class to MyDocumentController.
现在,进入MainMenu。将自定义对象添加到nib中。确保在这个对象上使用检查器,在检查器的第三个窗格中,将自定义类设置为MyDocumentController。
Now wire this object into your outlet by ctrl-clicking on your new object in the left list of things in the nib and drag (while still ctrl-clicking) to App Delegate. Release and it should flash and show myController. Click that and you're all set.
现在将这个对象连接到您的outlet,按ctrl单击您的新对象,在nib中左边的列表中,拖动(同时仍然按ctrl单击)到App委托。Release和它应该flash和显示myController。点击这个,你就全部搞定了。
Now you can test that you're getting your custom controller with the following code:
现在您可以通过以下代码测试您是否得到了自定义控制器:
// In MyAppDelegate.m
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
NSLog(@"sharedController is %@", [NSDocumentController sharedController]);
}
This should print something like
这应该打印出来。
<MyDocumentController: 0x600000044710>
You're done!
你已经完成了!
The key is to wire in your custom MyDocumentController in MainMenu.xib. If you try to just initialize it in applicationDidFinishLaunching, it's often too late and AppKit has already created and set sharedDocumentController (of which there can only be one).
关键是在MainMenu.xib中连接定制的MyDocumentController。如果您尝试在applicationDidFinishLaunching中初始化它,那么通常太晚了,AppKit已经创建并设置了sharedDocumentController(其中只有一个)。
Also, the outlet needs to be strong not weak because your custom controller is a top-level object in the nib not referenced by anything else in the nib.
另外,outlet需要强大而不是弱,因为您的自定义控制器是nib中的*对象,在nib中没有其他任何东西引用。
This works for me on multiple versions of OS X and Xcode.
这对我来说适用于多个版本的OS X和Xcode。
Please mark this as the correct answer if it works for you! :-)
请将此标记为正确的答案,如果它对您有用!:-)
#4
2
Easier method of getting the Custom Document Controller created in time is to implement the + (void) load
method:
在时间上创建定制文档控制器的更简单方法是实现+ (void)加载方法:
This is literally all you need to do it implement a custom document controller.
这就是实现自定义文档控制器所需的全部内容。
@implementation AppDocumentController
+ (void) load
{
[AppDocumentController new];
}
// ... your overriding goes here
@end
This is cool because there's no separation of the codez. Not 100% sure if the controller is created too early, but works well for me.
这很酷,因为没有分离的codez。不是100%确定控制器是否创建得太早,但对我来说很有效。
#5
1
If - as can sometimes happen - your NSDocumentController subclass seems to randomly NOT be returned as the sharedDocumentController, add a breakpoint on -[NSDocumentController init] and see on the stack what bit of your code is (probably accidentally) creating the regular NSDocumentController. The first controller created is always returned from sharedDocumentController.
如果- as有时会发生-您的NSDocumentController子类似乎不会被作为sharedDocumentController返回,添加一个断点(NSDocumentController init),并在堆栈上看到您的代码(可能是意外地)创建常规的NSDocumentController。创建的第一个控制器总是从sharedDocumentController返回。
This can happen when something in your XIB file gets init'd and (even indirectly) invokes sharedDocumentController. The order of object creation during XIB file loading isn't deterministic, so sometimes your NSDocumentController subclass will be created first, other times another offending object gets init'd first and subsequently your subclass gets ignored.
这可能发生在您的XIB文件中的某个东西得到init的时候(甚至间接地)调用sharedDocumentController。在XIB文件加载过程中,对象创建的顺序不是确定的,所以有时您的NSDocumentController子类会首先被创建,而其他时候,另一个违规对象会先得到init,然后您的子类会被忽略。
You'll have to move the other object's creation to a later point (e.g. applicationDidFinishLaunching:). Or have the object created at the same time, but delay whatever initialization its doing.
您将不得不将其他对象的创建移动到稍后的点(例如applicationDidFinishLaunching:)。或者同时创建对象,但是延迟任何初始化操作。
(Just ran into this with an inspector panel screwing things up, thought it worth noting here!)
(就这样,一个督察小组把事情搞砸了,觉得这里值得注意!)
#6
1
You can subclass easily in Swift 3:
你可以轻松地在Swift 3:
class AppDelegate: NSObject {
let docController = DocController()
}
class DocController: NSDocumentController {
}
#7
-1
// This looked like the best thread to post this.
// This is incomplete skeleton example of NSDocument/NSUndoManager usage.
// Use at your own risk
// Ultimately, would love if Apple reviewed/corrected this and added correct sample code to their docs.
// I put pseudo code in implementation specific spots:
// "... implementation specific ..."
// "... implement something inherently dangerous... loading your data from a file"
// "... implement something inherently dangerous... commit changes to a file ..."
// "... in my implementation, I prompt user with save/quit options when edit window is closed ..."
//
// Apple's documentation states that NSDocumentController should *rarely* be subclassed,
// but Apple fails to provide sample code to accomplish what you need.
// After trying to subclass NSDocumentController, I decided to rip all that code out.
// What I really needed, in my case, was this:
// @interface NSObject(NSApplicationDelegate)
// - (BOOL)application:(NSApplication *)sender openFile:(NSString *)filename
//
// My requirements in a nutshell:
// NSUndoManager, only allow 1 document open at a time (for now), multiple editing windows for 1 document,
// automatically save to temporary file, ability to quit and relaunch without saving,
// Custom "open", "save", "save as", "revert to saved", etc... MyDocumentController
// subclass below provides a simple way to bypass much of the NSDocumentController
// file dialogs, but still use other features of NSDocument (like track unsaved changes).
// Override only "NSDocumentController documentForURL".
/* your NSDocument subclass MUST be defined in your Info.plist.
If you don't, Lion will complain with a NSLog message that looks like this:
-[NSDocumentController openDocumentWithContentsOfURL:display:completionHandler:] failed during state restoration. Here's the error:
Error Domain=NSCocoaErrorDomain Code=256 "The document “blah.myfiletype” could not be opened. cannot open files in the “blah Document” format."
In this example, files of type .myfiletype are associated with my NSDocument subclass
Personally, I hate having to put a class name in my Info.plist, because its not maintainable!
Note to Apple developers: if you are going to force us to do this, then pleasssse make
sure that an XCode search "In Project" for "MyDocument" finds the entry in the plist file!
Oh wow, I just answered my own question: Apple! please make "All candidate files" the
default option for search!
MyDocument:
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeName</key>
<string>My Funky App File</string>
<key>NSDocumentClass</key>
<string>MyDocument</string>
<key>CFBundleTypeExtensions</key>
<array>
<string>myfiletype</string>
</array>
<key>CFBundleTypeIconFile</key>
<string>My_File_Icon.icns</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
</dict>
</array>
*/
#define kMyFileTypeExtension @"myfiletype"
@interface MyDocument : NSDocument
{
}
- (void) registerForUndoGrouping;
- (NSString *)filePath; // convenience function
- (void) setFilePath: (NSString *)filePath; // convenience function
+ (BOOL) openMyDocument: (NSString *)filename; // class method
@end
extern int gWantsToQuit; // global indicator that it's time to stop drawing/updating
// track my startup state so I can control order of initialization, and so I don't
// waste time drawing/updating before data is available.
enum // MyLaunchStatus
{
kFinishedPreWaking = 0x01, // step 1: applicationWillFinishLaunching called
kFinishedOpenFile = 0x02, // step 2: (optionally) application:openFile: called (important for detecting double-click file to launch app)
kFinishedWaking = 0x04, // step 3: NSApp run loop ready to run. applicationDidFinishLaunching
kFinishedPreLaunchCheck = 0x08, // step 4: error recovery check passed
kFinishedLoadingData = 0x10, // step 5: data loaded
kFinishedAndReadyToDraw = 0x20, // step 6: run loop ready for drawing
};
typedef NSUInteger MyLaunchStatus;
#pragma mark -
@interface MyAppController : NSResponder <MidiProtocol, NSOpenSavePanelDelegate, NSTextFieldDelegate, NSWindowDelegate>
MyDocument *toDocument;
@end
#pragma mark -
@implementation MyDocument
- (id)init
{
if ( !(self = [super init]) ) return self;
return self;
}
- (void) registerForUndoGrouping
{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(beginUndoGroup:)
name:NSUndoManagerDidOpenUndoGroupNotification
object:nil];
}
- (void)canCloseDocumentWithDelegate:(id)delegate shouldCloseSelector:(SEL)shouldCloseSelector contextInfo:(void *)contextInfo
{
if ( [[MyAppController instance] windowShouldClose: delegate] )
if ( [delegate respondsToSelector:@selector(close)] )
[delegate close];
//[delegate performSelector:shouldCloseSelector];
//if ( [delegate isKindOfClass:[NSWindow class]] )
// [delegate performClose:self]; // :self];
return; // handled by [[MyAppController instance] windowShouldClose:(id)sender
}
- (void)shouldCloseWindowController:(NSWindowController *)windowController delegate:(id)delegate shouldCloseSelector:(SEL)shouldCloseSelector contextInfo:(void *)contextInfo
{
if ( [[MyAppController instance] windowShouldClose: [windowController window]] )
if ( [[windowController window] respondsToSelector:@selector(close)] )
[[windowController window] close];
}
- (void) beginUndoGroup: (NSNotification *)iNotification
{
NSUndoManager *undoMgr = [self undoManager];
if ( [undoMgr groupingLevel] == 1 )
{
// do your custom stuff here
}
}
// convenience functions:
- (NSString *)filePath { return [[self fileURL] path]; }
- (void) setFilePath: (NSString *)filePath
{
if ( [filePath length] )
[self setFileURL:[NSURL fileURLWithPath: filePath]];
else
[self setFileURL:nil];
}
- (BOOL)validateUserInterfaceItem:(id <NSValidatedUserInterfaceItem>)anItem
{
if ( [self isDocumentEdited] && [anItem action] == @selector(revertDocumentToSaved:) )
return YES;
BOOL retVal = [super validateUserInterfaceItem:(id <NSValidatedUserInterfaceItem>)anItem];
return retVal;
}
- (IBAction)revertDocumentToSaved:(id)sender
{
NSInteger retVal = NSRunAlertPanel(@"Revert To Saved?", [NSString stringWithFormat: @"Revert to Saved File %@?", [self filePath]], @"Revert to Saved", @"Cancel", NULL);
if ( retVal == NSAlertDefaultReturn )
[[MyAppController instance] myOpenFile:[self filePath]];
}
+ (BOOL) openMyDocument: (NSString *)filename
{
if ( ![[filename pathExtension] isEqualToString: kMyAppConsoleFileExtension] )
return NO;
// If the user started up the application by double-clicking a file, the delegate receives the application:openFile: message FIRST
MyLaunchStatus launchStatus = [[MyAppController instance] isFinishedLaunching];
BOOL userDoubleClickedToLaunchApp = !( launchStatus & kFinishedPreLaunchCheck );
MyDocument *currDoc = [[MyAppController instance] document];
NSString *currPath = [currDoc filePath];
NSInteger retVal;
NSLog( @"open file %@ currPath %@ launchStatus %d", filename, currPath, launchStatus );
if ( userDoubleClickedFileToLaunchApp )
{
// user double-clicked a file to start MyApp
currPath = [[NSUserDefaults standardUserDefaults] objectForKey:@"LastSaveFile"];
if ( [currPath isEqualToString: filename] )
{
sWasAlreadyOpen = YES;
if ( [[[NSUserDefaults standardUserDefaults] objectForKey:@"isDocumentEdited"] boolValue] == YES )
{
retVal = NSRunAlertPanel(@"Open File", @"Revert to Saved?", @"Revert to Saved", @"Keep Changes", @"Quit", NULL);
if ( retVal == NSAlertDefaultReturn )
{
[[MyAppController instance] myOpenFile:filename];
}
else if ( retVal == NSAlertOtherReturn )
exit(0);
}
// proceed with normal startup
if ( currDoc )
return YES;
else
return NO;
}
}
if ( !(launchStatus & kFinishedPreLaunchCheck ) ) // not done launching
return YES; // startup in whatever state we were before
if ( [currPath isEqualToString: filename] )
{
sWasAlreadyOpen = YES;
NSLog( @"is edited %d currDoc %@", [currDoc isDocumentEdited], currDoc );
if ( [currDoc isDocumentEdited] )
[currDoc revertDocumentToSaved:self]; // will prompt
else // document is already open, so do what Apple's standard action is...
[currDoc showWindows];
}
else
{
if ( [currDoc isDocumentEdited] )
retVal = NSRunAlertPanel(@"Open File", [NSString stringWithFormat: @"The current file has unsaved changes. Discard unsaved changes and switch to file '%@'?", filename], @"Discard unsaved changes and switch to file", @"Keep Current", NULL);
else
retVal = NSRunAlertPanel(@"Switch to File", [NSString stringWithFormat: @"Switch to File '%@'?\n\nCurrent file '%@'", filename, currfilePath ? currfilePath : @"Untitled"], @"Switch", @"Keep Current", NULL);
if ( retVal == NSAlertDefaultReturn )
[[MyAppController instance] myOpenFile:filename];
}
// user cancelled
if ( currDoc )
return YES;
else
return NO;
}
// Note: readFromURL is here for completeness, but it should never be called,
// because we override NSDocumentController documentForURL below.
- (BOOL)readFromURL:(NSURL *)absoluteURL ofType:(NSString *)typeName error:(NSError **)outError
{
if ( outError )
*outError = nil;
if ( ![typeName isEqualToString: kMyFileTypeExtension ] ) //
return NO;
return YES;
}
// Note: writeToURL is here for completeness, but it should never be called,
// because we override NSDocumentController documentForURL below.
- (BOOL)writeToURL:(NSURL *)absoluteURL ofType:(NSString *)typeName error:(NSError **)outError
{
if ( outError )
*outError = nil;
return YES;
}
@end
// kpk migrating slowly toward NSDocument framework
// (currently most functionality is in MyAppController)
// Must bypass default NSDocumentController behavior to allow only 1 document
// and keep MyAppController responsible for read, write, dialogs, etc.
@implementation MyDocumentController
// this should be the only override needed to bypass NSDocument dialogs, readFromURL,
// and writeToURL calls.
// Note: To keep Lion happy, MainInfo.plist and Info.plist must define "MyDocument" for key "NSDocumentClass"
- (id)documentForURL:(NSURL *)absoluteURL
{
MyDocument *currDoc = [[MyAppController instance] document];
if ( [[currDoc filePath] isEqualToString: [absoluteURL path]] )
return currDoc;
else
return nil;
}
@end
#pragma mark -
@implementation MyAppController
static MyAppController *sInstance;
+ (MyAppController *)instance
{
return sInstance; // singleton... why is this not in all Apple's sample code?
}
// called by main.mm before MyAppController (or NSApp for that matter) is created.
// need to init some global variables here.
+ (void) beforeAwakeFromNib
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
... implementation specific ...
// disable fancy stuff that slows launch down
[[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithBool:NO] forKey: @"NSAutomaticWindowAnimationsEnabled"];
[pool release];
}
- (void) awakeFromNib
{
NSLog(@"MyAppController awake\n");
sInstance = self;
[toWindow setNextResponder:self];
[NSApp setDelegate:self];
[self addWindowToDocument:toWindow];
}
- (MyDocument *)document
{
if ( !toDocument )
{
toDocument = [[MyDocument alloc] init];
}
return toDocument;
}
- (NSUndoManager *)undoManager
{
// !!! WARNING: there are multiple NSUndoManager's in this App
// Note: when an editable text field is in focus,
// NSTextField will create
// a separate undo manager for editing text while that field is in focus.
// This means that hitting undo/redo while editing a text field will not go beyond the scope of that field.
// This will return the global undo manager if the keyWindow was registered
// via [self addWindowToDocument:];
// Windows which are NOT part of the document (such as preferences, popups, etc.), will
// return their own undoManager, and undo will do nothing while those windows are in front.
// You can't undo preferences window changes, so we don't want to surprise the user.
NSUndoManager *undomgr = [[NSApp keyWindow] undoManager];
if ( undomgr )
{
static bool sFirstTime = true;
if ( sFirstTime )
{
sFirstTime = false;
[undomgr setLevelsOfUndo:1000]; // set some sane limit
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(beginUndo:)
name:NSUndoManagerWillUndoChangeNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(beginUndo:)
name:NSUndoManagerWillRedoChangeNotification
object:nil];
[toDocument registerForUndoGrouping];
// [[NSNotificationCenter defaultCenter] addObserver:self
// selector:@selector(endUndo:)
// name:NSUndoManagerDidUndoChangeNotification
// object:nil];
// [[NSNotificationCenter defaultCenter] addObserver:self
// selector:@selector(endUndo:)
// name:NSUndoManagerDidRedoChangeNotification
// object:nil];
}
}
return undomgr;
}
- (void) showStatusText: (id)iStatusText
{
... implementation specific ...
}
- (void) beginUndo:(id)sender
{
// implementation specific stuff here
NSUndoManager *undomgr = [[NSApp keyWindow] undoManager];
if ( [sender object] == undomgr )
{
if ( [undomgr isUndoing] )
[self showStatusText: [NSString stringWithFormat:@"Undo %@", [undomgr undoActionName]]];
else if ( [undomgr isRedoing] )
[self showStatusText: [NSString stringWithFormat:@"Redo %@", [undomgr redoActionName]]];
}
}
// Add a window (with a window controller) to our document, so that the window
// uses the document's NSUndoManager. In the future, we may want to use other features of NSDocument.
- (void)addWindowToDocument:(NSWindow *)iWindow
{
NSString *autosaveName = [iWindow frameAutosaveName]; // preserve for "mainWindow", others.
NSWindowController *winController = [iWindow windowController];
if ( !winController )
winController = [[NSWindowController alloc] initWithWindow:iWindow];
// create document if needed, and add window to document.
[[self document] addWindowController: winController];
if ( autosaveName )
[iWindow setFrameAutosaveName:autosaveName]; // restore original for "mainWindow", others.
[winController setNextResponder:self]; // keep last hotkey destination... see keyDown:
}
- (void) myOpenFile:(NSString*)path
{
// this is just a skeleton of what I do to track unsaved changes between relaunches
[toDocument setFilePath:path];
... implementation specific ...
[[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithBool:NO] forKey:@"isDocumentEdited"];
[toDocument updateChangeCount:NSChangeCleared];
[[NSUserDefaults standardUserDefaults] setObject:[toDocument filePath] forKey:@"LastSaveFile"];
BOOL success = [[NSUserDefaults standardUserDefaults] synchronize]; // bootstrap
// kpk very important... resetStandardUserDefaults forces the immutable
// tree returned by dictionaryWithContentsOfFile to be mutable once re-read.
// Apple: "Synchronizes any changes made to the shared user defaults object and releases it from memory.
// A subsequent invocation of standardUserDefaults creates a new shared user defaults object with the standard search list."
[NSUserDefaults resetStandardUserDefaults];
NSString *name = [[NSUserDefaults standardUserDefaults] objectForKey:@"LastSaveFile"];
}
- (void) mySaveData:(NSString*)path
{
// this is just a skeleton of what I do to track unsaved changes between relaunches
@try
{
... implement something inherently dangerous... commit changes to a file ...
if ( !errorStr )
{
if ( [toDocument isDocumentEdited] )
{
// UInt64 theTimeNow = VMPGlue::GetMilliS();
[[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithBool:NO] forKey:@"isDocumentEdited"];
[[NSUserDefaults standardUserDefaults] synchronize]; // bootstrap
// DLog( @"synchronize success %d ms", (int)(VMPGlue::GetMilliS() - theTimeNow) );
}
// tbd consider MyDocument saveToURL:ofType:forSaveOperation:error:
[toDocument updateChangeCount:NSChangeCleared];
}
@catch (...)
{
... run critical alert ...
}
}
- (void) finishLoadingData
{
@try
{
if ( dataexists )
{
... implement something inherently dangerous... loading your data from a file
[toDocument setFilePath: [[NSUserDefaults standardUserDefaults] objectForKey:@"LastSaveFile"]];
NSNumber *num = [[NSUserDefaults standardUserDefaults] objectForKey:@"isDocumentEdited"];
if ( [num boolValue] == YES )
[toDocument updateChangeCount:NSChangeDone];
}
else
{
[[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithBool:NO] forKey:@"isDocumentEdited"];
[toDocument updateChangeCount:NSChangeCleared];
}
sFinishedLaunching |= kFinishedLoadingData;
}
@catch (...)
{
// !!! will not return !!!
... run critical alert ...
// !!! will not return !!!
}
}
#pragma mark NSApplication delegate
// Apple: Sent directly by theApplication to the delegate. The method should open the file filename,
//returning YES if the file is successfully opened, and NO otherwise.
//If the user started up the application by double-clicking a file, the delegate receives the application:openFile: message before receiving applicationDidFinishLaunching:.
//(applicationWillFinishLaunching: is sent before application:openFile:.)
- (BOOL)application:(NSApplication *)sender openFile:(NSString *)filename
{
BOOL didOpen = [MyDocument openMyDocument: filename];
sFinishedLaunching |= kFinishedOpenFile;
return didOpen;
}
// NSApplication notification
- (void) applicationDidFinishLaunching:(NSNotification*)note
{
// kpk note: currentEvent is often nil at this point! [[NSApp currentEvent] modifierFlags]
CGEventFlags modifierFlags = CGEventSourceFlagsState(kCGEventSourceStateHIDSystemState);
sFinishedLaunching |= kFinishedWaking;
if ( modifierFlags & (kCGEventFlagMaskShift | kCGEventFlagMaskCommand) )
{
... implementation specific ... alert: @"Shift or Command key held down at startup.\nWhat would you like to do?"
title: @"Startup Options"
canContinue: @"Continue" ];
}
sFinishedLaunching |= kFinishedPreLaunchCheck;
[self finishLoadingData];
sFinishedLaunching |= kFinishedAndReadyToDraw;
}
- (BOOL)windowShouldClose:(id)sender
{
if ( [sender isKindOfClass: [NSWindow class]] && sender != toWindow )
return YES; // allow non-document-edit windows to close normally
... in my implementation, I prompt user with save/quit options when edit window is closed ...
return NO;
}
- (NSApplicationTerminateReply) applicationShouldTerminate:(NSApplication*)sender
{
if ( !gWantsToQuit && [toDocument isDocumentEdited] )
{
if ( ![self windowShouldClose:self] )
return NSTerminateCancel;
}
return NSTerminateNow;
}
- (void) applicationWillTerminate:(NSNotification *)notification
{
if ( gWantsToQuit )
{
... implementation specific ... dont save potentially wonky data if relaunch is required
}
else
{
[self saveData: [toDocument filePath]];
}
}
@end
#1
13
In your application delegate:
在您的应用程序委托:
// LukeAppDelegate.h
#import "LukeAppDelegate.h"
#import "VRDocumentController"
- (void)applicationWillFinishLaunching:(NSNotification *)notification {
VRDocumentController *dc = [[VRDocumentController alloc] init];
}
This will make sure that an instance of VRDocumentController
is created and registered as the shared document controller, preventing Cocoa from using the default NSDocumentController
.
这将确保创建一个VRDocumentController实例并注册为共享文档控制器,防止Cocoa使用默认的NSDocumentController。
As to why you haven’t been able to use a custom object in your nib file, make sure that that you select Object (blue cube) instead of Object Controller (blue cube inside a green sphere) when dragging a new object into the nib file.
至于为什么不能在nib文件中使用自定义对象,请确保在将新对象拖进nib文件时,选择对象(蓝色立方体)而不是对象控制器(绿色球体内部的蓝色立方体)。
Edit: If you’re targeting an OS X version that supports restoration, -applicationWillFinishLaunching:
may be too late to register a custom document controller. If the application delegate is placed inside MainMenu.xib, it should be instantiated by the nib loading process before any documents are restored, hence you can move the NSDocumentController
subclass initialisation to the application delegate’s init method:
编辑:如果你的目标是支持恢复的OS X版本,那么-applicationWillFinishLaunching:可能已经来不及注册一个自定义文档控制器了。如果应用程序委托被放置在主菜单中。xib,在任何文档恢复之前,应该由nib加载过程实例化,因此您可以将NSDocumentController子类初始化到应用程序委托的init方法:
// LukeAppDelegate.h
#import "LukeAppDelegate.h"
#import "VRDocumentController"
- (id)init {
self = [super init];
VRDocumentController *dc = [[VRDocumentController alloc] init];
return self;
}
#2
5
The answer marked correct (@Bavarious'answer) fails to work on document-based apps because documents often load before any applicationWillFinishLaunching:
is called. After reading the helpful clues in @Graham Perks answer, I tried a few different approaches and this seems to work reliably:
答案是正确的(@Bavarious的答案),由于文档经常在任何applicationWillFinishLaunching之前加载,所以不能使用基于文档的应用程序。在阅读了@Graham津贴的有用提示后,我尝试了几种不同的方法,这似乎是可靠的:
@implementation AppDelegate
- (id)init
{
self = [super init];
if (self) {
MyDocumentController *dc = [[MyDocumentController alloc] init];
if (dc) {};
}
return self;
}
Note Obviously (if not already created) you'll need to create an AppDelegate yourself first and link it in your MainMenu.XIB.
注意,显然(如果还没有创建),首先需要创建一个AppDelegate,并将其链接到MainMenu.XIB。
#3
2
Here's a solution:
这里有一个解决方案:
// In MyDocumentController.h
@interface MyDocumentController : NSDocumentController
@end
// In MyDocumentController.m
@implementation MyDocumentController
// ... your custom code here
@end
// In MyAppDelegate.h
@interface AppDelegate : NSObject <NSApplicationDelegate>
@property (nonatomic, strong) IBOutlet MyDocumentController *myController;
@end
Now, go into MainMenu.xib and add a custom object to your nib. Be sure to use the inspector on this object, and in the third pane of the inspector, set the custom class to MyDocumentController.
现在,进入MainMenu。将自定义对象添加到nib中。确保在这个对象上使用检查器,在检查器的第三个窗格中,将自定义类设置为MyDocumentController。
Now wire this object into your outlet by ctrl-clicking on your new object in the left list of things in the nib and drag (while still ctrl-clicking) to App Delegate. Release and it should flash and show myController. Click that and you're all set.
现在将这个对象连接到您的outlet,按ctrl单击您的新对象,在nib中左边的列表中,拖动(同时仍然按ctrl单击)到App委托。Release和它应该flash和显示myController。点击这个,你就全部搞定了。
Now you can test that you're getting your custom controller with the following code:
现在您可以通过以下代码测试您是否得到了自定义控制器:
// In MyAppDelegate.m
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
NSLog(@"sharedController is %@", [NSDocumentController sharedController]);
}
This should print something like
这应该打印出来。
<MyDocumentController: 0x600000044710>
You're done!
你已经完成了!
The key is to wire in your custom MyDocumentController in MainMenu.xib. If you try to just initialize it in applicationDidFinishLaunching, it's often too late and AppKit has already created and set sharedDocumentController (of which there can only be one).
关键是在MainMenu.xib中连接定制的MyDocumentController。如果您尝试在applicationDidFinishLaunching中初始化它,那么通常太晚了,AppKit已经创建并设置了sharedDocumentController(其中只有一个)。
Also, the outlet needs to be strong not weak because your custom controller is a top-level object in the nib not referenced by anything else in the nib.
另外,outlet需要强大而不是弱,因为您的自定义控制器是nib中的*对象,在nib中没有其他任何东西引用。
This works for me on multiple versions of OS X and Xcode.
这对我来说适用于多个版本的OS X和Xcode。
Please mark this as the correct answer if it works for you! :-)
请将此标记为正确的答案,如果它对您有用!:-)
#4
2
Easier method of getting the Custom Document Controller created in time is to implement the + (void) load
method:
在时间上创建定制文档控制器的更简单方法是实现+ (void)加载方法:
This is literally all you need to do it implement a custom document controller.
这就是实现自定义文档控制器所需的全部内容。
@implementation AppDocumentController
+ (void) load
{
[AppDocumentController new];
}
// ... your overriding goes here
@end
This is cool because there's no separation of the codez. Not 100% sure if the controller is created too early, but works well for me.
这很酷,因为没有分离的codez。不是100%确定控制器是否创建得太早,但对我来说很有效。
#5
1
If - as can sometimes happen - your NSDocumentController subclass seems to randomly NOT be returned as the sharedDocumentController, add a breakpoint on -[NSDocumentController init] and see on the stack what bit of your code is (probably accidentally) creating the regular NSDocumentController. The first controller created is always returned from sharedDocumentController.
如果- as有时会发生-您的NSDocumentController子类似乎不会被作为sharedDocumentController返回,添加一个断点(NSDocumentController init),并在堆栈上看到您的代码(可能是意外地)创建常规的NSDocumentController。创建的第一个控制器总是从sharedDocumentController返回。
This can happen when something in your XIB file gets init'd and (even indirectly) invokes sharedDocumentController. The order of object creation during XIB file loading isn't deterministic, so sometimes your NSDocumentController subclass will be created first, other times another offending object gets init'd first and subsequently your subclass gets ignored.
这可能发生在您的XIB文件中的某个东西得到init的时候(甚至间接地)调用sharedDocumentController。在XIB文件加载过程中,对象创建的顺序不是确定的,所以有时您的NSDocumentController子类会首先被创建,而其他时候,另一个违规对象会先得到init,然后您的子类会被忽略。
You'll have to move the other object's creation to a later point (e.g. applicationDidFinishLaunching:). Or have the object created at the same time, but delay whatever initialization its doing.
您将不得不将其他对象的创建移动到稍后的点(例如applicationDidFinishLaunching:)。或者同时创建对象,但是延迟任何初始化操作。
(Just ran into this with an inspector panel screwing things up, thought it worth noting here!)
(就这样,一个督察小组把事情搞砸了,觉得这里值得注意!)
#6
1
You can subclass easily in Swift 3:
你可以轻松地在Swift 3:
class AppDelegate: NSObject {
let docController = DocController()
}
class DocController: NSDocumentController {
}
#7
-1
// This looked like the best thread to post this.
// This is incomplete skeleton example of NSDocument/NSUndoManager usage.
// Use at your own risk
// Ultimately, would love if Apple reviewed/corrected this and added correct sample code to their docs.
// I put pseudo code in implementation specific spots:
// "... implementation specific ..."
// "... implement something inherently dangerous... loading your data from a file"
// "... implement something inherently dangerous... commit changes to a file ..."
// "... in my implementation, I prompt user with save/quit options when edit window is closed ..."
//
// Apple's documentation states that NSDocumentController should *rarely* be subclassed,
// but Apple fails to provide sample code to accomplish what you need.
// After trying to subclass NSDocumentController, I decided to rip all that code out.
// What I really needed, in my case, was this:
// @interface NSObject(NSApplicationDelegate)
// - (BOOL)application:(NSApplication *)sender openFile:(NSString *)filename
//
// My requirements in a nutshell:
// NSUndoManager, only allow 1 document open at a time (for now), multiple editing windows for 1 document,
// automatically save to temporary file, ability to quit and relaunch without saving,
// Custom "open", "save", "save as", "revert to saved", etc... MyDocumentController
// subclass below provides a simple way to bypass much of the NSDocumentController
// file dialogs, but still use other features of NSDocument (like track unsaved changes).
// Override only "NSDocumentController documentForURL".
/* your NSDocument subclass MUST be defined in your Info.plist.
If you don't, Lion will complain with a NSLog message that looks like this:
-[NSDocumentController openDocumentWithContentsOfURL:display:completionHandler:] failed during state restoration. Here's the error:
Error Domain=NSCocoaErrorDomain Code=256 "The document “blah.myfiletype” could not be opened. cannot open files in the “blah Document” format."
In this example, files of type .myfiletype are associated with my NSDocument subclass
Personally, I hate having to put a class name in my Info.plist, because its not maintainable!
Note to Apple developers: if you are going to force us to do this, then pleasssse make
sure that an XCode search "In Project" for "MyDocument" finds the entry in the plist file!
Oh wow, I just answered my own question: Apple! please make "All candidate files" the
default option for search!
MyDocument:
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeName</key>
<string>My Funky App File</string>
<key>NSDocumentClass</key>
<string>MyDocument</string>
<key>CFBundleTypeExtensions</key>
<array>
<string>myfiletype</string>
</array>
<key>CFBundleTypeIconFile</key>
<string>My_File_Icon.icns</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
</dict>
</array>
*/
#define kMyFileTypeExtension @"myfiletype"
@interface MyDocument : NSDocument
{
}
- (void) registerForUndoGrouping;
- (NSString *)filePath; // convenience function
- (void) setFilePath: (NSString *)filePath; // convenience function
+ (BOOL) openMyDocument: (NSString *)filename; // class method
@end
extern int gWantsToQuit; // global indicator that it's time to stop drawing/updating
// track my startup state so I can control order of initialization, and so I don't
// waste time drawing/updating before data is available.
enum // MyLaunchStatus
{
kFinishedPreWaking = 0x01, // step 1: applicationWillFinishLaunching called
kFinishedOpenFile = 0x02, // step 2: (optionally) application:openFile: called (important for detecting double-click file to launch app)
kFinishedWaking = 0x04, // step 3: NSApp run loop ready to run. applicationDidFinishLaunching
kFinishedPreLaunchCheck = 0x08, // step 4: error recovery check passed
kFinishedLoadingData = 0x10, // step 5: data loaded
kFinishedAndReadyToDraw = 0x20, // step 6: run loop ready for drawing
};
typedef NSUInteger MyLaunchStatus;
#pragma mark -
@interface MyAppController : NSResponder <MidiProtocol, NSOpenSavePanelDelegate, NSTextFieldDelegate, NSWindowDelegate>
MyDocument *toDocument;
@end
#pragma mark -
@implementation MyDocument
- (id)init
{
if ( !(self = [super init]) ) return self;
return self;
}
- (void) registerForUndoGrouping
{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(beginUndoGroup:)
name:NSUndoManagerDidOpenUndoGroupNotification
object:nil];
}
- (void)canCloseDocumentWithDelegate:(id)delegate shouldCloseSelector:(SEL)shouldCloseSelector contextInfo:(void *)contextInfo
{
if ( [[MyAppController instance] windowShouldClose: delegate] )
if ( [delegate respondsToSelector:@selector(close)] )
[delegate close];
//[delegate performSelector:shouldCloseSelector];
//if ( [delegate isKindOfClass:[NSWindow class]] )
// [delegate performClose:self]; // :self];
return; // handled by [[MyAppController instance] windowShouldClose:(id)sender
}
- (void)shouldCloseWindowController:(NSWindowController *)windowController delegate:(id)delegate shouldCloseSelector:(SEL)shouldCloseSelector contextInfo:(void *)contextInfo
{
if ( [[MyAppController instance] windowShouldClose: [windowController window]] )
if ( [[windowController window] respondsToSelector:@selector(close)] )
[[windowController window] close];
}
- (void) beginUndoGroup: (NSNotification *)iNotification
{
NSUndoManager *undoMgr = [self undoManager];
if ( [undoMgr groupingLevel] == 1 )
{
// do your custom stuff here
}
}
// convenience functions:
- (NSString *)filePath { return [[self fileURL] path]; }
- (void) setFilePath: (NSString *)filePath
{
if ( [filePath length] )
[self setFileURL:[NSURL fileURLWithPath: filePath]];
else
[self setFileURL:nil];
}
- (BOOL)validateUserInterfaceItem:(id <NSValidatedUserInterfaceItem>)anItem
{
if ( [self isDocumentEdited] && [anItem action] == @selector(revertDocumentToSaved:) )
return YES;
BOOL retVal = [super validateUserInterfaceItem:(id <NSValidatedUserInterfaceItem>)anItem];
return retVal;
}
- (IBAction)revertDocumentToSaved:(id)sender
{
NSInteger retVal = NSRunAlertPanel(@"Revert To Saved?", [NSString stringWithFormat: @"Revert to Saved File %@?", [self filePath]], @"Revert to Saved", @"Cancel", NULL);
if ( retVal == NSAlertDefaultReturn )
[[MyAppController instance] myOpenFile:[self filePath]];
}
+ (BOOL) openMyDocument: (NSString *)filename
{
if ( ![[filename pathExtension] isEqualToString: kMyAppConsoleFileExtension] )
return NO;
// If the user started up the application by double-clicking a file, the delegate receives the application:openFile: message FIRST
MyLaunchStatus launchStatus = [[MyAppController instance] isFinishedLaunching];
BOOL userDoubleClickedToLaunchApp = !( launchStatus & kFinishedPreLaunchCheck );
MyDocument *currDoc = [[MyAppController instance] document];
NSString *currPath = [currDoc filePath];
NSInteger retVal;
NSLog( @"open file %@ currPath %@ launchStatus %d", filename, currPath, launchStatus );
if ( userDoubleClickedFileToLaunchApp )
{
// user double-clicked a file to start MyApp
currPath = [[NSUserDefaults standardUserDefaults] objectForKey:@"LastSaveFile"];
if ( [currPath isEqualToString: filename] )
{
sWasAlreadyOpen = YES;
if ( [[[NSUserDefaults standardUserDefaults] objectForKey:@"isDocumentEdited"] boolValue] == YES )
{
retVal = NSRunAlertPanel(@"Open File", @"Revert to Saved?", @"Revert to Saved", @"Keep Changes", @"Quit", NULL);
if ( retVal == NSAlertDefaultReturn )
{
[[MyAppController instance] myOpenFile:filename];
}
else if ( retVal == NSAlertOtherReturn )
exit(0);
}
// proceed with normal startup
if ( currDoc )
return YES;
else
return NO;
}
}
if ( !(launchStatus & kFinishedPreLaunchCheck ) ) // not done launching
return YES; // startup in whatever state we were before
if ( [currPath isEqualToString: filename] )
{
sWasAlreadyOpen = YES;
NSLog( @"is edited %d currDoc %@", [currDoc isDocumentEdited], currDoc );
if ( [currDoc isDocumentEdited] )
[currDoc revertDocumentToSaved:self]; // will prompt
else // document is already open, so do what Apple's standard action is...
[currDoc showWindows];
}
else
{
if ( [currDoc isDocumentEdited] )
retVal = NSRunAlertPanel(@"Open File", [NSString stringWithFormat: @"The current file has unsaved changes. Discard unsaved changes and switch to file '%@'?", filename], @"Discard unsaved changes and switch to file", @"Keep Current", NULL);
else
retVal = NSRunAlertPanel(@"Switch to File", [NSString stringWithFormat: @"Switch to File '%@'?\n\nCurrent file '%@'", filename, currfilePath ? currfilePath : @"Untitled"], @"Switch", @"Keep Current", NULL);
if ( retVal == NSAlertDefaultReturn )
[[MyAppController instance] myOpenFile:filename];
}
// user cancelled
if ( currDoc )
return YES;
else
return NO;
}
// Note: readFromURL is here for completeness, but it should never be called,
// because we override NSDocumentController documentForURL below.
- (BOOL)readFromURL:(NSURL *)absoluteURL ofType:(NSString *)typeName error:(NSError **)outError
{
if ( outError )
*outError = nil;
if ( ![typeName isEqualToString: kMyFileTypeExtension ] ) //
return NO;
return YES;
}
// Note: writeToURL is here for completeness, but it should never be called,
// because we override NSDocumentController documentForURL below.
- (BOOL)writeToURL:(NSURL *)absoluteURL ofType:(NSString *)typeName error:(NSError **)outError
{
if ( outError )
*outError = nil;
return YES;
}
@end
// kpk migrating slowly toward NSDocument framework
// (currently most functionality is in MyAppController)
// Must bypass default NSDocumentController behavior to allow only 1 document
// and keep MyAppController responsible for read, write, dialogs, etc.
@implementation MyDocumentController
// this should be the only override needed to bypass NSDocument dialogs, readFromURL,
// and writeToURL calls.
// Note: To keep Lion happy, MainInfo.plist and Info.plist must define "MyDocument" for key "NSDocumentClass"
- (id)documentForURL:(NSURL *)absoluteURL
{
MyDocument *currDoc = [[MyAppController instance] document];
if ( [[currDoc filePath] isEqualToString: [absoluteURL path]] )
return currDoc;
else
return nil;
}
@end
#pragma mark -
@implementation MyAppController
static MyAppController *sInstance;
+ (MyAppController *)instance
{
return sInstance; // singleton... why is this not in all Apple's sample code?
}
// called by main.mm before MyAppController (or NSApp for that matter) is created.
// need to init some global variables here.
+ (void) beforeAwakeFromNib
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
... implementation specific ...
// disable fancy stuff that slows launch down
[[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithBool:NO] forKey: @"NSAutomaticWindowAnimationsEnabled"];
[pool release];
}
- (void) awakeFromNib
{
NSLog(@"MyAppController awake\n");
sInstance = self;
[toWindow setNextResponder:self];
[NSApp setDelegate:self];
[self addWindowToDocument:toWindow];
}
- (MyDocument *)document
{
if ( !toDocument )
{
toDocument = [[MyDocument alloc] init];
}
return toDocument;
}
- (NSUndoManager *)undoManager
{
// !!! WARNING: there are multiple NSUndoManager's in this App
// Note: when an editable text field is in focus,
// NSTextField will create
// a separate undo manager for editing text while that field is in focus.
// This means that hitting undo/redo while editing a text field will not go beyond the scope of that field.
// This will return the global undo manager if the keyWindow was registered
// via [self addWindowToDocument:];
// Windows which are NOT part of the document (such as preferences, popups, etc.), will
// return their own undoManager, and undo will do nothing while those windows are in front.
// You can't undo preferences window changes, so we don't want to surprise the user.
NSUndoManager *undomgr = [[NSApp keyWindow] undoManager];
if ( undomgr )
{
static bool sFirstTime = true;
if ( sFirstTime )
{
sFirstTime = false;
[undomgr setLevelsOfUndo:1000]; // set some sane limit
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(beginUndo:)
name:NSUndoManagerWillUndoChangeNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(beginUndo:)
name:NSUndoManagerWillRedoChangeNotification
object:nil];
[toDocument registerForUndoGrouping];
// [[NSNotificationCenter defaultCenter] addObserver:self
// selector:@selector(endUndo:)
// name:NSUndoManagerDidUndoChangeNotification
// object:nil];
// [[NSNotificationCenter defaultCenter] addObserver:self
// selector:@selector(endUndo:)
// name:NSUndoManagerDidRedoChangeNotification
// object:nil];
}
}
return undomgr;
}
- (void) showStatusText: (id)iStatusText
{
... implementation specific ...
}
- (void) beginUndo:(id)sender
{
// implementation specific stuff here
NSUndoManager *undomgr = [[NSApp keyWindow] undoManager];
if ( [sender object] == undomgr )
{
if ( [undomgr isUndoing] )
[self showStatusText: [NSString stringWithFormat:@"Undo %@", [undomgr undoActionName]]];
else if ( [undomgr isRedoing] )
[self showStatusText: [NSString stringWithFormat:@"Redo %@", [undomgr redoActionName]]];
}
}
// Add a window (with a window controller) to our document, so that the window
// uses the document's NSUndoManager. In the future, we may want to use other features of NSDocument.
- (void)addWindowToDocument:(NSWindow *)iWindow
{
NSString *autosaveName = [iWindow frameAutosaveName]; // preserve for "mainWindow", others.
NSWindowController *winController = [iWindow windowController];
if ( !winController )
winController = [[NSWindowController alloc] initWithWindow:iWindow];
// create document if needed, and add window to document.
[[self document] addWindowController: winController];
if ( autosaveName )
[iWindow setFrameAutosaveName:autosaveName]; // restore original for "mainWindow", others.
[winController setNextResponder:self]; // keep last hotkey destination... see keyDown:
}
- (void) myOpenFile:(NSString*)path
{
// this is just a skeleton of what I do to track unsaved changes between relaunches
[toDocument setFilePath:path];
... implementation specific ...
[[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithBool:NO] forKey:@"isDocumentEdited"];
[toDocument updateChangeCount:NSChangeCleared];
[[NSUserDefaults standardUserDefaults] setObject:[toDocument filePath] forKey:@"LastSaveFile"];
BOOL success = [[NSUserDefaults standardUserDefaults] synchronize]; // bootstrap
// kpk very important... resetStandardUserDefaults forces the immutable
// tree returned by dictionaryWithContentsOfFile to be mutable once re-read.
// Apple: "Synchronizes any changes made to the shared user defaults object and releases it from memory.
// A subsequent invocation of standardUserDefaults creates a new shared user defaults object with the standard search list."
[NSUserDefaults resetStandardUserDefaults];
NSString *name = [[NSUserDefaults standardUserDefaults] objectForKey:@"LastSaveFile"];
}
- (void) mySaveData:(NSString*)path
{
// this is just a skeleton of what I do to track unsaved changes between relaunches
@try
{
... implement something inherently dangerous... commit changes to a file ...
if ( !errorStr )
{
if ( [toDocument isDocumentEdited] )
{
// UInt64 theTimeNow = VMPGlue::GetMilliS();
[[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithBool:NO] forKey:@"isDocumentEdited"];
[[NSUserDefaults standardUserDefaults] synchronize]; // bootstrap
// DLog( @"synchronize success %d ms", (int)(VMPGlue::GetMilliS() - theTimeNow) );
}
// tbd consider MyDocument saveToURL:ofType:forSaveOperation:error:
[toDocument updateChangeCount:NSChangeCleared];
}
@catch (...)
{
... run critical alert ...
}
}
- (void) finishLoadingData
{
@try
{
if ( dataexists )
{
... implement something inherently dangerous... loading your data from a file
[toDocument setFilePath: [[NSUserDefaults standardUserDefaults] objectForKey:@"LastSaveFile"]];
NSNumber *num = [[NSUserDefaults standardUserDefaults] objectForKey:@"isDocumentEdited"];
if ( [num boolValue] == YES )
[toDocument updateChangeCount:NSChangeDone];
}
else
{
[[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithBool:NO] forKey:@"isDocumentEdited"];
[toDocument updateChangeCount:NSChangeCleared];
}
sFinishedLaunching |= kFinishedLoadingData;
}
@catch (...)
{
// !!! will not return !!!
... run critical alert ...
// !!! will not return !!!
}
}
#pragma mark NSApplication delegate
// Apple: Sent directly by theApplication to the delegate. The method should open the file filename,
//returning YES if the file is successfully opened, and NO otherwise.
//If the user started up the application by double-clicking a file, the delegate receives the application:openFile: message before receiving applicationDidFinishLaunching:.
//(applicationWillFinishLaunching: is sent before application:openFile:.)
- (BOOL)application:(NSApplication *)sender openFile:(NSString *)filename
{
BOOL didOpen = [MyDocument openMyDocument: filename];
sFinishedLaunching |= kFinishedOpenFile;
return didOpen;
}
// NSApplication notification
- (void) applicationDidFinishLaunching:(NSNotification*)note
{
// kpk note: currentEvent is often nil at this point! [[NSApp currentEvent] modifierFlags]
CGEventFlags modifierFlags = CGEventSourceFlagsState(kCGEventSourceStateHIDSystemState);
sFinishedLaunching |= kFinishedWaking;
if ( modifierFlags & (kCGEventFlagMaskShift | kCGEventFlagMaskCommand) )
{
... implementation specific ... alert: @"Shift or Command key held down at startup.\nWhat would you like to do?"
title: @"Startup Options"
canContinue: @"Continue" ];
}
sFinishedLaunching |= kFinishedPreLaunchCheck;
[self finishLoadingData];
sFinishedLaunching |= kFinishedAndReadyToDraw;
}
- (BOOL)windowShouldClose:(id)sender
{
if ( [sender isKindOfClass: [NSWindow class]] && sender != toWindow )
return YES; // allow non-document-edit windows to close normally
... in my implementation, I prompt user with save/quit options when edit window is closed ...
return NO;
}
- (NSApplicationTerminateReply) applicationShouldTerminate:(NSApplication*)sender
{
if ( !gWantsToQuit && [toDocument isDocumentEdited] )
{
if ( ![self windowShouldClose:self] )
return NSTerminateCancel;
}
return NSTerminateNow;
}
- (void) applicationWillTerminate:(NSNotification *)notification
{
if ( gWantsToQuit )
{
... implementation specific ... dont save potentially wonky data if relaunch is required
}
else
{
[self saveData: [toDocument filePath]];
}
}
@end