【问题标题】:macOS SwiftUI TextEditor keyboard shortcuts for copy, paste, & cut用于复制、粘贴和剪切的 macOS SwiftUI TextEditor 键盘快捷键
【发布时间】:2020-12-13 21:16:50
【问题描述】:

我正在为 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

【问题讨论】:

  • 那你在问什么?当用户从编辑菜单中选择复制/剪切/粘贴时,您想运行代码吗?考虑到编辑菜单不属于您的应用,这没有任何意义。
  • @LeoDabus 我希望用户能够使用典型的键盘快捷键 (Cmd + C/V/X) 在 TextEditor 中复制/粘贴/剪切文本。
  • 我发现了一个非常相似的问题 (stackoverflow.com/questions/49637675/…),但唯一的答案已经过时,我不确定如何为 Big Sur/最新版本的 Swift 实现它。

标签: swift macos swiftui macos-big-sur


【解决方案1】:

我一直在为TextField 寻找类似的解决方案,但发现了一个有点老套的解决方案。这是使用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。 首先是从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 变量必须在某处声明,但由于某种原因不能是 @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 上的第一个答案,所以我希望一切都说得通,而且我做得对。但我确实希望其他人能提供更好的解决方案,当我遇到这个问题时,我正在自己寻找答案。

【讨论】:

  • 很好的解决方案,效果很好。谢谢!我不知道如何让撤消/重做工作,如果可以的话请告诉我。
  • 我不得不使用.introspectTextField {…},不确定我是否最终得到了不同版本的内省库。谢谢!
  • @lionello 请在下面查看我的解决方案!
【解决方案2】:

我在构建 SwiftUI macOS 弹出应用程序时偶然发现了这个问题(和解决方案),虽然当前的解决方案是可用的,但它存在一些缺点。最大的担忧是需要让我们的ContentView 了解并响应编辑操作,这可能无法很好地适应嵌套视图和复杂的导航。

我的解决方案依赖于NSResponder 链并使用NSApplication.sendAction(_:to:from:) 发送nil 目标。支持NSTextViewNSTextField 对象都使用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 进行测试

【讨论】:

    猜你喜欢
    • 2023-03-27
    • 1970-01-01
    • 2011-01-20
    • 1970-01-01
    • 2021-11-20
    • 2020-07-17
    • 2018-11-10
    • 1970-01-01
    • 2021-05-16
    相关资源
    最近更新 更多