Closing Window and Releasing NSWindowController - objective-c

I have a relatively-lengthy task. So I bring up a separate window (NSWindowController) from AppDelegate to show progress. It goes like
//AppDelegate.m
if (self.progresswindow == nil) {
self.progresswindow = [[ProgressController alloc] initWithWindowNibName:#"ProgressController"];
}
[progresswindow showWindow:self];
//[[progresswindow window] setReleasedWhenClosed:NO];
[NSApp runModalForWindow:progresswindow.window];
When a task is complete, the progress window will close itself.
//ProgressController.m
[NSApp stopModal];
[self close];
It works fine. But when I click on a button to start another session of a task with the same window, the application won't run a task although it opens. It appears that the last instance hasn't be released. The progress window has the following lines.
- (void)windowDidLoad {
NSLog(#"Hey!");
}
And NSLog won't be called for the 2nd time. I wonder what I'm doing wrong? Calling setReleasedWhenClosed from AppDelegate has no effect. I have the Release When Closed checkbox enabled, anyway. I read something like I need to observe NSWindowWillCloseNotification the progress window in a different topic so that I can release it when it closes. But I'm using ARC. So I can't manually release it, can I? Meanwhile, if I open Apple's sample (TableViewPlayground), it seems that they use this notification. Furthermore, I've read this topic and this topic. But I don't know what the problem is.
I appreciate any advice. Thank you for your time.

Release the Progress-Window-Controller.

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];
}

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");
}

Release a NSWindowController when the window is closed

I'm building a Cocoa application and have a question about using window controllers. The idea is that if the user selects New from the menu bar, an instance of MyWindowController which is a subclass of NSWindowController is created and a new window from MyWindow.xib is displayed.
I'm handling the action in the application delegate. From what I have seen after searching around something like the following could be done. Once the window is displayed I don't have any reason to store a pointer to the window controller anymore and since I allocated it I also autorelease it before displaying the window.
[[[[MyWindowController alloc] init] autorelease] showWindow:self];
Since the window is released soon afterwards the window will briefly display on the screen and then go away. I have found a solution where I retain the window controller in the -showWindow: method and let it release itself once it gets a windowWillClose notification.
- (IBAction)showWindow:(id)sender
{
[self retain];
[[NSNotificationCenter defaultCenter] addObserverForName:NSWindowWillCloseNotification
object:self.window
queue:nil
usingBlock:^(NSNotification *note) {
[self release];
}];
[super showWindow:sender];
}
Is there a better way to do this? I have searched the Apple documentation and have not found anything on which practices to use. It sounds like something very basic which it should cover so maybe I'm just searching with the wrong terms.
Normally you would hold on to the window controller, and only release it when you are done with it. I'd say that your app delegate would be responsible for that. Just store them in an array if there can be multiple. Whilst your solution may work, it's not very elegant.
If you are working on a document based Cocoa app, you create the window controller in your document subclass method makeWindowControllers and let that class hold a pointer to your window controller.
func windowShouldClose(_ sender: NSWindow) -> Bool {
#if DEBUG
let closingCtl = sender.contentViewController!
let closingCtlClass = closingCtl.className
print("\(closingCtlClass) is closing")
#endif
sender.contentViewController = nil // will force deinit.
return true // allow to close.
}

How do I run modals windows from code with cocoa?

I`m trying to run a window as modal directly from the code.
My program starts and a main NSWindows is showed.A thread still running to see if the user has a valid distribution. if he doesn't I need to run a modal.I mean, I have no buttons clicked in the interface.I've designed a NSWindow on the interface builder for a password set, and I want to call it only when my validation is not successful.
I have tested and realized that these methods which are responsible for modal windows running only work in a IBAction environment.
//This doesn't work
-(void) showPasswordWindow
{
[NSApp runModalForWindow:[self window]];
}
//this works But its not useful for me =(
- (IBAction) passwordWindowButton:(id)sender
{
[NSApp runModalForWindow:[self window]];
}
Please, help this newbie =)
One thing to check: are you calling the method on the main thread?
to check, add this to showPasswordWindow
NSLog(#"Main thread? %d", [NSThread isMainThread]);

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.