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
}
此应用程序的目的是确保用户已在 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
}