用于复制、粘贴和剪切的 macOS SwiftUI TextEditor 键盘快捷键
macOS SwiftUI TextEditor keyboard shortcuts for copy, paste, & cut
我正在为 SwiftUI 中的 macOS menu/status 栏制作一个应用程序,当点击它时,会打开一个 NSPopover
。该应用程序以 TextEditor
(Big Sur 中的新功能)为中心,但 TextEditor
似乎不响应用于复制、粘贴和剪切的典型 Cmd + C/V/X 键盘快捷键.我知道 TextEditors
确实支持这些快捷方式,因为如果我在 XCode 中开始一个新项目并且我不把它放在 NSPopover
中(我只是把它放在常规 Mac 应用程序,例如),它有效。 copy/paste/cut 选项仍然出现在右键菜单中,但我不确定为什么我不能在 NSPopover
.
中使用键盘快捷键访问它们
我认为这与以下事实有关:当您单击打开弹出窗口时,macOS 不会“聚焦”应用程序。通常,当您打开应用程序时,您会在 Mac 菜单栏的左上角(Apple 徽标旁边)看到应用程序名称和相关菜单选项。我的应用程序不这样做(大概是因为它是一个弹出窗口)。
相关代码如下:
ContentView.swift 中的文本编辑器:
TextEditor(text: $userData.note)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding(10)
.font(.body)
.background(Color(red: 30 / 255, green: 30 / 255, blue: 30 / 255))
NotedApp.swift 中的 NSPopover 逻辑:
@main
struct MenuBarPopoverApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
Settings{
EmptyView()
}
}
}
class AppDelegate: NSObject, NSApplicationDelegate {
var popover = NSPopover.init()
var statusBarItem: NSStatusItem?
func applicationDidFinishLaunching(_ notification: Notification) {
let contentView = ContentView()
popover.behavior = .transient
popover.animates = false
popover.contentViewController = NSViewController()
popover.contentViewController?.view = NSHostingView(rootView: contentView)
statusBarItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
statusBarItem?.button?.title = "Noted"
statusBarItem?.button?.action = #selector(AppDelegate.togglePopover(_:))
}
@objc func showPopover(_ sender: AnyObject?) {
if let button = statusBarItem?.button {
popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
}
}
@objc func closePopover(_ sender: AnyObject?) {
popover.performClose(sender)
}
@objc func togglePopover(_ sender: AnyObject?) {
if popover.isShown {
closePopover(sender)
} else {
showPopover(sender)
}
}
}
您可以在此处的 GitHub 存储库中找到整个应用程序:https://github.com/R-Taneja/Noted
我正在为 TextField
寻找类似的解决方案,但发现了一个有点老套的解决方案。这是使用 TextEditor
.
适用于您的情况的类似方法
我试图解决的第一个问题是使 textField 成为第一响应者(弹出窗口打开时聚焦)。
这可以使用 SwiftUI-Introspect 库 (https://github.com/timbersoftware/SwiftUI-Introspect) as seen in this answer for a TextField
() 完成。
同样,对于 TextEditor,您可以执行以下操作:
TextEditor(text: $userData.note)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding(10)
.font(.body)
.background(Color(red: 30 / 255, green: 30 / 255, blue: 30 / 255))
.introspect(
selector: TargetViewSelector.siblingContaining,
customize: { view in
view.becomeFirstResponder()
})
现在开始 cut/copy/paste 的主要问题,您也可以使用 Introspect。
第一件事是从 TextEditor
:
内部获取对 NSTextField
的引用
.introspect(
selector: TargetViewSelector.siblingContaining,
customize: { view in
view.becomeFirstResponder()
// Extract the NSText from the NSScrollView
mainText = ((view as! NSScrollView).documentView as! NSText)
//
})
mainText
变量必须在某处声明,但由于某种原因不能是 ContentView
内部的 @State
,运行 成为我的 TextField 的选择问题。我最后只是把它放在 swift 文件的根目录下:
import SwiftUI
import Introspect
// Stick this somewhere
var mainText: NSText!
struct ContentView: View {
...
接下来是用命令设置菜单,这是我认为没有你猜的cut/copy/paste的主要原因。
将命令菜单添加到您的应用程序并添加您想要的命令。
@main
struct MenuBarPopoverApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
Settings{
EmptyView()
}
.commands {
MenuBarPopoverCommands(appDelegate: appDelegate)
}
}
}
struct MenuBarPopoverCommands: Commands {
let appDelegate: AppDelegate
init(appDelegate: AppDelegate) {
self.appDelegate = appDelegate
}
var body: some Commands {
CommandMenu("Edit"){ // Doesn't need to be Edit
Section {
Button("Cut") {
appDelegate.contentView.editCut()
}.keyboardShortcut(KeyEquivalent("x"), modifiers: .command)
Button("Copy") {
appDelegate.contentView.editCopy()
}.keyboardShortcut(KeyEquivalent("c"), modifiers: .command)
Button("Paste") {
appDelegate.contentView.editPaste()
}.keyboardShortcut(KeyEquivalent("v"), modifiers: .command)
// Might also want this
Button("Select All") {
appDelegate.contentView.editSelectAll()
}.keyboardShortcut(KeyEquivalent("a"), modifiers: .command)
}
}
}
}
还需要使 contentView
可访问:
class AppDelegate: NSObject, NSApplicationDelegate {
var popover = NSPopover.init()
var statusBarItem: NSStatusItem?
// making this a class variable
var contentView: ContentView!
func applicationDidFinishLaunching(_ notification: Notification) {
// assign here
contentView = ContentView()
...
最后是实际命令。
struct ContentView: View {
...
func editCut() {
mainText?.cut(self)
}
func editCopy() {
mainText?.copy(self)
}
func editPaste() {
mainText?.paste(self)
}
func editSelectAll() {
mainText?.selectAll(self)
}
// Could also probably add undo/redo in a similar way but I haven't tried
...
}
这是我在 Whosebug 上的第一个答案,所以我希望一切都说得通,而且我做对了。但我确实希望其他人能提出更好的解决方案,当我遇到这个问题时,我正在寻找答案。
我在构建 SwiftUI macOS popover 应用程序时偶然发现了这个问题(和解决方案),虽然当前的解决方案可用,但它存在一些缺点。最大的担忧是需要让我们的 ContentView
了解并响应编辑操作,这可能无法很好地扩展嵌套视图和复杂的导航。
我的解决方案依赖于 NSResponder
链并使用 NSApplication.sendAction(_:to:from:) 发送 nil
目标。后备 NSTextView
和 NSTextField
对象都使用 NSText
并且当这些对象中的任何一个是第一响应者时,消息将传递给它们。
我已经确认以下工作具有复杂的层次结构,并提供了 NSText
上可用的所有文本编辑方法。
菜单示例
@main
struct MyApp: App {
var body: some Scene {
Settings {
EmptyView()
}
.commands {
CommandMenu("Edit") {
Section {
// MARK: - `Select All` -
Button("Select All") {
NSApp.sendAction(#selector(NSText.selectAll(_:)), to: nil, from: nil)
}
.keyboardShortcut(.a)
// MARK: - `Cut` -
Button("Cut") {
NSApp.sendAction(#selector(NSText.cut(_:)), to: nil, from: nil)
}
.keyboardShortcut(.x)
// MARK: - `Copy` -
Button("Copy") {
NSApp.sendAction(#selector(NSText.copy(_:)), to: nil, from: nil)
}
.keyboardShortcut(.c)
// MARK: - `Paste` -
Button("Paste") {
NSApp.sendAction(#selector(NSText.paste(_:)), to: nil, from: nil)
}
.keyboardShortcut(.v)
}
}
}
}
键盘事件修饰符(不需要)
这只是一个方便的修饰符,并不是实现有效解决方案所必需的。
// MARK: - `Modifiers` -
fileprivate struct KeyboardEventModifier: ViewModifier {
enum Key: String {
case a, c, v, x
}
let key: Key
let modifiers: EventModifiers
func body(content: Content) -> some View {
content.keyboardShortcut(KeyEquivalent(Character(key.rawValue)), modifiers: modifiers)
}
}
extension View {
fileprivate func keyboardShortcut(_ key: KeyboardEventModifier.Key, modifiers: EventModifiers = .command) -> some View {
modifier(KeyboardEventModifier(key: key, modifiers: modifiers))
}
}
希望这有助于为其他人解决此问题!
在 macOS Monterrey 12.1 上使用 SwiftUI 2.0 进行测试
我正在为 SwiftUI 中的 macOS menu/status 栏制作一个应用程序,当点击它时,会打开一个 NSPopover
。该应用程序以 TextEditor
(Big Sur 中的新功能)为中心,但 TextEditor
似乎不响应用于复制、粘贴和剪切的典型 Cmd + C/V/X 键盘快捷键.我知道 TextEditors
确实支持这些快捷方式,因为如果我在 XCode 中开始一个新项目并且我不把它放在 NSPopover
中(我只是把它放在常规 Mac 应用程序,例如),它有效。 copy/paste/cut 选项仍然出现在右键菜单中,但我不确定为什么我不能在 NSPopover
.
我认为这与以下事实有关:当您单击打开弹出窗口时,macOS 不会“聚焦”应用程序。通常,当您打开应用程序时,您会在 Mac 菜单栏的左上角(Apple 徽标旁边)看到应用程序名称和相关菜单选项。我的应用程序不这样做(大概是因为它是一个弹出窗口)。
相关代码如下:
ContentView.swift 中的文本编辑器:
TextEditor(text: $userData.note)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding(10)
.font(.body)
.background(Color(red: 30 / 255, green: 30 / 255, blue: 30 / 255))
NotedApp.swift 中的 NSPopover 逻辑:
@main
struct MenuBarPopoverApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
Settings{
EmptyView()
}
}
}
class AppDelegate: NSObject, NSApplicationDelegate {
var popover = NSPopover.init()
var statusBarItem: NSStatusItem?
func applicationDidFinishLaunching(_ notification: Notification) {
let contentView = ContentView()
popover.behavior = .transient
popover.animates = false
popover.contentViewController = NSViewController()
popover.contentViewController?.view = NSHostingView(rootView: contentView)
statusBarItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
statusBarItem?.button?.title = "Noted"
statusBarItem?.button?.action = #selector(AppDelegate.togglePopover(_:))
}
@objc func showPopover(_ sender: AnyObject?) {
if let button = statusBarItem?.button {
popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
}
}
@objc func closePopover(_ sender: AnyObject?) {
popover.performClose(sender)
}
@objc func togglePopover(_ sender: AnyObject?) {
if popover.isShown {
closePopover(sender)
} else {
showPopover(sender)
}
}
}
您可以在此处的 GitHub 存储库中找到整个应用程序:https://github.com/R-Taneja/Noted
我正在为 TextField
寻找类似的解决方案,但发现了一个有点老套的解决方案。这是使用 TextEditor
.
我试图解决的第一个问题是使 textField 成为第一响应者(弹出窗口打开时聚焦)。
这可以使用 SwiftUI-Introspect 库 (https://github.com/timbersoftware/SwiftUI-Introspect) as seen in this answer for a TextField
(
TextEditor(text: $userData.note)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding(10)
.font(.body)
.background(Color(red: 30 / 255, green: 30 / 255, blue: 30 / 255))
.introspect(
selector: TargetViewSelector.siblingContaining,
customize: { view in
view.becomeFirstResponder()
})
现在开始 cut/copy/paste 的主要问题,您也可以使用 Introspect。
第一件事是从 TextEditor
:
NSTextField
的引用
.introspect(
selector: TargetViewSelector.siblingContaining,
customize: { view in
view.becomeFirstResponder()
// Extract the NSText from the NSScrollView
mainText = ((view as! NSScrollView).documentView as! NSText)
//
})
mainText
变量必须在某处声明,但由于某种原因不能是 ContentView
内部的 @State
,运行 成为我的 TextField 的选择问题。我最后只是把它放在 swift 文件的根目录下:
import SwiftUI
import Introspect
// Stick this somewhere
var mainText: NSText!
struct ContentView: View {
...
接下来是用命令设置菜单,这是我认为没有你猜的cut/copy/paste的主要原因。 将命令菜单添加到您的应用程序并添加您想要的命令。
@main
struct MenuBarPopoverApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
Settings{
EmptyView()
}
.commands {
MenuBarPopoverCommands(appDelegate: appDelegate)
}
}
}
struct MenuBarPopoverCommands: Commands {
let appDelegate: AppDelegate
init(appDelegate: AppDelegate) {
self.appDelegate = appDelegate
}
var body: some Commands {
CommandMenu("Edit"){ // Doesn't need to be Edit
Section {
Button("Cut") {
appDelegate.contentView.editCut()
}.keyboardShortcut(KeyEquivalent("x"), modifiers: .command)
Button("Copy") {
appDelegate.contentView.editCopy()
}.keyboardShortcut(KeyEquivalent("c"), modifiers: .command)
Button("Paste") {
appDelegate.contentView.editPaste()
}.keyboardShortcut(KeyEquivalent("v"), modifiers: .command)
// Might also want this
Button("Select All") {
appDelegate.contentView.editSelectAll()
}.keyboardShortcut(KeyEquivalent("a"), modifiers: .command)
}
}
}
}
还需要使 contentView
可访问:
class AppDelegate: NSObject, NSApplicationDelegate {
var popover = NSPopover.init()
var statusBarItem: NSStatusItem?
// making this a class variable
var contentView: ContentView!
func applicationDidFinishLaunching(_ notification: Notification) {
// assign here
contentView = ContentView()
...
最后是实际命令。
struct ContentView: View {
...
func editCut() {
mainText?.cut(self)
}
func editCopy() {
mainText?.copy(self)
}
func editPaste() {
mainText?.paste(self)
}
func editSelectAll() {
mainText?.selectAll(self)
}
// Could also probably add undo/redo in a similar way but I haven't tried
...
}
这是我在 Whosebug 上的第一个答案,所以我希望一切都说得通,而且我做对了。但我确实希望其他人能提出更好的解决方案,当我遇到这个问题时,我正在寻找答案。
我在构建 SwiftUI macOS popover 应用程序时偶然发现了这个问题(和解决方案),虽然当前的解决方案可用,但它存在一些缺点。最大的担忧是需要让我们的 ContentView
了解并响应编辑操作,这可能无法很好地扩展嵌套视图和复杂的导航。
我的解决方案依赖于 NSResponder
链并使用 NSApplication.sendAction(_:to:from:) 发送 nil
目标。后备 NSTextView
和 NSTextField
对象都使用 NSText
并且当这些对象中的任何一个是第一响应者时,消息将传递给它们。
我已经确认以下工作具有复杂的层次结构,并提供了 NSText
上可用的所有文本编辑方法。
菜单示例
@main
struct MyApp: App {
var body: some Scene {
Settings {
EmptyView()
}
.commands {
CommandMenu("Edit") {
Section {
// MARK: - `Select All` -
Button("Select All") {
NSApp.sendAction(#selector(NSText.selectAll(_:)), to: nil, from: nil)
}
.keyboardShortcut(.a)
// MARK: - `Cut` -
Button("Cut") {
NSApp.sendAction(#selector(NSText.cut(_:)), to: nil, from: nil)
}
.keyboardShortcut(.x)
// MARK: - `Copy` -
Button("Copy") {
NSApp.sendAction(#selector(NSText.copy(_:)), to: nil, from: nil)
}
.keyboardShortcut(.c)
// MARK: - `Paste` -
Button("Paste") {
NSApp.sendAction(#selector(NSText.paste(_:)), to: nil, from: nil)
}
.keyboardShortcut(.v)
}
}
}
}
键盘事件修饰符(不需要)
这只是一个方便的修饰符,并不是实现有效解决方案所必需的。
// MARK: - `Modifiers` -
fileprivate struct KeyboardEventModifier: ViewModifier {
enum Key: String {
case a, c, v, x
}
let key: Key
let modifiers: EventModifiers
func body(content: Content) -> some View {
content.keyboardShortcut(KeyEquivalent(Character(key.rawValue)), modifiers: modifiers)
}
}
extension View {
fileprivate func keyboardShortcut(_ key: KeyboardEventModifier.Key, modifiers: EventModifiers = .command) -> some View {
modifier(KeyboardEventModifier(key: key, modifiers: modifiers))
}
}
希望这有助于为其他人解决此问题!
在 macOS Monterrey 12.1 上使用 SwiftUI 2.0 进行测试