I'm learning how to build programs with Cocoa. I'm using a sample Apple application that records video from a webcam. I'd like to start and stop the video by capturing the key press. I've tried to override the keydown event but I've read that it's not possible in an NSObject. How can I handle this kind of event?
The class of application extends a NSObject class.
This is the code:
- (void)keyDown:(NSEvent *)event {
NSLog(#"Hi there");
NSString *characters = [event characters];
if ([characters length]) {
switch ([characters characterAtIndex:0]) {
case NSUpArrowFunctionKey:
NSLog(#"Key UP");
break;
}
}
}
I've tried to override Keydown event but I've read that It's not possible in an NSObject.
Correct. Only a responder can respond to events.
How can I handle this kind of event?
Implement a responder. Subclassing NSWindow or NSWindowController will work. Make sure you make your actual window or window controller an instance of your subclass.
The Cocoa documentation explains further.
The class of application extends a NSObject class.
Why? Normally, the principal class of the application bundle is NSApplication or a subclass of that—and there aren't many good reasons to subclass NSApplication.
PS: What's a very good book to start learn MacOS Programming?
I didn't learn by the Hillegass book, myself (I stuck to Apple's docs), but it's a very popular recommendation and I have read it and can tell you it's good.
From the Cocoa Event-Handling Guide - The Responder Chain:
The responder chain is a linked series of responder objects to which an event or action message is applied. When a given responder object doesn’t handle a particular message, the object passes the message to its successor in the chain (that is, its next responder).
When you press a key the window receives the keyDown event. Then it dispatches the event to the first responder, that usually is the control with a blue bezel around its border (try to click on the address field in Safari or Firefox, when it's blue-bezeled then it has first-responder status).
If the first responder does not eat the keypress (the Safari address field does eat it when it displays a character) then it passes it down the responder chain to the next responder in the view hierarchy, then to the window and to the window controller as you can see in the Guide. (Take care that the action responder is another story.)
So you have to implement the keyDown: on a view of your window or in the window itself, if it has no views that eat events. The simplest way to test is to override the keyDown: method of an empty window
To put your hands into the inner workings you can even try overriding the sendEvent: method of a window. sendEvent: dispatches the events to the views of the window, and from there you can for example log all the events managed by the window.
Subclassing NSWindow or
NSWindowController will work.
Similarly, you can subclass NSView and override its event-handling methods.
What's a very good book to start learn
MacOS Programming?
Learn Objective-C on the Mac by Dalrymple is really straightforward, covers enough basics and moves fast enough to get you off the ground in a short bit. It touches on everything from Xcode and Interface Builder to OOP and Objective-C practices. Particularly helpful to beginners (IMHO) are the source file organization and Foundation kit chapters.
Best of luck!
Related
The setEnabled: method is not working in my subclass of NSButton in which I have overridden mouse{down,drag,up} and rightMouse{down,drag,up}. I feel the enabling/disabling of the button should be outside these functions -- the button should not be receiving mouse events in the first place when it is disabled.
Do I have to make a check explicitly when I am overriding these functions?
I think that you do need to do your own check.
As explained in Apple's writeup on Cocoa Event Architechture, the window containing your button is sending mouseDown: (or whichever other method is appropriate) to your button in response to recieving an event. In order for the window to decide not to send the message, it would have to first determine that the button is an NSControl subclass (enabled being a property of NSControl, but not NSView) and then check that enabled flag. That is beyond the window's area of responsibility. A control being enabled isn't part of the event dispatch system the way first responder status is.
As an interesting piece of insight, if you take a look at GNUStep's -[NSControl mouseDown:] implementation, they do indeed check [self isEnabled] before handling the event.
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.
I want to set key equivalent " "(space) without any modifiers for NSMenuItem (in App Main Menu).
As follows from documentation:
For example, in an application that plays media, the Play command may be mapped to just “ ” (space), without the command key. You can do this with the following code:
[menuItem setKeyEquivalent:#" "];
[menuItem setKeyEquivalentModifierMask:0];
Key Equivalent sets successfully, but it don't work. When I press "Space" key without modifiers nothing happens, but it's works when i press "Space" with "Fn" modifier key.
I need to use "Space" without modifiers. Any help please!
This is a tricky question. Like many answers suggest, intercepting the event at the application or window level is a solid way to force the menu item to work. At the same time it is likely to break other things, for example, if you have a focused NSTextField or NSButton you'd want them to consume the event, not the menu item. This might also fail if the user redefines the key equivalent for that menu item in system preferences, i.e., changes Space to P.
The fact that you're using the space key equivalent with the menu item makes things even trickier. Space is one of the special UI event characters, along with the arrow keys and a few others, that the AppKit treats differently and in certain cases will consume before it propagates up to the main menu.
So, there are two things to keep in mind. First, is the standard responder chain:
NSApplication.sendEvent sends event to the key window.
Key window receives the event in NSWindow.sendEvent, determines if it is a key event and invokes performKeyEquivalent on self.
performKeyEquivalent sends it to the current window's firstResponder.
If the responder doesn't consume it, the event gets recursively sent upwards to the nextResponder.
performKeyEquivalent returns true if one of the responders consumes the event, false otherwise.
Now, the second and tricky part, if the event doesn't get consumed (that is when performKeyEquivalent returns false) the window will try to process it as a special keyboard UI event – this is briefly mentioned in Cocoa Event Handling Guide:
The Cocoa event-dispatch architecture treats certain key events as commands to move control focus to a different user-interface object in a window, to simulate a mouse click on an object, to dismiss modal windows, and to make selections in objects that allow selections. This capability is called keyboard interface control. Most of the user-interface objects involved in keyboard interface control are NSControl objects, but objects that aren’t controls can participate as well.
The way this part works is pretty straightforward:
The window converts the key event in a corresponding action (selector).
It checks with the first responder if it respondsToSelector and invokes it.
If the action was invoked the event gets treated as consumed and the event propagation stops.
So, with all that in mind, you must ensure two things:
The responder chain is correctly set up.
Responders consumes only what they need and propagate events otherwise.
The first point rarely gives troubles. The second one, and this is what happens in your example, needs taking care of – the AVPlayer would typically be the first responder and consume the space key event, as well as a few others. To make this work you need to override keyUp and keyDown methods to propagate the event up the responder chain as would happen in the default NSView implementation.
// All player keyboard gestures are disabled.
override func keyDown(with event: NSEvent) {
self.nextResponder?.keyDown(with: event)
}
// All player keyboard gestures are disabled.
override func keyUp(with event: NSEvent) {
self.nextResponder?.keyUp(with: event)
}
The above forwards the event up the responder chain and it will eventually be received by main menu. There's one gotcha, if first responder is a control, like NSButton or any custom NSControl-inheriting object, it WILL consume the event. Typically you do want this to happen, but if not, for example when implementing custom controls, you can override respondsToSelector:
override func responds(to selector: Selector!) -> Bool {
if selector == #selector(performClick(_:)) { return false }
return super.responds(to: selector)
}
This will prevent the window from consuming the keyboard UI event, so the main menu can receive it instead. However, if you want to intercept ALL keyboard UI events, including when the first responder is able to consume it, you do want to override your window's or application's performKeyEquivalent, but without duplicating it as other answers suggest:
override func performKeyEquivalent(with event: NSEvent) -> Bool {
// Attempt to perform the key equivalent on the main menu first.
if NSApplication.shared.mainMenu?.performKeyEquivalent(with: event) == true { return true }
// Continue with the standard implementation if it doesn't succeed.
return super.performKeyEquivalent(with: event)
}
If you invoke performKeyEquivalent on the main menu without checking for result you might end up invoking it twice – first, manually, and second, automatically from the super implementation, if the event doesn't get consumed by the responder chain. This would be the case when AVPlayer is the first responder and keyDown and keyUp methods not overwritten.
P.S. Snippets are Swift 4, but the idea is the same! ✌️
P.P.S. There's a brilliant WWDC 2010 Session 145 – Key Event Handling in Cocoa Applications that covers this subject in depth with excellent examples. WWDC 2010-11 is no longer listed on Apple Developer Portal but the full session list can be found here.
I had the same problem. I haven't investigated very hard, but as far as I can tell, the spacebar doesn't "look" like a keyboard shortcut to Cocoa so it gets routed to -insertText:. My solution was to subclass the NSWindow, catch it as it goes up the responder chain (presumably you could subclass NSApp instead), and send it off to the menu system explicitly:
- (void)insertText:(id)insertString
{
if ([insertString isEqual:#" "]) {
NSEvent *fakeEvent = [NSEvent keyEventWithType:NSKeyDown
location:[self mouseLocationOutsideOfEventStream]
modifierFlags:0
timestamp:[[NSProcessInfo processInfo] systemUptime]
windowNumber:self.windowNumber
context:[NSGraphicsContext currentContext]
characters:#" "
charactersIgnoringModifiers:#" "
isARepeat:NO
keyCode:49];
[[NSApp mainMenu] performKeyEquivalent:fakeEvent];
} else {
[super insertText:insertString];
}
}
I have just been experiencing the same problem with a twist...
The spacebar key equivalent works fine in my app while the NSMenuItem's linked IBAction is located in the App Delegate.
If I move the IBAction into a dedicated controller it fails. All other menu item key equivalents continue to work but the spacebar does not respond (it is ok with a modifier key, but unmodified #" " will not work).
I have tried various workarounds, like linking directly to the controller vs. linking via the responder chain, to no avail. I tried the code way:
[menuItem setKeyEquivalent:#" "];
[menuItem setKeyEquivalentModifierMask:0];
and the Interface Builder way, the behaviour is the same
I have tried subclassing NSWindow, as per Justin's answer, but so far have failed to get that to work.
So for now I have surrendered and relocated this one IBAction to the App Delegate where it works. I don't regard this as a solution, just making do... perhaps it's a bug, or (more likely) I just don't understand event messaging and the responder chain well enough.
Up this post because i need to use space too but no of those solutions work for me.
So, I subclass NSApplication and use the sendEvent: selector with the justin k solution :
- (void)sendEvent:(NSEvent *)anEvent
{
[super sendEvent:anEvent];
switch ([anEvent type]) {
case NSKeyDown:
if (([anEvent keyCode] == 49) && (![anEvent isARepeat])) {
NSPoint pt; pt.x = pt.y = 0;
NSEvent *fakeEvent = [NSEvent keyEventWithType:NSKeyDown
location:pt
modifierFlags:0
timestamp:[[NSProcessInfo processInfo] systemUptime]
windowNumber: 0 // self.windowNumber
context:[NSGraphicsContext currentContext]
characters:#" "
charactersIgnoringModifiers:#" "
isARepeat:NO
keyCode:49];
[[NSApp mainMenu] performKeyEquivalent:fakeEvent];
}
break;
default:
break;
}
}
Hope it will help
Quick Swift 4-5 method:
In view controller:
// Capture space and call main menu
override func keyDown(with event: NSEvent) {
if event.keyCode == 49 && !event.isARepeat{
NSApp.mainMenu?.performKeyEquivalent(with: event)
}
super.keyDown(with: event)
}
So I'm building a program that features the use of the IKImageBrowserView component as a subview in an NSWindow. As a side note, I have a controller object called ImageBrowserController which subclasses NSWindow and is set as the delegate of the NSWindow object of my app.
I have sent IKImageBrowserView the message setCanControlQuickLookPanel:YES to enable it to automatically use the QuickLook functionality to preview image files when the IKImageBrowserView is a first responder to receive key events. Then it took me a while to figure out how to make the IKImageBrowserView a first responder which I finally got working by overriding acceptsFirstResponder inside my ImageBrowserController.
Now I understand that as the delegate to the NSWindow, ImageBrowserController has a place in the responder chain after the event gets triggered on NSWindow. And I understand that as a subview of NSWindow, IKImageBrowserView is in line to be passed events for event handling. What I don't get is where the connection is between the ImageBrowserController being a first responder and the event somehow making it to the IKImageBrowserView. I didn't set NSWindow or IKImageBrowserView as first responders explicitly. So why isn't it necessary for me to implement event handling inside my ImageBrowserController?
EDIT: So after reading the accepted answer and going back to my code I tried removing the acceptsFirstResponder override in my ImageBrowserController and the QuickLook functionality still triggered just like the accepted answer said it would. Commenting out the setCanControlQuickLookPanel:YES made the app beep at me when I tried to invoke QuickLook functionality via the spacebar. I'm getting the feeling that my troubles were caused by user error of XCode in hitting the RUN button instead of the BUILD button after making changes to my code (sigh).
Some of what you are saying regarding the interactions between your objects does not make sense, and it is hard to address your stated question without some background.
As you say, your window delegate has a place at the end of the responder chain, after the window itself. The key point I think you are missing is that GUI elements, such as your IKImageBrowserView, will be at the beginning of the chain, and any one of them in a given window could be the current firstResponder.
When your application gets an event, it passes it off to the key window (which is just the window which currently accepts "key" (i.e., "keystroke") events). That window begins by asking its firstResponder to handle the event. If that object refuses, it passes the event to its own nextResponder, usually its superview, which either handles it or passes it on, until the event has either been handled or passed all the way up to the window object itself. Only then will the window (if it does not handle the event itself) ask its delegate to handle the event.
This means that the connection between the window delegate and the IKImageBrowserView is only through the Responder Chain, and its nature is simply that if the view declines to handle any given event, the delegate may eventually be asked to handle it, if no other object in between them handles it first.
Your window delegate does not need to be a firstResponder. Nor does overriding acceptsFirstResponder on the window delegate have any effect on one of the window's subviews.*
Your window delegate also does not need to (and, indeed should not) be a subclass of NSWindow. All it needs is to be a subclass of NSObject which implements whatever methods from the NSWindowDelegate Protocol you are interested in, and methods to handle any events you might want to catch if they are not handled by other objects.
So, the answer to your explicit question at the end is (and I do not mean this sarcastically): you only need to implement event handling in your window delegate if you want it to handle events itself.
*: IKImageBrowserView already responds YES to acceptsFirstResponder. If there are no other subviews in your window, it will automatically be the firstResponder when your application starts. You can set this initialFirstResponder explicitly in Interface Builder by connecting that outlet on the window to whatever object you want.
I have an document-based Cocoa application with a TextView and I would like to capture clicks on it, so I'm trying to intercept Window events like mouseDown, mouseUp, etc. then relate them to my TextView.
I've tried a two things:
1.) I made the TextView the initial first responder for the Window of my document, and overrode the mouseDown event on my document class, but it's not hitting.
2.) I subclassed NSWindow and override mouseDown, and set that subclass to my Window's class in my document xib. That event didn't hit either.
I noticed that the Window's delegate is already set to my File's Owner which is my NSDocument subclass. Why don't the events fire on my NSDocument if my document subclass is the delegate for my Window?
It's not clear why you would expect NSDocument to handle -mouseDown: events for a view in a window. NSDocument doesn't respond to -mouseDown:. NSTextView (as its name suggests) is a subclass of NSView, which is a subclass of NSResponder, which does respond to -mouseDown:.
You should give the Cocoa Event-Handling Guide a good read.
It's also not clear why you want to handle the events and pass them on to views yourself. Cocoa takes care of all of this stuff for you and will likely do a far better job of it. You should clarify your overall goal (as in "why do you want to intercept clicks and forward them to views yourself?") - there may be a far better (and likely easier) way to accomplish it.