NSTextField 在 NSPopover 之后保持 focus/first 响应者

NSTextField keep focus/first responder after NSPopover

此应用程序的目的是确保用户已在 NSTextField 中输入特定文本。如果该文本不在该字段中,则不应允许他们离开该字段。

给定一个带有子类文本字段、一个按钮和另一个通用 NSTextField 的 macOS 应用程序。单击该按钮时,会显示一个 NSPopover,它 'attached' 到由名为 myPopoverVC 的 NSViewController 控制的字段。

例如,用户在顶部字段中输入 3,然后单击显示弹出框并提供提示的显示弹出框按钮:'What does 1 + 1 equal'。

请注意,此弹出框有一个标记为 1st resp 的字段,因此当弹出框显示时,该字段将成为第一响应者。此时不会输入任何内容 - 仅用于此问题。

用户将单击“关闭”按钮以关闭弹出窗口。届时,如果用户单击或 Tab 离开带有“3”的字段,应用程序不应允许该移动 - 可能会发出哔哔声或其他消息。但是当弹出窗口关闭并且用户按下 Tab

时会发生什么

即使带有“3”的字段 有一个焦点环 ,它应该再次指示 window 中的第一响应者,用户可以单击或远离它,因为未调用 textShouldEndEditing 函数。在这种情况下,我单击弹出窗口中的关闭按钮,“3”字段有一个焦点环,然后我点击选项卡,然后转到下一个字段。

这是子类文本字段中的函数,在将文本输入字段后可以正常工作。在这种情况下,如果用户键入 3 然后点击 Tab,光标将停留在该字段中。

override func textShouldEndEditing(_ textObject: NSText) -> Bool {

    if self.aboutToShowPopover == true {
       return true
    }

    if let editor = self.currentEditor() { //or use the textObject
        let s = editor.string

        if s == "2" {
            return true
        }

        return false
    }

showPopover 按钮代码将 aboutToShowPopover 标志设置为 true,这将允许子类显示弹出窗口。 (弹出窗口关闭时设置为 false)

那么问题来了,当popover关闭的时候,如何将firstResponder状态return到原来的text field?它似乎具有第一响应者状态,并且它认为它具有该状态,尽管未调用 textShouldEndEditing。 如果您在字段中输入另一个字符,那么一切都会正常进行。就好像 window 的字段编辑器和其中带有“3”的字段断开连接,因此字段编辑器不会将调用传递到该字段。

该按钮调用一个包含以下内容的函数:

    let contentSize = myPopoverVC.view.frame
    theTextField.aboutToShowPopover = true
    parentVC.present(myPopoverVC, asPopoverRelativeTo: contentSize, of: theTextField, preferredEdge: NSRectEdge.maxY, behavior: NSPopover.Behavior.applicationDefined)
    NSApplication.shared.activate(ignoringOtherApps: true)

NSPopover 关闭是

parentVC.dismiss(myPopoverVC)

另一条信息。我将这段代码添加到子类化的 NSTextField 控件中。

override func becomeFirstResponder() -> Bool {
    let e = self.currentEditor()
    print(e)
    return super.becomeFirstResponder()
}

当弹出窗口关闭并且 textField 成为 windows 第一响应者时,该代码将执行但打印 nil。这表明虽然它是第一响应者,但它与 window fieldEditor 没有连接,也不会接收事件。为什么?

有什么不明白的请追问

如果你子class你的每个NSTextField,你可以覆盖方法becomeFirstResponder并让它发送self到您将创建的委托 class,它将保留对当前第一响应者的引用:

NSTextField 超级class:

override func becomeFirstResponder() -> Bool {
        self.myRespondersDelegate.setCurrentResponder(self)
        return super.becomeFirstResponder()
    }

(myRespondersDelegate: 可以选择是你的 NSViewController)

注意:不要对警报文本字段和 ViewController 文本字段使用相同的超级class。使用此 superclass 和附加功能仅适用于您希望在警报关闭后 return 到 firstResponder 的 TextFields。

NSTextField 委托:

class MyViewController: NSViewController, MyFirstResponderDelegate {
    var currentFirstResponderTextField: NSTextField?

    func setCurrentResponder(textField: NSTextField) {
        self.currentFirstResponderTextField = textField
    }
}

现在,在关闭弹出窗口后,您可以在 viewWillAppear 中或创建一个将在关闭弹出窗口时调用的委托函数 didDismisss(取决于你的弹出窗口是如何实现的,我会展示委托选项) 检查 TextField 是否存在,并重新创建 firstResponder.

弹出委托:

class MyViewController: NSViewController, MyFirstResponderDelegate, MyPopUpDismissDelegate {
    var currentFirstResponderTextField: NSTextField?

    func setCurrentResponder(textField: NSTextField) {
        self.currentFirstResponderTextField = textField
    }

    func didDismisssPopUp() {
        guard let isLastTextField = self.currentFirstResponderTextField else  {
            return
        }
        self.isLastTextField?.window?.makeFirstResponder(self.isLastTextField)
    }
}

希望有用。

这是我在 How can one programatically begin a text editing session in a NSTextField? and 的帮助下所做的尝试:

所选范围保存在textShouldEndEditing中并在becomeFirstResponder中恢复。 insertText(_:replacementRange:) 开始编辑会话。

var savedSelectedRanges: [NSValue]?

override func becomeFirstResponder() -> Bool {
    if super.becomeFirstResponder() {
        if self.aboutToShowPopover {
            if let ranges = self.savedSelectedRanges {
                if let fieldEditor = self.currentEditor() as? NSTextView {
                    fieldEditor.insertText("", replacementRange: NSRange(location: 0, length:0))
                    fieldEditor.selectedRanges = ranges
                }
            }
        }
        return true
    }
    return false
}

override func textShouldEndEditing(_ textObject: NSText) -> Bool {
    if super.textShouldEndEditing(textObject) {
        if self.aboutToShowPopover {
            let fieldEditor = textObject as! NSTextView
            self.savedSelectedRanges = fieldEditor.selectedRanges
            return true
        }
        let s = textObject.string
        if s == "2" {
            return true
        }
    }
    return false
}

也许重命名 aboutToShowPopover

非常感谢 Willeke 的帮助和回答,这导致了一个非常简单的解决方案。

这里的大问题是当弹出窗口关闭时,'focused' 字段是原始字段。但是,似乎(出于某种原因)windows 字段编辑器委托与该字段断开连接,因此 control:textShouldEndEditing 等函数未传递到问题中的子类字段。

当字段成为第一个响应者时执行此行似乎重新连接 windows 字段编辑器与此字段,因此它将接收委托消息

fieldEditor.insertText("", replacementRange: range)

所以最终的解决方案是结合以下两个功能。

override func textShouldEndEditing(_ textObject: NSText) -> Bool {

    if self.aboutToShowPopover == true {
        return true
    }

    let s = textObject.string

    if s == "2" {
        return true
    }

    return false
}

override func becomeFirstResponder() -> Bool {

    if super.becomeFirstResponder() == true {
        if let myEditor = self.currentEditor() as? NSTextView {
            let range = NSMakeRange(0, 0)
            myEditor.insertText("", replacementRange: range)
        }
        return true
    }

    return false
}