Question

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

Was it helpful?

Solution 2

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

OTHER TIPS

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

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.

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];
}

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

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

}

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

}

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.

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.

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

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

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

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

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

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!
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top