- (void) keyDown: (NSEvent *) event does not work - cocoa

Below is the sample code.
- (void) keyDown: (NSEvent *) event
{
NSString *chars = [event characters];
unichar character = [chars characterAtIndex: 0];
if (character == 27) {
NSLog (#"ESCAPE!");
}
}
Should I need to set any delegate in InterfaceBuilder or any kinda binding??
Help Appreciated...

keyDown needs to be overridden in an NSView subclass whose object is set as first responder. The responder chain should pass down the event, but to be sure you get it, make sure that your object is first responder.

In cocoa only views participate in responder chain for this event. So you should override a some view method. The easy way is to find out what view is first responder for particular event you want to handle and use it.
window sends keyDown(with: ) stright to first responder which could handle it or pass up to responder chain. Not all views pass the events up. NSCollectionView doesn't pass the key event. It plays a bump sound instead.
It is also possible that a key you want to handle is a Key equivalent read more here. If so you should override performKeyEquivalent(with: ) method to receive this type of events instead. This events unlike keyDown events passed down from the window to the all subviews until someone handle them.
As mentioned NSCollectionView keyDown(with: ) method do not pass the key events up the responder chain. To handle such events in one of it's super views you should override it in collection view first and send event manually by calling self.nextResponder?.keyDown(with: event) for such events that you want to handle by yourself.

Related

How to make a custom NSView within a NSMenuItem becomeFirstResponder?

I have a custom NSView within a NSMenuItem (attached to a MenuBar) that respond to a mouseDown event. But I need to click twice on the custom view for the mouseDown function to be called, this is because the custom view should be first responder. And when I override the method acceptsFirstResponder in my CustomView Controller as indicated by the Cocoa Event Handling Guide, it does not work. What is the solution? Is it doable?
Override the NSView method acceptsFirstMouse: to return YES for the event in question. If you only want to accept the first mouse click for some types of events, you can do that by examining the event parameter passed in. Unless there is something special about the NSMenuItem case in particular, this should be what you want; it's the standard Cocoa mechanism for this. Note that this method is not the same as the acceptsFirstResponder method you have tried. See Apple's doc for details.
For reference I have just added to my custom view the following:
- (BOOL)acceptsFirstMouse:(NSEvent *)theEvent {
return YES;
}

Cocoa: Forward actions (copy:, paste: etc.) up to the responder chain

I have a subclass of NSOutlineView that implements copy:, paste:, cut: etc.
Also, the NSDocument subclass implements these same methods.
When the outline view is in the responder chain (is first responder or a parent view of it), all copy/paste events are intercepted by the NSOutlineView subclass. What I want, is depending on the context catch some of these messages, or let them propagate and be caught by the NSDocument subclass.
What I want is basically:
- (void)copy:(id)sender
{
// If copy paste is enabled
if ([self isCopyPasteEnabled]) {
[[NSPasteboard generalPasteboard] clearContents];
[[NSPasteboard generalPasteboard] writeObjects:self.selectedItems];
return;
}
// If copy paste is disabled
// ... forward copy: message to the next responder,
// up to the NSDocument or whatever
}
I've already tried many tricks, none was successful:
[[self nextResponder] copy:sender] that doesn't work because the next responder may not implement copy:
[super copy:sender] same here, super doesn't implement copy:
[NSApp sendAction:anAction to:nil from:sender] this is nice to send an action to the first responder. If used inside an action
Sure I could manually loop on the responder chain until I find something that responds to copy: or even directly call copy: on the current document, but I'm looking for the right way of doing it.
Many thanks in advance!
This should work:
[[self nextResponder] tryToPerform:_cmd with:sender];
There's a problem, though: the presence of a responder in the responder chain which implements -copy: will, in and of itself, cause the Copy menu item to be enabled even if it wouldn't otherwise be if your object weren't in the chain or didn't implement -copy:. Your object could disable that item using -validateMenuItem: or -validateUserInterfaceItem:, but it will be nontrivial to enable if and only if there's another potential target up the chain and that target would enable the item.
A different approach is to just make the search for a responder that implements the action method skip your outline view if you disable pasteboard support. Override -respondsToSelector:. If the selector is one of the pasteboard operations and your pasteboard support is disabled, return false even though your class really does implement it. That is, lie and claim your object just doesn't respond to those selectors. For any other selector, or if your pasteboard support is on, call through to super and return what it returns.

Best place to intercept Cmd-Key in NSDocument based app

What would be the most appropriate place to intercept CmdAnyKey key events in an NSDocument based application?
Intention is to switch to some component in the active window - kind of like Firefox allows you to switch the tabs - without having a matching shortcut on a menu command to perform that task.
I.e. ideally the framework should do it's normal processing including handling menu commands and just after all other responders fail to respond to that particular shortcut it should be routed to the custom method.
I've searched NSDocument, NSAppDelegate, NSWindowController but couldn't locate any appropriate mechanism to hook into in order to receive these commands on window level.
So lacking any existing customization mechanism does override keyDown: in a custom NSWindowController look like the most appropriate way to achieve the desired effect?
Yes, subclassing NSWindow is the way to do this if you need to get the keyboard event after everything up the responder chain refused to handle it.
Here's how I did it in one of my projects:
- (void)keyDown:(NSEvent*)event
{
SEL keyDownBool = #selector(keyDownBool:);
if ([[self delegate] respondsToSelector:keyDownBool]
&& [[self delegate] performSelector:keyDownBool withObject:event])
{
return;
}
[super keyDown:event];
}
My custom keyDownBool: delegate method returned YES if it handled particular key event. Otherwise this method passes key event down to super.
Now I'm using + (id)addLocalMonitorForEventsMatchingMask:(NSEventMask)mask handler:(NSEvent* (^)(NSEvent*))block instead of subclassing. The difference is that it handles (and optionally discards) events before they are dispatched.

How to catch Command+W key action in OS X?

I subclassed NSWindows and implemented the keyDown: and keyUp: method like this:
- (void)keyDown:(NSEvent*)event
{
NSLog(#"Down: '%#'", [event characters]);
[super keyDown:event];
}
- (void)keyUp:(NSEvent*)event
{
NSLog(#"Up: '%#'", [event characters]);
[super keyUp:event];
}
If I pressed "W" key, it prints both my "Down" and "Up" correctly. But If I pressed Command+W combination, It prints ONLY the "Down" message, and windows' close action was not triggered. How should I do?
Command-W is typically the key equivalent of a menu item. It is handled by the menu when it is told to -performKeyEquivalent:. The menu item sends its action method to its target. The action method is typically -performClose: and the target is usually the first responder.
So, the normal processing of Command-W does not use the window's -keyDown: or -keyUp: at all.
If you want to control whether the window closes, have the window delegate implement -windowShouldClose:. If you want to be notified when the window is about to close, implement -windowWillClose: in the window delegate or observe the NSWindowWillCloseNotification notification.
Regarding keyboard handling, a quirk of -[NSApplication sendEvent:] is that it just doesn't dispatch any key up event if the Command key was down. If for some reason you really need to see the key up event, you'll have to implement a custom subclass of NSApplication, configure your Info.plist to make sure it's used, and implement an override of -sendEvent:.

NSMenuItem KeyEquivalent " "(space) bug

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

Resources