Cocoa Keyboard Shortcuts in Dialog without an Edit Menu
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];
}
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 {}
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)
}
}
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.