Showing a non-modal template chooser synchronously - cocoa

I'm writing a Cocoa app using the document architecture. Whenever an untitled document is created in this app, the user should be shown a window that lets them pick a template and prompts for other information. Only one of these windows should show up at a time, and preferably it should be possible to interact with the rest of the application while the template chooser is visible. (This is how Pages behaves.)
I've got most of this working by overriding -[NSDocumentController openUntitledDocumentAndDisplay:error:]:
- (id)openUntitledDocumentAndDisplay:(BOOL)displayDocument
error:(NSError *__autoreleasing *)outError {
TsDocument * doc = [self makeUntitledDocumentOfType:self.defaultType
error:outError];
if(!doc) {
return nil;
}
TsNewWindowController * newController = [TsNewWindowController new];
newController.document = doc;
if([NSApp runModalForWindow:newController.window] == NSRunAbortedResponse) {
if(outError) {
*outError = [NSError errorWithDomain:NSCocoaErrorDomain
code:NSUserCancelledError
userInfo:nil];
}
return nil;
}
[self addDocument:doc];
if(displayDocument) {
[doc makeWindowControllers];
[doc showWindows];
}
return doc;
}
However, as you can see, the window is displayed modally, blocking access to the rest of the app. Is there a simple way to achieve what I want without making the template picker modal?
To explain a couple things more clearly:
I do of course know that -runModalForWindow: will run the window modally—it's right there in the name! I'm looking for another way to display the window that will still block -openUntitledDocumentAndDisplay:error:, or that doesn't require me to block the method at all.
I don't believe I can simply create the document, show the newController's window, and call the document's makeWindowControllers and showWindows later because, if the app quits, Restore will not show the template chooser—it shows the normal editing interface.

You do need to create and use a NSWindowController, but you need to do so before openUntitledDocument…:: is called.
In the unreleased Adium Xtras Creator, I tapped in at several points:
In the application's delegate, in applicationOpenUntitledFile:, I show the template-chooser window and return YES.
In the document controller, in removeDocument:, I pass the message on to super, then check whether there are still any documents open. If not, I show the template-chooser window.
In the document controller, in addDocument:, I hide the template-chooser window, then pass to super.
Thus:
If the user tries to create a new document (of no particular type) by any means, the template-chooser will be shown instead.
If the user creates a new document (of an explicit type) by any means, the template-chooser will be hidden. (The application I did this in had its “New” menu item set up as a submenu containing specific types.)
If the user opens any document by any means, the template-chooser will be hidden.
If the user closes the last open document, the template-chooser will be shown.
If the user or another application tries to reopen the application by any means, the template-chooser will be shown.

You are calling runModalForWindow: so of course it's running the window as a modal window.
Why not just show the window instead? Use an NSWindowController and call showWindow: to display the template window. In your window controller, implement actions that react to the user's selection and then create the appropriate document (or cancel).
I don't think you need to actually create a document in openUntitledDocumentAndDisplay:error:.

Related

Cocoa: Avoiding 'Updates Continuously' in control binds

I have several panels that contain NSTextField controls bound to properties within the File's Owner object. If the user edits a field and then presses Tab, to move to the next field, it works as expected. However if the user doesn't press Tab and just presses the OK button, the new value is not set in the File's Owner object.
In order to workaround this I have set Updates Continuously in the binding, but this must be expensive (EDIT: or at least it's inelegant).
Is there a way to force the bind update when the OK button is pressed rather than using Updates Continuously?
You're right that you don't need to use the continuously updates value option.
If you're using bindings (which you are), then what you should be doing is calling the -commitEditing method of the NSController subclass that's managing the binding. You'd normally do this in your method that closes the sheet that you're displaying.
-commitEditing tells the controller to finish editing in the active control and commit the current edits to the bound object.
It's a good idea to call this whenever you are performing a persistence operation such as a save.
The solution to this is to 'end editing' in the action method that gets called by the OK button. As the pane is a subclass of NSWindowController, the NSWindow is easily accessible, however in your code you might have to get the NSWindow via a control you have bound to the controller; for example NSWindow *window = [_someControl window].
Below is the implementation of my okPressed action method.
In summary I believe this is a better solution to setting Updated Continuously in the bound controls.
- (IBAction)okPressed:(id)sender
{
NSWindow *window = [self window];
BOOL editingEnded = [window makeFirstResponder:window];
if (!editingEnded)
{
logwrn(#"Unable to end editing");
return;
}
if (_delegateRespondsToEditComplete)
{
[_delegate detailsEditComplete:&_mydetails];
}
}
Although this is really old, I absolutely disagree with the assumption that this question is based on.
Countinously updating the binding is absolutely not expensive. I guess you might think this updates the value continuously, understanding as "regularly based on some interval".
But this is not true. This just means it updates whenever you change the bound value. This means, when you type something in a textView, it would update as you write; this is what you'd want in this situation.

Cocoa screen saver config panel floating freely

I'm writing a screen saver using Cocoa's ScreenSaver API. It's compiled for 64-bit arch and I'm running it on Lion.
In order to enable configuration, I have added the following to the main view:
- (BOOL)hasConfigureSheet
{
return YES;
}
- (NSWindow*)configureSheet
{
if (configureSheet == nil) {
if (![NSBundle loadNibNamed: #"WTConfigureSheet" owner: self]) {
NSLog(#"Failed to load config sheet");
return nil;
}
}
ScreenSaverDefaults *defaults =
[ScreenSaverDefaults defaultsForModuleWithName: WTModuleName];
backgroundColorWell.color = [defaults objectForKey: #"BackgroundColor"];
lightLetterColorWell.color = [defaults objectForKey: #"LightLetterColor"];
darkLetterColorWell.color = [defaults objectForKey: #"DarkLetterColor"];
return configureSheet;
}
After installing the saver freshly, clicking "Options" makes the config sheet appear not as a sheet, but floating freely on the screen, without a border. Otherwise, it works correctly and disappears after being dismissed.
When I click "Options" a second time, the config sheet appears again, this time correctly as a sheet of the preferences window. It then immediately freezes, so that I can't click any of its controls.
Does anyone have an idea what causes this behavior?
I had the same problem as you today and it took me quite some time to figure this one out, so here's my solution:
I discovered that the NSWindow appears as soon as you call loadNibNamed:owner:. So there had to be some sort of mechanism to automatically open windows from nibs.
So I re-checked the nib and saw that there is an option called "Visible At Launch" on the attribute inspector pane which is checked by default.
The solution is very simple: just uncheck that checkbox and it works as expected.
I find it easy to overlook since you expect the window to open, but it actually opens twice (once automatically and a second time because System Preferences.app shows it as a sheet) which leads to the glitches.
Another problem that could happen, depending on how you defined the ivar / property on your class is that after the first close and re-open of the window it just freezes.
This is because per default the window releases itself when closed.
So be sure to also uncheck "Release When Closed" in interface builder.
For this code to work as written, you need to create an IBOutlet of type NSWindow* named configureSheet in your main view's header file, save that file so Interface Builder can see the change, then load WTConfigureSheet.xib in Interface Builder and connect up the toplevel window component to Files Owner -> configureSheet.

When is the NSApp's keyWindow created?

When does the keyWindow get created?
I thought the NSWindow would be created before the corresponding view controller's updateView method was called (Which I call in response to awakeFromNib), however if I create an alert sheet using NSApp's keyWindow, it does not appear correctly.
If I place a button on that view, however, and then bring up the alert when the user clicks on it, the keyWindow is defined, and the alert displays correctly (as expected).
My Application Delegate is almost completely empty.
I don't actually want to display the alert at startup, but I do want to know when the key window is set up. :)
When does the keyWindow get created?
-[NSApp keyWindow] points to an existing window (e.g. a window that has already been loaded from a nib file) that is currently the key window, typically by sending it -makeKeyAndOrderFront:.
When an application starts, Cocoa:
Loads the main nib file;
Unarchives the contents of the nib file and instantiates its objects;
Reestablishes the connections defined in the nib file;
Sends -awakeFromNib to (a subset of) the nib file objects;
Displays windows that have been marked as Visible at launch time;
as described in the Resource Programming Guide.
If the nib file contains a single window, that window becomes key upon being shown provided it can become a key window, and this happens after -awakeFromNib has been sent.
Also, the documentation for -[NSApplication keyWindow] states that:
This method might return nil if the application’s nib file hasn’t finished loading yet or if the receiver is not active.

MacRuby + Interface Builder: How to display, then close, then display a window again

I'm a complete n00b with MacRuby and Cocoa, so keep that in mind when answering - I need lots of details and explanation. :)
I've set up a simple project that has 2 windows in it, both of which are built with Interface Builder. The first window is a simple list of accounts using a table view. It has a "+" button below the table. When I click the + button, I want to show an "Add New Account" window.
I also have an AccountsController < NSWindowController and a AddNewAccountController < NSWindowController class, set up as the delegates for these windows, with the appropriate button click methods wired up, and outlets to reference the needed windows.
When I click the "+" button in the Accounts window, I have this code fire:
#add_account.center
#add_account.display
#add_account.makeKeyAndOrderFront(nil)
#add_account.orderFrontRegardless
this works great the first time I click the + button. Everything shows up, I'm able to enter my data and have it bind to my model. however, when I close the add new account form, things start going bad.
if I set the add new account window to release on close, then the second time I click the + button, the window will still pop up but it's frozen. i can't click any buttons, enter any data, or even close the form. i assume this is because the form's code has been released, so there is no message loop processing the form... but i'm not entirely sure about this.
if i set the add new account window to not release on close, then the second time i click the + button, the window shows up fine and it is usable - but it still has all the data that i had previously entered... it's still bound to my previous Account class instance.
what am I doing wrong? what's the correct way to create a new instance of the Add New Account form, create a new Account model, bind that model to the form and show the form, when I click the + button on the Accounts form?
... this is all being done on OSX 10.6.6, 64bit, with XCode 3.2.4
The issue is that it doesn't create the window each time. Release on close is a bit of an annoying option and generally is only used if you know the window controller is also being released when the window closes. (Note I've never used MacRuby so I'll be giving code in Obj-C as I know that it is correct, hopefully you can convert it. I'll be assuming GC is on as it should be with MacRuby).
Now there are two ways to do this. I'm not entirely sure how your NIB/classes are set up as it could be one of two ways.
--
The first way to solve it is to use the outlets you use to reference the form elements to blank them out when you display the window again eg [myTextField setStringValue:#""]. If you're using cocoa bindings then it's a little trickier, but basically you have to make sure the bound object is blanked out. I would recommend against bindings though if you are new to Cocoa.
--
The second way is to make the AddNewAccountController class a subclass of NSWindowController. When you press the + button you would then create a new instance of it and display it (remember to store it in an ivar). The best way to do it would be as so:
if (!addAccountController) {
addAccountController = [[AddNewAccountController alloc] initWithWindowNibName:#"AddNewAccountController"];
[[addAccountController window] setDelegate:self];
}
[addAccountController showWindow:self];
This prevents a new instance being made if the window is already visible. You then need to implement the delegate:
- (void)windowWillClose:(NSNotification *)notification {
//If you don't create the account in the AddNewAccountController then do it here
addAccountController = nil;
}
Obviously you would need to move the window to a separate NIB called "AddNewAccountController". In this NIB make sure to set the class of the File's Owner to AddNewAccountController and then to connect the File's Owner's window outlet to the window.
When all this is set up, you will get a fresh controller/window each time. It also has the benefit of splitting up nibs and controllers into more focused units.
--
One last thing. While it is fine to do something like this in a window, you may want to eventually look at doing this via a sheet, as it would then prevent the possibility of the add account window getting hidden behind other windows.

Document Based Application, preinitialize window (enter serial, buy, trial)

I need to create several windows before NSDocument is loaded, or create a window that blocks NSDocument window and top menu.
I tried several solutions - but they didn't work right.
modal window, one after another. there were some problems with Async URLConnection, and some other problems with my NSDocument content.
I created custom MainMenu.xib with no menu, that opens my preinitialize windows.
here i found some other problems, when a file(associated with my application) is opened - the Document Window initializes. Here i tried to subclass NSDocumentController, but i found no way to pause the "open document". (i want the document to be opened anyway, but only after the preinitalize windows would be closed).
So what is the right way to do this?
Implement applicationShouldOpenUntitledFile: in your app delegate to return NO if the user has to go through the not-registered-yet dialog first.
In the action methods for your “Trial” and “Confirm Registration” buttons, create the untitled document yourself (by sending the necessary message to the document controller).
So the right answer is to implement:
* application:openFiles:
* applicationShouldOpenUntitledFile:
and implement your own document creation. this is the way it worked for me.
MyDocument* document = [[MyDocument alloc]
initWithContentsOfURL:fileURL
ofType:[fileName pathExtension]
error:nil
];
if(document)
{
[[NSDocumentController sharedDocumentController] addDocument:document];
[document makeWindowControllers];
[document showWindows];
}
of course you need to write error handling code.

Resources