How to show a NSPanel as a sheet - objective-c

I'm trying to show a NSPanel as a sheet. I'm naively doing something along those lines:
SheetController *sheetController = [[[SheetController alloc]
initWithWindowNibName:#"Sheet"] autorelease];
[[NSApplication sharedApplication] beginSheet:sheetController.window
modalForWindow:self.window
modalDelegate:self
didEndSelector:#selector(sheetDidEnd:returnCode:contextInfo:)
contextInfo:nil];
For some reason that eludes me, this isn't working. When this part of the code is called, the sheet momentarily flashes (because of the autorelease message). The sheet is never hooked to window.
If anyone can point me to where I can find more information, that would be very appreciated.

This sounds like a classic case of having checked the "Visible at Launch" box for the panel in IB. Turn that off.

Yes, you need to own this controller for as long as you want it to continue functioning. You can't just create it, autorelease it, and let it die—you need to hold onto it for as long as you need it.

Don't forget that if you're trying to run this as a "modal" sheet (i.e. it takes over the app until the user dismisses it), you'll need to push a new run loop.
What you've done is shown the sheet, and then not pushed a new loop, so the OS just shows the sheet, sees there's no reason to keep it running, and thus shuts it down and resumes execution on the next line:
I typically do sheets the following way:
- (id)showPanelModalAgainstWindow: (NSWindow *)window
{
[[NSApplication sharedApplication] beginSheet: panelToShow
modalForWindow: window
modalDelegate: self
didEndSelector: #selector(sheetDidEnd:returnCode:contextInfo:)
contextInfo: nil];
[[NSApplication sharedApplication] runModalForWindow: panelToShow];
if (m_returnCode == NSCancelButton) return nil;
}
- (void)sheetDidEnd:(NSWindow *)sheet
returnCode:(int)returnCode
contextInfo:(void *)contextInfo
{
UNUSED(sheet);
UNUSED(contextInfo);
m_returnCode = returnCode;
}
Then, in your accept and/or cancel button routines:
- (IBAction)continueButtonClicked:(id)sender
{
UNUSED(sender);
[[NSApplication sharedApplication] stopModal];
[createAccountWizardPanel orderOut: nil];
[[NSApplication sharedApplication] endSheet: createAccountWizardPanel
returnCode: NSOKButton];
}
I'm sure there is a slightly less code-y way of doing this, but I've not looked to deeply into it, because this way works perfectly fine thus far ....
Previous comments about the lifetime of the controller and panel objects are also relevant -- be sure to understand exactly what objects you need for what lifetimes when showing a modal panel.

Related

Menu Bar App Never Becomes Reactivated

I'm building a Mac app that only sits in the menu bar with no dock item and no key window and no main menu (it's LSUIElement in the info.plist is set to YES). When I first launch the app, applicationDidBecomeActive: is called, as I expect. However, once another app gains focus, applicationDidBecomeActive: is never called again.
This prevents a text field I have within my app from becoming the first responder. When I first open the app, the text field is editable:
But after another app comes to the foreground, the text field is not editable:
What I've tried:
When the menu is opened, menuWillOpen: is called on the NSMenu's delegate. I've tried placing the following with no success:
[NSApp unhide];
[NSApp arrangeInFront:self];
[NSApp activateIgnoringOtherApps:YES];
[NSApp requestUserAttention:NSCriticalRequest];
[[NSRunningApplication currentApplication] activateWithOptions:NSApplicationActivateIgnoringOtherApps];
[[NSRunningApplication currentApplication] unhide];
I think the issue is probably related to not having any windows to bring to the front. I feel like I'm grasping at straws here. Any help would be greatly appreciated.
I think the issue is with that how the runloop operates when a NSMenu is open, so you should try activating the app before you display the menu. If you're having the NSStatusItem display it, I'd suggest doing it yourself like this:
- (void)toggleMenu:(id)sender
{
// App might already be active
if ([NSApp isActive]) {
[self.statusItem popUpStatusItemMenu:self.menu];
} else {
[NSApp activateIgnoringOtherApps:YES];
}
}
- (void)applicationDidBecomeActive:(NSNotification *)notification
{
[self.statusItem popUpStatusItemMenu:self.menu];
}
That should work, but I think though in general you'll have better luck with an actual window instead of a menu.
You probably need to allow your input to -becomeFirstResponder, maybe by overriding -canBecomeFirstResponder or by calling the become method yourself.
You'd likely have to implement/call these methods for whatever view is housing your text input, or maybe tell your input view to become the first responder.
Either way, it smells like a responder chain issue.
Try calling -makeFirstResponder: on your window. NSWindow is usually the start of the NSResponder chain.
- (void)menuWillOpen:(NSMenu *)menu {
[[NSApp mainWindow] makeFirstResponder:yourTextInputField];
}
I'm assuming your text field already accepts first responder since you said your app launches initially with it as the first responder. If not, make sure your text field overrides -acceptsFirstResponder: to return YES
- (BOOL)acceptsFirstResponder {
return YES;
}
Edit: Ah, see that you don't have a key window. It looks like NSMenu actually has a window associated with it though, and it's safe to call -makeFirstResponder:. Some discussion here suggests overriding -viewDidMoveToWindow: on your view containing your text field in the NSMenu like so:
- (void)viewDidMoveToWindow {
[super viewDidMoveToWindow];
[[self window] makeFirstResponder:yourTextInputField];
}

Show Window without activating (keep application below it active)

I need to show a window (without title bar) above third party applications without my window taking focus.
I have tried using an NSPanel and setting enabling non-activating, but that didn't help.
I tried orderFront:self, but that didn't help either.
I always needed to add [NSApp activateIgnoringOtherApps:YES]; because the window wouldn't show otherwise.
I have here a sample project for just this functionality:
http://users.telenet.be/prullen/TopW2.zip
UIElement is set to true in the application's plist file, so there is no dock. You can activate the window by pressing ALT + SPACE at the same time. You will see that the app below it looses focus. Any thoughts on how to fix this? I've seen other apps do it so I know it's possible.
Edit: here's the code so far. Remember the window is a non-activating NSPanel.
I still need that last NSApp activateIgnoringOtherApps line or otherwise it doesn't display. But of course that makes the window the active one.
_windowController = [[MyWindowController alloc] initWithWindowNibName:#"MyWindowController"];
[[_windowController window] setLevel:NSNormalWindowLevel+1];
[[_windowController window] orderFrontRegardless];
[_windowController showWindow:self];
[NSApp activateIgnoringOtherApps:YES];
I've also subclassed NSPanel and added two methods:
- (BOOL)canBecomeKeyWindow
{
return YES;
}
- (BOOL)canBecomeMainWindow
{
return YES;
}
Edit: OK, unchecking setHidesOnDeactivate fixes this, but now the window will never hide. I need it to hide when the user presses the app below it or switches to another app.
Edit 2: OK, this seems to fix the above issue:
- (void)awakeFromNib
{
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(hideWindow) name:NSWindowDidResignKeyNotification object:nil];
}
- (void)hideWindow {
[self setHidesOnDeactivate:YES];
}
Not sure if there's a better way.
And for those that want to know how to display the window:
[[_windowController window] setLevel:NSPopUpMenuWindowLevel];
[[_windowController window] orderFrontRegardless];
[[_windowController window] makeKeyWindow];
[_windowController showWindow:self];
Either one of these should do the trick:
Use -[NSWindow orderFrontRegardless] to get a normal level window to the front without activating the corresponding app, or
Use -[NSWindow setLevel:] to increase the window level to something higher than NSNormalWindowLevel
Not to take away from #puzzle's useful answer, but it sounds like your problem has something to do with using an NSPanel instead of an NSWindow.
The "How Panels Work" docs say:
Onscreen panels, except for alert dialogs, are removed from the screen when the application isn’t active and are restored when the application again becomes active. This reduces screen clutter.
Specifically, the NSWindow implementation of the hidesOnDeactivate method returns NO, but the NSPanel implementation of the same method returns YES.
So perhaps you could override hidesOnDeactivate to return NO, or change to NSWindow

Opening a new window and waiting for it to close

I have a Mac OS X app written in objetive-c Cocoa. You can see most of the code in this previous question. Essentially you click a button on the main window (the app delegate) and it opens another window where the user can enter information.
In the following code (that gets called when the user press the button in the app's main window)
- (IBAction)OnLaunch:(id)sender {
MyClass *controllerWindow = [[MyClass alloc] initWithWindowNibName:#"pop"];
[controllerWindow showWindow:self];
NSLog(#"this is a log line");
}
The NSLog line gets printer immediately after I called showWindow. Is there any way to wait until controllerWindow is closed to continue with the NSlog?
The reason for this is that the user set's a value on the new window I opened and I need to collect that value on the same OnLaunch so I need to wait.
I know that modal windows are bad form in Mac, but I have no control over this feature.
I've tried with
[NSApp runModalForWindow:[controllerWindow window]];
and then setting the popup window to
[[NSApplication sharedApplication] runModalForWindow:popupwin];
and it works but then the focus never gets passed to the main window anymore
Thanks!
If you want the window to be modal for your application, use a sheet: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Sheets/Tasks/UsingCustomSheets.html
However, there is no way to suspend execution of a method while the sheet is displayed, this would be tantamount to blocking the current run loop. You would have to break you code into the begin and end methods as described in the linked documentation.
Here are the steps you need to follow:
In TestAppAppDelegate create an NSWindow outlet to hold your sheet and an action to dismiss the sheet
Create a nib with an NSWindow as the root object. I think you already have this in "pop". Set the Visible at Launch option to NO (this is very important)
Set the file's owner of this nib to TestAppAppDelegate and connect the window to your new outlet, and the close button to your new action
In your method to launch the sheet (OnLaunch), use the following code:
(ignore this it's to make the code format properly!)
if(!self.sheet)
[NSBundle loadNibNamed:#"Sheet" owner:self];
[NSApp beginSheet:self.sheet
modalForWindow:self.window
modalDelegate:self
didEndSelector:#selector(didEndSheet:returnCode:contextInfo:)
contextInfo:nil];
Your close button action should be [NSApp endSheet:self.sheet];
Your didEndSheet: method should be [self.sheet orderOut:self];
You can use UIVIew method animateWithDuration:delay:options:animations:completion: to accomplish this.
You said you want the next line to execute once the window is closed, rather than after it is opened. In any case, you may end the OnLaunch method this way:
- (IBAction)OnLaunch:(id)sender {
MyClass *controllerWindow = [[MyClass alloc] initWithWindowNibName:#"pop"];
[controllerWindow animateWithDuration:someDelay:options: someUIAnimationOption
animations:^{
[controllerWindow showWindow:self]; // now you can animate it in the showWindow method
}
completion:^{
[self windowDidFinishShowing]; // or [self windowDidFinishDisappearing]
}
}
- (void) windowDidFinishShowing {
NSLog(#"this is a log line");
}

Cocoa: Processing thread results, and queuing multiple sheets

I have a multithreaded application that has many concurrent operations going on at once. When each thread is finished it calls one of two methods on the main thread
performSelectorOnMainThread:#selector(operationDidFinish:)
// and
performSelectorOnMainThread:#selector(operationDidFail:withMessage:)
When an operation fails, I launch a sheet that displays the error message and present the user with 2 buttons, "cancel" and "try again". Here is the code I use to launch the sheet:
// failureSheet is a NSWindowController subclass
[NSApp beginSheet:[failureSheet window]
modalForWindow:window
modalDelegate:self
didEndSelector:#selector(failureSheetDidEnd:returnCode:contextInfo:)
contextInfo:nil];
The problem is that if 2 concurrent operations fail at the same time, then the current sheet that is displayed gets overwritten with the last failure message, and then the user's "try again" action will only retry the last failed operation. Ideally, I would like to "queue" these failure sheets. If 2 operations fail at the same time then you should see 2 sheets one right after the other, allowing the user to cancel or retry them individually.
I've tried using:
[NSApp runModalSessionForWindow:[failureSheet window]]
which seems to do what I want, but doesn't work in my situation. Maybe it isn't thread safe?
For example the following code works...
- (void)displaySheet
{
[NSApp beginSheet:[failureSheet window]
modalForWindow:window
modalDelegate:self
didEndSelector:#selector(failureSheetDidEnd:returnCode:contextInfo:)
contextInfo:nil];
[NSApp runModalForWindow:[failureSheet window]];
[NSApp endSheet:[failureSheet window]];
[[failureSheet window] orderOut:nil];
}
// Calling this method from a button press works...
- (IBAction)testDisplayTwoSheets
{
[self displaySheet];
[self displaySheet];
}
However if I have 2 different threaded operations invoke displaySheet (on the main thread) when they are done, I only see one sheet and when I close it the modal session is still running and my app is essentially stuck.
Any suggestions as to what I'm doing wrong?
If you want to queue them, then just queue them. Create an NSMutableArray of result objects (you could use the operation, or the failure sheet itself, or a data object that gives you the information for the sheet; whatever is convenient). In operationDidFinish: (which always runs on the main thread, so no locking issues here), you'd do something like this:
[self.failures addObject:failure];
if ([[self window] attachedSheet] == nil)
{
// Only start showing sheets if one isn't currently being shown.
[self displayNextFailure];
}
Then you'd have:
- (void)displayNextFailure
{
if ([self.failures count] > 0)
{
MYFailure runFailure = [self.failures objectAtIndex:0];
[self.failures removeObjectAtIndex:0];
[displaySheetForFailure:failure];
}
}
And at the end of failureSheetDidEnd:returnCode:contextInfo:, just make sure to call [self displayNextFailure].
That said, this is probably a horrible UI if it can happen often (few things are worse than displaying sheet after sheet). I'd probably look for ways to modify the existing sheet to display multiple errors.
I don't think you're using the "runModalForWindow:" command properly. You wouldn't use both [NSApp beginSheet...] and "runModalForWindow:" at the same time. One is for document modal (where the application keeps running but the window with the sheet is locked) and one is for application modal (which stops everything in the entire app until the sheet is dismissed). You only want the application modal dialog so do this...
For application modal start the window with the following only:
[[failureSheet window] center];
[NSApp runModalForWindow:[failureSheet window]];
Hook the "OK" button and others to regular IBAction methods. They'll get called when a button is pressed. In the IBAction methods you need to do something like this to dismiss the window and process the action:
-(IBAction)okBtnPressed:(id)sender {
[NSApp stopModal];
NSLog(#"ok button");
[[sender window] close];
}

App modal NSPanel / sheet / dialog + NSThread == window hangs?

I'm in the midst of debugging an extremely unusual problem, and I was wondering if anybody might have any insight into what might be going wrong:
In a controller class from a NIB, I take an NSPanel from that same NIB, and then show it app modally on a NSWindow (that was created by hand in code):
[[NSApplication sharedApplication] beginSheet: myPanel
modalForWindow: window
modalDelegate: self
didEndSelector: #selector(sheetDidEnd:returnCode:contextInfo:)
contextInfo: nil];
[[NSApplication sharedApplication] runModalForWindow: myPanel];
Now, when the "finish" button on that sheet is clicked, I run some code to disable some buttons and fire off a thread to make sure the user input is valid (I have to validate with a remote service). This thread is fired from a separate validator object I create:
// controller calls:
[validator validateCreds: creds
notify: #selector(validationComplete:)
onObject: self];
// validator object
validateInfo: (NSDictionary *)parms
notify: (SEL)notifySelector
onObject: (id)notifyObject
{
// build up data with parms and notify info
[[NSThread detachNewThreadSelector: #selector(remotevalidate:)
toTarget: self withObject: data];
}
Next, when the validation is finished, the validator notifies my controller object:
[notifyObject performSelectorOnMainThread: notifySelector
withObject: results waitUntilDone: NO];
And then my controller object, in the method that the validator object calls, kills the dialog:
- (void)validationComplete: (id)data
{
[[NSApplication sharedApplication] stopModal];
[createTwitterPanel orderOut: nil];
[[NSApplication sharedApplication] endSheet: createTwitterPanel
returnCode: NSOKButton];
}
- (void)sheetDidEnd:(NSWindow *)sheet
returnCode:(int)returnCode
contextInfo:(void *)contextInfo
{
m_returnCode = returnCode;
}
My problem: Although the panel is closed / disappears, the top NSApp runModalForWindow: does not exit until some system event is sent to the window that was showing the dialog. Trying to move, resize, or do anything to the window, or otherwise switching away from the application suddenly causes the method to exit and execution to continue. No amount of waiting seems to help, otherwise, however.
I have verified that all methods being invoked on the controller class are all being invoked on the main app thread.
An even more interesting clue is that the dialog has two controls, a WebView, and an NSTextField: Even if I force the exit of runModalForWindow: by clicking on the window, TABbing between the two controls remains screwed up — it simply never works again. It's like my event loop is horked.
I've tried changing validationComplete: to instead post a notification to the main thread, and I've also played with the waitUntilDone on the performSelectorOnMainThread method, all to no effect.
Any ideas? Things I should try looking at?
From the NSApplication documentation:
abortModal must be used instead of
stopModal or stopModalWithCode: when
you need to stop a modal event loop
from anywhere other than a callout
from that event loop. In other words,
if you want to stop the loop in
response to a user’s actions within
the modal window, use stopModal;
otherwise, use abortModal. For
example, use abortModal when running
in a different thread from the
Application Kit’s main thread or when
responding to an NSTimer that you have
added to the NSModalPanelRunLoopMode
mode of the default NSRunLoop.
So, I learned something today.