Allow views in childWindow to become key without losing focus on parentWindow - cocoa

I added a childWindow of a custom subclass of NSWindow to a parentWindow (also a custom subclass of NSWindow). The childWindow has the NSBorderlessWindowMask and canBecomeKeyWindow: is overridden to return YES and canBecomeMainWindow: to return NO.
The childWindow is set to resize with the parentWindow. So I want to create the illusion that the views of the childWindow are part of the parentWindow. The main idea is to arrange the document windows created by the document-based application within a main window to provide a tabbed interface (just like in a browser) to switch between the documents.
My problem is that whenever I click in one of the views of the childWindow, the parentWindow (the main window) looses focus and the traffic light buttons are getting greyed out. This is obviously contrary to what I want to achieve.
I found this answer:
Make NSView in NSPanel first responder without key window status
But even if I override isKeyWindow: (of the main window) to always return YES, the title bar gets greyed out nonetheless when I click into the childWindow.
I also tried to follow this advice:
http://www.cocoabuilder.com/archive/cocoa/143945-non-focused-child-window.html
But I'm not sure what "include the child window in its responder chain just ahead of its nextResponder" means. With canBecomeKeyWindow: to return NO (for the childWindow), the views within the child never can become key and are always greyed out.
Any clue what I am doing wrong?
One addition: Is it possible to make the views in the childWindow FirstResponder without giving the childWindow key-status?

I got this working by mimicking the behaviour of NSPopover. On investigation I found that a popover (which uses a private NSPanel subclass "_NSPopoverWindow") believed it was the key & main window, even though it is not the window returned from [NSApp keyWindow].
Create your own custom NSPanel subclass, attach it to the parent window, and then override the following methods as so:
- (BOOL)isKeyWindow {
return YES;
}
- (BOOL)isMainWindow {
return YES;
}
- (BOOL)canBecomeKeyWindow {
return YES;
}
- (BOOL)canBecomeMainWindow {
return YES;
}
- (void)makeKeyWindow {
[super makeKeyWindow];
[self.parentWindow makeKeyWindow];
}
- (void)makeMainWindow {
[super makeMainWindow];
[self.parentWindow makeMainWindow];
}
- (void)becomeKeyWindow {
[super becomeKeyWindow];
}
- (void)becomeMainWindow {
[super becomeMainWindow];
[self.parentWindow becomeMainWindow];
}
- (void)resignMainWindow {
}
- (void)resignKeyWindow {
}

Related

How to stop NSPanel from stealing input of another app

I have an NSPanel. It's purpose is to sit on top of everything in all spaces and it does that just fine.
I don't want it to ever become firstResponder and take focus from another app.
I sub classed NSPanel like so
class SomePanelClass: NSPanel {
override var acceptsFirstResponder: Bool{
return false
}
}
When the window shows or I drag it around the app I was in still appears to have focus and it's name is in the menu but keystrokes do not register in that app until I click back into it's window.
Is there something else I can do to prevent my app and panel from stealing input?
You probably want to override canBecomeKeyWindow on your Panel subclass to return NO.
- (BOOL) canBecomeKeyWindow {
return NO;
}

NSTrackingArea not fully working when not key window

A bit of context first. Essentially I have a window that is covering the desktop. On it I have a few WebKit WebView views which allow user interaction. By deafult, as one would expect, when another application is active it does not receive these events (such as hovering, mouse entered, and clicking). I can make it work by clicking my window first, then moving the mouse, but this is not good for usability. I've also managed to make it activate the window when the cursor enters, but it's far from ideal and rather hacky.
So instead I'm trying to use a tracking area. At the moment on the WebViews superview I have this tracking area:
NSTrackingArea *trackingArea = [[NSTrackingArea alloc] initWithRect:[self visibleRect]
options:NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingInVisibleRect | NSTrackingActiveAlways
owner:self
userInfo:nil];
This works as I want it to, I'm receiving the all the mouse events. However, the WebViews don't seem to be responding as intended. JavaScript mouse move events only fire when I hold and drag, not just hover and drag.
I've tried using hitTest to get the correct view, but nothing seems to work. Here's an example method, I'm using the isHandlingMouse boolean because without it an infinite loop seemed to be created for some reason:
- (NSView *)handleTrackedMouseEvent: (NSEvent *)theEvent{
if(isHandlingMouse)
return nil;
isHandlingMouse = true;
NSView *hit = [self hitTest: theEvent.locationInWindow];
if (hit && hit != self) {
return hit;
}
return nil;
}
- (void)mouseMoved:(NSEvent *)theEvent{
NSView *hit = [self handleTrackedMouseEvent: theEvent];
if (hit){
[hit mouseMoved: theEvent];
}
isHandlingMouse = false;
}
The 'hit' view, is a WebHTMLView, which appears to be a private class. Everything seems like it should be working,but perhaps there's something I'm doing that's breaking it, or I'm sending the event to the WebHTMLView incorrectly.
Post a sample Xcode project to make it easier for people to test solutions to this problem.
I was doing something similar and it took a lot of trial and error to find a solution. You will likely need to subclass NSWindow and add - (BOOL)canBecomeKeyWindow { return YES; }, then whenever you detect the mouse is over your window, you might call [window orderFrontRegardless] just so it can properly capture the mouse events.

Cocoa Storyboard Responder Chain

Storyboards for Cocoa apps seems like a great solution as I prefer the methodology you find in iOS. However, while breaking things up into separate view controllers makes a lot of logical sense, I'm not clear as to how to pass window control (toolbar buttons) or menu interaction down to the view controllers that care. My app delegate is the first responder and it receives the the menu or toolbar actions, however, how can I access the view controller that I need to get that message to? Can you just drill down into the view controllers hierarchy. If so, how do you get there from the app delegate since it's the first responder? Can you make the window controller the first responder instead. If so, how? In the storyboard? Where?
Since this is a high level question it may not matter, however, I am using Swift for this project if you're wondering.
I'm not sure if there is a "proper" way to solve this, however, I have come up with a solution that I'll use for now. First a couple of details
My app is a document based application so each window has an instance of the document.
The document the app uses can act as the first responder and forward any actions I've connected
The document is able to get a hold of the top level window controller and from there I am able to drill down through the view controller hierarchy to get to the view controller I need.
So, in my windowDidLoad on the window controller, I do this:
override func windowDidLoad() {
super.windowDidLoad()
if self.contentViewController != nil {
var vc = self.contentViewController! as NSSplitViewController
var innerSplitView = vc.splitViewItems[0] as NSSplitViewItem
var innerSplitViewController = innerSplitView.viewController as NSSplitViewController
var layerCanvasSplitViewItem = innerSplitViewController.splitViewItems[1] as NSSplitViewItem
self.layerCanvasViewController = layerCanvasSplitViewItem.viewController as LayerCanvasViewController
}
}
Which gets me the view controller (which controls the view you see outlined in red below) and sets a local property in the window view controller.
So now, I can forward the toolbar button or menu item events directly in the document class which is in the responder chain and therefore receives the actions I setup in the menu and toolbar items. Like this:
class LayerDocument: NSDocument {
#IBAction func addLayer(sender:AnyObject) {
var windowController = self.windowControllers[0] as MainWindowController
windowController.layerCanvasViewController.addLayer()
}
// ... etc.
}
Since the LayerCanvasViewController was set as a property of the main window controller when it got loaded, I can just access it and call the methods I need.
For the action to find your view controllers, you need to implement -supplementalTargetForAction:sender: in your window and view controllers.
You could list all child controllers potentially interested in the action, or use a generic implementation:
- (id)supplementalTargetForAction:(SEL)action sender:(id)sender
{
id target = [super supplementalTargetForAction:action sender:sender];
if (target != nil) {
return target;
}
for (NSViewController *childViewController in self.childViewControllers) {
target = [NSApp targetForAction:action to:childViewController from:sender];
if (![target respondsToSelector:action]) {
target = [target supplementalTargetForAction:action sender:sender];
}
if ([target respondsToSelector:action]) {
return target;
}
}
return nil;
}
I had the same Storyboard problem but with a single window app with no Documents. It's a port of an iOS app, and my first OS X app. Here's my solution.
First add an IBAction as you did above in your LayerDocument. Now go to Interface Builder. You'll see that in the connections panel to First Responder in your WindowController, IB has now added a Sent Action of addLayer. Connect your toolBarItem to this. (If you look at First Responder connections for any other controller, it will have a Received Action of addLayer. I couldn't do anything with this. Whatever.)
Back to windowDidLoad. Add the following two lines.
// This is the top view that is shown by the window
NSView *contentView = self.window.contentView;
// This forces the responder chain to start in the content view
// instead of simply going up to the chain to the AppDelegate.
[self.window makeFirstResponder: contentView];
That should do it. Now when you click on the toolbarItem it will go directly to your action.
I've been struggling with this question myself.
I think the 'correct' answer is to lean on the responder chain. For example, to connect a tool bar item action, you can select the root window controller's first responder. And then show the attributes inspector. In the attributes inspector, add your custom action (see photo).
Then connect your toolbar item to that action. (Control drag from your Toolbar item to the first responder and select the action you just added.)
Finally, you can then go to the ViewController (+ 10.10) or other object, so long as its in the responder chain, where you want to receive this event and add the handler.
Alternatively, instead of defining the action in the attributes inspector. You can simply write your IBAction in your ViewController. Then, go to the toolbar item, and control drag to the window controller's first responder -- and select the IBAction you just added. The event will then travel thru the responder chain until received by the view controller.
I think this is the correct way to do this without introducing any additional coupling between your controllers and/or manually forwarding the call.
The only challenge I've run into -- being new to Mac dev myself -- is sometimes the Toolbar item disabled itself after receiving the first event. So, while I think this is the correct approach, there are still some issues I've run into myself.
But I am able to receive the event in another location without any additional coupling or gymnastics.
As i'm a very lazy person i came up with the following solution based on Pierre Bernard
's version
#include <objc/runtime.h>
//-----------------------------------------------------------------------------------------------------------
IMP classSwizzleMethod(Class cls, Method method, IMP newImp)
{
auto methodReplacer = class_replaceMethod;
auto methodSetter = method_setImplementation;
IMP originalImpl = methodReplacer(cls, method_getName(method), newImp, method_getTypeEncoding(method));
if (originalImpl == nil)
originalImpl = methodSetter(method, newImp);
return originalImpl;
}
// ----------------------------------------------------------------------------
#interface NSResponder (Utils)
#end
//------------------------------------------------------------------------------
#implementation NSResponder (Utils)
//------------------------------------------------------------------------------
static IMP originalSupplementalTargetForActionSender;
//------------------------------------------------------------------------------
static id newSupplementalTargetForActionSenderImp(id self, SEL _cmd, SEL action, id sender)
{
assert([NSStringFromSelector(_cmd) isEqualToString:#"supplementalTargetForAction:sender:"]);
if ([self isKindOfClass:[NSWindowController class]] || [self isKindOfClass:[NSViewController class]]) {
id target = ((id(*)(id, SEL, SEL, id)) originalSupplementalTargetForActionSender)(self, _cmd, action, sender);
if (target != nil)
return target;
id childViewControllers = nil;
if ([self isKindOfClass:[NSWindowController class]])
childViewControllers = [[(NSWindowController*) self contentViewController] childViewControllers];
if ([self isKindOfClass:[NSViewController class]])
childViewControllers = [(NSViewController*) self childViewControllers];
for (NSViewController *childViewController in childViewControllers) {
target = [NSApp targetForAction:action to:childViewController from:sender];
if (NO == [target respondsToSelector:action])
target = [target supplementalTargetForAction:action sender:sender];
if ([target respondsToSelector:action])
return target;
}
}
return nil;
}
// ----------------------------------------------------------------------------
+ (void) load
{
Method m = nil;
m = class_getInstanceMethod([NSResponder class], NSSelectorFromString(#"supplementalTargetForAction:sender:"));
originalSupplementalTargetForActionSender = classSwizzleMethod([self class], m, (IMP)newSupplementalTargetForActionSenderImp);
}
// ----------------------------------------------------------------------------
#end
//------------------------------------------------------------------------------
This way you do not have to add the forwarder code to the window controller and all the viewcontrollers (although subclassing would make that a bit easier), the magic happens automatically if you have a viewcontroller for the window contentview.
Swizzling always a bit dangerous so it is far not a perfect solution, but I've tried it with a very complex view/viewcontroller hierarchy that using container views, worked fine.

OS X - How can a NSViewController find its window?

I have a Document based core data app. The main document window has a number of views, each controlled by its own custom NSViewController which are switched in as necessary. I want each of these view controllers to be able to drop down a custom modal sheet from the document window. However because the views are separate and not in the MyDocument nib I cannot link the view to the document window in IB. This means that when I call
[NSApp beginSheet: sheetWindow modalForWindow: mainWindow modalDelegate: self didEndSelector: #selector(didEndSheet:returnCode:contextInfo:) contextInfo: nil];
I’m supplying nil for mainWindow and the sheet therefore appears detached.
Any suggestions?
Many Thanks
You can use [[self view] window]
Indeed, it's self.view.window (Swift).
This may be nil in viewDidLoad() and viewWillAppear(), but is set properly by the time you get to viewDidAppear().
One issue with the other answers (i.e., just looking at self.view.window) is that they don't take into account the case that when a view is hidden, its window property will be nil. A view might be hidden for a lot of reasons (for example, it might be in one of the unselected views in a tab view).
The following (swift) extension will provide the windowController for a NSViewController by ascending the view controller hierarchy, from which the window property may then be examined:
public extension NSViewController {
/// Returns the window controller associated with this view controller
var windowController: NSWindowController? {
return ((self.isViewLoaded == false ? nil : self.view)?.window?.windowController)
?? self.parent?.windowController // fallback to the parent; hidden views like those in NSTabView don't have a window
}
}
If your controller can get access to the NSDocument subclass, you can use -windowForSheet
more about Tim Closs answer :
-(void)viewDidAppear
{
self.view.window.title = #"title-viewDidAppear"; //this only works when and after viewDidAppeer is called
}
-(void)viewWillDisappear
{
self.view.window.title = #"title-viewWillDisappear"; //this only works before and when viewWillDisappear is called
}

Custom NSView in NSMenuItem not receiving mouse events

I have an NSMenu popping out of an NSStatusItem using popUpStatusItemMenu. These NSMenuItems show a bunch of different links, and each one is connected with setAction: to the openLink: method of a target. This arrangement has been working fine for a long time. The user chooses a link from the menu and the openLink: method then deals with it.
Unfortunately, I recently decided to experiment with using NSMenuItem's setView: method to provide a nicer/slicker interface. Basically, I just stopped setting the title, created the NSMenuItem, and then used setView: to display a custom view. This works perfectly, the menu items look great and my custom view is displayed.
However, when the user chooses a menu item and releases the mouse, the action no longer works (i.e., openLink: isn't called). If I just simply comment out the setView: call, then the actions work again (of course, the menu items are blank, but the action is executed properly). My first question, then, is why setting a view breaks the NSMenuItem's action.
No problem, I thought, I'll fix it by detecting the mouseUp event in my custom view and calling my action method from there. I added this method to my custom view:
- (void)mouseUp:(NSEvent *)theEvent {
NSLog(#"in mouseUp");
}
No dice! This method is never called.
I can set tracking rects and receive mouseEntered: events, though. I put a few tests in my mouseEntered routine, as follows:
if ([[self window] ignoresMouseEvents]) { NSLog(#"ignoring mouse events"); }
else { NSLog(#"not ignoring mouse events"); }
if ([[self window] canBecomeKeyWindow]) { dNSLog((#"canBecomeKeyWindow")); }
else { NSLog(#"not canBecomeKeyWindow"); }
if ([[self window] isKeyWindow]) { dNSLog((#"isKeyWindow")); }
else { NSLog(#"not isKeyWindow"); }
And got the following responses:
not ignoring mouse events
canBecomeKeyWindow
not isKeyWindow
Is this the problem? "not isKeyWindow"? Presumably this isn't good because Apple's docs say "If the user clicks a view that isn’t in the key window, by default the window is brought forward and made key, but the mouse event is not dispatched." But there must be a way do detect these events. HOW?
Adding:
[[self window] makeKeyWindow];
has no effect, despite the fact that canBecomeKeyWindow is YES.
Add this method to your custom NSView and it will work fine with mouse events
- (void)mouseUp:(NSEvent*) event {
NSMenuItem* mitem = [self enclosingMenuItem];
NSMenu* m = [mitem menu];
[m cancelTracking];
[m performActionForItemAtIndex: [m indexOfItem: mitem]];
}
But i'm having problems with keyhandling, if you solved this problem maybe you can go to my question and help me a little bit.
Add this to your custom view and you should be fine:
- (BOOL)acceptsFirstMouse:(NSEvent *)theEvent
{
return YES;
}
I added this method to my custom view, and now everything works beautifully:
- (void)viewDidMoveToWindow {
[[self window] becomeKeyWindow];
}
Hope this helps!
I've updated this version for SwiftUI Swift 5.3:
final class HostingView<Content: View>: NSHostingView<Content> {
override func viewDidMoveToWindow() {
window?.becomeKey()
}
}
And then use like so:
let item = NSMenuItem()
let contentView = ContentView()
item.view = HostingView(rootView: contentView)
let menu = NSMenu()
menu.items = [item]
So far, the only way to achieve the goal, is to register a tracking area manually in updateTrackingAreas - that is thankfully called, like this:
override func updateTrackingAreas() {
let trackingArea = NSTrackingArea(rect: bounds, options: [.enabledDuringMouseDrag, .mouseEnteredAndExited, .activeInActiveApp], owner: self, userInfo: nil)
addTrackingArea(trackingArea)
}
Recently I needed to show a Custom view for a NSStatusItem, show a regular NSMenu when clicking on it and supporting drag and drop operations on the Status icon.
I solved my problem using, mainly, three different sources that can be found in this question.
Hope it helps other people.
See the sample code from Apple named CustomMenus
In there you'll find a good example in the ImagePickerMenuItemView class.
It's not simple or trivial to make a view in a menu act like a normal NSMenuItem.
There are some real decisions and coding to do.

Resources