4

The Swift 2.1 solution for Cut/Copy/Paste/SelectAll/Undo/Redo is here, but this now produces 6 warnings with Xcode 7.3/Swift 2.2. The Selector keyword has been deprecated in future versions of Swift.

Here is a partial solution, which compiles without warnings for Cut/Copy/Paste/SelectAll:

if NSApp.sendAction(Selector("cut:"), to:nil, from:self) { return true }

Becomes

if NSApp.sendAction(#selector(NSText.cut(_:)), to:nil, from:self) { return true }

However Undo/Redo still needs to be solved.

Attempt 1: This produces an error as NSText.undo does not exist

if NSApp.sendAction(#selector(NSText.undo(_:)), to:nil, from:self) { return true }

Attempt 2: This compiles but does not function:

if NSApp.sendAction(#selector(NSUndoManager.undo), to:nil, from:self) { return true }

Here is the entire code fragment:

class NSTextFieldWithKeyboard: 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":
                    // New Swift 2.2 #selector works for cut, copy, paste and select all
                    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":
                    // Old method from Swift 2.1
                    // This functions, but produces the warning:
                    // No method declared with Objective-C selector 'undo:'
                    if NSApp.sendAction(Selector("undo:"), to:nil, from:self) { return true }

                    // Attempt 1: New use of Swift 2.2, but NSText.undo does not exist
                    //  Type 'NSText' has no member 'undo'
                    // if NSApp.sendAction(#selector(NSText.undo(_:)), to:nil, from:self) { return true }

                    // Attempt 2: Swift 2.2, but NSUndoManager.undo exists and compiles, but does not work
                    // if NSApp.sendAction(#selector(NSUndoManager.undo), to:nil, from:self) { return true }
                case "a":
                    if NSApp.sendAction(#selector(NSText.selectAll(_:)), to:nil, from:self) { return true }
                default:
                    break
                }
            }
            else if (event.modifierFlags.rawValue & NSEventModifierFlags.DeviceIndependentModifierFlagsMask.rawValue) == commandShiftKey {
                if event.charactersIgnoringModifiers == "Z" {
                    // The same warning we get with undo, we get with redo
                    if NSApp.sendAction(Selector("redo:"), to:nil, from:self) { return true }
                }
            }
        }
        return super.performKeyEquivalent(event)
    }
}
Community
  • 1
  • 1
Dave
  • 145
  • 1
  • 9

3 Answers3

4

Attempt #2 doesn't work because NSUndoManager has a method with the selector undo and not undo:.

I suggest you create a protocol to represent "an entity that responds to the undo action" and use that protocol to hold your selector:

@objc protocol UndoActionRespondable {
    func undo(sender: AnyObject)
}

let undoSelector = #selector(UndoActionRespondable.undo(_:))
Scott Thompson
  • 22,629
  • 4
  • 32
  • 34
  • The functionality we are looking for is the ability where "the sharedApplication looks for an object that can respond to the message—that is, an object that implements a method matching anAction. It begins with the first responder of the key window. If the first responder can’t respond, it tries the first responder’s next responder and continues following next responder links up the responder chain." -- AppleDocs – Dave Mar 23 '16 at 01:52
  • Yes. I understand exactly what you are trying to do and gave you a solution. You need to construct the selector "undo:" to accomplish that and the code I have given you will do that. Put the protocol definition above your class and when you try to send the action use #selector(UndoActionRespondable.undo(_:)) as the selector expression – Scott Thompson Mar 23 '16 at 02:34
  • OK, I see how this works now. Your solution essentially tells the compiler, yes, it's OK to send the undo and redo selector. I was originally thinking we would need to provide an implementation to the UndoActionRespondable protocol, but we don't. Calling NSApp.sendAction on the selector lets the underlying implementation find the right method. – Dave Mar 23 '16 at 09:19
  • Exactly. Another way to think about it is that the NSApp Action mechanism uses an informal protocol in Objective-C. That protocol says that there are objects who respond do the "undo:" message out there. What you have done is taken that informal protocol and made it a true protocol. – Scott Thompson Mar 23 '16 at 14:47
0

I just had the same problem. Just to clarify a detail; you need the protocol definition to have an underscore before sender for this to work. So the protocol looks like this:

@objc protocol UndoActionRespondable {
    func undo(_ sender: AnyObject)
    func redo(_ sender: AnyObject)
}

If you are putting together an edit menu you can do this like so (with the protocol outside of the NSMenu class):

Inside the NSMenu class, my entire edit menu looks like this:

let editMenu = NSMenuItem()
editMenu.submenu = NSMenu(title: "Edit")
editMenu.submenu?.items = [
    NSMenuItem(title: "Undo", action: #selector(UndoActionRespondable.undo(_:)), keyEquivalent: "z"),
    NSMenuItem(title: "Redo", action: #selector(UndoActionRespondable.redo(_:)), keyEquivalent: "Z"),
    NSMenuItem.separator(),
    NSMenuItem(title: "Cut", action: #selector(NSText.cut(_:)), keyEquivalent: "x"),
    NSMenuItem(title: "Copy", action: #selector(NSText.copy(_:)), keyEquivalent: "c"),
    NSMenuItem(title: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "v"),
    NSMenuItem.separator(),
    NSMenuItem(title: "Select All", action: #selector(NSText.selectAll(_:)), keyEquivalent: "a"),
    NSMenuItem.separator(),
    NSMenuItem(title: "Delete", target: self, action: nil, keyEquivalent: "⌫", modifier: .init()),
    NSMenuItem(title: "Duplicate", action: #selector(NSApplication.copy), keyEquivalent: "d"),
]
Relstoch
  • 21
  • 4
0

Another way to silence the warnings, working on Swift 5.4, is:

Selector(StringLiteralType("undo:"))
Selector(StringLiteralType("redo:"))

or even shorter

Selector(("undo:"))
Selector(("redo:"))
vicegax
  • 4,709
  • 28
  • 37