13

In my Mac app, I override and accept certain keystrokes via the keyUp function in an NSView, which isn't meant to accept keystrokes.

When a key is pressed, the keyUp function is called, and I do process the keystroke, without even calling super keyUp:, and everything works, except that it also makes that default 'doonk' sound that happens when you press a key somewhere you shouldn't.

Is there any way to indicate that the keystroke was handled and accepted, and that I don't need a beep to tell the user it wasn't?

Greg
  • 9,068
  • 6
  • 49
  • 91

5 Answers5

17

I think (but am not 100% certain, it's been a bit of time since I did this) you also need to override the NSView and/or NSResponder performKeyEquivalent: method. There, you'll return a YES to indicate to the caller that you did indeed handle the event.

And that will keep the "dooonk" sound from happening.

Michael Dautermann
  • 88,797
  • 17
  • 166
  • 215
  • Thanks! Just copy and pasted my `if` statement from my `keyUp:` function to this one and added `return`s. :) – Greg Jan 15 '12 at 22:16
  • Is there anyway to get rid of this sound without subclassing? – Nikolay Tsenkov May 20 '14 at 08:30
  • I'd like to clarify that you need to put your code from `keyDown:` or equivalent into the `performKeyEquivalent:` (i.e. this replaces the other methods). Also if you're using the `flagsChanged:` method to detect keys like CMD down then you need to keep using `flagsChanged:`. – Rob Sanders Mar 23 '15 at 21:50
  • 1
    There is an elegant solution is that we shouldn't call **super.keyDown** or **super.keyUp** if we already handle the keystroke action. – Nghia Tran Apr 17 '20 at 14:27
1

The accepted answer might do the trick but I would like to suggest a more correct way to solve the problem :)

Using keyUp: and keyDown: is totally fine, but most importantly, your view must also follow the AppKit responder pattern (You can learn more about NSResponder on the official documentation).

In your case, the view would need to signal itself as a valid first responder in the event chain. Assuming you have an NSView subclass, you will need to implement the following method:

@implementation MyNSView

//...

- (BOOL)acceptsFirstResponder {
    return YES;
}

@end
Mr_Pouet
  • 4,061
  • 8
  • 36
  • 47
1

In my case performKeyEquivalent did not capture the Enter key. The beep was triggered in the keyDown event. This Swift code prevents beeping when handling the Enter key:

override func keyDown(with event: NSEvent) {
    if event.keyCode != 36 {
        super.keyDown(with: event)
    }
}

override func keyUp(with event: NSEvent) {
    if event.keyCode == 36 {
        // Do your thing
    }
}
Ely
  • 8,259
  • 1
  • 54
  • 67
1

Instead of using keyUp:, you may want to use the moveUp: action, as it takes all of the hassle of determining which key to handle out of the mix. There are also similarly named routines for down and a variety for handling movement with selections, etc.

For further documentation on this, please see the Cocoa Event-Handling Guide, and in particular "Handling Keyboard Actions and Inserting Text", where it discusses the use of these commands in "Applications other that those that deal with text".

In particular, the other benefit to using these actions is that it avoids any problems with either key interpretation or special keyboards and keyboard layouts.

gaige
  • 17,263
  • 6
  • 57
  • 68
  • The whole problem is in that I am not using `NSTextView`s, and the custom objects are not text bodies, but things like images and other controls, and I need keystrokes like Delete, Cmd+C, etc., not mouse drag and arrow keys. – Greg Jan 15 '12 at 22:19
  • I'm not sure what NSTextView has to do with anything. I use this technique on a number of views which are direct subclasses of NSView and they work fine. The various move*: actions are implemented as part of the default NSView responder chain and I'm pretty sure that any view that accepts are handed those actions. In fact, I have a number of shipping products that do just that without an NSTextView in sight. – gaige Jan 15 '12 at 23:42
  • But the various `move*:` actions are only for arrow keys, and I need other keys and combinations as well. – Greg Jan 16 '12 at 01:45
  • 1
    OK, clearly I misread your original note in this regard. Chances are you want to override `keyDown:` then instead of keyUp: (or as well). The keyDown: is where the majority of processing is done in the OS, and that's where things like interpretKeyEvents: are called. the `keyUp:` routines is really for situations where you are monitoring state while the key is down (which you can do, but the beeping is most likely happening in `keyDown:`). – gaige Jan 16 '12 at 12:18
  • 1
    I tried, and overriding `keyDown:` and not calling `super` also prevents the beep, but I still find overriding `performKeyEquivalent:` to be more elegant. +1 for the `keyDown:` method though :) – Greg Jan 16 '12 at 12:32
0

Every answer on this page is not quite right. The following is best practice, in my experience. If the key press is not valid the alert will sound; but if it is valid, it will not…

class YourNSViewSubclass: NSView {
  // Make sure your view will accept responder chain events
  override var acceptsFirstResponder: Bool { true }
  
  override func becomeFirstResponder() -> Bool {
    true
  }
  
  override func resignFirstResponder() -> Bool {
    true
  }
  
  
  override func keyDown(with event: NSEvent) {
    if !validateKeyPress(event: event) {
      // If we get here, we need to send the key press up the responder chain
      // So other views can have an opportunity to deal with the event
      super.keyDown(with: event)
    }
  }
  
  override func keyUp(with event: NSEvent) {
    guard validateKeyPress(event: event) else {
      super.keyUp(with: event)
      return
    }
    
    // Do something as a result of the key press
  }
  
  
  // Check key press is valid. This example checks for the Delete key
  private func validateKeyPress(event: NSEvent) -> Bool {
    event.charactersIgnoringModifiers == String(UnicodeScalar(NSEvent.SpecialKey.delete.rawValue)!)
  }
}
marksc111
  • 1
  • 1