force NSDocument to save after creation - cocoa

In its documents, my application uses a lot of assets that are relative to the document path. So the document must be saved before assets can be added. How can I force-call a [NSDocument saveDocumentAs] ?
I managed to do parts of it : by creating my own document controller, and inside openUntitledDocumentAndDisplay: force a call like this :
- (id)openUntitledDocumentAndDisplay:(BOOL)displayDocument error:(NSError **)outError
{
NSDocument * res = [super openUntitledDocumentAndDisplay:displayDocument error:outError];
[res saveDocumentAs:self];
return res;
}
This forces the save dialog to appear, but unfortunately I can not check whether the user pressed cancel : the saveDocumentAs call is asynchronous and continues immediately !
Is there a way to fix this ?

I had a similar problem. By using:
saveDocumentWithDelegate:(id)delegate didSaveSelector:(SEL)didSaveSelector contextInfo:(void *)contextInfo
you can defer your processing (or not) until after the document save dialogue has completed. This means you can find out whether the user cancelled or not. You split your processing in two, do whatever preparation you need and put the rest (that depends upon a successful save) into another method. If you use something like:
[self saveDocumentWithDelegate:self didSaveSelector:#selector(actuallyDoIt: didSave: contextInfo:) contextInfo:nil];
The document will be saved but, critically, if it has not been saved before, the Save dialogue will appear so the user can input a file name. Once he/she has done that, or cancelled, your method actuallyDoIt: (or whatever) is invoked. The didSave: parameter tells you whether the save actually happened (essentially, did the user cancel) so you can either continue or offer an alert explaining politely to the user that nothing's going to happen until they save.

I have a similar thing in my application, in my case if the user tries to do something, I pull up a prompt to say 'This requires you to save the document first' with buttons to cancel or save.
If you want to absolutely force it, then instead of using saveDocumentAs, just display your own NSSavePanel. Run it modally, check the result, save the document with the result, and if this doesn;t go smoothly, call it again. You can check if the document is saved by looking for a valid value for it's file path.

NSSavePanel can run modally. Here is how it can/should look like.
- (id)openUntitledDocumentAndDisplay:(BOOL)displayDocument error:(NSError *__autoreleasing *)outError
{
Document *document;
NSSavePanel *panel = [NSSavePanel savePanel];
panel.prompt = #"Create";
NSInteger modalCode = [panel runModal];
if (modalCode == NSModalResponseOK) {
NSURL *URL = [panel URL];
NSError *docError;
document = [[Document alloc] initWithType:nil & docError];
[document saveToURL:URL ofType:nil forSaveOperation:NSSaveOperation completionHandler:^(NSError *error){
if(error) {
return nil;
}
[self addDocument:document];
[document makeWindowControllers];
if (displayDocument) {
[document showWindows];
}
}];
}
return document;
}
To sum up for reference:
Create custom nsdocumentsubclass in XIB (no XIB -> app did finish launching)
override openUntitledDocumentAndDisplay
(NSData *)dataOfType:(NSString *)typeName error:(NSError **)outError;

Related

Update NSWindow content while asynchronously loading a file

In my document-based MacOS application, I have some big files that are loaded (especially recent files opened at application launch). I created ProgressController (a NSWindowController subclass) to inform user in a window that file loading is in progress. It is allocated by the makeWindowControllers method of the NSDocument subclass I use to manage documents. These are created when the user opens a file (and notably at startup when documents were displayed as the user did quit the application) and they load their content asynchronously in a background queue which in principle should not affect the main thread performance:
-(instancetype) initForURL:(NSURL *)urlOrNil withContentsOfURL:(NSURL *)contentsURL ofType:(NSString *)typeName error:(NSError *
{
if (self = [super init]){
... assign some variables before loading content...
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND,0), ^{
[[[NSURLSession sharedSession] dataTaskWithURL:urlOrNil completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
self.content = [[NSMutableString alloc] initWithData:data encoding:encoder];
dispatch_async(dispatch_get_main_queue(), ^(){
[self contentIsLoaded];
});
}] resume];
});
}
return self;
}
In contentIsLoaded, the progress window is closed.
- (void) contentIsLoaded
{
... do something ...
[self.progressController close];
}
This behaviour is OK, the window is displayed and closed when necessary.
The problem occurs when I want to update the content of this window on the main queue. I tried setting a NSTimer but is is never fired, even though it is created in the main queue. So, in MyApplicationDelegate I created a GCD timer like this:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
if (self->timer !=nil) dispatch_source_cancel(timer);
self.queue = dispatch_queue_create( "my session queue", DISPATCH_QUEUE_CONCURRENT);
timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.queue);
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0);
dispatch_source_set_event_handler(timer, ^{
[self updateProgress];
});
dispatch_resume(timer);
…
}
In MyApplicationDelegate, the updateProgress method is defined as:
- (void) updateProgress
{
dispatch_async(self.queue, ^{
NSLog(#"timer method fired");
dispatch_async(dispatch_get_main_queue(), ^(){
NSLog(#"access to UI fired");
... update window (e.g. NSProgressIndicator)
}
});
if (self.shouldCancelTimer) dispatch_source_cancel(self->timer);
});
}
When running the application, The "Timer method fired" is logged every second. The "timer method fired" message (in the main queue) is logged once or twice only, then the logging seems suspended until the file has been loaded. Then this missing message appears several times in a row, as it was suspended before.
What did I wrong? I supposed that the background queue used for file loading should not affect the main queue and UI updates. Many applications behave like that and I need such behaviour as files in my App (strings, csv, json) can be hundreds of Mbytes!
For starters, have you looked into NSURLSessionTask's progress reporting APIs? There are a bunch of facilities for measuring and getting called back as data loads. It might be possible for you to avoid doing the polling, and just update your UI as these are called back - all on the main thread.
In fact, you might not even need one at all. As I recall, the networking operations carried out by NSURLSession are all done in the background anyways. I bet you can remove all of this, and just use some extra delegate callbacks from NSURLSession APIs to get this done. Way simpler too :)
Good luck!

Sandbox NSOpenPanel Error 1000

I'm pretty much ready to release the first sandbox-enabled Mac application. The only entitlement that I need is User Selected File. The user clicks on a toolbar button to select one or more image files, which doesn't cause trouble. The user also clicks on a button to select a folder. When they do, the Xcode output window indicates the following error message. CGSSetIgnoresCycle: error 1000 setting or clearing window tags. If the user cancels the select-folder operation without selecting one, they get an additional error message on top of the first one. It says PSsetwindowlevel, error setting window level (1000). The application does not crash. Are these error messages things that I need to worry? If I ask Google, I don't get many search results. Anyway, the following code is used when the user clicks on a button to select a folder.
- (IBAction)system1Selected:(id)sender {
NSOpenPanel *panel = [NSOpenPanel openPanel];
[panel setAllowsMultipleSelection:NO];
[panel setCanChooseDirectories:YES];
[panel setCanChooseFiles:NO];
NSString *currentpath = systempath1.stringValue;
if ([self fileExists:currentpath]) {
[panel setDirectoryURL:[NSURL fileURLWithPath:currentpath]];
} else {
[panel setDirectoryURL:[NSURL fileURLWithPath:[self filePathA]]];
}
if ([panel runModal] != NSFileHandlingPanelOKButton) {
//return nil;
} else {
NSURL *url = [[panel URLs] lastObject];
systempath1.stringValue = [url path];
}
}
Thank you for your advice.
This error has been there for a while in all my applications. It doesn't seem something you need to worry about. It disappears without changing anything and, probably, it depends on a bug of the NSOpenPanel (I didn't manage to get the same error using the NSSavePanel).
In my opinion, there's no need to investigate further.

PDFViewAnnotationHitNotification not being delivered

I'm implementing a PDF viewer on the Mac and I want to let the user add annotations.
I've added a PDFAnnotationText to the page, and it appears just fine, but when the user clicks on it, the whole document is shrunk and an annotation list appears down the left side.
I want to customize this to display the annotation as a pop-up, similar to what Preview does. The PDFAnnotationText class reference says I can do this:
Each PDFAnnotationText object has a PDFAnnotationPopup object associated with it. In its closed state, the annotation appears as an icon. In its open state, it displays as a pop-up window containing the text of the note. Note that your application must do the work to put up a window containing the text in response to a PDFViewAnnotationHitNotification.
But when I add an observer for PDFViewAnnotationHitNotification, no notification is delivered when I click on the annotation.
I've contacted Apple about this, and the answer I received back was that it's a bug. A workaround is to handle the mouse click yourself, walk the annotations and look for a hit.
Something like this (code which runs in a mouseDown handler in a PDFView subclass):
NSPoint windowPoint = [self.window convertScreenToBase:[NSEvent mouseLocation]];
NSPoint viewPoint = [self convertPoint:windowPoint fromView:nil];
PDFPage *page = [self pageForPoint:viewPoint nearest:NO];
if (page != nil) {
NSPoint pointOnPage = [self convertPoint:viewPoint toPage:page];
for (PDFAnnotation *annotation in page.annotations) {
NSRect annotationBounds;
// Hit test annotation.
annotationBounds = [annotation bounds];
if (NSPointInRect(pointOnPage, annotationBounds))
{
NSLog(#"Annotation hit: %#", annotation);
}
}
}

Prompting the user to logon for encrypted documents

I have encrypted documents. After unarchiving each document, I need to prompt the user to enter the document password from a logon sheet. I have means to validate password entry against file contents (this part is done). If the password is incorrect the document shall be closed. If the password is correct the document window shall be presented with document contents.
When I attempt to load the logon sheet (via its controller) in the document's windowControllerDidLoadNib method I have unrecognized selector error as shown below:
- (void)windowControllerDidLoadNib:(NSWindowController *)aController
{
[super windowControllerDidLoadNib:aController];
if (!newPasswordController){
newPasswordController = [[NewPasswordController alloc] init];
newPasswordSheet = [newPasswordController window];
}
[NSApp beginSheet:newPasswordSheet modalForWindow:[self window]
modalDelegate:self
didEndSelector:#selector(didNewPasswordEnd:returnCode:contextInfo:)
contextInfo:nil];
}
[_NSControllerObjectProxy copyWithZone:]: unrecognized selector sent to instance
The method [NewPasswordController init] is implemented as follows:
-(id)init
{
self = [super initWithWindowNibName:#"NewPassword"];
if (self) {
}
return self;
}
where the logon sheet nib file is called NewPassword.
I wonder what went wrong. What is the best way to solve this problem?
Aside from unrecognized selector issue (which I left with Apple) I managed to address the original issue Prompting the user to logon for encrypted documents and here is the solution.
Note every document is password protected and they could potentially respond to different passwords (so the issue is not 'password protected application' but 'password protected documents').
Simply inject the following code when we are about to read document contents to pop up an application modal window to verify the document password:
- (BOOL)readFromData:(NSData *)data
ofType:(NSString *)typeName
error:(NSError **)outError
{
PasswordController *passwordController = [[PasswordController alloc] init];
NSWindow *passwordSheet = [passwordController window];
NSApplication* app = [NSApplication sharedApplication];
NSInteger iret = [app runModalForWindow:passwordSheet];
NSLog(#"password dialog returned = %ld", iret);
if (iret != 0)
{
[app stop:self];
return NO;
}
[passwordController release];
...
You may also pop up another kind of window when the document is saved the first time in dataOfType, forcing the user to set the document's password.
This issue is answered now.
The issue unrecognized selector was resolved after I constructed the xib file from scratch. This indicates a serious problem though concerning IB in XCode 4, as properties of the sheet and steps taken to create bindings, key-payths etc seemed identical in both cases. Something I have done during interface construction from IB caused a corruption in the xib file in my first attempt.
I'll leave it to forum administration to delete or keep this issue. If it is value to anyone I'll file a bug report with Apple (see below)

Cocoa QuickLook initiated by NSTableView Cell

I have an NSTableView that contains 2 different Columns - one is an NSImageCell that shows a file icon, and the second is a custom subclass of NSTextFieldCell that contains a quick look button on the right of the text. When I click the Quick Look button, the following code is invoked:
[[QLPreviewPanel sharedPreviewPanel] makeKeyAndOrderFront:nil];
This does it's job and shows the blank Quick Look panel saying "No Items Selected." After I did a bit of research on the internet, I implemented a custom NSTableView subclass to be the Delegate and Data Source for the Quick Look panel. I get the notification that Quick Look asks if I want to be the delegate, and I respond with return YES. Even though I implement all methods in both QLPreviewPanelDataSource and QLPreviewPanelDelegate, at runtime I get this error on the console:
2010-12-24 15:32:17.235 BackMeUp[4763:80f] clicked: ~/Desktop/HUDTape.mov
2010-12-24 15:32:17.489 BackMeUp[4763:80f] [QL] QLError(): -[QLPreviewPanel setDelegate:] called while the panel has no controller - Fix this or this will raise soon.
See comments in QLPreviewPanel.h for -acceptsPreviewPanelControl:/-beginPreviewPanelControl:/-endPreviewPanelControl:.
2010-12-24 15:32:17.490 BackMeUp[4763:80f] [QL] QLError(): -[QLPreviewPanel setDataSource:] called while the panel has no controller - Fix this or this will raise soon.
See comments in QLPreviewPanel.h for -acceptsPreviewPanelControl:/-beginPreviewPanelControl:/-endPreviewPanelControl:.
2010-12-24 15:32:17.491 BackMeUp[4763:80f] We can now receive QL Events.
2010-12-24 15:32:18.291 BackMeUp[4763:80f] -[NSPathStore2 stringValue]: unrecognized selector sent to instance 0x5ecb10
2010-12-24 15:32:18.292 BackMeUp[4763:80f] -[NSPathStore2 stringValue]: unrecognized selector sent to instance 0x5ecb10
And the Quick Look panel does not show up, which I find rather odd. The first line above is just that I know the cell has been clicked. Anyways, here is the .m file of the custom table view subclass:
//
// BackupListTableView.m
// BackMeUp
//
// Created by Tristan Seifert on 12/24/10.
// Copyright 2010 24/7 Server. All rights reserved.
//
#import "BackupListTableView.h"
#implementation BackupListTableView
- (void) awakeFromNib {
}
// Quick Look Delegates
- (BOOL)acceptsPreviewPanelControl:(QLPreviewPanel *)panel;
{
[QLPreviewPanel sharedPreviewPanel].delegate = self;
[QLPreviewPanel sharedPreviewPanel].dataSource = self;
NSLog(#"We can now receive QL Events.");
return YES;
}
- (void)beginPreviewPanelControl:(QLPreviewPanel *)panel
{
// This document is now responsible of the preview panel
// It is allowed to set the delegate, data source and refresh panel.
[QLPreviewPanel sharedPreviewPanel].delegate = self;
[QLPreviewPanel sharedPreviewPanel].dataSource = self;
}
- (void)endPreviewPanelControl:(QLPreviewPanel *)panel
{
// This document loses its responsisibility on the preview panel
// Until the next call to -beginPreviewPanelControl: it must not
// change the panel's delegate, data source or refresh it.
return;
}
// Quick Look panel data source
- (NSInteger)numberOfPreviewItemsInPreviewPanel:(QLPreviewPanel *)panel
{
return 1;
}
- (id <QLPreviewItem>)previewPanel:(QLPreviewPanel *)panel previewItemAtIndex:(NSInteger)index
{
int selectedRow = [self selectedRow];
return [NSURL URLWithString:[[[self dataSource] tableView:self objectValueForTableColumn:fileColumn row:selectedRow] stringValue]];
}
// Quick Look panel delegate
- (BOOL)previewPanel:(QLPreviewPanel *)panel handleEvent:(NSEvent *)event
{
// redirect all key down events to the table view
return NO;
}
// This delegate method provides the rect on screen from which the panel will zoom.
- (NSRect)previewPanel:(QLPreviewPanel *)panel sourceFrameOnScreenForPreviewItem:(id <QLPreviewItem>)item
{
NSRect iconRect = [self rectOfColumn:1];
/*
// check that the icon rect is visible on screen
NSRect visibleRect = [self visibleRect];
// convert icon rect to screen coordinates
iconRect = [self convertRectToBase:iconRect];
iconRect.origin = [[self window] convertBaseToScreen:iconRect.origin];
*/
return iconRect;
}
// This delegate method provides a transition image between the table view and the preview panel
- (id)previewPanel:(QLPreviewPanel *)panel transitionImageForPreviewItem:(id <QLPreviewItem>)item contentRect:(NSRect *)contentRect
{
int selectedRow = [self selectedRow];
NSImage *fileIcon = [[NSWorkspace sharedWorkspace] iconForFile:[[[self dataSource] tableView:self objectValueForTableColumn:fileColumn row:selectedRow] stringValue]];
return fileIcon;
}
#end
Thanks for any help.
The documentation isn't the best for this, since it's a new feature that was added in 10.6. (Well, there is obviously the class and protocol references, but in my experience, I've always found the Companion Guides to be more helpful in understanding how the objects are intended to be used in a real-world scenario).
The QLPreviewPanelController Protocol Reference defines 3 methods:
QLPreviewPanelController Protocol Reference
The Quick Look preview panel shows previews for items provided by the first object in the responder chain that implements the methods in this protocol. You typically implement these methods in your window controller or delegate. You should never try to modify preview panel state if you’re not controlling the panel.
- (BOOL)acceptsPreviewPanelControl:(QLPreviewPanel *)panel;
- (BOOL)beginPreviewPanelControl:(QLPreviewPanel *)panel;
- (void)endPreviewPanelControl:(QLPreviewPanel *)panel;
I'm guessing that your code should look like this:
- (BOOL)acceptsPreviewPanelControl:(QLPreviewPanel *)panel
{
return YES;
}
You shouldn't be doing anything in that method besides returning YES. acceptsPreviewPanelControl: is sent to every object in the responder chain until something returns YES. By returning YES, that object effectively becomes "the controller". The latter 2 methods are called on the controller object after it returns YES from the first method. So you should only be setting the delegate and datasource in the beginPreviewPanelControl: method (at which time you will be regarded as the current controller).
- (void)beginPreviewPanelControl:(QLPreviewPanel *)panel
{
// This document is now responsible of the preview panel
// It is allowed to set the delegate, data source and refresh panel.
[QLPreviewPanel sharedPreviewPanel].delegate = self;
[QLPreviewPanel sharedPreviewPanel].dataSource = self;
NSLog(#"We can now receive QL Events.");
}
First:
-acceptsPreviewPanelControl should only return YES and not try to set delegate and datasource.
Then, the problem is that you get an exception breaking the panel:
2010-12-24 15:32:18.291 BackMeUp[4763:80f] -[NSPathStore2 stringValue]: unrecognized selector sent to instance 0x5ecb10
The exception is very likely caused by these invocations:
[[[self dataSource] tableView:self objectValueForTableColumn:fileColumn row:selectedRow] stringValue]
Very likely, [[self dataSource] tableView:self objectValueForTableColumn:fileColumn row:selectedRow] is a file path (a NSPathStore instance which is a subclass of NSString) so it does not respond to -stringValue
So replace:
[NSURL URLWithString:[[[self dataSource] tableView:self objectValueForTableColumn:fileColumn row:selectedRow] stringValue]]
by:
[NSURL fileURLWithPath:[[self dataSource] tableView:self objectValueForTableColumn:fileColumn row:selectedRow]]
Also remove the call to -stringValue in transitionImageForPreviewItem.
As a side note, it seems to be suboptimal to load the image at each call of transitionImageForPreviewItem. I suppose you already have the image computed elsewhere (displayed in your table view), try to use it.

Resources