NSWindow tracking - macos

I would like to track each time a certain window appears (becomes visible to the user) in a OS X app. Where would be the most adequate place to call the tracker?
windowWillLoad, maybe?
I expected to find something like windowWillAppear but it seems I'm thinking too much iOS.

How about getting notification such as NSWindowDidBecomeMainNotification, By main I guess the one which is top most on screen directly visible by user.
see : Apple Documentation

Yes, one would expect that a window would notify its delegate or its controller with a windowWillAppear or windowDidAppear message, or post a documented notification like NSWindowDidAppearNotification. But alas, none of those exist. I filed a bug report with Apple and was given the advice to use a storyboard and a view controller instead. This is unhelpful in legacy apps that already use a bunch of window controllers and xibs.
You could subclass NSWindow and override orderWindow:relativeTo: to send a notification. Most, but not quite all, of the messages that make a window show itself ultimately go through this method, including orderBack:, orderFront:, makeKeyAndOrderFront:, and -[NSWindowController showWindow:]. But orderFrontRegardless does not go through orderWindow:relativeTo:, so you would also want to override that for completeness.
Another way to be notified is to make a subclass of NSViewController that controls some view that's always visible in the window. The view controller will receive viewWillAppear and viewDidAppear.
If you're subclassing NSWindow or NSViewController already for some other reason, either of these is a reasonable solution.
If you're not subclassing NSWindow already, and don't have an NSViewController subclass for a view that's always visible in the window, then another way is to use Cocoa bindings to connect the window's visible binding to a property one of your objects. For example, I have a custom NSWindowController subclass. I gave it a windowIsVisible property:
#interface MyWindowController ()
#property (nonatomic) BOOL windowIsVisible;
#end
and I implemented the accessors like this:
- (BOOL)windowIsVisible { return self.window.visible; }
- (void)setWindowIsVisible:(BOOL)windowIsVisible {
NSLog(#"window %# became %s", self.window, windowIsVisible ? "visible" : "hidden");
}
and in awakeFromNib, I bind the window's visible binding to the property like this:
- (void)awakeFromNib {
[super awakeFromNib];
[self.window bind:NSVisibleBinding toObject:self withKeyPath:NSStringFromSelector(#selector(windowIsVisible)) options:nil];
}
When the window becomes visible, the setWindowIsVisible: setter is called with an argument of YES. Note that if the whole app is hidden and reappears, the setter is called again, even though it wasn't called with argument NO when the app was hidden. So be careful not to assume the window was previously hidden.
Also, the binding might create a retain cycle, so you should probably unbind it when the window is closed, unless you want to keep the window and controller around. Note that the window does post NSWindowWillCloseNotification when it's closing, so you don't need any special magic to detect that.

Related

Is there a way for an NSView to know when its window is closed?

I'm working on an NSView subclass and I'd like to know when its window is closed so I can do certain housekeeping tasks. What is the standard way to do this? If possible, I'd like to do this within the NSView subclass since I'd like to be able to use this subclass elsewhere without other dependencies.
You can override -viewDidMoveToWindow and, if self.window is not nil, have the view start observing the NSWindowWillCloseNotification notification from the window.
Likewise, override -viewWillMoveToWindow: and, if self.window is not nil, stop observing that notification for that window.
You can perhaps create an extending method for viewWillDisappear?
According to the doc description:
This method is called when:
The view is about to be removed from the view hierarchy of the window
The view is about to be hidden or obscured, such as in the case of a view controller whose parent is a tab view controller and the user switched to another tab
The window is being closed
However, this is for NSViewController, not NSView.

How to get notified when NSWindow opens?

How to take notice when an NSWindow is about to be opened or have just opened? That is, the opposite of windowWillClose: delegate method (likewise the opposite of NSWindowWillCloseNotification.)
This is related to this question, but from the other direction.
The background is, I'm looking to couple a window with a tickmark on the main menu (among other things). When the window is shown, the corresponding ̨ menu item should be checked and vice-versa.
It should not normally be a mystery when or how a window is made visible. It should only happen in response to something that your own code is doing. If the window is in a NIB and is marked Visible At Launch, then it shows when your code loads that NIB. Otherwise, it should only show if you call one of the -order... methods other than -orderOut: (e.g. -orderFront:) or -makeKeyAndOrderFront:. If the window is controlled by a window controller, then it would show if you invoke -[NSWindowController showWindow:]. Etc.
If you really feel the need to be notified, you can subclass NSWindow and override -orderWindow:relativeTo: and, if orderingMode is not NSWindowOut and the window was not already visible, post a notification of your own.
By macOS 10.10, this is somewhat solved by the call to NSViewController's viewWillAppear or viewDidAppear. Have an NSViewController subclass and set it as the contentViewController of the window. Then its viewWillAppear/ viewDidAppear implementation can post a notification that the window will (or did) open.
You can bind your NSMenuItem value to the NSWindows visible binding Zero lines of code if you do it in IB.
visible:
A Boolean value that specifies if the NSWindow is visible.
If visible evaluates to YES, the NSWindow is visible.
Availability:
Available in OS X v10.3 and later.
See the NSWindow Binding Documentation for more info.
You can either bind the NSMenuItem value binding to an existing NSWindow property on one of your existing classes, or add an NSObjectController to your nib and set its content to the NSWindow instance then bind to the controller.
Tested and confirmed on Mac OS 10.9. Works for window minimization and restoration too.

Cocoa NSWindowController And NSWindow Not Deallocing

I'm working with an NSWindowController to implement a preferences window. Apple's documentation states that by default the controller and window aren't deallocated, because it's useful to not have to reload everything, which makes sense. But their documentation goes on to say that you can override that behavior, but not explain how.
Apple's Docs:
When a window is closed and it is part of a document-based
application, the document removes the window’s window
controller from its list of window controllers. This results
in the system deallocating the window controller and the
window, and possibly the NSDocument object itself. When a
window controller is not part of a document-based application,
closing the window does not by default result in the
deallocation of the window or window controller. This is the
desired behavior for a window controller that manages something
like an inspector; you shouldn’t have to load the nib file
again and re-create the objects the next time the user requests
the inspector.
If you want the closing of a window to make both
window and window controller go away when it isn’t
part of a document, your subclass of NSWindowController
can observe the NSWindowWillCloseNotification notification
or, as the window delegate, implement the windowWillClose: method.
I can't find anywhere that explains what to "implement" in the windowWillClose: method.
The window controller can be seen here:
https://github.com/gngrwzrd/gwpreferences/blob/master/GWPreferences/GWPreferences/GWPreferences/GWPrefsWindowController.m
Using the controller can be seen here:
https://github.com/gngrwzrd/gwpreferences/blob/master/GWPreferences/GWPreferences/GWAppDelegate.m - you can see in this code where I'm trying some bridge casting to try and force release objects but it doesn't work.
So the GWPrefsWindowController.dealloc method never gets called. Any ideas?
I understand this question is old, but for those who came here from google, the answer is quite simple.
As stated in the documentation, for non document base applications, you can simply:
Keep a reference for your NSWindowController wherever your are calling it. (In the example below it's referenced by myWindowController;
Make the class calling your NSWindowController implement the protocol NSWindowDelegate;
Release your Window Controller by setting it to nil on windowWillClose: method
To answer the question more precisely. When lazy instantiating your controller, set your class as the delegate:
-(IBAction)showMyWindowAction:(id)sender
{
// If my window controller is not nil
if (!myWindowController)
{
//instantiate it
myWindowController = [[MyWindowController alloc] initWithWindowNibName:#"myWindow"];
// set your class as delegate
[myWindowController setDelegate:self];
}
[myWindowController.window orderFront:self];
}
And then implement the windowWillClose: method from the NSWindowDelegate protocol
-(void)windowWillClose:(NSNotification *)notification
{
//Check if it's the right window that will close
if ([notification.object isEqualTo:myWindowController.window])
{
//Set your controller to nil
myWindowController = nil;
}
}
That's it, your window controller will now dealloc and since we are verifying if it's controller is nil before showing the window, everything will work!
I believe the reason why this is not implemented by default is because the initWithWindowNibName: is a somewhat heavy operation, and thus you have to think if dealloc'ing whatever is on your window will impact more or less than loading your window nib file.
I hope it helped

Receive window notifications

I have an NSWindow set up in Interface Builder. I have set the class of File's Owner to my NSWindowController and linked the window property of the controller to my NSWindow.
My controller implements NSWindowDelegate.
Now, in my controller, I have added the following:
- (void)windowDidLoad
{
[super windowDidLoad];
[self.window setDelegate:self];
}
- (void)windowDidBecomeMain:(NSNotification *)notification
{
NSLog(#"Did become main.");
}
Still, -windowDidBecomeMain: isn't called. Does anyone know why this is?
EDIT:
Trying to show a window from AppDelegate on launch. The main nib (declared in Info.plist) contains a menu item only which is linked to the AppDelegate. In the application delegate, I show an icon on the status bar and when this icon is clicked, I display the menu from the main nib.
In the application delegate, I also want to display a window which should have a window controller assigned to take care of the logic.
I believe that when this works, I will receive my window notifications.
Now, the following code doesn't show the window and I can't figure out why.
DemoWindowController *dwc = [[DemoWindowController alloc] initWithWindowNibName:#"DemoWindowController"];
[dwc showWindow:self];
Note that self is the application delegate.
I suspect your problem is due to the fact that your window controller is not actually the object that is the nibs file owner.
When you change the class in interface builder you are telling it what outlets and actions are available (which is why you are able to drag to the window outlet) but you are still responsible for passing in this object yourself.
In the case of a non-document based application, you will have a main method which calls NSApplicationMain. What this does is basically look up and load the window nib that is specified in your info.plist file and pass the current NSApplication instance to this nib as the files owner (so even though you changed the class type to NSWindowController, the object being passed in is actually of type NSApplication).
The easiest way to fix your problem is to get rid of your window controller for now (as it isn't actually doing anything yet).
You should implement the -windowDidBecomeMain: method in your app delegate. Then Ctrl+drag from your window to your appDelegate to set it as the delegate of the window to get your notifications.
Update
To answer your question regarding the WindowController beware of the following two issues:
You are creating your window controller variable (dwc) in your applicationDidFinishLaunching: method. This is released the moment you leave the method taking your window with it. Create an instance variable to hold onto the window controller instead.
Ensure that your second window nib has its file owner set to NSWindowController (or your window controller type) and that its window outlet is connected to the window in the nib file.
Your window should now display.

Not being able to edit NSTextField on NSPopover even though Editable behavior is set

I have an application, which open popover with NSTextField. The text field is not editable. Behavior for text field is set to Editable. I still can paste and copy text to this field but i can't edit it.
Anyone knows, what can be wrong?
Not sure if you still need the answer, but there may be some others still looking. I found a solution on apple developer forums. Quoting the original author:
The main problem is the way keyboard events works. Although the NSTextField (and all its superviews) receives keyboard events, it doesn't make any action. That happens because the view where the popover is atached, is in a window which can't become a key window. You can't access that window in any way, at least I couldn't. So the solution is override the method canBecomeKeyWindow for every NSWindow in our application using a category.
NSWindow+canBecomeKeyWindow.h
#interface NSWindow (canBecomeKeyWindow)
#end
NSWindow+canBecomeKeyWindow.m
#implementation NSWindow (canBecomeKeyWindow)
//This is to fix a bug with 10.7 where an NSPopover with a text field cannot be edited if its parent window won't become key
//The pragma statements disable the corresponding warning for overriding an already-implemented method
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation"
- (BOOL)canBecomeKeyWindow
{
return YES;
}
#pragma clang diagnostic pop
#end
That makes the popover fully resposive. If you need another window which must respond NO to canBecomeKeyWindow, you can always make a subclass.
I struggled with this for a while as well, until I realized it was a bug.
However, instead of relying on an isActive state of a NSStatusItem view, I find it much more reliable to use the isShown property of the NSPopover you have implemented.
In my code, I have a NSPopover in a NSViewController:
- (BOOL)canBecomeKeyWindow
{
if([self class]==NSClassFromString(#"NSStatusBarWindow"))
{
NSPopover *mainPopover = [[((AppDelegate*)[NSApp delegate]) mainViewController] mainPopover];
if(![mainPopover isShown])
return NO;
}
return YES;
}
Balazs Toth's answer works, but if you're attaching the popover to NSStatusItem.view the status item becomes unresponsive - requiring two clicks to focus.
What i found when working with this solution is that when NSStatusItem becomes unresponsive, you can easily override this behavior like this
- (BOOL)canBecomeKeyWindow {
if([self class]==NSClassFromString(#"NSStatusBarWindow")) {
CBStatusBarView* view = [((CBAppDelegate*)[NSApp delegate]) statusItemView];
if(![view isActive]) return NO;
}
return YES;
}
You will check for the class of the window, if it matches the NSStatusBarWindow we can then check somehow if the NSStatusItem is active. If it is, that means we have to return YES, because this way the NSPopover from NSStatusItem will have all keyboard events.
What I'm using for checking if the NSStatusItem was clicked (or is active) is that in my own custom view i have a bool value which changes when user clicks on the NSStatusItem, system automatically checks for "canBecomeKeyWindow" and when it does it will return NO and after user clicks on it (while it is returning the NO) it will change the bool value and return YES when system asks again (when NSPopover is being clicked for NSTextField editing).
Sidenotes:
CBStatusBarView is my custom view for NSStatusItem
CBAppDelegate is my App Delegate class
If anyone is still looking for an answer to this, I am working in Swift.
At the time where you wish the field to allow text entry, I have used myTextField.becomeFirstReponder()
To opt out; just use myTextField.resignFirstResponder()
Definitely a bug. That bug report is exactly what I was trying to do. Even down to creating the status item and overriding mousdown.
I can confirm that Balazs Toth's answer works. I just wonder if it might get in the way down the road.
If someone gets it and the solution above didn't do the trick for him.
The problem in my app was in the info tab in the targets my application was set to
Application is background only = true
and shulde of been
Application is agent = true
Spent an entire day on this thing.
Bug. http://openradar.appspot.com/9722231

Resources