-
-
Notifications
You must be signed in to change notification settings - Fork 355
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
Also adds big performance improvements to show the thumbnails and focus windows
- Loading branch information
louis.pontoise
committed
Dec 20, 2019
1 parent
87647b9
commit 4525ec2
Showing
19 changed files
with
406 additions
and
155 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
// In this .h file, we define symbols of private APIs so we can use these APIs in the project | ||
// see Webkit repo: https://github.com/WebKit/webkit/blob/master/Source/WebCore/PAL/pal/spi/cg/CoreGraphicsSPI.h | ||
// see Hammerspoon issue: https://github.com/Hammerspoon/hammerspoon/issues/370#issuecomment-545545468 | ||
// see Alt-tab-macos issue: https://github.com/lwouis/alt-tab-macos/pull/87#issuecomment-558624755 | ||
|
||
#import <CoreGraphics/CoreGraphics.h> | ||
#import <CoreImage/CoreImage.h> | ||
|
||
typedef uint32_t CGSConnectionID; | ||
typedef uint32_t CGSWindowID; | ||
typedef uint32_t CGSWindowCount; | ||
typedef uint32_t CGSWindowCaptureOptions; | ||
enum { | ||
kCGSWindowCaptureNominalResolution = 0x0200, | ||
kCGSCaptureIgnoreGlobalClipShape = 0x0800, | ||
}; | ||
|
||
extern CGSConnectionID CGSMainConnectionID(void); | ||
|
||
// returns an array of CGImage of the windows which ID is given as `windowList`. `windowList` is supposed to be an array of IDs but in my test on High Sierra, the function ignores other IDs than the first, and always returns the screenshot of the first window in the array | ||
// * performance: the `HW` in the name seems to imply better performance, and it was observed by some contributors that it seems to be faster (see https://github.com/lwouis/alt-tab-macos/issues/45) than other methods | ||
// * minimized windows: the function can return screenshots of minimized windows | ||
// * windows in other spaces: ? | ||
extern CFArrayRef CGSHWCaptureWindowList(CGSConnectionID connectionId, CGSWindowID *windowList, CGSWindowCount windowCount, CGSWindowCaptureOptions options); | ||
|
||
// returns the CGImage of the window which ID is given in `wid` | ||
// * performance: it seems that this function performs similarly to public API `CGWindowListCreateImage` | ||
// * minimized windows: the function can return screenshots of minimized windows | ||
// * windows in other spaces: ? | ||
extern CGError CGSCaptureWindowsContentsToRectWithOptions(CGSConnectionID connectionId, CGSWindowID *windowId, bool windowOnly, CGRect rect, CGSWindowCaptureOptions options, CGImageRef *image); | ||
|
||
// returns the size of a window | ||
// * performance: it seems that this function is faster than the public API AX calls to get a window bounds | ||
// * minimized windows: ? | ||
// * windows in other spaces: ? | ||
extern CGError SLSGetWindowBounds(CGSConnectionID connectionId, CGSWindowID *windowId, CGRect *frame); | ||
|
||
typedef uint32_t SLPSMode; | ||
enum { | ||
kCPSAllWindows = 0x100, | ||
kCPSUserGenerated = 0x200, | ||
kCPSNoWindows = 0x400, | ||
}; | ||
|
||
// focuses a window | ||
// * performance: faster than AXUIElementPerformAction(kAXRaiseAction) | ||
// * minimized windows: yes | ||
// * windows in other spaces: ? | ||
extern CGError _SLPSSetFrontProcessWithOptions(ProcessSerialNumber *psn, CGSWindowID windowId, SLPSMode mode); | ||
|
||
extern CGError SLSGetWindowOwner(CGSConnectionID connectionId, CGSWindowID windowId, CGSConnectionID *windowConnectionId); | ||
|
||
extern CGError SLSGetConnectionPSN(CGSConnectionID connectionId, ProcessSerialNumber *psn); | ||
|
||
extern CGError SLPSPostEventRecordTo(ProcessSerialNumber *psn, uint8_t *bytes); | ||
|
||
// The following function was taken from https://github.com/Hammerspoon/hammerspoon/issues/370#issuecomment-545545468 | ||
static void window_manager_make_key_window(ProcessSerialNumber *psn, uint32_t windowId) { | ||
// the information specified in the events below consists of the "special" category, event type, and modifiers, | ||
// basically synthesizing a mouse-down and up event targetted at a specific window of the application, | ||
// but it doesn't actually get treated as a mouse-click normally would. | ||
uint8_t bytes1[0xf8] = { | ||
[0x04] = 0xF8, | ||
[0x08] = 0x01, | ||
[0x3a] = 0x10 | ||
}; | ||
uint8_t bytes2[0xf8] = { | ||
[0x04] = 0xF8, | ||
[0x08] = 0x02, | ||
[0x3a] = 0x10 | ||
}; | ||
memcpy(bytes1 + 0x3c, &windowId, sizeof(uint32_t)); | ||
memset(bytes1 + 0x20, 0xFF, 0x10); | ||
memcpy(bytes2 + 0x3c, &windowId, sizeof(uint32_t)); | ||
memset(bytes2 + 0x20, 0xFF, 0x10); | ||
SLPSPostEventRecordTo(psn, bytes1); | ||
SLPSPostEventRecordTo(psn, bytes2); | ||
} | ||
|
||
// returns the window ID of the provided AXUIElement | ||
extern AXError _AXUIElementGetWindow(AXUIElementRef axUiElement, uint32_t *windowId); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import Cocoa | ||
import Foundation | ||
|
||
class CoreGraphicsApis { | ||
static func windows(_ option: CGWindowListOption) -> [NSDictionary] { | ||
return (CGWindowListCopyWindowInfo([.excludeDesktopElements, option], kCGNullWindowID) as! [NSDictionary]) | ||
.filter { return windowIsNotMenubarOrOthers($0) && windowIsReasonablyBig($0) } | ||
} | ||
|
||
// workaround: filtering this criteria seems to remove non-windows UI elements | ||
private static func windowIsNotMenubarOrOthers(_ window: NSDictionary) -> Bool { | ||
return value(window, kCGWindowLayer, Int(0)) == 0 | ||
} | ||
|
||
// workaround: some apps like chrome use a window to implement the search popover | ||
private static func windowIsReasonablyBig(_ window: NSDictionary) -> Bool { | ||
let windowBounds = CGRect(dictionaryRepresentation: value(window, kCGWindowBounds, [:] as CFDictionary))! | ||
return windowBounds.width > Preferences.minimumWindowSize && windowBounds.height > Preferences.minimumWindowSize | ||
} | ||
|
||
static func value<T>(_ cgWindow: NSDictionary, _ key: CFString, _ fallback: T) -> T { | ||
return cgWindow[key] as? T ?? fallback | ||
} | ||
|
||
static func image(_ windowNumber: CGWindowID) -> CGImage? { | ||
return CGWindowListCreateImage(.null, .optionIncludingWindow, windowNumber, [.boundsIgnoreFraming, .bestResolution]) | ||
} | ||
} |
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import Cocoa | ||
import Foundation | ||
|
||
enum WindowScreenshotApi { | ||
case CGWindowListCreateImage | ||
case CGSHWCaptureWindowList | ||
case CGSCaptureWindowsContentsToRectWithOptions | ||
} | ||
|
||
enum WindowDimensionsApi { | ||
case AXValueGetValue | ||
case SLSGetWindowBounds | ||
} | ||
|
||
enum FocusWindowApi { | ||
case AXUIElementPerformAction | ||
case _SLPSSetFrontProcessWithOptions | ||
} | ||
|
||
let cgsMainConnectionId = CGSMainConnectionID() | ||
|
||
// This class wraps different public and private APIs that achieve similar functionality. | ||
// It lets the user pick the API as a parameter, and thus the level of service they want | ||
class PreferredApis { | ||
static func windowScreenshot(_ windowId: CGSWindowID, _ api: WindowScreenshotApi) -> CGImage { | ||
switch api { | ||
case .CGWindowListCreateImage: | ||
return CGWindowListCreateImage(.null, .optionIncludingWindow, windowId, [.boundsIgnoreFraming, .bestResolution])! | ||
case .CGSHWCaptureWindowList: | ||
var windowId_ = windowId | ||
let options = CGSWindowCaptureOptions(kCGSCaptureIgnoreGlobalClipShape | kCGSWindowCaptureNominalResolution) | ||
return (CGSHWCaptureWindowList(cgsMainConnectionId, &windowId_, 1, options)!.takeRetainedValue() as! Array<CGImage>).first! | ||
case .CGSCaptureWindowsContentsToRectWithOptions: | ||
var windowId_ = windowId | ||
var windowImage: Unmanaged<CGImage>? | ||
CGSCaptureWindowsContentsToRectWithOptions(cgsMainConnectionId, &windowId_, true, .zero, (1 << 8), &windowImage) | ||
return windowImage!.takeRetainedValue() | ||
} | ||
} | ||
|
||
static func windowDimensions(_ windowId: CGSWindowID?, _ axUiElement: AXUIElement?, _ api: WindowDimensionsApi) -> CGSize { | ||
switch api { | ||
case .AXValueGetValue: | ||
return AccessibilityApis.value(axUiElement!, kAXSizeAttribute, CGSize(), .cgSize)! | ||
case .SLSGetWindowBounds: | ||
var windowId_ = windowId! | ||
var frame = CGRect() | ||
SLSGetWindowBounds(cgsMainConnectionId, &windowId_, &frame); | ||
return frame.size | ||
} | ||
} | ||
|
||
static func focusWindow(_ axUiElement: AXUIElement, _ windowId: CGSWindowID?, _ ownerPid: Int32?, _ api: FocusWindowApi) -> Void { | ||
DispatchQueue.global(qos: .userInteractive).async { | ||
switch api { | ||
case .AXUIElementPerformAction: | ||
NSRunningApplication(processIdentifier: ownerPid!)?.activate(options: [.activateIgnoringOtherApps]) | ||
AccessibilityApis.focus(axUiElement) | ||
case ._SLPSSetFrontProcessWithOptions: | ||
var elementConnection = UInt32.zero | ||
SLSGetWindowOwner(cgsMainConnectionId, windowId!, &elementConnection) | ||
var psn = ProcessSerialNumber() | ||
SLSGetConnectionPSN(elementConnection, &psn) | ||
_SLPSSetFrontProcessWithOptions(&psn, windowId!, SLPSMode(kCPSUserGenerated)) | ||
window_manager_make_key_window(&psn, windowId!) | ||
AccessibilityApis.focus(axUiElement) | ||
} | ||
} | ||
} | ||
} |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import Cocoa | ||
import Foundation | ||
|
||
class OpenWindow { | ||
var cgWindow: NSDictionary | ||
var ownerPid: Int32 | ||
var cgId: CGWindowID | ||
var cgTitle: String | ||
var cgRect: CGRect | ||
var thumbnail: NSImage? | ||
var icon: NSImage? | ||
var app: NSRunningApplication? | ||
var axWindow: AXUIElement? | ||
var isMinimized: Bool | ||
|
||
init(_ cgWindow: NSDictionary, _ cgId: CGWindowID, _ isMinimized: Bool, _ axWindow: AXUIElement?) { | ||
self.cgWindow = cgWindow | ||
self.cgId = cgId | ||
self.ownerPid = CoreGraphicsApis.value(cgWindow, kCGWindowOwnerPID, Int32(0)) | ||
let cgTitle = CoreGraphicsApis.value(cgWindow, kCGWindowName, "") | ||
let cgOwnerName = CoreGraphicsApis.value(cgWindow, kCGWindowOwnerName, "") | ||
self.cgTitle = cgTitle.isEmpty ? cgOwnerName : cgTitle | ||
self.app = NSRunningApplication(processIdentifier: ownerPid) | ||
self.icon = self.app?.icon | ||
self.cgRect = CGRect(dictionaryRepresentation: cgWindow[kCGWindowBounds] as! NSDictionary)! | ||
let cgImage = PreferredApis.windowScreenshot(cgId, .CGSHWCaptureWindowList) | ||
self.thumbnail = NSImage(cgImage: cgImage, size: NSSize(width: cgImage.width, height: cgImage.height)) | ||
self.axWindow = axWindow | ||
self.isMinimized = isMinimized | ||
} | ||
|
||
func focus() { | ||
if axWindow == nil { | ||
axWindow = AccessibilityApis.windowThatMatchCgWindow(ownerPid, cgId) | ||
} | ||
if axWindow != nil { | ||
PreferredApis.focusWindow(axWindow!, cgId, nil, ._SLPSSetFrontProcessWithOptions) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.