Ensure tab focus owns key input
This commit is contained in:
parent
57ae8d9c0c
commit
ca9c680da7
4 changed files with 157 additions and 0 deletions
|
|
@ -28,6 +28,7 @@
|
|||
A5001206 /* UpdateBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001216 /* UpdateBadge.swift */; };
|
||||
A500120A /* UpdateTiming.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001220 /* UpdateTiming.swift */; };
|
||||
A500120B /* UpdateTestSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001221 /* UpdateTestSupport.swift */; };
|
||||
A500120E /* UpdateTestURLProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001224 /* UpdateTestURLProtocol.swift */; };
|
||||
A500120C /* WindowAccessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001222 /* WindowAccessor.swift */; };
|
||||
A500120D /* UpdateLogStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001223 /* UpdateLogStore.swift */; };
|
||||
A5001207 /* UpdatePopoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001217 /* UpdatePopoverView.swift */; };
|
||||
|
|
@ -89,6 +90,7 @@
|
|||
A5001216 /* UpdateBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update/UpdateBadge.swift; sourceTree = "<group>"; };
|
||||
A5001220 /* UpdateTiming.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update/UpdateTiming.swift; sourceTree = "<group>"; };
|
||||
A5001221 /* UpdateTestSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update/UpdateTestSupport.swift; sourceTree = "<group>"; };
|
||||
A5001224 /* UpdateTestURLProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update/UpdateTestURLProtocol.swift; sourceTree = "<group>"; };
|
||||
A5001217 /* UpdatePopoverView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update/UpdatePopoverView.swift; sourceTree = "<group>"; };
|
||||
A5001218 /* UpdateTitlebarAccessory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update/UpdateTitlebarAccessory.swift; sourceTree = "<group>"; };
|
||||
A5001219 /* WindowToolbarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowToolbarController.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -176,6 +178,7 @@
|
|||
A5001216 /* UpdateBadge.swift */,
|
||||
A5001220 /* UpdateTiming.swift */,
|
||||
A5001221 /* UpdateTestSupport.swift */,
|
||||
A5001224 /* UpdateTestURLProtocol.swift */,
|
||||
A5001223 /* UpdateLogStore.swift */,
|
||||
A5001217 /* UpdatePopoverView.swift */,
|
||||
A5001218 /* UpdateTitlebarAccessory.swift */,
|
||||
|
|
@ -310,6 +313,7 @@
|
|||
A5001206 /* UpdateBadge.swift in Sources */,
|
||||
A500120A /* UpdateTiming.swift in Sources */,
|
||||
A500120B /* UpdateTestSupport.swift in Sources */,
|
||||
A500120E /* UpdateTestURLProtocol.swift in Sources */,
|
||||
A500120D /* UpdateLogStore.swift in Sources */,
|
||||
A5001207 /* UpdatePopoverView.swift in Sources */,
|
||||
A5001208 /* UpdateTitlebarAccessory.swift in Sources */,
|
||||
|
|
|
|||
|
|
@ -1771,6 +1771,41 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
}
|
||||
}
|
||||
|
||||
func ensureFocus(for tabId: UUID, surfaceId: UUID, attempt: Int = 0) {
|
||||
let maxAttempts = 6
|
||||
guard attempt < maxAttempts else { return }
|
||||
guard let tabManager = AppDelegate.shared?.tabManager,
|
||||
tabManager.selectedTabId == tabId,
|
||||
tabManager.focusedSurfaceId(for: tabId) == surfaceId else { return }
|
||||
|
||||
guard let window else {
|
||||
scheduleFocusRetry(for: tabId, surfaceId: surfaceId, attempt: attempt)
|
||||
return
|
||||
}
|
||||
|
||||
guard window.isKeyWindow else {
|
||||
scheduleFocusRetry(for: tabId, surfaceId: surfaceId, attempt: attempt)
|
||||
return
|
||||
}
|
||||
|
||||
if window.firstResponder === surfaceView {
|
||||
return
|
||||
}
|
||||
|
||||
window.makeFirstResponder(surfaceView)
|
||||
|
||||
if window.firstResponder !== surfaceView {
|
||||
scheduleFocusRetry(for: tabId, surfaceId: surfaceId, attempt: attempt)
|
||||
}
|
||||
}
|
||||
|
||||
private func scheduleFocusRetry(for tabId: UUID, surfaceId: UUID, attempt: Int) {
|
||||
let delay = 0.05 * pow(2.0, Double(attempt))
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
|
||||
self?.ensureFocus(for: tabId, surfaceId: surfaceId, attempt: attempt + 1)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateFocusForWindow() {
|
||||
let shouldFocus = isActive && (window?.isKeyWindow ?? false)
|
||||
surfaceView.desiredFocus = shouldFocus
|
||||
|
|
|
|||
|
|
@ -395,6 +395,7 @@ class TabManager: ObservableObject {
|
|||
tabs.first(where: { $0.id == id })?.focusedSurface
|
||||
}
|
||||
surface.hostedView.moveFocus(from: previousSurface?.hostedView)
|
||||
surface.hostedView.ensureFocus(for: selectedTabId, surfaceId: surface.id)
|
||||
}
|
||||
|
||||
private func updateTabTitle(tabId: UUID, title: String) {
|
||||
|
|
|
|||
117
Sources/Update/UpdateTestURLProtocol.swift
Normal file
117
Sources/Update/UpdateTestURLProtocol.swift
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
#if DEBUG
|
||||
import Foundation
|
||||
|
||||
final class UpdateTestURLProtocol: URLProtocol {
|
||||
static let host = "cmuxterm.test"
|
||||
static let appcastPath = "/appcast.xml"
|
||||
static let updatePath = "/cmuxterm-test.zip"
|
||||
|
||||
private static var isRegistered = false
|
||||
private static let registrationLock = NSLock()
|
||||
|
||||
static func registerIfNeeded() {
|
||||
registrationLock.lock()
|
||||
defer { registrationLock.unlock() }
|
||||
guard !isRegistered else { return }
|
||||
URLProtocol.registerClass(UpdateTestURLProtocol.self)
|
||||
isRegistered = true
|
||||
}
|
||||
|
||||
override class func canInit(with request: URLRequest) -> Bool {
|
||||
guard let url = request.url else { return false }
|
||||
guard url.host == host else { return false }
|
||||
return true
|
||||
}
|
||||
|
||||
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
|
||||
request
|
||||
}
|
||||
|
||||
override func startLoading() {
|
||||
guard let url = request.url else {
|
||||
client?.urlProtocol(self, didFailWithError: URLError(.badURL))
|
||||
return
|
||||
}
|
||||
|
||||
let method = request.httpMethod?.uppercased() ?? "GET"
|
||||
let (statusCode, data, contentType) = payload(for: url)
|
||||
let headers = [
|
||||
"Content-Type": contentType,
|
||||
"Content-Length": "\(data.count)"
|
||||
]
|
||||
|
||||
let response = HTTPURLResponse(
|
||||
url: url,
|
||||
statusCode: statusCode,
|
||||
httpVersion: "HTTP/1.1",
|
||||
headerFields: headers
|
||||
)!
|
||||
|
||||
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
|
||||
|
||||
if method != "HEAD" {
|
||||
client?.urlProtocol(self, didLoad: data)
|
||||
}
|
||||
client?.urlProtocolDidFinishLoading(self)
|
||||
}
|
||||
|
||||
override func stopLoading() {}
|
||||
|
||||
private func payload(for url: URL) -> (Int, Data, String) {
|
||||
switch url.path {
|
||||
case Self.appcastPath:
|
||||
let data = Self.appcastData()
|
||||
return (200, data, "application/xml")
|
||||
case Self.updatePath:
|
||||
let data = Self.updateArchiveData()
|
||||
return (200, data, "application/octet-stream")
|
||||
default:
|
||||
let data = Data("Not Found".utf8)
|
||||
return (404, data, "text/plain")
|
||||
}
|
||||
}
|
||||
|
||||
private static func appcastData() -> Data {
|
||||
let env = ProcessInfo.processInfo.environment
|
||||
let mode = env["CMUX_UI_TEST_FEED_MODE"] ?? "available"
|
||||
let version = env["CMUX_UI_TEST_UPDATE_VERSION"] ?? "9.9.9"
|
||||
let updateURL = "https://\(host)\(updatePath)"
|
||||
let updateLength = updateArchiveData().count
|
||||
|
||||
let item: String
|
||||
if mode == "none" {
|
||||
item = ""
|
||||
} else {
|
||||
item = """
|
||||
<item>
|
||||
<title>cmuxterm \(version)</title>
|
||||
<sparkle:version>\(version)</sparkle:version>
|
||||
<sparkle:shortVersionString>\(version)</sparkle:shortVersionString>
|
||||
<enclosure url="\(updateURL)" length="\(updateLength)" type="application/octet-stream" />
|
||||
</item>
|
||||
"""
|
||||
}
|
||||
|
||||
let xml = """
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<rss version="2.0"
|
||||
xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<channel>
|
||||
<title>cmuxterm Test Updates</title>
|
||||
<link>https://\(host)</link>
|
||||
<description>Test updates feed</description>
|
||||
<language>en</language>
|
||||
\(item)
|
||||
</channel>
|
||||
</rss>
|
||||
"""
|
||||
|
||||
return Data(xml.utf8)
|
||||
}
|
||||
|
||||
private static func updateArchiveData() -> Data {
|
||||
Data("cmuxterm test update".utf8)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Loading…
Add table
Add a link
Reference in a new issue