Skip to content

Commit

Permalink
feat: display other spaces/minimized windows (closes #14)
Browse files Browse the repository at this point in the history
Also closes #11 closes #45 closes #62

BREAKING CHANGE: this brings huge changes to core parts of the codebase. It introduces the use of private APIs that hopefully are should be compatible from macOS 10.12+, but I couldn't test them. I reviewed the whole codebase to clean and improve on performance and readability
  • Loading branch information
louis.pontoise authored and lwouis committed Dec 27, 2019
1 parent b50fa5b commit 3f5ea25
Show file tree
Hide file tree
Showing 24 changed files with 858 additions and 297 deletions.
88 changes: 71 additions & 17 deletions alt-tab-macos.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions alt-tab-macos/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,7 @@
<string>NSApplication</string>
<key>LSUIElement</key>
<string>1</string>
<key>ATSApplicationFontsPath</key>
<string></string>
</dict>
</plist>
83 changes: 83 additions & 0 deletions alt-tab-macos/api-wrappers/AXUIElement.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import Cocoa
import Foundation

// This list of keys is not exhaustive; it contains only the values used by this app
// full public list: ApplicationServices.HIServices.AXAttributeConstants.swift
// Note that the String value is transformed by the getters (e.g. kAXWindowsAttribute -> AXWindows)
enum AXAttributeKey: String {
case windows = "AXWindows"
case minimized = "AXMinimized"
case focusedWindow = "AXFocusedWindow"
}

extension AXUIElement {
func value<T>(_ key: AXAttributeKey, _ target: T, _ type: AXValueType) -> T? {
if let a = attribute(key, AXValue.self) {
var value = target
AXValueGetValue(a, type, &value)
return value
}
return nil
}

func attribute<T>(_ key: AXAttributeKey, _ type: T.Type) -> T? {
var value: AnyObject?
let result = AXUIElementCopyAttributeValue(self, key.rawValue as CFString, &value)
if result == .success, let value = value as? T {
return value
}
return nil
}

func cgId() -> CGWindowID {
var id = CGWindowID(0)
_AXUIElementGetWindow(self, &id)
return id
}

func focusedWindow() -> AXUIElement? {
return attribute(.focusedWindow, AXUIElement.self)
}

func windows() -> [AXUIElement]? {
return attribute(.windows, [AXUIElement].self)
}

func isMinimized() -> Bool {
return attribute(.minimized, Bool.self) == true
}

func focus(_ id: CGWindowID) {
var elementConnection = UInt32(0)
CGSGetWindowOwner(cgsMainConnectionId, id, &elementConnection)
var psn = ProcessSerialNumber()
CGSGetConnectionPSN(elementConnection, &psn)
_SLPSSetFrontProcessWithOptions(&psn, id, .userGenerated)
makeKeyWindow(psn, id)
AXUIElementPerformAction(self, kAXRaiseAction as CFString)
}

// The following function was ported from https://github.com/Hammerspoon/hammerspoon/issues/370#issuecomment-545545468
func makeKeyWindow(_ psn: ProcessSerialNumber, _ wid: CGWindowID) -> Void {
var wid_ = wid
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], &wid_, MemoryLayout<UInt32>.size)
memset(&bytes1[0x20], 0xFF, 0x10)
memcpy(&bytes2[0x3c], &wid_, MemoryLayout<UInt32>.size)
memset(&bytes2[0x20], 0xFF, 0x10)

SLPSPostEventRecordTo(&psn_, &(UnsafeMutablePointer(mutating: UnsafePointer<UInt8>(bytes1)).pointee))
SLPSPostEventRecordTo(&psn_, &(UnsafeMutablePointer(mutating: UnsafePointer<UInt8>(bytes2)).pointee))
}
}
36 changes: 36 additions & 0 deletions alt-tab-macos/api-wrappers/CGWindow.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import Cocoa
import Foundation

typealias CGWindow = [CGWindowKey.RawValue: Any]

extension CGWindow {
static func windows(_ option: CGWindowListOption) -> [CGWindow] {
return CGWindowListCopyWindowInfo([.excludeDesktopElements, option], kCGNullWindowID) as! [CGWindow]
}

func value<T>(_ key: CGWindowKey, _ type: T.Type) -> T? {
return self[key.rawValue] as? T
}

// workaround: filtering this criteria seems to remove non-windows UI elements
func isNotMenubarOrOthers() -> Bool {
return value(.layer, Int.self) == 0
}

// workaround: some apps like chrome use a window to implement the search popover
func isReasonablyBig() -> Bool {
let windowBounds = CGRect(dictionaryRepresentation: value(.bounds, CFDictionary.self)!)!
return windowBounds.width > Preferences.minimumWindowSize && windowBounds.height > Preferences.minimumWindowSize
}
}

// This list of keys is not exhaustive; it contains only the values used by this app
// full public list: CoreGraphics.CGWindow.swift
enum CGWindowKey: String {
case number = "kCGWindowNumber"
case layer = "kCGWindowLayer"
case bounds = "kCGWindowBounds"
case ownerPID = "kCGWindowOwnerPID"
case ownerName = "kCGWindowOwnerName"
case name = "kCGWindowName"
}
32 changes: 32 additions & 0 deletions alt-tab-macos/api-wrappers/CGWindowID.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import Cocoa
import Foundation

extension CGWindowID {
func AXUIElement(_ ownerPid: pid_t) -> AXUIElement? {
return AXUIElementCreateApplication(ownerPid).windows()?.first(where: { return $0.cgId() == self })
}

func AXUIElementOfOtherSpaceWindow(_ ownerPid: pid_t) -> AXUIElement? {
CGSAddWindowsToSpaces(cgsMainConnectionId, [self], [Spaces.currentSpaceId])
let axWindow = AXUIElement(ownerPid)
CGSRemoveWindowsFromSpaces(cgsMainConnectionId, [self], [Spaces.currentSpaceId])
return axWindow
}

func screenshot() -> CGImage? {
// CGSHWCaptureWindowList
var windowId_ = self
let options: CGSWindowCaptureOptions = [.captureIgnoreGlobalClipShape, .windowCaptureNominalResolution]
let list = CGSHWCaptureWindowList(cgsMainConnectionId, &windowId_, 1, options) as! [CGImage]
return list.first

// // CGWindowListCreateImage
// return CGWindowListCreateImage(.null, .optionIncludingWindow, self, [.boundsIgnoreFraming, .bestResolution])

// // CGSCaptureWindowsContentsToRectWithOptions
// var windowId_ = self
// var windowImage = CIContext(options: nil).createCGImage(CIImage.empty(), from: CIImage.empty().extent)!
// CGSCaptureWindowsContentsToRectWithOptions(cgsMainConnectionId, &windowId_, true, .zero, [.windowCaptureNominalResolution, .captureIgnoreGlobalClipShape], &windowImage)
// return windowImage
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import Foundation
import Cocoa

// add CGFloat constructor from String
extension CGFloat {
// add CGFloat constructor from String
init?(_ string: String) {
guard let number = NumberFormatter().number(from: string) else {
return nil
Expand All @@ -11,8 +11,8 @@ extension CGFloat {
}
}

// add throw-on-nil method on Optional
extension Optional {
// add throw-on-nil method on Optional
func orThrow() throws -> Wrapped {
switch self {
case .some(let value):
Expand All @@ -24,8 +24,8 @@ extension Optional {
}
}

// add String constructor from CGFloat that round up at 1 decimal
extension String {
// add String constructor from CGFloat that round up at 1 decimal
init?(_ cgFloat: CGFloat) {
let formatter = NumberFormatter()
formatter.maximumFractionDigits = 1
Expand All @@ -36,8 +36,8 @@ extension String {
}
}

// add recursive lookup in subviews for specific type
extension NSView {
// add recursive lookup in subviews for specific type
func findNestedViews<T: NSView>(subclassOf: T.Type) -> [T] {
return recursiveSubviews.compactMap { $0 as? T }
}
Expand All @@ -47,8 +47,8 @@ extension NSView {
}
}

// add convenience to NSError
extension NSError {
// add convenience to NSError
class func make(domain: String, message: String, code: Int = 9999) -> NSError {
return NSError(
domain: domain,
Expand All @@ -57,3 +57,10 @@ extension NSError {
)
}
}

extension Collection {
// recursive flatMap
func joined() -> [Any] {
return flatMap { ($0 as? [Any])?.joined() ?? [$0] }
}
}
Loading

0 comments on commit 3f5ea25

Please sign in to comment.