I am new to NSOpenPanel/NSSavePanel/NSPanel. I am using NSOpenPanel to choose a directory whose files my app will iterate over and do some fairly time-consuming processing.
I can call -close on the panel, but that does not return focus to the main window. I have read a lot about "dismissing" the panel - but I haven't found any methods that "dismiss" rather than "close" a panel or a window.
Is it just that I need to spawn a background thread (NSOperation)?
This is what my -chooseDidEnd:returnCode:contextInfo:
-(void) chooseDidEnd:(NSOpenPanel *)panel returnCode:(int)returnCode contextInfo:(void *)contextInfo
{
[panel orderOut:self];
[panel release];
if (returnCode == NSFileHandlingPanelOKButton)
{
[progressIndicator startAnimation:self];
[self doLotsOfTimeConsumingWork:[[panel URL] path]];
[progressIndicator stopAnimation:self];
}
}
While the NSOpenPanel does go away, my NSProgressIndicator does not animate and the main window doesn't come alive until after -doLotsOfTimeConsumingWork: completes.
Update
Just looked at NSOperationSample code, and it is looking like that's the way to go.
Two notes:
First, in Cocoa, the event handling and drawing happens on the main thread. Hence it is never a good idea to synchronously call lengthy methods there (which is the reason for your unresponsive UI).
So yes, you should hand off computationally expensive tasks to a secondary thread from this method, like from any IBAction.
Second, calling [panel release] in that method violates Cocoa's rules of object ownership! So if you would be leaking the panel without that call, you should fix that in the method where you're creating the panel.
Related
The problem: Attempting to display a window with text from applicationWillFinishLaunching will NOT draw itself if other processor-intensive non-UI code is immediately called.
Background: I have a helper app that when launched may or may not interact with the end user. While it is "deciding" if it needs to put up a window to ask user questions, there may be anywhere from 1 second to 10 seconds that elapse (after launch it's off in non-UI capable library code communicating over the internet).
So I wanted to be kind to the user and put up a "mini-alert"* window with "working, please wait...", prior to heading into that library code, which I will dismiss once that processing has elapsed.
It seems as if the app itself doesn't have time after launch to even draw this mini-alert (it's just an NSWindow, with an NSView, some text, and no buttons).
If after the library code returns and want to put up either an error alert or a query window for the user -- then at that point the mini-alert draws as expected. However, if I close the mini-alert (see below) and then put up an NSAlert -- the mini-alert doesn't have enough time to dismiss itself.
- (void)applicationWillFinishLaunching:(NSNotification *)notification
{
[NSApp activateIgnoringOtherApps:YES];
briefAlertWindowController = [[NSWindowController alloc] initWithWindowNibName:#"BriefAlertWindow"];
[[briefAlertWindowController window] center];
[briefAlertWindowController showWindow:self ];
[[briefAlertWindowController window] orderFront:self ];
[[briefAlertWindowController window] display];
[[briefAlertWindowController window] makeKeyAndOrderFront:nil];
}
and dismissing the mini-alert:
- (void)dismissMiniAlert
{
NSWindow * theWindow = [briefAlertWindowController window];
[theWindow orderOut:nil];
}
NOTE that neither NSWindow not NSWindowController have been derived/subclassed for this mini-alert.
I'm using the term "mini-alert", because I've noticed people get annoyed about the concept of a "splash screen". While the functionality IS similar -- I'm really just trying to let the user know that an unavoidably long operation is taking place.
It sounds like a threading problem. The splash window can't draw itself on the main thread because the main thread is busy doing the processor-intensive operation. Properly, your processor-intensive stuff should all be happening on a background thread. If you can't do that, you need at least to get off the main thread long enough to give the runloop a chance to draw your window. Just introduce a delay.
I have the following method in my GameWindowController (subclass of NSWindowController):
- (void)windowWillClose:(NSNotification *)notification {
AppDelegate *delegate = [NSApp delegate];
[delegate removeGameWindowController:self];
}
The code for removeGameWindowController in AppDelegate is:
- (void)removeGameWindowController:(GameWindowController*)controller {
[self.controllers removeObject:controller];
}
self.controllers is an NSMutableArray with all my GameWindowControllers.
The above code seems to have a race condition. It will randomly crash with EXC_BAD_ACCESS when I close windows, almost every time if I close all windows at once.
My guess is that ARC is deallocating the window controller before or as removeGameWindowController: returns, leaving the window with a dangling pointer to the controller. I have tried adding controller.window.windowController = nil; to no avail.
For some reason, using the (BOOL)windowShouldClose:(id)sender delegate method instead as suggested in https://stackoverflow.com/a/11782844/344544 works, but is not an acceptable solution as it is not called upon quit.
How can I reliably remove my window controllers from the array of controllers after each window has closed? Is there some other delegate method which gets called or some NSNotification I can subscribe to which fire after a window has finished closing?
After lengthy investigation and step by step running in the debugger I figured out the source of the problem and a possible solution.
The window controller was indeed being released at some point after the end of removeGameWindowController: along with all its strong references which include the NSWindow. If the window was released before the stack had unwound back to the close call on the window itself, the program would crash while finishing the that function as self in this particular case is a dangling pointer.
I was unable to find a way to get notified after a window had closed, however it is likely such an approach would have had the exact same problem.
In order to ensure no reference to the window was left anywhere on the stack I queued the removal of the window controller from the array to happen as a subsequent event on the runloop:
- (void)removeGameWindowController:(GameWindowController*)controller {
[self.controllers performSelectorOnMainThread:#selector(removeObject:) withObject:controller waitUntilDone:NO];
}
I am very much new to OSX development. Consider this as my first app. I want to display a sheet when a button is clicked on the main window. I am using Nib
Following is my code for .h file
#import <Foundation/Foundation.h>
#import "WebKit/Webkit.h"
#interface MainViewObject : NSObject
- (IBAction)accountButtonPressed:(id)sender;
- (IBAction)cancelSheetButtonPressed:(id)sender;
.m file as follows
#import "MainViewObject.h"
#implementation MainViewObject
- (IBAction)accountButtonPressed:(id)sender {
[NSApp beginSheet:self.accountSheet
modalForWindow:[mainWindowView window]
modalDelegate:nil
didEndSelector:nil
contextInfo:nil];
[NSApp runModalForWindow:self.accountSheet];
[NSApp endSheet:self.accountSheet];
[self.accountSheet orderOut:self];
}
- (IBAction)cancelSheetButtonPressed:(id)sender {
// Return to normal event handling
[NSApp endSheet:self.accountSheet];
// Hide the sheet
[self.accountSheet orderOut:sender];
}
When I run the app I get something like this:
http://i.imgur.com/DzJJ6.png
I am stuck and I have no idea what wrong in this. I am not able to get the sheet and the not able to even close the app. I have referred to some examples on internet.
- (IBAction)accountButtonPressed:(id)sender {
[NSApp beginSheet:self.accountSheet
modalForWindow:[mainWindowView window]
modalDelegate:nil
didEndSelector:nil
contextInfo:nil];
[NSApp runModalForWindow:self.accountSheet];
[NSApp endSheet:self.accountSheet];
[self.accountSheet orderOut:self];
}
Wow, looking at that, it's no surprise the screenshot looks as it is.
Let's walk through that one line at a time. When you click the Accounts button, you're doing 4 things immediately in succession:
You're telling the application to begin showing the sheet attached to your main window. This is OK, and is actually the only code you want in that accountButtonPressed: method.
Right after beginning that sheet, you tell the application you want to also show that sheet all by itself (not attached to any windows but right in the middle of the screen), in an application-modal fashion, which blocks all other events from being processed in the application. In other words, this line doesn't really make sense. You either show a window as a sheet in a "document-modal" fashion (which only ties up the window that the sheet is attached to) or in an "application-modal" fashion, but not both at the same time. ;-)
Immediately after having just shown the sheet, you tell NSApp to stop showing the sheet. Now, you do want to do this eventually, but dismissing the sheet 0.0005 seconds after having just shown it will likely leave your users a little frustrated.
You now tell the sheet to hide itself. This needs to be done from your didEndSelector: method, which brings us to the problems in your first method.
-
[NSApp beginSheet:self.accountSheet
modalForWindow:[mainWindowView window]
modalDelegate:nil
didEndSelector:nil
contextInfo:nil];
This is good but read the documentation for beginSheet:modalForWindow:modalDelegate:didEndSelector:contextInfo: and also the Sheet Programming Topics: Using Custom Sheets. (The Companion guides links at the top of Class Reference pages are especially helpful for learning how to use the APIs in the real world. They were extremely helpful when I was learning).
Specifying nil for the modalDelegate: means you don't have anything that's waiting to be notified about when the sheet has stopped being shown (this happens when you call [NSApp endSheet:sheet]). You also haven't specified the #selector you want called when the sheet is ended. A selector is kind of like a function, aka a "method".
Your code should look something like this:
#implementation MDAppDelegate
- (IBAction)showSheet:(id)sender {
[NSApp beginSheet:self.sheet
modalForWindow:self.window
modalDelegate:self
didEndSelector:#selector(sheetDidEnd:returnCode:contextInfo:)
contextInfo:NULL];
}
- (IBAction)cancel:(id)sender {
[NSApp endSheet:self.sheet];
}
- (IBAction)ok:(id)sender {
[NSApp endSheet:self.sheet];
}
- (void)sheetDidEnd:(NSWindow *)sheet
returnCode:(NSInteger)returnCode
contextInfo:(void *)contextInfo {
[sheet orderOut:nil];
}
#end
In this example, you click the Show Sheet button, and the sheet starts being shown attached to the main window. In the sheet, there is a Cancel and an OK button, which both call their respective methods. In each of these methods, you call [NSApp endSheet:self.sheet]. This tells NSApp that it should then call the sheetDidEnd:returnCode:contextInfo: method on the object specified to be the modal delegate. In sheetDidEnd:returnCode:contextInfo: you then tell the sheet to hide itself.
EDIT:
Every NSWindow has a "Visible at launch" flag that can be set in Interface Builder. If this flag is set, the window will be visible at the time the nib file is loaded. If it isn't set, the window is hidden until you programmatically show it. Just edit the flag in the nib file like shown:
I show my main window by calling
[window makeKeyAndOrderFront:self];
[NSApp activateIgnoringOtherApps:YES];
[window setIsVisible:YES];
[window display];
which works, but doesn't set the window to the key window right after this calls. I have to wait "some time" until [NSApp keyWindow] returns the actual window.
I'm wondering now, how long does this take and how can I force a window to become the key window immediately?
I think there are probably good reasons that makeKeyAndOrderFront isn't a synchronous call, namely there could be coordination involved with multiple windows and objects that NSApplication need to take care of to make it happen, so forcing window to become key immediately is probably not supported by Cocoa. This however may not be a problem depending on the problem you are trying to solve.
Now, my guess is that some of your methods depend on the window being key, and at the moment they are not happening properly because the window doesn't become key immediately. However, you can implement the NSWindowDelegate protocol, set yourself as window delegate, and override - (void)windowDidBecomeKey:(NSNotification *)notification method to find out when the window did become key. This should also be a global notification in case that works better for you.
For more details, check out apple docs at http://developer.apple.com/library/mac/#documentation/cocoa/reference/NSWindowDelegate_Protocol/Reference/Reference.html
When I open an NSSavePanel or NSOpenPanel instance with beginWithCompletionHandler: the handler is never called. Instead I see the panel appear for a fraction of a second, before it goes away again without letting the user select a file. When I open the panel with runModal it works just fine. Here the code:
NSSavePanel *savePanel = [NSSavePanel savePanel];
//[savePanel runModal]; // Works
[savePanel beginWithCompletionHandler:^(NSInteger result){
NSLog(#"DONE"); // Never called, dialog disappears right away
}];
Is there anything I'm missing here?
Thanks, Mark
Found the problem: in the above code, the savePanel instance is autoreleased as soon as the surrounding method ends. This causes the panel to disappear. The solution is to hold on to the panel reference until the completion block is called.