guard devtools restore from unsafe first-responder churn
This commit is contained in:
parent
71d468255e
commit
9140371fcd
3 changed files with 60 additions and 1 deletions
|
|
@ -6691,8 +6691,22 @@ private var cmuxFirstResponderGuardCurrentEventOverride: NSEvent?
|
|||
private var cmuxFirstResponderGuardHitViewOverride: NSView?
|
||||
#endif
|
||||
private var cmuxBrowserReturnForwardingDepth = 0
|
||||
private var cmuxWindowFirstResponderBypassDepth = 0
|
||||
private var cmuxFieldEditorOwningWebViewAssociationKey: UInt8 = 0
|
||||
|
||||
@discardableResult
|
||||
func cmuxWithWindowFirstResponderBypass<T>(_ body: () -> T) -> T {
|
||||
cmuxWindowFirstResponderBypassDepth += 1
|
||||
defer {
|
||||
cmuxWindowFirstResponderBypassDepth = max(0, cmuxWindowFirstResponderBypassDepth - 1)
|
||||
}
|
||||
return body()
|
||||
}
|
||||
|
||||
func cmuxIsWindowFirstResponderBypassActive() -> Bool {
|
||||
cmuxWindowFirstResponderBypassDepth > 0
|
||||
}
|
||||
|
||||
private final class CmuxFieldEditorOwningWebViewBox: NSObject {
|
||||
weak var webView: CmuxWebView?
|
||||
|
||||
|
|
@ -6703,6 +6717,16 @@ private final class CmuxFieldEditorOwningWebViewBox: NSObject {
|
|||
|
||||
private extension NSWindow {
|
||||
@objc func cmux_makeFirstResponder(_ responder: NSResponder?) -> Bool {
|
||||
if cmuxIsWindowFirstResponderBypassActive() {
|
||||
#if DEBUG
|
||||
dlog(
|
||||
"focus.guard bypassFirstResponder responder=\(String(describing: responder.map { type(of: $0) })) " +
|
||||
"window=\(ObjectIdentifier(self))"
|
||||
)
|
||||
#endif
|
||||
return false
|
||||
}
|
||||
|
||||
let currentEvent = Self.cmuxCurrentEvent(for: self)
|
||||
let responderWebView = responder.flatMap {
|
||||
Self.cmuxOwningWebView(for: $0, in: self, event: currentEvent)
|
||||
|
|
|
|||
|
|
@ -2150,7 +2150,12 @@ extension BrowserPanel {
|
|||
dlog("browser.devtools refresh.forceShowWhenHidden panel=\(id.uuidString.prefix(5)) \(debugDeveloperToolsStateSummary())")
|
||||
}
|
||||
#endif
|
||||
inspector.cmuxCallVoid(selector: selector)
|
||||
// WebKit inspector "show" can trigger transient first-responder churn while
|
||||
// panel attachment is still stabilizing. Keep this auto-restore path from
|
||||
// mutating first responder so AppKit doesn't walk tearing-down responder chains.
|
||||
cmuxWithWindowFirstResponderBypass {
|
||||
inspector.cmuxCallVoid(selector: selector)
|
||||
}
|
||||
preferredDeveloperToolsVisible = true
|
||||
let visibleAfterShow = inspector.cmuxCallBool(selector: NSSelectorFromString("isVisible")) ?? false
|
||||
if visibleAfterShow {
|
||||
|
|
|
|||
|
|
@ -500,6 +500,36 @@ final class CmuxWebViewKeyEquivalentTests: XCTestCase {
|
|||
)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testWindowFirstResponderBypassBlocksSwizzledMakeFirstResponder() {
|
||||
_ = NSApplication.shared
|
||||
AppDelegate.installWindowResponderSwizzlesForTesting()
|
||||
|
||||
let window = NSWindow(
|
||||
contentRect: NSRect(x: 0, y: 0, width: 640, height: 420),
|
||||
styleMask: [.titled, .closable],
|
||||
backing: .buffered,
|
||||
defer: false
|
||||
)
|
||||
let container = NSView(frame: window.contentRect(forFrameRect: window.frame))
|
||||
window.contentView = container
|
||||
|
||||
let responder = FirstResponderView(frame: NSRect(x: 0, y: 0, width: 80, height: 40))
|
||||
container.addSubview(responder)
|
||||
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
defer { window.orderOut(nil) }
|
||||
|
||||
_ = window.makeFirstResponder(nil)
|
||||
cmuxWithWindowFirstResponderBypass {
|
||||
XCTAssertFalse(
|
||||
window.makeFirstResponder(responder),
|
||||
"Bypass scope should block transient first-responder changes during devtools auto-restore"
|
||||
)
|
||||
}
|
||||
XCTAssertTrue(window.makeFirstResponder(responder))
|
||||
}
|
||||
|
||||
private func installMenu(spy: ActionSpy, key: String, modifiers: NSEvent.ModifierFlags) {
|
||||
let mainMenu = NSMenu()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue