macOS 开发之 NSTextField 支持文本快捷键(一): 基本操作
在日常 macOS 开发中经常会用到 NSTextField 控件,但是会发现一个问题,如果开发的应用没有顶栏应用菜单,编辑控件中的文本内容的时候,按下文本操作快捷键无效,而得到的是系统警告音。本文就跟十里一起看一下如何让没有应用菜单的 app 中的 NSTextField 支持常用的文本操作快捷键。
- 复制:
Command
+c
- 剪切:
Command
+x
- 粘贴:
Command
+v
- 全选:
Command
+a
实现平台
- macOS 10.14.3
- swift 4.2.1
- xcode 10.1
键盘事件
每个 app 都有自己的主循环线程(Main Run Loop),在这个循环中会遍历系统的事件消息队列,逐一发给对应的响应对象进行处理。具体来说就是 NSApp 的 sendAction:
或 sendEvent:
方法发送事件消息给 NSWindow,NSWindow 再分发给具体的视图对象,最后由相应的事件响应方法进行处理。其中一种事件,就是这里要说的键盘事件,NSApp 对不同的键盘事件处理方式会不同,主要是以下三种:
- 快捷键:如果按下快捷键,系统会先向当前的活动窗口或菜单发送
performKeyEquivalent:
消息,此时窗口会依次遍历所有子视图控件按照响应链传递给performKeyEquivalent:
的响应者。 - 控制键:NSApp 会将消息转发给键盘窗口,完成不同的控件切换控制的功能
- 其它按键:NSApp 会将消息转发给键盘窗口,窗口对象会先定位到第一响应对象,根据响应链优先级寻找对 keyDown 键盘事件响应的视图对象,如果可以找到就由相应对象处理,否则就会按照 insertText 方法进行处理(如果是个文本控件,就会插入相应键盘文本)
很明显,我们要实现本文的主题,关注快捷键的处理流程就可以了,其实 NSTextField 有上述 performKeyEquivalent:
消息的处理,只不过是没有任何功能实现而已,所以我们只需为 NSTextField 复写一下这个消息的处理即可,这个消息的处理返回值是一个布尔类型,返回 true 则标志着按键事件消息传递的结束。
新建 Demo 工程
为了更好的说明实现方法,我们还是要通过一个实际的Demo演示!新建的工程包含 storyboard ,新建完成后添加一个文本框控件,如果您对这些操作很熟悉可以跳过本小节。
-
打开 Xcode,按下快捷键
Shift
+Command
+N
就会触发新建工程的导航窗口 -
选择 macOS -> Cocoa App,点击
Next
-
工程取名为 TextFieldKeyDemo,勾选
Use Storyboards
,点击 Next 选择合适的目录,点击create 创建 -
进入工程后,单击 Main.storyboard ,按下快捷键
Shift
+Command
+l
打开控件选择器,搜索 textfield ,将 Text Field 拖入 ViewController ,水平垂直居中放置:
运行工程
运行结果
点击快捷键 Command
+ r
运行工程,在打开的窗口中可以看到刚刚添加的 Text Field,在其中输入一些文本,然后试一下快件键,比如全选、复制、粘贴、剪切,发现都支持。
此时你会不会有疑问,我们并没有为 NSTextField 类复写 performKeyEquivalent:
方法呀,为什么文本框就支持这些快捷键了,原因是最终键盘信息消息传给了应用菜单,应用菜单中有关于文本编辑的快捷键的处理,那么问题又来了,为什么应用菜单就会对 Text Field 进行操作呢?主要两点:
-
菜单栏上编辑一组中的操作正是对应文章一开头说的几个快捷键,每个操作对应都绑定了一个对象那就是 First Responder,比如 Copy 对应的绑定 action:
-
当前活动窗口中的 First Responder 就是文本框
试验操作
现在打开工程,打开 Main.storyboard 文件,删除 菜单栏,就是下图标注的东西:
此时再次运行工程,在文本框中尝试使用快捷键,你就会发现如我们所想失效了!
快捷键加持
有时我们开发 app 就是不想保留菜单栏,比如状态栏小工具。所以继续研究怎么在没有菜单栏的情况下让 Text Field 支持快捷键是有必要的!很自然的我们就会想到上面说的为 NSTextField 复写 performKeyEquivalent:
方法的方式来解决!
这里直接贴出代码:
// NSTextField 自身支持快捷键
extension NSTextField {
open override func performKeyEquivalent(with event: NSEvent) -> Bool {
switch event.charactersIgnoringModifiers {
case "a":
return NSApp.sendAction(#selector(NSText.selectAll(_:)), to: self.window?.firstResponder, from: self)
case "c":
return NSApp.sendAction(#selector(NSText.copy(_:)), to: self.window?.firstResponder, from: self)
case "v":
return NSApp.sendAction(#selector(NSText.paste(_:)), to: self.window?.firstResponder, from: self)
case "x":
return NSApp.sendAction(#selector(NSText.cut(_:)), to: self.window?.firstResponder, from: self)
default:
return super.performKeyEquivalent(with: event)
}
}
}
上面的代码贴到合适位置即可,比如 AppDelegate.swift 文件 AppDelegate 类的后面。
可以看到上面的方法实现了四个快捷键的功能,分别是:全选、复制、粘贴和剪切,都是通过 NSApp 向文本框所在窗口的 First Responder(也就是文本框自己) 发起相应行为,而这些行为是 NStext 类的基本方法。另外:
event.charactersIgnoringModifiers
: 除去修饰键剩余的按键
charactersIgnoringModifiers: The characters generated by a key event as if no modifier key (except for Shift) applies
- 修饰键类型为
NSevent:ModifierFlags
类型,是一个集合,有 capsLock、shift、control、option、command、numericPad、help、function 和 deviceIndependentFlagsMask
运行工程,窗口中的 Text Field 支持四个快捷键了!但是此时有一个问题,将 Command
键替换为 Ctrl
按下快捷键也能实现相应文本操作,所以在 switch 语句之前加一个判断,如果修饰键不包含 Command
就按照正常响应:
if event.modifierFlags.isDisjoint(with: .command) {
return super.performKeyEquivalent(with: event)
}
event.modifierFlags.isDisjoint(with: .command)
: 判断快捷键中的修饰键(modifier)是不是不包含 Command 键,不包含就返回 true
isDisjoint: Returns a Boolean value that indicates whether the set has no members in common with the given set.
此时,还是不完美的,如果按下文本操作快捷键的同时按下了其它有效修饰键(比如 Ctrl
) 同样可以完成文本操作,目前还没想到其他更好的处理方式!后面有所发现就会更新这里!
不知道您会不会有疑问:怎么不支持撤销和重做两个快捷键?不是不做,撤销和重做 操作实现是挺大的一块内容,所以放到这个主题的第二篇文章介绍。
本文工程下载:TextFieldKeyDemo