swift - 用于复制、粘贴和剪切的 macOS SwiftUI TextEditor 键盘快捷键
问题描述
我正在为 SwiftUI 中的 macOS 菜单/状态栏制作一个应用程序,单击该应用程序会打开一个NSPopover
. 该应用程序以TextEditor
(Big Sur 中的新功能)为中心,但这TextEditor
似乎无法响应用于复制、粘贴和剪切的典型 Cmd + C/V/X 键盘快捷键。我知道TextEditors
确实支持这些快捷方式,因为如果我在 XCode 中开始一个新项目并且我没有将它放入NSPopover
(例如,我只是将它放入常规的 Mac 应用程序中),它就可以工作。复制/粘贴/剪切选项仍然出现在右键菜单中,但我不确定为什么我不能使用键盘快捷键在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
解决方案
我一直在为 a 寻找类似的解决方案,但TextField
发现了一个有点 hacky 的解决方案。对于您的情况,这是使用TextEditor
.
我试图解决的第一个问题是让 textField 成为第一响应者(弹出窗口打开时的焦点)。
这可以使用 SwiftUI-Introspect 库 ( https://github.com/timbersoftware/SwiftUI-Introspect ) 来完成,如本答案中所示TextField
( https://stackoverflow.com/a/59277051/14847761 )。同样,对于 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()
})
现在要解决剪切/复制/粘贴的主要问题,您还可以使用 Introspect。首先是NSTextField
从内部获取对 的引用TextEditor
:
.introspect(
selector: TargetViewSelector.siblingContaining,
customize: { view in
view.becomeFirstResponder()
// Extract the NSText from the NSScrollView
mainText = ((view as! NSScrollView).documentView as! NSText)
//
})
该mainText
变量必须在某处声明,但由于某种原因不能在@State
其中ContentView
,遇到了我的 TextField 的选择问题。我最终只是将它放在 swift 文件的根级别:
import SwiftUI
import Introspect
// Stick this somewhere
var mainText: NSText!
struct ContentView: View {
...
接下来是使用命令设置菜单,这是我认为没有您猜到的剪切/复制/粘贴的主要原因。向您的应用程序添加命令菜单并添加您想要的命令。
@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
...
}
这是我在 StackOverflow 上的第一个答案,所以我希望一切都说得通,而且我做得对。但我确实希望其他人能提供更好的解决方案,当我遇到这个问题时,我正在自己寻找答案。
推荐阅读
- java - 与跨越多个图块的对象发生碰撞时如何从分块地图编辑器中删除多个单元格
- python - 如何解决python中unident不匹配任何外部缩进级别的问题?
- git - 詹金斯无法使用登录名和密码创建令牌
- reactjs - 按钮在反应中使用功能组件不起作用
- android - Crashlytics android 11 设备在捕获照片和打开画廊时崩溃
- python - 为什么 Python 不读取下一行(如果)?
- firebase - 如何通过fire store文档查询文本输入是否与文档名称匹配?
- laravel - 如何验证 Laravel 请求类中的数组值?
- python - Python可变对象连接到列表
- php - [路由:投诉输入] [URI:投诉输入/{id}/{pres_id}] 缺少必需的参数 - Laravel 6