I'm a beginner in SpriteKit programming, and have been trying to figure out how to handle input from the keyboard.
What I've found so far is that you should subclass NSResponder and implement it like this:
#interface AppDelegate : NSResponder <NSApplicationDelegate>
-(void)keyUp:(NSEvent *)theEvent;
-(void)keyDown:(NSEvent *)theEvent;
#end
#implementation AppDelegate
-(void)keyUp:(NSEvent *)theEvent
{
NSLog(#"Key Released");
}
-(void)keyDown:(NSEvent *)theEvent
{
NSLog(#"Key Pressed");
}
#end
Obviously, there are a few more methods/properties in the interface and implementation of AppDelegate but I didn't put them there to keep the question relevant.
Next, I would start using key codes to detect which keys are being pressed, but the keyUp and keyDown methods don't even get called. I'm not sure why.
Any Help?
Update:
Thanks for your answers! I discovered that you have to implement keyUp and keyDown directly in your scene class, because they won't get called from the AppDelegate. Thanks again for the help!
The easiest way I know is to implement the keyDown method in your SKScene (and not directly in the AppDelegate). You don't have to subclass anything.
- (void)keyDown:(NSEvent *)event {
[self handleKeyEvent:event keyDown:YES];
}
- (void)keyUp:(NSEvent *)event {
[self handleKeyEvent:event keyDown:NO];
}
Then use the method handleKeyEvent:(NSEvent *)event keyDown:(BOOL)downOrUp to check which key has been pressed :
- (void)handleKeyEvent:(NSEvent *)event keyDown:(BOOL)downOrUp {
// First check the arrow keys since they are on the numeric keypad.
if ([event modifierFlags] & NSNumericPadKeyMask) { // arrow keys have this mask
NSString *theArrow = [event charactersIgnoringModifiers];
unichar keyChar = 0;
if ([theArrow length] == 1) {
keyChar = [theArrow characterAtIndex:0];
switch (keyChar) {
case NSUpArrowFunctionKey:
self.defaultPlayer.moveForward = downOrUp;
break;
case NSLeftArrowFunctionKey:
self.defaultPlayer.moveLeft = downOrUp;
break;
case NSRightArrowFunctionKey:
self.defaultPlayer.moveRight = downOrUp;
break;
case NSDownArrowFunctionKey:
self.defaultPlayer.moveBack = downOrUp;
break;
}
}
}
// Now check the rest of the keyboard
NSString *characters = [event characters];
for (int s = 0; s<[characters length]; s++) {
unichar character = [characters characterAtIndex:s];
switch (character) {
case 'w':
self.defaultPlayer.moveForward = downOrUp;
break;
case 'a':
self.defaultPlayer.moveLeft = downOrUp;
break;
case 'd':
self.defaultPlayer.moveRight = downOrUp;
break;
case 's':
self.defaultPlayer.moveBack = downOrUp;
break;
case ' ':
self.defaultPlayer.fireAction = downOrUp;
break;
}
}
}
I took this code from the Apple SpriteKit Adventure game. I found it very usefull to learn SpriteKit :)
Swift (2.0) version of HeyFara's answer. I've just popped "breaks" in where you would make your actual function calls.
public override func keyDown(theEvent: NSEvent) {
handleKeyEvent(theEvent, keyDown: true)
}
public override func keyUp(theEvent: NSEvent) {
handleKeyEvent(theEvent, keyDown: false)
}
public func handleKeyEvent(event:NSEvent, keyDown:Bool){
if event.modifierFlags.contains(NSEventModifierFlags.NumericPadKeyMask){
if let theArrow = event.charactersIgnoringModifiers, keyChar = theArrow.unicodeScalars.first?.value{
switch Int(keyChar){
case NSUpArrowFunctionKey:
break
case NSDownArrowFunctionKey:
break
case NSRightArrowFunctionKey:
break
case NSLeftArrowFunctionKey:
break
default:
break
}
}
} else {
if let characters = event.characters{
for character in characters.characters{
switch(character){
case "w":
break
default:
print(character)
}
}
}
}
}
Here is rougeExciter version of HeyFara's answer but in Swift 4.2... in case someone out there find this old post... as myself a few minutes ago :)...
public override func keyDown(with event: NSEvent) {
handleKeyEvent(event, keyDown: true)
}
public override func keyUp(with event: NSEvent) {
handleKeyEvent(event, keyDown: false)
}
public func handleKeyEvent(_ event: NSEvent, keyDown: Bool){
if event.modifierFlags.contains(NSEvent.ModifierFlags.numericPad) {
if let theArrow = event.charactersIgnoringModifiers, let keyChar = theArrow.unicodeScalars.first?.value{
switch Int(keyChar){
case NSUpArrowFunctionKey:
break
case NSDownArrowFunctionKey:
break
case NSRightArrowFunctionKey:
break
case NSLeftArrowFunctionKey:
break
default:
break
}
}
} else {
if let characters = event.characters{
for character in characters {
switch(character){
case "w":
break
default:
print(character)
}
}
}
}
}
If you want to know whenever a key is pressed, you should subscribe to the global event manager.
[NSEvent addGlobalMonitorForEventsMatchingMask:(NSKeyDownMask) handler:^(NSEvent *event) {
[NSEvent removeMonitor:event];
This will call the handler when ever a key is pressed.
Related
How can I do this in Swift?
- (void)handleLongPress:(UILongPressGestureRecognizer*)sender {
if (sender.state == UIGestureRecognizerStateEnded) {
NSLog(#"UIGestureRecognizerStateEnded");
//Do Whatever You want on End of Gesture
}
else if (sender.state == UIGestureRecognizerStateBegan){
NSLog(#"UIGestureRecognizerStateBegan.");
//Do Whatever You want on Began of Gesture
}
}
I don't know how to transform that sender declaration in Swift. Thanks!
It's essentially the same code -- a function that checks the value of the parameter's state property:
func handleLongPress(gestureRecognizer: UILongPressGestureRecognizer) {
switch gestureRecognizer.state {
case .Began:
println("UIGestureRecognizerState.Began")
case .Ended:
println("UIGestureRecognizerState.Ended")
default:
break;
}
}
func handleLongPress (sender: UILongPressGestureRecognizer){
if sender.state == UIGestureRecognizerStateEnded {
println(UIGestureRecognizerStateEnded)
}else if (sender.state == UIGestureRecognizerStateBegan){
println(UIGestureRecognizerStateBegan)
}
}
try this. I just translated what you have written in obj-c
I have been using a similar method to as this:
https://github.com/oscardelben/CocoaNavigationGestures
To capture two finger swipes on the Mac, under Yosemite it is no longer working. Anyone know what has change, or what I need to change for this to work.
The accepted answer didn't work well for me - it would often not detect the swipe. Instead, I overrode wantsScrollEventsForSwipeTrackingOnAxis:(NSEventGestureAxis)axis to return YES for the appropriate axis, then overrode scrollWheel:(NSEvent *)theEvent to detect scrolling. Works perfect every time.
The complete answer in Swift 5.3, based on #bmuller would be:
override func wantsScrollEventsForSwipeTracking(on axis: NSEvent.GestureAxis) -> Bool {
return axis == .horizontal
}
override func scrollWheel(with event: NSEvent) {
if event.scrollingDeltaX < 0 {
print("Go forward")
}
else {
print("Go back")
}
}
Multiple swipe events might be sent in a single gesture with this code. You may need to add phase(NSEventPhase) handling code to the scrollWheel(...) function e.g.
override func scrollWheel(with event: NSEvent) {
guard event.phase == .began else {
return
}
if event.scrollingDeltaX < 0 {
print("Go forward")
}
else {
print("Go back")
}
}
On your Mac, go to system preferences and you will find all the different settings of the trackpad.
From here: https://discussions.apple.com/thread/5710582
Hope I helped.
This was my solution, seems to be working for me.
#define kSwipeMinimumLength 0.2
- (void)touchesBeganWithEvent:(NSEvent *)event{
if(event.type == NSEventTypeGesture){
NSSet *touches = [event touchesMatchingPhase:NSTouchPhaseAny inView:self];
if(touches.count == 2){
self.twoFingersTouches = [[NSMutableDictionary alloc] init];
for (NSTouch *touch in touches) {
[self.twoFingersTouches setObject:touch forKey:touch.identity];
}
}
}
}
- (void)touchesMovedWithEvent:(NSEvent*)event {
NSSet *touches = [event touchesMatchingPhase:NSTouchPhaseEnded inView:self];
if(touches.count > 0){
NSMutableDictionary *beginTouches = [self.twoFingersTouches copy];
self.twoFingersTouches = nil;
NSMutableArray *magnitudes = [[NSMutableArray alloc] init];
for (NSTouch *touch in touches)
{
NSTouch *beginTouch = [beginTouches objectForKey:touch.identity];
if (!beginTouch) continue;
float magnitude = touch.normalizedPosition.x - beginTouch.normalizedPosition.x;
[magnitudes addObject:[NSNumber numberWithFloat:magnitude]];
}
float sum = 0;
for (NSNumber *magnitude in magnitudes)
sum += [magnitude floatValue];
// See if absolute sum is long enough to be considered a complete gesture
float absoluteSum = fabsf(sum);
if (absoluteSum < kSwipeMinimumLength) return;
// Handle the actual swipe
// This might need to be > (i am using flipped coordinates), you can use an else to go forward also.
if (sum > 0){
NSLog(#"go back");
}
}
}
I find it very hard to find anything official on this matter.
I have a TextView and override the keyDown event and try to detect if the user pressed Ctrl + Enter.
- (void)keyDown:(NSEvent *)theEvent
{
if([theEvent modifierFlags] & NSControlKeyMask && /* how to check if enter is pressed??? */)
{
NSLog(#"keyDown: ctrl+enter");
if(_delegate)
{
if([_delegate respondsToSelector:#selector(didSomething:)])
{
[_delegate performSelector:#selector(didSomething:) withObject:nil];
}
}
}else
{
[super keyDown:theEvent];
}
}
I have tried different things but nothing worked.
anyone?
(i'm sorry to ask such a trivial question but i have googled for a while now and haven't found anything)
unichar c = [theEvent charactersIgnoringModifiers] characterAtIndex:0];
if(([theEvent modifierFlags] & NSControlKeyMask) && (c == NSCarriageReturnCharacter || c == NSEnterCharacter) {
// do stuff
}
Alternatively, you can use the delegate's textView:doCommandBySelector::
- (BOOL)textView:(NSTextView *)aTextView doCommandBySelector:(SEL)aSelector {
// insertLineBreak: is the method sent when a user presses control-return in a text view
if (aSelector == #selector(insertLineBreak:)) {
// do stuff
return YES;
}
}
return NO;
}
Instead of overriding -keyDown:, you could override the keyboard action (insert a line break) that’s sent when ctrl-return is typed in a text view:
- (void)insertLineBreak:(id)sender {
NSLog(#"ctrl-return");
if(_delegate)
{
…
}
}
I want to use a wrapping text field that can potentially contain carriage returns in my app. Is there any way to force the NSTextField object to write a carriage return into the text area instead of sending its action to its target when the Return key is pressed?
This is covered in Technical Q&A QA1454, which also enumerates reasons why one would use NSTextField instead of NSTextView in this case.
You can implement the following method in the text field delegate:
- (BOOL)control:(NSControl*)control
textView:(NSTextView*)textView
doCommandBySelector:(SEL)commandSelector
{
BOOL result = NO;
if (commandSelector == #selector(insertNewline:))
{
// new line action:
// always insert a line-break character and don’t cause the receiver
// to end editing
[textView insertNewlineIgnoringFieldEditor:self];
result = YES;
}
return result;
}
Okay, I figured out one way to do it, but this very well may not be the best (or even a good) way. I subclassed NSTextField, and overrode -textShouldEndEditing: like so:
-(BOOL)textShouldEndEditing:(NSText *)textObject {
NSEvent * event = [[NSApplication sharedApplication] currentEvent];
if ([event type] == NSKeyDown && [event keyCode] == 36) {
[self setStringValue:[[self stringValue] stringByAppendingString:#"\n"]];
return NO;
}
else {
return [super textShouldEndEditing:textObject];
}
}
I found a combination of Sean and Bevarious worked best for me. Sean's answer assumes that the new line is always wanted to be added to the end (instead of for instance where the user's cursor is placed).
-(BOOL)textShouldEndEditing:(NSText *)textObject
{
NSEvent * event = [[NSApplication sharedApplication] currentEvent];
if ([event type] == NSKeyDown && [event keyCode] == 36)
{
[textObject insertNewlineIgnoringFieldEditor:nil];
return NO;
}
else
{
return [super textShouldEndEditing:textObject];
}
}
Swift version:
override func textShouldEndEditing(textObject: NSText) -> Bool {
let event = NSApplication.sharedApplication().currentEvent
if event?.type == NSEventType.KeyDown && event?.keyCode == 36 {
self.stringValue = self.stringValue.stringByAppendingString("\n")
return false
} else {
return super.textShouldEndEditing(textObject)
}
}
Is there a way to make my app respond to the play/pause button on Mac?
EDIT:
Using the suggested code,I get this console message:
Could not connect the action buttonPressed: to target of class NSApplication
Why would that be?
I accomplished this in my own application by subclassing NSApplication (and setting the app's principal class to this subclass). It catches seek and play/pause keys and translates them to specific actions in my app delegate.
Relevant lines:
#import <IOKit/hidsystem/ev_keymap.h>
- (void)sendEvent:(NSEvent *)event
{
// Catch media key events
if ([event type] == NSSystemDefined && [event subtype] == 8)
{
int keyCode = (([event data1] & 0xFFFF0000) >> 16);
int keyFlags = ([event data1] & 0x0000FFFF);
int keyState = (((keyFlags & 0xFF00) >> 8)) == 0xA;
// Process the media key event and return
[self mediaKeyEvent:keyCode state:keyState];
return;
}
// Continue on to super
[super sendEvent:event];
}
- (void)mediaKeyEvent:(int)key state:(BOOL)state
{
switch (key)
{
// Play pressed
case NX_KEYTYPE_PLAY:
if (state == NO)
[(TSAppController *)[self delegate] togglePlayPause:self];
break;
// Rewind
case NX_KEYTYPE_FAST:
if (state == YES)
[(TSAppController *)[self delegate] seekForward:self];
break;
// Previous
case NX_KEYTYPE_REWIND:
if (state == YES)
[(TSAppController *)[self delegate] seekBack:self];
break;
}
}
Edit:
Swift 4:
override func sendEvent(_ event: NSEvent)
{
if event.type == .systemDefined &&
event.subtype == .screenChanged
{
let keyCode : Int32 = (Int32((event.data1 & 0xFFFF0000) >> 16))
let keyFlags = (event.data1 & 0x0000FFFF)
let keyState = ((keyFlags & 0xFF00) >> 8) == 0xA
self.mediaKeyEvent(withKeyCode: keyCode, andState: keyState)
return
}
super.sendEvent(event)
}
private func mediaKeyEvent(withKeyCode keyCode : Int32, andState state : Bool)
{
guard let delegate = self.delegate as? AppDelegate else { return }
switch keyCode
{
// Play pressed
case NX_KEYTYPE_PLAY:
if state == false
{
delegate.musicPlayerWC.handleUserPressedPlayButton()
}
break
// Rewind
case NX_KEYTYPE_FAST:
if state == true
{
delegate.musicPlayerWC.handleUserPressedNextSongButton()
}
break
// Previous
case NX_KEYTYPE_REWIND:
if state == true
{
delegate.musicPlayerWC.handleUserPressedPreviousSongButton()
}
break
default:
break
}
}
Here's a great article on the subject: http://www.rogueamoeba.com/utm/2007/09/29/
Check this: https://gist.github.com/gauravk92/546311
Works perfectly.
As example, this repo uses it: https://github.com/onepill/PauseIt
In case anyone comes looking, there is sample code to do this here. This approach does allow you to "eat" the events they do not reach iTunes or other media-key-aware apps.
But be warned that event taps are not allowed by the sandbox, so this won't work in the App Store. If anyone has a workaround for that I'd love to hear it.