Cocoa Keyboard Shortcuts in Dialog without an Edit Menu
Asked Answered
T

16

40

I have an LSUIElement application that displays a menubar status item. The application can display a dialog window that contains a text field.

If the user right-clicks/control-clicks the text field, a menu appears that allows cut, copy, paste, etc. However, the standard Command-X, Command-C, and Command-V keyboard shortcuts do not work in the field. I assume this is because my application does not provide an Edit menu with those shortcuts defined.

I've tried adding an Edit menu item to my application's menu, as suggested in the Ship Some Code blog, but that did not work. The menu items in the Edit menu can be used, but keyboard shortcuts still don't work.

I can imagine a few ways to hack the keyboard handling, but is there a "recommended" way to make this work?

(For details about the app, see Menubar Countdown.)

Related question: Copy/Paste Not Working in Modal Window

Tommietommy answered 9/6, 2009 at 15:15 Comment(0)
M
31

What worked for me was using The View Solution presented in Copy and Paste Keyboard Shortcuts at CocoaRocket.

Basically, this means subclassing NSTextField and overriding performKeyEquivalent:.

Update: The CocoaRocket site is apparently gone. Here's the Internet Archive link: http://web.archive.org/web/20100126000339/http://www.cocoarocket.com/articles/copypaste.html

Edit: The Swift code looks like this

class Editing: NSTextField {

  private let commandKey = NSEventModifierFlags.CommandKeyMask.rawValue
  private let commandShiftKey = NSEventModifierFlags.CommandKeyMask.rawValue | NSEventModifierFlags.ShiftKeyMask.rawValue
  override func performKeyEquivalent(event: NSEvent) -> Bool {
    if event.type == NSEventType.KeyDown {
      if (event.modifierFlags.rawValue & NSEventModifierFlags.DeviceIndependentModifierFlagsMask.rawValue) == commandKey {
        switch event.charactersIgnoringModifiers! {
        case "x":
          if NSApp.sendAction(Selector("cut:"), to:nil, from:self) { return true }
        case "c":
          if NSApp.sendAction(Selector("copy:"), to:nil, from:self) { return true }
        case "v":
          if NSApp.sendAction(Selector("paste:"), to:nil, from:self) { return true }
        case "z":
          if NSApp.sendAction(Selector("undo:"), to:nil, from:self) { return true }
        case "a":
          if NSApp.sendAction(Selector("selectAll:"), to:nil, from:self) { return true }
        default:
          break
        }
      }
      else if (event.modifierFlags.rawValue & NSEventModifierFlags.DeviceIndependentModifierFlagsMask.rawValue) == commandShiftKey {
        if event.charactersIgnoringModifiers == "Z" {
          if NSApp.sendAction(Selector("redo:"), to:nil, from:self) { return true }
        }
      }
    }
    return super.performKeyEquivalent(event)
  }
}

Edit: The Swift 3 code looks like this

class Editing: NSTextView {

private let commandKey = NSEventModifierFlags.command.rawValue
private let commandShiftKey = NSEventModifierFlags.command.rawValue | NSEventModifierFlags.shift.rawValue

override func performKeyEquivalent(with event: NSEvent) -> Bool {
    if event.type == NSEventType.keyDown {
        if (event.modifierFlags.rawValue & NSEventModifierFlags.deviceIndependentFlagsMask.rawValue) == commandKey {
            switch event.charactersIgnoringModifiers! {
            case "x":
                if NSApp.sendAction(#selector(NSText.cut(_:)), to:nil, from:self) { return true }
            case "c":
                if NSApp.sendAction(#selector(NSText.copy(_:)), to:nil, from:self) { return true }
            case "v":
                if NSApp.sendAction(#selector(NSText.paste(_:)), to:nil, from:self) { return true }
            case "z":
                if NSApp.sendAction(Selector(("undo:")), to:nil, from:self) { return true }
            case "a":
                if NSApp.sendAction(#selector(NSResponder.selectAll(_:)), to:nil, from:self) { return true }
            default:
                break
            }
        }
        else if (event.modifierFlags.rawValue & NSEventModifierFlags.deviceIndependentFlagsMask.rawValue) == commandShiftKey {
            if event.charactersIgnoringModifiers == "Z" {
                if NSApp.sendAction(Selector(("redo:")), to:nil, from:self) { return true }
            }
        }
    }
    return super.performKeyEquivalent(with: event)
 }
}
Meteorite answered 9/6, 2009 at 15:15 Comment(3)
A Swift 2.2 solution is answered here.Audacious
But this doesn't work with other languages :( Just use event.keyCode instead event.charactersIgnoringModifiersGenevagenevan
This doesn't work when Caps Lock is on. You have to remove the Caps Lock flag before comparing equality with command or command-shift.Nw
R
45

Improving on that CocoaRocket solution:

The following saves having to subclass NSTextField and remembering to use the subclass throughout your application; it will also enable copy, paste and friends for other responders that handle them, eg. NSTextView.

Put this in a subclass of NSApplication and alter the principal class in your Info.plist accordingly.

- (void) sendEvent:(NSEvent *)event {
    if ([event type] == NSKeyDown) {
        if (([event modifierFlags] & NSDeviceIndependentModifierFlagsMask) == NSCommandKeyMask) {
            if ([[event charactersIgnoringModifiers] isEqualToString:@"x"]) {
                if ([self sendAction:@selector(cut:) to:nil from:self])
                    return;
            }
            else if ([[event charactersIgnoringModifiers] isEqualToString:@"c"]) {
                if ([self sendAction:@selector(copy:) to:nil from:self])
                    return;
            }
            else if ([[event charactersIgnoringModifiers] isEqualToString:@"v"]) {
                if ([self sendAction:@selector(paste:) to:nil from:self])
                    return;
            }
            else if ([[event charactersIgnoringModifiers] isEqualToString:@"z"]) {
                if ([self sendAction:@selector(undo:) to:nil from:self])
                    return;
            }
            else if ([[event charactersIgnoringModifiers] isEqualToString:@"a"]) {
                if ([self sendAction:@selector(selectAll:) to:nil from:self])
                    return;
            }
        }
        else if (([event modifierFlags] & NSDeviceIndependentModifierFlagsMask) == (NSCommandKeyMask | NSShiftKeyMask)) {
            if ([[event charactersIgnoringModifiers] isEqualToString:@"Z"]) {
                if ([self sendAction:@selector(redo:) to:nil from:self])
                    return;
            }
        }
    }
    [super sendEvent:event];
}

// Blank Selectors to silence Xcode warnings: 'Undeclared selector undo:/redo:'
- (IBAction)undo:(id)sender {}
- (IBAction)redo:(id)sender {}
Rosalia answered 9/6, 2009 at 15:15 Comment(7)
Perfect solution! Another downside of the original was that you'd have to 'redefine' subclasses of NSTextField too, like NSSearchField.Ortega
I've amended this to include redo.Rosalia
In addition I had to replace NSApplication with my subclass in the main.mScirrhous
While I don't like the idea of subclassing NSApplication, this code works well. My app is without xib's and a mainmenu, and whenever I tried to add an NSTextField as a subview to a borderless window, none of the cmd shortcuts would work (I even overrode canBecomeKeyWindow).Saintsimonianism
Thanks Adrian, it works like a charm, I just want to take a notice that my Xcode issues 2 warnings that selector (undo:) and selector (redo:) are not recognized but they actually work in runtime, just ignore the warnings. I think my notice may help someoneManoff
@ScofieldTran I just added a couple of blank selectors to silence the Xcode warnings.Discriminatory
This doesn't work when Caps Lock is on. You have to remove the Caps Lock flag before comparing equality with command or command-shift.Nw
M
31

What worked for me was using The View Solution presented in Copy and Paste Keyboard Shortcuts at CocoaRocket.

Basically, this means subclassing NSTextField and overriding performKeyEquivalent:.

Update: The CocoaRocket site is apparently gone. Here's the Internet Archive link: http://web.archive.org/web/20100126000339/http://www.cocoarocket.com/articles/copypaste.html

Edit: The Swift code looks like this

class Editing: NSTextField {

  private let commandKey = NSEventModifierFlags.CommandKeyMask.rawValue
  private let commandShiftKey = NSEventModifierFlags.CommandKeyMask.rawValue | NSEventModifierFlags.ShiftKeyMask.rawValue
  override func performKeyEquivalent(event: NSEvent) -> Bool {
    if event.type == NSEventType.KeyDown {
      if (event.modifierFlags.rawValue & NSEventModifierFlags.DeviceIndependentModifierFlagsMask.rawValue) == commandKey {
        switch event.charactersIgnoringModifiers! {
        case "x":
          if NSApp.sendAction(Selector("cut:"), to:nil, from:self) { return true }
        case "c":
          if NSApp.sendAction(Selector("copy:"), to:nil, from:self) { return true }
        case "v":
          if NSApp.sendAction(Selector("paste:"), to:nil, from:self) { return true }
        case "z":
          if NSApp.sendAction(Selector("undo:"), to:nil, from:self) { return true }
        case "a":
          if NSApp.sendAction(Selector("selectAll:"), to:nil, from:self) { return true }
        default:
          break
        }
      }
      else if (event.modifierFlags.rawValue & NSEventModifierFlags.DeviceIndependentModifierFlagsMask.rawValue) == commandShiftKey {
        if event.charactersIgnoringModifiers == "Z" {
          if NSApp.sendAction(Selector("redo:"), to:nil, from:self) { return true }
        }
      }
    }
    return super.performKeyEquivalent(event)
  }
}

Edit: The Swift 3 code looks like this

class Editing: NSTextView {

private let commandKey = NSEventModifierFlags.command.rawValue
private let commandShiftKey = NSEventModifierFlags.command.rawValue | NSEventModifierFlags.shift.rawValue

override func performKeyEquivalent(with event: NSEvent) -> Bool {
    if event.type == NSEventType.keyDown {
        if (event.modifierFlags.rawValue & NSEventModifierFlags.deviceIndependentFlagsMask.rawValue) == commandKey {
            switch event.charactersIgnoringModifiers! {
            case "x":
                if NSApp.sendAction(#selector(NSText.cut(_:)), to:nil, from:self) { return true }
            case "c":
                if NSApp.sendAction(#selector(NSText.copy(_:)), to:nil, from:self) { return true }
            case "v":
                if NSApp.sendAction(#selector(NSText.paste(_:)), to:nil, from:self) { return true }
            case "z":
                if NSApp.sendAction(Selector(("undo:")), to:nil, from:self) { return true }
            case "a":
                if NSApp.sendAction(#selector(NSResponder.selectAll(_:)), to:nil, from:self) { return true }
            default:
                break
            }
        }
        else if (event.modifierFlags.rawValue & NSEventModifierFlags.deviceIndependentFlagsMask.rawValue) == commandShiftKey {
            if event.charactersIgnoringModifiers == "Z" {
                if NSApp.sendAction(Selector(("redo:")), to:nil, from:self) { return true }
            }
        }
    }
    return super.performKeyEquivalent(with: event)
 }
}
Meteorite answered 9/6, 2009 at 15:15 Comment(3)
A Swift 2.2 solution is answered here.Audacious
But this doesn't work with other languages :( Just use event.keyCode instead event.charactersIgnoringModifiersGenevagenevan
This doesn't work when Caps Lock is on. You have to remove the Caps Lock flag before comparing equality with command or command-shift.Nw
C
23

I had the same problem as you, and I think I've managed to find a simpler solution. You just need to leave the original main menu in MainMenu.xib - it won't be displayed, but all the actions will be handled properly. The trick is that it needs to be the original one, if you just drag a new NSMenu from the library, the app won't recognize it as Main Menu and I have no idea how to mark it as such (if you uncheck LSUIElement, you'll see that it won't show up at the top if it's not the original one). If you've already deleted it, you can create a new sample app and drag a menu from its NIB, that works too.

Cooker answered 9/6, 2009 at 15:15 Comment(3)
This worked for me in 2017. I created a new xib from the "Main Menu" Xcode template - didn't need a new app.Glary
Thank you! This was a simple and easy solution.Prospector
This should be the accepted answer - no need to add significant amounts of code and it is extensible. The tip of copy/pasting the main menu from a sample app works a treat (Xcode 10.1).Jovitajovitah
T
10

I've improved Adrian's solution to work when Caps Lock is on as well:

- (void)sendEvent:(NSEvent *)event
{
    if (event.type == NSKeyDown)
    {
        NSString *inputKey = [event.charactersIgnoringModifiers lowercaseString];
        if ((event.modifierFlags & NSDeviceIndependentModifierFlagsMask) == NSCommandKeyMask ||
            (event.modifierFlags & NSDeviceIndependentModifierFlagsMask) == (NSCommandKeyMask | NSAlphaShiftKeyMask))
        {
            if ([inputKey isEqualToString:@"x"])
            {
                if ([self sendAction:@selector(cut:) to:nil from:self])
                    return;
            }
            else if ([inputKey isEqualToString:@"c"])
            {
                if ([self sendAction:@selector(copy:) to:nil from:self])
                    return;
            }
            else if ([inputKey isEqualToString:@"v"])
            {
                if ([self sendAction:@selector(paste:) to:nil from:self])
                    return;
            }
            else if ([inputKey isEqualToString:@"z"])
            {
                if ([self sendAction:NSSelectorFromString(@"undo:") to:nil from:self])
                    return;
            }
            else if ([inputKey isEqualToString:@"a"])
            {
                if ([self sendAction:@selector(selectAll:) to:nil from:self])
                    return;
            }
        }
        else if ((event.modifierFlags & NSDeviceIndependentModifierFlagsMask) == (NSCommandKeyMask | NSShiftKeyMask) ||
                 (event.modifierFlags & NSDeviceIndependentModifierFlagsMask) == (NSCommandKeyMask | NSShiftKeyMask | NSAlphaShiftKeyMask))
        {
            if ([inputKey isEqualToString:@"z"])
            {
                if ([self sendAction:NSSelectorFromString(@"redo:") to:nil from:self])
                    return;
            }
        }
    }
    [super sendEvent:event];
}
Transfigure answered 9/6, 2009 at 15:15 Comment(0)
E
8

Thomas Kilian solution in swift 3.

private let commandKey = NSEventModifierFlags.command.rawValue
private let commandShiftKey = NSEventModifierFlags.command.rawValue | NSEventModifierFlags.shift.rawValue
override func performKeyEquivalent(with event: NSEvent) -> Bool {
  if event.type == NSEventType.keyDown {
    if (event.modifierFlags.rawValue & NSEventModifierFlags.deviceIndependentFlagsMask.rawValue) == commandKey {
    switch event.charactersIgnoringModifiers! {
    case "x":
      if NSApp.sendAction(#selector(NSText.cut(_:)), to:nil, from:self) { return true }
    case "c":
      if NSApp.sendAction(#selector(NSText.copy(_:)), to:nil, from:self) { return true }
    case "v":
      if NSApp.sendAction(#selector(NSText.paste(_:)), to:nil, from:self) { return true }
    case "z":
      if NSApp.sendAction(Selector(("undo:")), to:nil, from:self) { return true }
    case "a":
      if NSApp.sendAction(#selector(NSResponder.selectAll(_:)), to:nil, from:self) { return true }
    default:
      break
    }
  }
  else if (event.modifierFlags.rawValue & NSEventModifierFlags.deviceIndependentFlagsMask.rawValue) == commandShiftKey {
    if event.charactersIgnoringModifiers == "Z" {
      if NSApp.sendAction(Selector(("redo:")), to:nil, from:self) { return true }
    }
  }
}
return super.performKeyEquivalent(with: event)
}
Eterne answered 9/6, 2009 at 15:15 Comment(1)
A few things changed for swift 4. But just click fix to resolve. ✌️Bifocals
L
7

Xcode10/Swift 4.2 solution:

import Cocoa

extension NSTextView {
override open func performKeyEquivalent(with event: NSEvent) -> Bool {
    let commandKey = NSEvent.ModifierFlags.command.rawValue
    let commandShiftKey = NSEvent.ModifierFlags.command.rawValue | NSEvent.ModifierFlags.shift.rawValue
    if event.type == NSEvent.EventType.keyDown {
        if (event.modifierFlags.rawValue & NSEvent.ModifierFlags.deviceIndependentFlagsMask.rawValue) == commandKey {
            switch event.charactersIgnoringModifiers! {
            case "x":
                if NSApp.sendAction(#selector(NSText.cut(_:)), to:nil, from:self) { return true }
            case "c":
                if NSApp.sendAction(#selector(NSText.copy(_:)), to:nil, from:self) { return true }
            case "v":
                if NSApp.sendAction(#selector(NSText.paste(_:)), to:nil, from:self) { return true }
            case "z":
                if NSApp.sendAction(Selector(("undo:")), to:nil, from:self) { return true }
            case "a":
                if NSApp.sendAction(#selector(NSResponder.selectAll(_:)), to:nil, from:self) { return true }
            default:
                break
            }
        } else if (event.modifierFlags.rawValue & NSEvent.ModifierFlags.deviceIndependentFlagsMask.rawValue) == commandShiftKey {
            if event.charactersIgnoringModifiers == "Z" {
                if NSApp.sendAction(Selector(("redo:")), to:nil, from:self) { return true }
            }
        }
    }
    return super.performKeyEquivalent(with: event)
}
}
Lytle answered 9/6, 2009 at 15:15 Comment(1)
This worked well for me, using Swift 5.3. However, I also had to add textView.allowsUndo = true when creating the TextView. ThanksLauricelaurie
C
5

Swift 4.2 for Thomas Kilian solution

class MTextField: NSSecureTextField {

    private let commandKey = NSEvent.ModifierFlags.command.rawValue
    private let commandShiftKey = NSEvent.ModifierFlags.command.rawValue | NSEvent.ModifierFlags.shift.rawValue

    override func performKeyEquivalent(with event: NSEvent) -> Bool {
        if event.type == NSEvent.EventType.keyDown {
            if (event.modifierFlags.rawValue & NSEvent.ModifierFlags.deviceIndependentFlagsMask.rawValue) == commandKey {
                switch event.charactersIgnoringModifiers! {
                case "x":
                    if NSApp.sendAction(#selector(NSText.cut(_:)), to:nil, from:self) { return true }
                case "c":
                    if NSApp.sendAction(#selector(NSText.copy(_:)), to:nil, from:self) { return true }
                case "v":
                    if NSApp.sendAction(#selector(NSText.paste(_:)), to:nil, from:self) { return true }
                case "z":
                    if NSApp.sendAction(Selector(("undo:")), to:nil, from:self) { return true }
                case "a":
                    if NSApp.sendAction(#selector(NSResponder.selectAll(_:)), to:nil, from:self) { return true }
                default:
                    break
                }
            }
            else if (event.modifierFlags.rawValue & NSEvent.ModifierFlags.deviceIndependentFlagsMask.rawValue) == commandShiftKey {
                if event.charactersIgnoringModifiers == "Z" {
                    if NSApp.sendAction(Selector(("redo:")), to:nil, from:self) { return true }
                }
            }
        }
        return super.performKeyEquivalent(with: event)
    }

}
Crematory answered 9/6, 2009 at 15:15 Comment(0)
T
4

Here's a quick step-by-step guide for swift, based on the excellent answers by @Adrian, Travis B and Thomas Kilian.

The goal will be to subclass the NSApplication, instead of the NSTextField. Once you have created this class, do link it in your "principal Class" setting of the Info.plist, as stated by Adrian. As opposed to the Objective-C folks, we swiftlers will have to add an additional prefix to the principalClass configuration. So, because my Project is called "Foo", I am going to set "Principal Class" to "Foo.MyApplication". You will get an "Class MyApplication not found" runtime error otherwise.

The contents of MyApplication read as follows (copied and adapted from all the answers given so far)

import Cocoa

class MyApplication: NSApplication {
    override func sendEvent(event: NSEvent) {
        if event.type == NSEventType.KeyDown {
            if (event.modifierFlags & NSEventModifierFlags.DeviceIndependentModifierFlagsMask == NSEventModifierFlags.CommandKeyMask) {
                switch event.charactersIgnoringModifiers!.lowercaseString {
                case "x":
                    if NSApp.sendAction(Selector("cut:"), to:nil, from:self) { return }
                case "c":
                    if NSApp.sendAction(Selector("copy:"), to:nil, from:self) { return }
                case "v":
                    if NSApp.sendAction(Selector("paste:"), to:nil, from:self) { return }
                case "z":
                    if NSApp.sendAction(Selector("undo:"), to:nil, from:self) { return }
                case "a":
                    if NSApp.sendAction(Selector("selectAll:"), to:nil, from:self) { return }
                default:
                    break
                }
            }
            else if (event.modifierFlags & NSEventModifierFlags.DeviceIndependentModifierFlagsMask == (NSEventModifierFlags.CommandKeyMask | NSEventModifierFlags.ShiftKeyMask)) {
                if event.charactersIgnoringModifiers == "Z" {
                    if NSApp.sendAction(Selector("redo:"), to:nil, from:self) { return }
                }
            }
        }
        return super.sendEvent(event)
    }

}
Television answered 9/6, 2009 at 15:15 Comment(0)
W
3

I explain what worked for me in XCode 8 / Swift 3.

I created MyApplication.swift inside my project folder MyApp:

import Foundation
import Cocoa

class MyApplication: NSApplication {
    override func sendEvent(_ event: NSEvent) {
        if event.type == NSEventType.keyDown {

            if (event.modifierFlags.contains(NSEventModifierFlags.command)) {
                switch event.charactersIgnoringModifiers!.lowercased() {
                case "x":
                    if NSApp.sendAction(#selector(NSText.cut(_:)), to:nil, from:self) { return }
                case "c":
                    if NSApp.sendAction(#selector(NSText.copy(_:)), to:nil, from:self) { return }
                case "v":
                    if NSApp.sendAction(#selector(NSText.paste(_:)), to:nil, from:self) { return }
                case "a":
                    if NSApp.sendAction(#selector(NSText.selectAll(_:)), to:nil, from:self) { return }
                default:
                    break
                }
            }
        }
        return super.sendEvent(event)
    }

}

Then change the Info.plist Principal class to MyApp.MyApplication. Build, and run to validate that my text fields and text views have support for Cmd + X, Cmd + C, Cmd + V and Cmd + A.

White answered 9/6, 2009 at 15:15 Comment(2)
This instruction is very clear and straightforward. Easily adapted and work on Swift 3Synder
good solution ! if you have trouble with principal class, try this post: https://mcmap.net/q/393467/-subclass-nsapplication-in-swiftHemihedral
B
2

No need to add a new class, extension or really any code at all.

  1. Just add a new MenuItems in one of your menus and name them 'Copy', 'Cut' and 'Paste'.
  2. Add the correct shortcut keys to each item.
  3. Control+drag to connect them up to the corresponding methods listed under first responder.

The bonus here is that the items are not hidden from your users, and this takes less time that creating a new class and reassigning all of your existing TextFields to it.

Broadway answered 9/6, 2009 at 15:15 Comment(1)
Where does the user see the menu in an LSUIElement application?Willson
D
1

Swift 5 solution for NSApplication subclass

open override func sendEvent(_ event: NSEvent) {
    if event.type == .keyDown {
        if event.modifierFlags.contains(.command)  && NSEvent.ModifierFlags.deviceIndependentFlagsMask.contains(.command) {
            if event.modifierFlags.contains(.shift) && NSEvent.ModifierFlags.deviceIndependentFlagsMask.contains(.shift) {
                if event.charactersIgnoringModifiers == "Z" {
                    if NSApp.sendAction(Selector("redo:"), to:nil, from:self) { return }
                }
            }
            guard let key = event.charactersIgnoringModifiers else { return super.sendEvent(event) }
            switch key {
            case "x":
                if NSApp.sendAction(Selector("cut:"), to:nil, from:self) { return }
            case "c":
                if NSApp.sendAction(Selector("copy:"), to:nil, from:self) { return }
            case "v":
                if NSApp.sendAction(Selector("paste:"), to:nil, from:self) { return }
            case "z":
                if NSApp.sendAction(Selector("undo:"), to:nil, from:self) { return }
            case "a":
                if NSApp.sendAction(Selector("selectAll:"), to:nil, from:self) { return }
            default:
                break
          }
        }
    }
    super.sendEvent(event)
}
Disappear answered 9/6, 2009 at 15:15 Comment(0)
C
0

Thank you for this solution! It helped me greatly, so I decided to contribute some code in hope it helps someone else. The suggested solution above worked perfectly after I converted it to Swift 4.2. I then refactored the code a little. I think this is a little cleaner. This is Swift 4.2 compatible:

// NSEventExtensions.swift

import AppKit

extension NSEvent {
    func containsKeyModifierFlags(_ flags: NSEvent.ModifierFlags) -> Bool {
        switch modifierFlags.intersection(.deviceIndependentFlagsMask) {
        case [flags]: return true
        default: return false
        }
    }
}

// SearchFiled.swift

import AppKit
import Carbon

final class SearchField: NSSearchField {
    override func performKeyEquivalent(with event: NSEvent) -> Bool {
        switch event.type {
        case .keyDown: return performKeyDownEquivalent(with: event)
        default: return super.performKeyEquivalent(with: event)
        }
    }

    // MARK: - private

    private func performKeyDownEquivalent(with event: NSEvent) -> Bool {
        if event.containsKeyModifierFlags(.command) {
            switch Int(event.keyCode) {
            case kVK_ANSI_X: return NSApp.sendAction(#selector(NSText.cut(_:)), to: nil, from: self)
            case kVK_ANSI_C: return NSApp.sendAction(#selector(NSText.copy(_:)), to: nil, from: self)
            case kVK_ANSI_V: return NSApp.sendAction(#selector(NSText.paste(_:)), to: nil, from: self)
            case kVK_ANSI_Z: return NSApp.sendAction(Selector(("undo:")), to: nil, from: self)
            case kVK_ANSI_A: return NSApp.sendAction(#selector(NSResponder.selectAll(_:)), to: nil, from: self)
            default: break
            }
        } else if event.containsKeyModifierFlags([.command, .shift]) {
            switch Int(event.keyCode) {
            case kVK_ANSI_Z: return NSApp.sendAction(Selector(("redo:")), to: nil, from: self)
            default: break
            }
        }
        return false
    }
}
Calie answered 9/6, 2009 at 15:15 Comment(0)
M
0

Based on Thomas Kilian's answer, you can actually create an extension for NSTextField

let commandKey = NSEvent.ModifierFlags.command.rawValue
let commandShiftKey = NSEvent.ModifierFlags.command.rawValue | NSEvent.ModifierFlags.shift.rawValue

extension NSTextField {
    func performEditingKeyEquivalent(with event: NSEvent) -> Bool {
        guard event.type == NSEvent.EventType.keyDown else { return false }

        if (event.modifierFlags.rawValue & NSEvent.ModifierFlags.deviceIndependentFlagsMask.rawValue) == commandKey {
            if let character = event.charactersIgnoringModifiers {
                switch character {
                case "x":
                    if NSApp.sendAction(#selector(NSText.cut(_:)), to: nil, from: self) { return true }
                case "c":
                    if NSApp.sendAction(#selector(NSText.copy(_:)), to: nil, from: self) { return true }
                case "v":
                    if NSApp.sendAction(#selector(NSText.paste(_:)), to: nil, from: self) { return true }
                case "z":
                    if NSApp.sendAction(Selector(("undo:")), to: nil, from: self) { return true }
                case "a":
                    if NSApp.sendAction(#selector(NSResponder.selectAll(_:)), to: nil, from: self) { return true }
                default:
                    break
                }
            }
        } else if (event.modifierFlags.rawValue & NSEvent.ModifierFlags.deviceIndependentFlagsMask.rawValue) == commandShiftKey {
            if event.charactersIgnoringModifiers == "Z" {
                if NSApp.sendAction(Selector(("redo:")), to: nil, from: self) { return true }
            }
        }

        return false
    }
}

In the followingg example, one can actually replace NSTextField with any of the NSTextField inheriting classes (e.g NSSearchField, NSSecureTextField) to have the new functionality

class SearchField: NSTextField {
    override func performKeyEquivalent(with event: NSEvent) -> Bool {
        if performEditingKeyEquivalent(with: event) {
            return true
        }

        return super.performEditingKeyEquivalent(with: event)
    }
}
Mastigophoran answered 9/6, 2009 at 15:15 Comment(0)
F
0

Here's Travis' answer as C# for use with Xamarin.Mac:

    public override bool PerformKeyEquivalent (AppKit.NSEvent e)
    {
        if (e.Type == NSEventType.KeyDown) {
            var inputKey = e.CharactersIgnoringModifiers.ToLower ();
            if (   (e.ModifierFlags & NSEventModifierMask.DeviceIndependentModifierFlagsMask) == NSEventModifierMask.CommandKeyMask
                || (e.ModifierFlags & NSEventModifierMask.DeviceIndependentModifierFlagsMask) == (NSEventModifierMask.CommandKeyMask | NSEventModifierMask.AlphaShiftKeyMask)) {
                switch (inputKey) {
                case "x":
                    NSApplication.SharedApplication.SendAction (new Selector ("cut:"), null, this);
                    return true;
                case "c":
                    NSApplication.SharedApplication.SendAction (new Selector ("copy:"), null, this);
                    return true;
                case "v":
                    NSApplication.SharedApplication.SendAction (new Selector ("paste:"), null, this);
                    return true;
                case "z":
                    NSApplication.SharedApplication.SendAction (new Selector ("undo:"), null, this);
                    return true;
                case "a":
                    NSApplication.SharedApplication.SendAction (new Selector ("selectAll:"), null, this);
                    return true;
                }
            } else if (   (e.ModifierFlags & NSEventModifierMask.DeviceIndependentModifierFlagsMask) == (NSEventModifierMask.CommandKeyMask | NSEventModifierMask.ShiftKeyMask)
                       || (e.ModifierFlags & NSEventModifierMask.DeviceIndependentModifierFlagsMask) == (NSEventModifierMask.CommandKeyMask | NSEventModifierMask.ShiftKeyMask | NSEventModifierMask.AlphaShiftKeyMask)) {
                switch (inputKey) {
                case "z":
                    NSApplication.SharedApplication.SendAction (new Selector ("redo:"), null, this);
                    return true;
                }
            }
        }
        return base.PerformKeyEquivalent(e);
    }
Footy answered 9/6, 2009 at 15:15 Comment(0)
N
0

Just about 1 hour ago I stumbled upon the same problem. You don't need to code anything. I could do this in Interface Builder:

  • Create a menu (e.g. "Edit") which contains your Cut / Copy / Paste menu items
  • Add the KeyEquivalent for the CMD key to your "Edit" menu (don't know, if this is really needed, I just copied the structure from another project)
  • Add the KeyEquivalents to these menu items (CMD + X and so on)
  • Link the FirstResponder's cut:, copy: and paste: functions to your corresponding menu items

That worked for me. Unfortunately this (default) behavior doesn't seem to work when you hide the "Edit" menu (just tried it).

Nickolai answered 9/6, 2009 at 15:15 Comment(0)
H
-1

Adrian's solution is good, but better I think to use a switch statement rather than all those string comparisons, e.g.:

    uint const modifierCode = (theEvent.modifierFlags & NSDeviceIndependentModifierFlagsMask);
    BOOL usingModifiers = ( modifierCode != 0 );
    //BOOL const usingShiftKey = ((theEvent.modifierFlags & NSShiftKeyMask) != 0);
    //BOOL const usingCommandKey = ((theEvent.modifierFlags & NSCommandKeyMask) != 0);
    NSString * ch = [theEvent charactersIgnoringModifiers];
    if ( ( usingModifiers ) && ( ch.length == 1 ) ) switch ( [ch characterAtIndex:0] )
    {
        case 'x':
            if ( modifierCode == NSCommandKeyMask ) [m cut]; // <-- m = model
            break;
        case 'c':
            if ( modifierCode == NSCommandKeyMask ) [m copy];
            break;
        case 'v':
            if ( modifierCode == NSCommandKeyMask ) [m paste];
            break;
        case 'z':
            if ( modifierCode == NSCommandKeyMask ) [m undo];
            break;
        case 'Z':
            if ( modifierCode == ( NSCommandKeyMask | NSShiftKeyMask ) ) [m redo];
            break;
        default: // etc.
            break;
    }
    else switch ( theEvent.keyCode ) // <-- for independent keycodes!
    {
        case kVK_Home:
            [m moveToBeginningOfDocument:nil];
            break;
        case kVK_End: // etc!
Huppert answered 9/6, 2009 at 15:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.