Add DEBUG logging for browser omnibar/content focus handoff (#910)
* Add browser focus debug logging around omnibar/content handoff * Avoid release-only unused-value warnings in focus debug logs * Add omnibar focus writer trace logs * Fix omnibar tap focus race * Stabilize omnibar focus state transitions * Propagate event context into responder guard * Fix webview pointer hit testing in focus guard * Stop omnibar reacquire on pointer blur intent * Blur omnibar on webview click intent * Preserve pointer intent through webview focus handoff * Restore page input focus after omnibar escape * Fix omnibar escape focus handoff and restore retry * Track editable focus for omnibar restore and improve test diagnostics * Add omnibar focus tracker telemetry to failing UI test * Wait for page readiness before seeding focused input in UI test setup * Strengthen omnibar escape focus regression with post-click assertion * Use deterministic window offsets for post-escape web input click test * Always enforce webview responder on omnibar escape * Harden omnibar focus restore and address PR review feedback --------- Co-authored-by: tiffanysun1 <tiffanysun8@gmail.com>
This commit is contained in:
parent
58bcc929b2
commit
e680f1de55
6 changed files with 1659 additions and 71 deletions
|
|
@ -5510,7 +5510,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
}
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
|
||||
guard let self else { return }
|
||||
guard self != nil else { return }
|
||||
runSetupWhenWindowReady()
|
||||
}
|
||||
}
|
||||
|
|
@ -5607,6 +5607,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
"ghosttyGotoSplitDownShortcut": ghosttyGotoSplitDownShortcut?.displayString ?? "",
|
||||
"webViewFocused": "true"
|
||||
])
|
||||
if ProcessInfo.processInfo.environment["CMUX_UI_TEST_GOTO_SPLIT_INPUT_SETUP"] == "1" {
|
||||
setupFocusedInputForGotoSplitUITest(panel: browserPanel)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -5652,6 +5655,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
guard let self else { return }
|
||||
guard let panelId = notification.object as? UUID else { return }
|
||||
self.recordGotoSplitUITestWebViewFocus(panelId: panelId, key: "webViewFocusedAfterAddressBarFocus")
|
||||
self.recordGotoSplitUITestActiveElement(panelId: panelId, keyPrefix: "addressBarFocus")
|
||||
})
|
||||
|
||||
gotoSplitUITestObservers.append(NotificationCenter.default.addObserver(
|
||||
|
|
@ -5662,6 +5666,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
guard let self else { return }
|
||||
guard let panelId = notification.object as? UUID else { return }
|
||||
self.recordGotoSplitUITestWebViewFocus(panelId: panelId, key: "webViewFocusedAfterAddressBarExit")
|
||||
self.recordGotoSplitUITestActiveElement(panelId: panelId, keyPrefix: "addressBarExit")
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -5689,6 +5694,329 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
}
|
||||
}
|
||||
|
||||
private func setupFocusedInputForGotoSplitUITest(panel: BrowserPanel, attempt: Int = 0) {
|
||||
let maxAttempts = 80
|
||||
guard attempt < maxAttempts else {
|
||||
writeGotoSplitTestData([
|
||||
"webInputFocusSeeded": "false",
|
||||
"setupError": "Timed out focusing page input for omnibar restore test"
|
||||
])
|
||||
return
|
||||
}
|
||||
|
||||
let script = """
|
||||
(() => {
|
||||
try {
|
||||
const trackerInstalled = window.__cmuxAddressBarFocusTrackerInstalled === true;
|
||||
const readyState = String(document.readyState || "");
|
||||
if (!trackerInstalled || readyState !== "complete") {
|
||||
const active = document.activeElement;
|
||||
return {
|
||||
focused: false,
|
||||
id: "",
|
||||
activeId: active && typeof active.id === "string" ? active.id : "",
|
||||
activeTag: active && active.tagName ? active.tagName.toLowerCase() : "",
|
||||
trackerInstalled,
|
||||
trackedStateId:
|
||||
window.__cmuxAddressBarFocusState &&
|
||||
typeof window.__cmuxAddressBarFocusState.id === "string"
|
||||
? window.__cmuxAddressBarFocusState.id
|
||||
: "",
|
||||
readyState
|
||||
};
|
||||
}
|
||||
|
||||
const ensureInput = (id, value) => {
|
||||
const existing = document.getElementById(id);
|
||||
const input = (existing && existing.tagName && existing.tagName.toLowerCase() === "input")
|
||||
? existing
|
||||
: (() => {
|
||||
const created = document.createElement("input");
|
||||
created.id = id;
|
||||
created.type = "text";
|
||||
created.value = value;
|
||||
return created;
|
||||
})();
|
||||
input.autocapitalize = "off";
|
||||
input.autocomplete = "off";
|
||||
input.spellcheck = false;
|
||||
input.style.display = "block";
|
||||
input.style.width = "100%";
|
||||
input.style.margin = "0";
|
||||
input.style.padding = "8px 10px";
|
||||
input.style.border = "1px solid #5f6368";
|
||||
input.style.borderRadius = "6px";
|
||||
input.style.boxSizing = "border-box";
|
||||
input.style.fontSize = "14px";
|
||||
input.style.fontFamily = "system-ui, -apple-system, sans-serif";
|
||||
input.style.background = "white";
|
||||
input.style.color = "black";
|
||||
return input;
|
||||
};
|
||||
|
||||
let container = document.getElementById("cmux-ui-test-focus-container");
|
||||
if (!container || !container.tagName || container.tagName.toLowerCase() !== "div") {
|
||||
container = document.createElement("div");
|
||||
container.id = "cmux-ui-test-focus-container";
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
container.style.position = "fixed";
|
||||
container.style.left = "24px";
|
||||
container.style.top = "24px";
|
||||
container.style.width = "min(520px, calc(100vw - 48px))";
|
||||
container.style.display = "grid";
|
||||
container.style.rowGap = "12px";
|
||||
container.style.padding = "12px";
|
||||
container.style.background = "rgba(255,255,255,0.92)";
|
||||
container.style.border = "1px solid rgba(95,99,104,0.55)";
|
||||
container.style.borderRadius = "8px";
|
||||
container.style.boxShadow = "0 2px 10px rgba(0,0,0,0.2)";
|
||||
container.style.zIndex = "2147483647";
|
||||
|
||||
const input = ensureInput("cmux-ui-test-focus-input", "cmux-ui-focus-primary");
|
||||
const secondaryInput = ensureInput("cmux-ui-test-focus-input-secondary", "cmux-ui-focus-secondary");
|
||||
if (input.parentElement !== container) {
|
||||
container.appendChild(input);
|
||||
}
|
||||
if (secondaryInput.parentElement !== container) {
|
||||
container.appendChild(secondaryInput);
|
||||
}
|
||||
|
||||
input.focus({ preventScroll: true });
|
||||
if (typeof input.setSelectionRange === "function") {
|
||||
const end = input.value.length;
|
||||
input.setSelectionRange(end, end);
|
||||
}
|
||||
|
||||
let trackedFocusId = input.getAttribute("data-cmux-addressbar-focus-id");
|
||||
if (!trackedFocusId) {
|
||||
trackedFocusId = "cmux-ui-test-focus-input-tracked";
|
||||
input.setAttribute("data-cmux-addressbar-focus-id", trackedFocusId);
|
||||
}
|
||||
const selectionStart = typeof input.selectionStart === "number" ? input.selectionStart : null;
|
||||
const selectionEnd = typeof input.selectionEnd === "number" ? input.selectionEnd : null;
|
||||
if (
|
||||
!window.__cmuxAddressBarFocusState ||
|
||||
typeof window.__cmuxAddressBarFocusState.id !== "string" ||
|
||||
window.__cmuxAddressBarFocusState.id !== trackedFocusId
|
||||
) {
|
||||
window.__cmuxAddressBarFocusState = { id: trackedFocusId, selectionStart, selectionEnd };
|
||||
}
|
||||
|
||||
const secondaryRect = secondaryInput.getBoundingClientRect();
|
||||
const viewportWidth = Math.max(Number(window.innerWidth) || 0, 1);
|
||||
const viewportHeight = Math.max(Number(window.innerHeight) || 0, 1);
|
||||
const secondaryCenterX = Math.min(
|
||||
0.98,
|
||||
Math.max(0.02, (secondaryRect.left + (secondaryRect.width / 2)) / viewportWidth)
|
||||
);
|
||||
const secondaryCenterY = Math.min(
|
||||
0.98,
|
||||
Math.max(0.02, (secondaryRect.top + (secondaryRect.height / 2)) / viewportHeight)
|
||||
);
|
||||
const active = document.activeElement;
|
||||
return {
|
||||
focused: active === input,
|
||||
id: input.id || "",
|
||||
secondaryId: secondaryInput.id || "",
|
||||
secondaryCenterX,
|
||||
secondaryCenterY,
|
||||
activeId: active && typeof active.id === "string" ? active.id : "",
|
||||
activeTag: active && active.tagName ? active.tagName.toLowerCase() : "",
|
||||
trackerInstalled,
|
||||
trackedStateId:
|
||||
window.__cmuxAddressBarFocusState &&
|
||||
typeof window.__cmuxAddressBarFocusState.id === "string"
|
||||
? window.__cmuxAddressBarFocusState.id
|
||||
: "",
|
||||
readyState
|
||||
};
|
||||
} catch (_) {
|
||||
return {
|
||||
focused: false,
|
||||
id: "",
|
||||
secondaryId: "",
|
||||
secondaryCenterX: -1,
|
||||
secondaryCenterY: -1,
|
||||
activeId: "",
|
||||
activeTag: "",
|
||||
trackerInstalled: false,
|
||||
trackedStateId: "",
|
||||
readyState: ""
|
||||
};
|
||||
}
|
||||
})();
|
||||
"""
|
||||
|
||||
panel.webView.evaluateJavaScript(script) { [weak self] result, _ in
|
||||
guard let self else { return }
|
||||
let payload = result as? [String: Any]
|
||||
let focused = (payload?["focused"] as? Bool) ?? false
|
||||
let inputId = (payload?["id"] as? String) ?? ""
|
||||
let secondaryInputId = (payload?["secondaryId"] as? String) ?? ""
|
||||
let secondaryCenterX = (payload?["secondaryCenterX"] as? NSNumber)?.doubleValue ?? -1
|
||||
let secondaryCenterY = (payload?["secondaryCenterY"] as? NSNumber)?.doubleValue ?? -1
|
||||
let activeId = (payload?["activeId"] as? String) ?? ""
|
||||
let trackerInstalled = (payload?["trackerInstalled"] as? Bool) ?? false
|
||||
let trackedStateId = (payload?["trackedStateId"] as? String) ?? ""
|
||||
let readyState = (payload?["readyState"] as? String) ?? ""
|
||||
var secondaryClickOffsetX = -1.0
|
||||
var secondaryClickOffsetY = -1.0
|
||||
if let window = panel.webView.window {
|
||||
let webFrame = panel.webView.convert(panel.webView.bounds, to: nil)
|
||||
let contentHeight = Double(window.contentView?.bounds.height ?? 0)
|
||||
if webFrame.width > 1,
|
||||
webFrame.height > 1,
|
||||
contentHeight > 1,
|
||||
secondaryCenterX > 0,
|
||||
secondaryCenterX < 1,
|
||||
secondaryCenterY > 0,
|
||||
secondaryCenterY < 1 {
|
||||
let xInContent = Double(webFrame.minX) + (secondaryCenterX * Double(webFrame.width))
|
||||
let yFromTopInWeb = secondaryCenterY * Double(webFrame.height)
|
||||
let yInContent = Double(webFrame.maxY) - yFromTopInWeb
|
||||
let yFromTopInContent = contentHeight - yInContent
|
||||
let titlebarHeight = max(0, Double(window.frame.height) - contentHeight)
|
||||
secondaryClickOffsetX = xInContent
|
||||
secondaryClickOffsetY = titlebarHeight + yFromTopInContent
|
||||
}
|
||||
}
|
||||
if focused,
|
||||
!inputId.isEmpty,
|
||||
!secondaryInputId.isEmpty,
|
||||
inputId == activeId,
|
||||
trackerInstalled,
|
||||
!trackedStateId.isEmpty,
|
||||
secondaryCenterX > 0,
|
||||
secondaryCenterX < 1,
|
||||
secondaryCenterY > 0,
|
||||
secondaryCenterY < 1,
|
||||
secondaryClickOffsetX > 0,
|
||||
secondaryClickOffsetY > 0 {
|
||||
self.writeGotoSplitTestData([
|
||||
"webInputFocusSeeded": "true",
|
||||
"webInputFocusElementId": inputId,
|
||||
"webInputFocusSecondaryElementId": secondaryInputId,
|
||||
"webInputFocusSecondaryCenterX": "\(secondaryCenterX)",
|
||||
"webInputFocusSecondaryCenterY": "\(secondaryCenterY)",
|
||||
"webInputFocusSecondaryClickOffsetX": "\(secondaryClickOffsetX)",
|
||||
"webInputFocusSecondaryClickOffsetY": "\(secondaryClickOffsetY)",
|
||||
"webInputFocusActiveElementId": activeId,
|
||||
"webInputFocusTrackerInstalled": trackerInstalled ? "true" : "false",
|
||||
"webInputFocusTrackedStateId": trackedStateId,
|
||||
"webInputFocusReadyState": readyState
|
||||
])
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [weak self] in
|
||||
self?.setupFocusedInputForGotoSplitUITest(panel: panel, attempt: attempt + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func recordGotoSplitUITestActiveElement(panelId: UUID, keyPrefix: String) {
|
||||
recordGotoSplitUITestActiveElementRetry(panelId: panelId, keyPrefix: keyPrefix, attempt: 0)
|
||||
}
|
||||
|
||||
private func recordGotoSplitUITestActiveElementRetry(panelId: UUID, keyPrefix: String, attempt: Int) {
|
||||
let delays: [Double] = [0.05, 0.1, 0.25, 0.5]
|
||||
let delay = attempt < delays.count ? delays[attempt] : delays.last!
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
|
||||
guard let self,
|
||||
let tabManager,
|
||||
let tab = tabManager.selectedWorkspace,
|
||||
let panel = tab.browserPanel(for: panelId) else { return }
|
||||
|
||||
self.evaluateGotoSplitUITestActiveElement(panel: panel) { snapshot in
|
||||
let activeId = snapshot["id"] ?? ""
|
||||
let expectedInputId = self.gotoSplitUITestExpectedInputId() ?? ""
|
||||
if keyPrefix == "addressBarExit",
|
||||
!expectedInputId.isEmpty,
|
||||
activeId != expectedInputId,
|
||||
attempt < delays.count - 1 {
|
||||
self.recordGotoSplitUITestActiveElementRetry(
|
||||
panelId: panelId,
|
||||
keyPrefix: keyPrefix,
|
||||
attempt: attempt + 1
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
self.writeGotoSplitTestData([
|
||||
"\(keyPrefix)PanelId": panelId.uuidString,
|
||||
"\(keyPrefix)ActiveElementId": activeId,
|
||||
"\(keyPrefix)ActiveElementTag": snapshot["tag"] ?? "",
|
||||
"\(keyPrefix)ActiveElementType": snapshot["type"] ?? "",
|
||||
"\(keyPrefix)ActiveElementEditable": snapshot["editable"] ?? "false",
|
||||
"\(keyPrefix)TrackedFocusStateId": snapshot["trackedFocusStateId"] ?? "",
|
||||
"\(keyPrefix)FocusTrackerInstalled": snapshot["focusTrackerInstalled"] ?? "false"
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func evaluateGotoSplitUITestActiveElement(
|
||||
panel: BrowserPanel,
|
||||
completion: @escaping ([String: String]) -> Void
|
||||
) {
|
||||
let script = """
|
||||
(() => {
|
||||
try {
|
||||
const active = document.activeElement;
|
||||
if (!active) {
|
||||
return { id: "", tag: "", type: "", editable: "false" };
|
||||
}
|
||||
const tag = (active.tagName || "").toLowerCase();
|
||||
const type = (active.type || "").toLowerCase();
|
||||
const editable =
|
||||
!!active.isContentEditable ||
|
||||
tag === "textarea" ||
|
||||
(tag === "input" && type !== "hidden");
|
||||
return {
|
||||
id: typeof active.id === "string" ? active.id : "",
|
||||
tag,
|
||||
type,
|
||||
editable: editable ? "true" : "false",
|
||||
trackedFocusStateId:
|
||||
window.__cmuxAddressBarFocusState &&
|
||||
typeof window.__cmuxAddressBarFocusState.id === "string"
|
||||
? window.__cmuxAddressBarFocusState.id
|
||||
: "",
|
||||
focusTrackerInstalled:
|
||||
window.__cmuxAddressBarFocusTrackerInstalled === true ? "true" : "false"
|
||||
};
|
||||
} catch (_) {
|
||||
return {
|
||||
id: "",
|
||||
tag: "",
|
||||
type: "",
|
||||
editable: "false",
|
||||
trackedFocusStateId: "",
|
||||
focusTrackerInstalled: "false"
|
||||
};
|
||||
}
|
||||
})();
|
||||
"""
|
||||
|
||||
panel.webView.evaluateJavaScript(script) { result, _ in
|
||||
let payload = result as? [String: Any]
|
||||
completion([
|
||||
"id": (payload?["id"] as? String) ?? "",
|
||||
"tag": (payload?["tag"] as? String) ?? "",
|
||||
"type": (payload?["type"] as? String) ?? "",
|
||||
"editable": (payload?["editable"] as? String) ?? "false",
|
||||
"trackedFocusStateId": (payload?["trackedFocusStateId"] as? String) ?? "",
|
||||
"focusTrackerInstalled": (payload?["focusTrackerInstalled"] as? String) ?? "false"
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
private func gotoSplitUITestExpectedInputId() -> String? {
|
||||
let env = ProcessInfo.processInfo.environment
|
||||
guard let path = env["CMUX_UI_TEST_GOTO_SPLIT_PATH"], !path.isEmpty else { return nil }
|
||||
return loadGotoSplitTestData(at: path)["webInputFocusElementId"]
|
||||
}
|
||||
|
||||
private func recordGotoSplitMoveIfNeeded(direction: NavigationDirection) {
|
||||
guard isGotoSplitUITestRecordingEnabled() else { return }
|
||||
guard let tabManager, let workspace = tabManager.selectedWorkspace else { return }
|
||||
|
|
@ -6591,7 +6919,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
if browserAddressBarFocusedPanelId != nil,
|
||||
cmuxOwningGhosttyView(for: NSApp.keyWindow?.firstResponder) != nil {
|
||||
#if DEBUG
|
||||
dlog("handleCustomShortcut: clearing stale browserAddressBarFocusedPanelId")
|
||||
let stalePanelToken = browserAddressBarFocusedPanelId.map { String($0.uuidString.prefix(5)) } ?? "nil"
|
||||
let firstResponderType = NSApp.keyWindow?.firstResponder.map { String(describing: type(of: $0)) } ?? "nil"
|
||||
dlog(
|
||||
"browser.focus.addressBar.staleClear panel=\(stalePanelToken) " +
|
||||
"reason=terminal_first_responder fr=\(firstResponderType)"
|
||||
)
|
||||
#endif
|
||||
browserAddressBarFocusedPanelId = nil
|
||||
stopBrowserOmnibarSelectionRepeat()
|
||||
|
|
@ -7274,6 +7607,27 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
}
|
||||
dlog(line)
|
||||
}
|
||||
|
||||
private func browserFocusStateSnapshot() -> String {
|
||||
let selected = tabManager?.selectedTabId.map { String($0.uuidString.prefix(5)) } ?? "nil"
|
||||
let focused = tabManager?.selectedWorkspace?.focusedPanelId.map { String($0.uuidString.prefix(5)) } ?? "nil"
|
||||
let addressBar = browserAddressBarFocusedPanelId.map { String($0.uuidString.prefix(5)) } ?? "nil"
|
||||
let keyWindow = NSApp.keyWindow?.windowNumber ?? -1
|
||||
let firstResponderType = NSApp.keyWindow?.firstResponder.map { String(describing: type(of: $0)) } ?? "nil"
|
||||
return "selected=\(selected) focused=\(focused) addr=\(addressBar) keyWin=\(keyWindow) fr=\(firstResponderType)"
|
||||
}
|
||||
|
||||
private func redactedDebugURL(_ url: URL?) -> String {
|
||||
guard let url else { return "nil" }
|
||||
guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
|
||||
return "<invalid>"
|
||||
}
|
||||
components.user = nil
|
||||
components.password = nil
|
||||
components.query = nil
|
||||
components.fragment = nil
|
||||
return components.string ?? "<redacted>"
|
||||
}
|
||||
#endif
|
||||
|
||||
@discardableResult
|
||||
|
|
@ -7281,9 +7635,28 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
guard let tabManager,
|
||||
let workspace = tabManager.selectedWorkspace,
|
||||
let panel = workspace.browserPanel(for: panelId) else {
|
||||
#if DEBUG
|
||||
dlog(
|
||||
"browser.focus.addressBar.route panel=\(panelId.uuidString.prefix(5)) " +
|
||||
"result=miss \(browserFocusStateSnapshot())"
|
||||
)
|
||||
#endif
|
||||
return false
|
||||
}
|
||||
#if DEBUG
|
||||
dlog(
|
||||
"browser.focus.addressBar.route panel=\(panel.id.uuidString.prefix(5)) " +
|
||||
"workspace=\(workspace.id.uuidString.prefix(5)) result=hit \(browserFocusStateSnapshot())"
|
||||
)
|
||||
#endif
|
||||
workspace.focusPanel(panel.id)
|
||||
#if DEBUG
|
||||
let focusedAfter = workspace.focusedPanelId.map { String($0.uuidString.prefix(5)) } ?? "nil"
|
||||
dlog(
|
||||
"browser.focus.addressBar.route panel=\(panel.id.uuidString.prefix(5)) " +
|
||||
"workspace=\(workspace.id.uuidString.prefix(5)) focusedAfter=\(focusedAfter)"
|
||||
)
|
||||
#endif
|
||||
focusBrowserAddressBar(in: panel)
|
||||
return true
|
||||
}
|
||||
|
|
@ -7291,16 +7664,56 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
@discardableResult
|
||||
func openBrowserAndFocusAddressBar(url: URL? = nil, insertAtEnd: Bool = false) -> UUID? {
|
||||
guard let panelId = tabManager?.openBrowser(url: url, insertAtEnd: insertAtEnd) else {
|
||||
#if DEBUG
|
||||
dlog(
|
||||
"browser.focus.openAndFocus result=open_failed insertAtEnd=\(insertAtEnd ? 1 : 0) " +
|
||||
"url=\(redactedDebugURL(url)) \(browserFocusStateSnapshot())"
|
||||
)
|
||||
#endif
|
||||
return nil
|
||||
}
|
||||
#if DEBUG
|
||||
dlog(
|
||||
"browser.focus.openAndFocus result=open_ok panel=\(panelId.uuidString.prefix(5)) " +
|
||||
"insertAtEnd=\(insertAtEnd ? 1 : 0) url=\(redactedDebugURL(url))"
|
||||
)
|
||||
#endif
|
||||
#if DEBUG
|
||||
let didFocus = focusBrowserAddressBar(panelId: panelId)
|
||||
dlog(
|
||||
"browser.focus.openAndFocus result=focus_request panel=\(panelId.uuidString.prefix(5)) " +
|
||||
"focused=\(didFocus ? 1 : 0) \(browserFocusStateSnapshot())"
|
||||
)
|
||||
#else
|
||||
_ = focusBrowserAddressBar(panelId: panelId)
|
||||
#endif
|
||||
return panelId
|
||||
}
|
||||
|
||||
private func focusBrowserAddressBar(in panel: BrowserPanel) {
|
||||
#if DEBUG
|
||||
let requestId = panel.requestAddressBarFocus()
|
||||
dlog(
|
||||
"browser.focus.addressBar.request panel=\(panel.id.uuidString.prefix(5)) " +
|
||||
"request=\(requestId.uuidString.prefix(8)) \(browserFocusStateSnapshot())"
|
||||
)
|
||||
#else
|
||||
_ = panel.requestAddressBarFocus()
|
||||
#endif
|
||||
browserAddressBarFocusedPanelId = panel.id
|
||||
#if DEBUG
|
||||
dlog(
|
||||
"browser.focus.addressBar.sticky panel=\(panel.id.uuidString.prefix(5)) " +
|
||||
"request=\(requestId.uuidString.prefix(8)) \(browserFocusStateSnapshot())"
|
||||
)
|
||||
#endif
|
||||
NotificationCenter.default.post(name: .browserFocusAddressBar, object: panel.id)
|
||||
#if DEBUG
|
||||
dlog(
|
||||
"browser.focus.addressBar.notify panel=\(panel.id.uuidString.prefix(5)) " +
|
||||
"request=\(requestId.uuidString.prefix(8))"
|
||||
)
|
||||
#endif
|
||||
}
|
||||
|
||||
func focusedBrowserAddressBarPanelId() -> UUID? {
|
||||
|
|
@ -7309,11 +7722,44 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
|
||||
private func focusedBrowserAddressBarPanelIdForShortcutEvent(_ event: NSEvent) -> UUID? {
|
||||
guard let panelId = browserAddressBarFocusedPanelId else { return nil }
|
||||
guard let context = preferredMainWindowContextForShortcutRouting(event: event),
|
||||
let workspace = context.tabManager.selectedWorkspace,
|
||||
workspace.browserPanel(for: panelId) != nil else {
|
||||
|
||||
guard let context = preferredMainWindowContextForShortcutRouting(event: event) else {
|
||||
#if DEBUG
|
||||
dlog(
|
||||
"browser.focus.addressBar.shortcutContext panel=\(panelId.uuidString.prefix(5)) " +
|
||||
"accepted=0 reason=no_context event=\(NSWindow.keyDescription(event))"
|
||||
)
|
||||
#endif
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let workspace = context.tabManager.selectedWorkspace else {
|
||||
#if DEBUG
|
||||
dlog(
|
||||
"browser.focus.addressBar.shortcutContext panel=\(panelId.uuidString.prefix(5)) " +
|
||||
"accepted=0 reason=no_workspace event=\(NSWindow.keyDescription(event))"
|
||||
)
|
||||
#endif
|
||||
return nil
|
||||
}
|
||||
|
||||
guard workspace.browserPanel(for: panelId) != nil else {
|
||||
#if DEBUG
|
||||
dlog(
|
||||
"browser.focus.addressBar.shortcutContext panel=\(panelId.uuidString.prefix(5)) " +
|
||||
"accepted=0 reason=panel_not_in_workspace workspace=\(workspace.id.uuidString.prefix(5)) " +
|
||||
"event=\(NSWindow.keyDescription(event))"
|
||||
)
|
||||
#endif
|
||||
return nil
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
dlog(
|
||||
"browser.focus.addressBar.shortcutContext panel=\(panelId.uuidString.prefix(5)) " +
|
||||
"accepted=1 workspace=\(workspace.id.uuidString.prefix(5)) event=\(NSWindow.keyDescription(event))"
|
||||
)
|
||||
#endif
|
||||
return panelId
|
||||
}
|
||||
|
||||
|
|
@ -7330,7 +7776,17 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
let normalizedFlags = browserOmnibarNormalizedModifierFlags(flags)
|
||||
let isCommandOrControlOnly = normalizedFlags == [.command] || normalizedFlags == [.control]
|
||||
guard isCommandOrControlOnly else { return false }
|
||||
return chars == "n" || chars == "p"
|
||||
let shouldBypass = chars == "n" || chars == "p"
|
||||
#if DEBUG
|
||||
if shouldBypass {
|
||||
let panelToken = browserAddressBarFocusedPanelId.map { String($0.uuidString.prefix(5)) } ?? "nil"
|
||||
dlog(
|
||||
"browser.focus.addressBar.shortcutBypass panel=\(panelToken) " +
|
||||
"chars=\(chars) flags=\(normalizedFlags.rawValue)"
|
||||
)
|
||||
}
|
||||
#endif
|
||||
return shouldBypass
|
||||
}
|
||||
|
||||
private func commandOmnibarSelectionDelta(
|
||||
|
|
@ -7347,6 +7803,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
private func dispatchBrowserOmnibarSelectionMove(delta: Int) {
|
||||
guard delta != 0 else { return }
|
||||
guard let panelId = browserAddressBarFocusedPanelId else { return }
|
||||
#if DEBUG
|
||||
dlog(
|
||||
"browser.focus.omnibar.selectionMove panel=\(panelId.uuidString.prefix(5)) " +
|
||||
"delta=\(delta) repeatKey=\(browserOmnibarRepeatKeyCode.map(String.init) ?? "nil")"
|
||||
)
|
||||
#endif
|
||||
NotificationCenter.default.post(
|
||||
name: .browserMoveOmnibarSelection,
|
||||
object: panelId,
|
||||
|
|
@ -7356,15 +7818,37 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
|
||||
private func startBrowserOmnibarSelectionRepeatIfNeeded(keyCode: UInt16, delta: Int) {
|
||||
guard delta != 0 else { return }
|
||||
guard browserAddressBarFocusedPanelId != nil else { return }
|
||||
guard browserAddressBarFocusedPanelId != nil else {
|
||||
#if DEBUG
|
||||
dlog(
|
||||
"browser.focus.omnibar.repeat.start key=\(keyCode) delta=\(delta) " +
|
||||
"result=skip_no_focused_address_bar"
|
||||
)
|
||||
#endif
|
||||
return
|
||||
}
|
||||
|
||||
if browserOmnibarRepeatKeyCode == keyCode, browserOmnibarRepeatDelta == delta {
|
||||
#if DEBUG
|
||||
let panelToken = browserAddressBarFocusedPanelId.map { String($0.uuidString.prefix(5)) } ?? "nil"
|
||||
dlog(
|
||||
"browser.focus.omnibar.repeat.start panel=\(panelToken) " +
|
||||
"key=\(keyCode) delta=\(delta) result=reuse"
|
||||
)
|
||||
#endif
|
||||
return
|
||||
}
|
||||
|
||||
stopBrowserOmnibarSelectionRepeat()
|
||||
browserOmnibarRepeatKeyCode = keyCode
|
||||
browserOmnibarRepeatDelta = delta
|
||||
#if DEBUG
|
||||
let panelToken = browserAddressBarFocusedPanelId.map { String($0.uuidString.prefix(5)) } ?? "nil"
|
||||
dlog(
|
||||
"browser.focus.omnibar.repeat.start panel=\(panelToken) " +
|
||||
"key=\(keyCode) delta=\(delta) result=armed"
|
||||
)
|
||||
#endif
|
||||
|
||||
let start = DispatchWorkItem { [weak self] in
|
||||
self?.scheduleBrowserOmnibarSelectionRepeatTick()
|
||||
|
|
@ -7376,11 +7860,21 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
private func scheduleBrowserOmnibarSelectionRepeatTick() {
|
||||
browserOmnibarRepeatStartWorkItem = nil
|
||||
guard browserAddressBarFocusedPanelId != nil else {
|
||||
#if DEBUG
|
||||
dlog("browser.focus.omnibar.repeat.tick result=stop_no_focused_address_bar")
|
||||
#endif
|
||||
stopBrowserOmnibarSelectionRepeat()
|
||||
return
|
||||
}
|
||||
guard browserOmnibarRepeatKeyCode != nil else { return }
|
||||
|
||||
#if DEBUG
|
||||
let panelToken = browserAddressBarFocusedPanelId.map { String($0.uuidString.prefix(5)) } ?? "nil"
|
||||
dlog(
|
||||
"browser.focus.omnibar.repeat.tick panel=\(panelToken) " +
|
||||
"delta=\(browserOmnibarRepeatDelta)"
|
||||
)
|
||||
#endif
|
||||
dispatchBrowserOmnibarSelectionMove(delta: browserOmnibarRepeatDelta)
|
||||
|
||||
let tick = DispatchWorkItem { [weak self] in
|
||||
|
|
@ -7391,12 +7885,24 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
}
|
||||
|
||||
private func stopBrowserOmnibarSelectionRepeat() {
|
||||
#if DEBUG
|
||||
let previousKeyCode = browserOmnibarRepeatKeyCode
|
||||
let previousDelta = browserOmnibarRepeatDelta
|
||||
#endif
|
||||
browserOmnibarRepeatStartWorkItem?.cancel()
|
||||
browserOmnibarRepeatTickWorkItem?.cancel()
|
||||
browserOmnibarRepeatStartWorkItem = nil
|
||||
browserOmnibarRepeatTickWorkItem = nil
|
||||
browserOmnibarRepeatKeyCode = nil
|
||||
browserOmnibarRepeatDelta = 0
|
||||
#if DEBUG
|
||||
if previousKeyCode != nil || previousDelta != 0 {
|
||||
dlog(
|
||||
"browser.focus.omnibar.repeat.stop key=\(previousKeyCode.map(String.init) ?? "nil") " +
|
||||
"delta=\(previousDelta)"
|
||||
)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private func handleBrowserOmnibarSelectionRepeatLifecycleEvent(_ event: NSEvent) {
|
||||
|
|
@ -7405,11 +7911,23 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
switch event.type {
|
||||
case .keyUp:
|
||||
if event.keyCode == browserOmnibarRepeatKeyCode {
|
||||
#if DEBUG
|
||||
dlog(
|
||||
"browser.focus.omnibar.repeat.lifecycle event=keyUp key=\(event.keyCode) " +
|
||||
"action=stop"
|
||||
)
|
||||
#endif
|
||||
stopBrowserOmnibarSelectionRepeat()
|
||||
}
|
||||
case .flagsChanged:
|
||||
let flags = event.modifierFlags.intersection(.deviceIndependentFlagsMask)
|
||||
if !flags.contains(.command) {
|
||||
#if DEBUG
|
||||
dlog(
|
||||
"browser.focus.omnibar.repeat.lifecycle event=flagsChanged " +
|
||||
"flags=\(flags.rawValue) action=stop"
|
||||
)
|
||||
#endif
|
||||
stopBrowserOmnibarSelectionRepeat()
|
||||
}
|
||||
default:
|
||||
|
|
@ -9180,6 +9698,9 @@ enum MenuBarIconRenderer {
|
|||
private var cmuxFirstResponderGuardCurrentEventOverride: NSEvent?
|
||||
private var cmuxFirstResponderGuardHitViewOverride: NSView?
|
||||
#endif
|
||||
private var cmuxFirstResponderGuardCurrentEventContext: NSEvent?
|
||||
private var cmuxFirstResponderGuardHitViewContext: NSView?
|
||||
private var cmuxFirstResponderGuardContextWindowNumber: Int?
|
||||
private var cmuxBrowserReturnForwardingDepth = 0
|
||||
private var cmuxWindowFirstResponderBypassDepth = 0
|
||||
private var cmuxFieldEditorOwningWebViewAssociationKey: UInt8 = 0
|
||||
|
|
@ -9221,6 +9742,7 @@ private extension NSWindow {
|
|||
let responderWebView = responder.flatMap {
|
||||
Self.cmuxOwningWebView(for: $0, in: self, event: currentEvent)
|
||||
}
|
||||
var pointerInitiatedWebFocus = false
|
||||
|
||||
if AppDelegate.shared?.shouldBlockFirstResponderChangeWhileCommandPaletteVisible(
|
||||
window: self,
|
||||
|
|
@ -9244,6 +9766,7 @@ private extension NSWindow {
|
|||
event: currentEvent
|
||||
)
|
||||
if pointerInitiatedFocus {
|
||||
pointerInitiatedWebFocus = true
|
||||
#if DEBUG
|
||||
dlog(
|
||||
"focus.guard allowPointerFirstResponder responder=\(String(describing: type(of: responder))) " +
|
||||
|
|
@ -9280,7 +9803,16 @@ private extension NSWindow {
|
|||
)
|
||||
}
|
||||
#endif
|
||||
let result = cmux_makeFirstResponder(responder)
|
||||
let result: Bool
|
||||
if pointerInitiatedWebFocus, let webView = responderWebView {
|
||||
// `NSWindow.makeFirstResponder` may run before `CmuxWebView.mouseDown(with:)`.
|
||||
// Preserve pointer intent during this synchronous responder change.
|
||||
result = webView.withPointerFocusAllowance {
|
||||
cmux_makeFirstResponder(responder)
|
||||
}
|
||||
} else {
|
||||
result = cmux_makeFirstResponder(responder)
|
||||
}
|
||||
if result {
|
||||
if let fieldEditor = responder as? NSTextView, fieldEditor.isFieldEditor {
|
||||
Self.cmuxTrackFieldEditor(fieldEditor, owningWebView: responderWebView)
|
||||
|
|
@ -9292,6 +9824,18 @@ private extension NSWindow {
|
|||
}
|
||||
|
||||
@objc func cmux_sendEvent(_ event: NSEvent) {
|
||||
let previousContextEvent = cmuxFirstResponderGuardCurrentEventContext
|
||||
let previousContextHitView = cmuxFirstResponderGuardHitViewContext
|
||||
let previousContextWindowNumber = cmuxFirstResponderGuardContextWindowNumber
|
||||
cmuxFirstResponderGuardCurrentEventContext = event
|
||||
cmuxFirstResponderGuardHitViewContext = Self.cmuxHitViewForEventDispatch(in: self, event: event)
|
||||
cmuxFirstResponderGuardContextWindowNumber = self.windowNumber
|
||||
defer {
|
||||
cmuxFirstResponderGuardCurrentEventContext = previousContextEvent
|
||||
cmuxFirstResponderGuardHitViewContext = previousContextHitView
|
||||
cmuxFirstResponderGuardContextWindowNumber = previousContextWindowNumber
|
||||
}
|
||||
|
||||
guard shouldSuppressWindowMoveForFolderDrag(window: self, event: event),
|
||||
let contentView = self.contentView else {
|
||||
cmux_sendEvent(event)
|
||||
|
|
@ -9549,37 +10093,63 @@ private extension NSWindow {
|
|||
return found
|
||||
}
|
||||
|
||||
private static func cmuxCurrentEvent(for _: NSWindow) -> NSEvent? {
|
||||
private static func cmuxCurrentEvent(for window: NSWindow) -> NSEvent? {
|
||||
#if DEBUG
|
||||
if let override = cmuxFirstResponderGuardCurrentEventOverride {
|
||||
return override
|
||||
}
|
||||
#endif
|
||||
if cmuxFirstResponderGuardContextWindowNumber == window.windowNumber {
|
||||
return cmuxFirstResponderGuardCurrentEventContext
|
||||
}
|
||||
return NSApp.currentEvent
|
||||
}
|
||||
|
||||
private static func cmuxHitViewInThemeFrame(in window: NSWindow, event: NSEvent) -> NSView? {
|
||||
guard let contentView = window.contentView,
|
||||
let themeFrame = contentView.superview else {
|
||||
return nil
|
||||
}
|
||||
let pointInTheme = themeFrame.convert(event.locationInWindow, from: nil)
|
||||
return themeFrame.hitTest(pointInTheme)
|
||||
}
|
||||
|
||||
private static func cmuxHitViewInContentView(in window: NSWindow, event: NSEvent) -> NSView? {
|
||||
guard let contentView = window.contentView else {
|
||||
return nil
|
||||
}
|
||||
let pointInContent = contentView.convert(event.locationInWindow, from: nil)
|
||||
return contentView.hitTest(pointInContent)
|
||||
}
|
||||
|
||||
private static func cmuxTopHitViewForEvent(in window: NSWindow, event: NSEvent) -> NSView? {
|
||||
if let hitInThemeFrame = cmuxHitViewInThemeFrame(in: window, event: event) {
|
||||
return hitInThemeFrame
|
||||
}
|
||||
return cmuxHitViewInContentView(in: window, event: event)
|
||||
}
|
||||
|
||||
private static func cmuxHitViewForEventDispatch(in window: NSWindow, event: NSEvent) -> NSView? {
|
||||
if event.windowNumber != 0, event.windowNumber != window.windowNumber {
|
||||
return nil
|
||||
}
|
||||
if let eventWindow = event.window, eventWindow !== window {
|
||||
return nil
|
||||
}
|
||||
return cmuxTopHitViewForEvent(in: window, event: event)
|
||||
}
|
||||
|
||||
private static func cmuxHitViewForCurrentEvent(in window: NSWindow, event: NSEvent) -> NSView? {
|
||||
#if DEBUG
|
||||
if let override = cmuxFirstResponderGuardHitViewOverride {
|
||||
return override
|
||||
}
|
||||
#endif
|
||||
guard let contentView = window.contentView else { return nil }
|
||||
|
||||
if contentView.className == "NSGlassEffectView" {
|
||||
let pointInContent = contentView.convert(event.locationInWindow, from: nil)
|
||||
return contentView.hitTest(pointInContent)
|
||||
if cmuxFirstResponderGuardContextWindowNumber == window.windowNumber,
|
||||
let contextHitView = cmuxFirstResponderGuardHitViewContext {
|
||||
return contextHitView
|
||||
}
|
||||
|
||||
if let themeFrame = contentView.superview {
|
||||
let pointInTheme = themeFrame.convert(event.locationInWindow, from: nil)
|
||||
if let hit = themeFrame.hitTest(pointInTheme) {
|
||||
return hit
|
||||
}
|
||||
}
|
||||
|
||||
let pointInContent = contentView.convert(event.locationInWindow, from: nil)
|
||||
return contentView.hitTest(pointInContent)
|
||||
return cmuxTopHitViewForEvent(in: window, event: event)
|
||||
}
|
||||
|
||||
private static func cmuxTrackFieldEditor(_ fieldEditor: NSTextView, owningWebView webView: CmuxWebView?) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue