feat: improve window focusing action
This commit is contained in:
1 parent
e98dff325d
commit
8dd63c722f
3 files changed
+183
-24
No files matched your search
@@ -17,6 +17,7 @@ extension AXUIElement {
|
||||
// we add 5s to make sure to not do an extra retry
|
||||
AXUIElementSetMessagingTimeout(AXUIElementCreateSystemWide(), globalTimeoutInSeconds + 5)
|
||||
}
|
||||
|
||||
static var windowResizedOrMovedMap = DebounceMap()
|
||||
static var windowTitleChangedMap = DebounceMap()
|
||||
|
||||
@@ -423,4 +424,4 @@ enum AxError: Error {
|
||||
/// it starts at 0 for each app, and increments over time, for each new UI element
|
||||
/// this means that long-lived apps (e.g. Finder) may have high IDs
|
||||
/// we don't know how high it can go, and if it wraps around
|
||||
typealias AXUIElementID = UInt
|
||||
typealias AXUIElementID = UInt64
|
||||
+11
-20
@@ -185,7 +185,7 @@ class Window {
|
||||
var psn = ProcessSerialNumber()
|
||||
GetProcessForPID(self.application.pid, &psn)
|
||||
_SLPSSetFrontProcessWithOptions(&psn, self.cgWindowId!, SLPSMode.userGenerated.rawValue)
|
||||
self.makeKeyWindow(psn)
|
||||
self.makeKeyWindow(&psn)
|
||||
self.axUiElement!.focusWindow()
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(50)) {
|
||||
Windows.previewFocusedWindowIfNeeded()
|
||||
@@ -195,25 +195,16 @@ class Window {
|
||||
}
|
||||
|
||||
/// The following function was ported from https://github.com/Hammerspoon/hammerspoon/issues/370#issuecomment-545545468
|
||||
func makeKeyWindow(_ psn: ProcessSerialNumber) -> Void {
|
||||
var psn_ = psn
|
||||
var bytes1 = [UInt8](repeating: 0, count: 0xf8)
|
||||
bytes1[0x04] = 0xF8
|
||||
bytes1[0x08] = 0x01
|
||||
bytes1[0x3a] = 0x10
|
||||
var bytes2 = [UInt8](repeating: 0, count: 0xf8)
|
||||
bytes2[0x04] = 0xF8
|
||||
bytes2[0x08] = 0x02
|
||||
bytes2[0x3a] = 0x10
|
||||
memcpy(&bytes1[0x3c], &cgWindowId, MemoryLayout<UInt32>.size)
|
||||
memset(&bytes1[0x20], 0xFF, 0x10)
|
||||
memcpy(&bytes2[0x3c], &cgWindowId, MemoryLayout<UInt32>.size)
|
||||
memset(&bytes2[0x20], 0xFF, 0x10)
|
||||
[bytes1, bytes2].forEach { bytes in
|
||||
_ = bytes.withUnsafeBufferPointer { pointer in
|
||||
SLPSPostEventRecordTo(&psn_, &UnsafeMutablePointer(mutating: pointer.baseAddress)!.pointee)
|
||||
}
|
||||
}
|
||||
func makeKeyWindow(_ psn: inout ProcessSerialNumber) -> Void {
|
||||
var bytes = [UInt8](repeating: 0, count: 0xf8)
|
||||
bytes[0x04] = 0xf8
|
||||
bytes[0x3a] = 0x10
|
||||
memcpy(&bytes[0x3c], &cgWindowId, MemoryLayout<UInt32>.size)
|
||||
memset(&bytes[0x20], 0xff, 0x10)
|
||||
bytes[0x08] = 0x01
|
||||
SLPSPostEventRecordTo(&psn, &bytes)
|
||||
bytes[0x08] = 0x02
|
||||
SLPSPostEventRecordTo(&psn, &bytes)
|
||||
}
|
||||
|
||||
// for some windows (e.g. Slack), the AX API doesn't return a title; we try CG API; finally we resort to the app name
|
||||
|
||||
@@ -1,13 +1,62 @@
|
||||
//import XCTest
|
||||
//
|
||||
//final class ImageScalingTests: XCTestCase {
|
||||
// /// 20ms
|
||||
// func testWindowsByBruteForce() throws {
|
||||
// /// average: 0.021
|
||||
// func testWindowsByBruteForce1000() throws {
|
||||
// measure(options: options) {
|
||||
// let _ = windowsByBruteForce(41414)
|
||||
// let _ = windowsByBruteForce(721)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /// average: 0.042
|
||||
// func testWindowsByBruteForceData() throws {
|
||||
// measure(options: options) {
|
||||
// let _ = windowsByBruteForceData(721)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /// average: 0.040
|
||||
// /// average: 0.019 without CFDataCreate
|
||||
// func testWindowsByBruteForceUint8() throws {
|
||||
// measure(options: options) {
|
||||
// var pid = pid_t(721)
|
||||
// let _ = windowsByBruteForceUint8(&pid)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// func testRangeToBruteForce() {
|
||||
// // all windows is empty
|
||||
// XCTAssertEqual(Windows.rangeToBruteForce([], []), nil)
|
||||
// XCTAssertEqual(Windows.rangeToBruteForce([], [2, 3]), nil)
|
||||
// // all windows are known
|
||||
// XCTAssertEqual(Windows.rangeToBruteForce([2, 3, 4, 5], [2, 3, 4, 5]), nil)
|
||||
// // no known window helps us narrow the range
|
||||
// XCTAssertEqual(Windows.rangeToBruteForce([2, 3, 4, 5], []), [nil, nil])
|
||||
// XCTAssertEqual(Windows.rangeToBruteForce([2, 3, 4, 5], [3]), [nil, nil])
|
||||
// XCTAssertEqual(Windows.rangeToBruteForce([2, 3, 4, 5], [3, 4]), [nil, nil])
|
||||
// // some known windows are not in allWindows
|
||||
// XCTAssertEqual(Windows.rangeToBruteForce([2, 3, 4, 5], [1]), [nil, nil])
|
||||
// XCTAssertEqual(Windows.rangeToBruteForce([2, 3, 4, 5], [6]), [nil, nil])
|
||||
// XCTAssertEqual(Windows.rangeToBruteForce([2, 3, 4, 5], [1, 2, 3]), [nil, nil])
|
||||
// XCTAssertEqual(Windows.rangeToBruteForce([2, 3, 4, 5], [1, 2, 3, 4, 5, 6]), [nil, nil])
|
||||
// XCTAssertEqual(Windows.rangeToBruteForce([2, 3, 4, 5], [2, 3, 4, 5, 6]), [nil, nil])
|
||||
// XCTAssertEqual(Windows.rangeToBruteForce([2, 3, 4, 5], [1, 2, 3, 4, 5]), [nil, nil])
|
||||
// // only start is narrowed
|
||||
// XCTAssertEqual(Windows.rangeToBruteForce([2, 3, 4, 5], [2]), [2, nil])
|
||||
// XCTAssertEqual(Windows.rangeToBruteForce([2, 3, 4, 5], [2, 4]), [2, nil])
|
||||
// XCTAssertEqual(Windows.rangeToBruteForce([2, 3, 4, 5], [2, 3]), [3, nil])
|
||||
// XCTAssertEqual(Windows.rangeToBruteForce([2, 3, 4, 5], [2, 3, 4]), [4, nil])
|
||||
// // only end is narrowed
|
||||
// XCTAssertEqual(Windows.rangeToBruteForce([2, 3, 4, 5], [5]), [nil, 5])
|
||||
// XCTAssertEqual(Windows.rangeToBruteForce([2, 3, 4, 5], [3, 5]), [nil, 5])
|
||||
// XCTAssertEqual(Windows.rangeToBruteForce([2, 3, 4, 5], [4, 5]), [nil, 4])
|
||||
// XCTAssertEqual(Windows.rangeToBruteForce([2, 3, 4, 5], [3, 4, 5]), [nil, 3])
|
||||
// // start and end are narrowed
|
||||
// XCTAssertEqual(Windows.rangeToBruteForce([2, 3, 4, 5], [2, 5]), [2, 5])
|
||||
// XCTAssertEqual(Windows.rangeToBruteForce([2, 3, 4, 5], [2, 3, 5]), [3, 5])
|
||||
// XCTAssertEqual(Windows.rangeToBruteForce([2, 3, 4, 5], [2, 4, 5]), [2, 4])
|
||||
// }
|
||||
//
|
||||
// private var options: XCTMeasureOptions = {
|
||||
// let o = XCTMeasureOptions()
|
||||
// o.iterationCount = 10
|
||||
@@ -15,6 +64,79 @@
|
||||
// }()
|
||||
//}
|
||||
//
|
||||
//class Windows {
|
||||
//// static func CGWindowIDRangeToAXUIElementIDRange(_ range: [CGWindowID?]) -> Range<AXUIElementID> {
|
||||
////// print(range)
|
||||
//// var start = AXUIElementID(0)
|
||||
//// var end = AXUIElementID(1000)
|
||||
//// if let startCGWindowID = range[0],
|
||||
//// let startAXUIElementID = (Windows.list.first { $0.cgWindowId == startCGWindowID }!.axUiElement.id()) {
|
||||
//// start = startAXUIElementID
|
||||
//// }
|
||||
//// if let endCGWindowID = range[1],
|
||||
//// let endAXUIElementID = (Windows.list.first { $0.cgWindowId == endCGWindowID }!.axUiElement.id()) {
|
||||
//// end = endAXUIElementID
|
||||
//// } else {
|
||||
//// end = start + 1000
|
||||
//// }
|
||||
//// let range2 = start..<end
|
||||
////// print(range2)
|
||||
//// return range2
|
||||
//// }
|
||||
//
|
||||
// static func rangeToBruteForce(_ allWindowIds: [CGWindowID], _ knownWindowIds: [CGWindowID]) -> [CGWindowID?]? {
|
||||
// if allWindowIds.isEmpty {
|
||||
// return nil
|
||||
// }
|
||||
// if knownWindowIds.isEmpty {
|
||||
// return [nil, nil]
|
||||
// }
|
||||
// let allSorted = allWindowIds.sorted()
|
||||
// let knownSorted = knownWindowIds.sorted()
|
||||
// if knownSorted.first! < allSorted.first! || knownSorted.last! > allSorted.last! {
|
||||
// Logger.error("we track some windows, yet they are not returned by CGWindowListCopyWindowInfo")
|
||||
// return [nil, nil]
|
||||
// }
|
||||
// if allSorted.count == knownSorted.count {
|
||||
// var same = true
|
||||
// for i in 0..<allSorted.count {
|
||||
// if allSorted[i] != knownSorted[i] {
|
||||
// same = false
|
||||
// }
|
||||
// }
|
||||
// if same {
|
||||
// return nil
|
||||
// }
|
||||
// }
|
||||
// var startIsShared = knownSorted.first! == allSorted.first!
|
||||
// let endIsShared = knownSorted.last! == allSorted.last!
|
||||
// if !startIsShared && !endIsShared {
|
||||
// return [nil, nil]
|
||||
// }
|
||||
// var start: CGWindowID? = nil
|
||||
// var end: CGWindowID? = endIsShared ? allSorted.last! : nil
|
||||
// for windowId in allSorted {
|
||||
// if startIsShared {
|
||||
// if knownSorted.contains(windowId) {
|
||||
// start = windowId
|
||||
// } else {
|
||||
// startIsShared = false
|
||||
// }
|
||||
// }
|
||||
// if endIsShared {
|
||||
// if knownSorted.contains(windowId) {
|
||||
// if windowId < end! {
|
||||
// end = windowId
|
||||
// }
|
||||
// } else {
|
||||
// end = allSorted.last!
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// return [start, end]
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//func windowsByBruteForce(_ pid: pid_t) -> [AXUIElement] {
|
||||
// // we use this to call _AXUIElementCreateWithRemoteToken; we reuse the object for performance
|
||||
// // tests showed that this remoteToken is 20 bytes: 4 + 4 + 4 + 8; the order of bytes matters
|
||||
@@ -35,6 +157,51 @@
|
||||
// return axWindows
|
||||
//}
|
||||
//
|
||||
//func windowsByBruteForceData(_ pid: pid_t) -> [AXUIElement] {
|
||||
// // we use this to call _AXUIElementCreateWithRemoteToken; we reuse the object for performance
|
||||
// // tests showed that this remoteToken is 20 bytes: 4 + 4 + 4 + 8; the order of bytes matters
|
||||
// var remoteToken = Data(count: 20)
|
||||
// remoteToken.replaceSubrange(0..<4, with: withUnsafeBytes(of: pid) { Data($0) })
|
||||
// remoteToken.replaceSubrange(4..<8, with: withUnsafeBytes(of: Int32(0)) { Data($0) })
|
||||
// remoteToken.replaceSubrange(8..<12, with: withUnsafeBytes(of: Int32(0x636f636f)) { Data($0) })
|
||||
// let axWindows = [AXUIElement]()
|
||||
// // we iterate to 1000 as a tradeoff between performance, and missing windows of long-lived processes
|
||||
// for axUiElementId: AXUIElementID in 0..<100000 {
|
||||
// remoteToken.replaceSubrange(12..<20, with: withUnsafeBytes(of: axUiElementId) { Data($0) })
|
||||
//// if let axUiElement = _AXUIElementCreateWithRemoteToken(remoteToken as CFData)?.takeRetainedValue(),
|
||||
//// let subrole = try? axUiElement.subrole(),
|
||||
//// [kAXStandardWindowSubrole, kAXDialogSubrole].contains(subrole) {
|
||||
//// axWindows.append(axUiElement)
|
||||
//// }
|
||||
// }
|
||||
// return axWindows
|
||||
//}
|
||||
//
|
||||
//func windowsByBruteForceUint8(_ pid: inout pid_t) -> [AXUIElement] {
|
||||
// // we use this to call _AXUIElementCreateWithRemoteToken; we reuse the object for performance
|
||||
// // tests showed that this remoteToken is 20 bytes: 4 + 4 + 4 + 8; the order of bytes matters
|
||||
// var tid = Int32(0x636f636f)
|
||||
// var remoteToken = [UInt8](repeating: 0, count: 20)
|
||||
// var dataCursor = 0
|
||||
// memcpy(&remoteToken[dataCursor], &pid, MemoryLayout<UInt32>.size)
|
||||
// dataCursor += MemoryLayout<UInt32>.size
|
||||
// memset(&remoteToken[dataCursor], 0, MemoryLayout<UInt32>.size)
|
||||
// dataCursor += MemoryLayout<UInt32>.size
|
||||
// memcpy(&remoteToken[dataCursor], &tid, MemoryLayout<Int32>.size)
|
||||
// dataCursor += MemoryLayout<Int32>.size
|
||||
// let axWindows = [AXUIElement]()
|
||||
// // we iterate to 1000 as a tradeoff between performance, and missing windows of long-lived processes
|
||||
// for var axUiElementId: AXUIElementID in 0..<100000 {
|
||||
// memcpy(&remoteToken[dataCursor], &axUiElementId, MemoryLayout<AXUIElementID>.size);
|
||||
// let _ = CFDataCreate(kCFAllocatorDefault, remoteToken, remoteToken.count)
|
||||
//// let axUiElement = _AXUIElementCreateWithRemoteToken(cfdata)?.takeRetainedValue(),
|
||||
//// let subrole = try? axUiElement.subrole(),
|
||||
//// [kAXStandardWindowSubrole, kAXDialogSubrole].contains(subrole) {
|
||||
//// axWindows.append(axUiElement)
|
||||
// }
|
||||
// return axWindows
|
||||
//}
|
||||
//
|
||||
//typealias AXUIElementID = UInt
|
||||
//
|
||||
//enum AxError: Error {
|
||||
|
||||
Reference in new issue
Block a user