831 lines
28 KiB
Objective-C
831 lines
28 KiB
Objective-C
#import "HSuicore.h"
|
|
|
|
#pragma mark - HSapplication implementation
|
|
|
|
@implementation HSapplication
|
|
|
|
#pragma mark - Class methods
|
|
+(HSapplication *)frontmostApplicationWithState:(lua_State *)L {
|
|
LuaSkin *skin = [LuaSkin sharedWithState:L];
|
|
HSapplication *frontmostApp = nil;
|
|
|
|
NSRunningApplication *runningApp = [[NSWorkspace sharedWorkspace] frontmostApplication];
|
|
if (runningApp) {
|
|
frontmostApp = [HSapplication applicationForNSRunningApplication:runningApp withState:L];
|
|
if (!frontmostApp) {
|
|
[skin logError:[NSString stringWithFormat:@"HSapplication::frontmostApplication failed for app: %@", runningApp.localizedName]];
|
|
}
|
|
} else {
|
|
[skin logError:@"Unable to fetch frontmost application"];
|
|
}
|
|
return frontmostApp;
|
|
}
|
|
|
|
+(HSapplication *)applicationForNSRunningApplication:(NSRunningApplication *)app withState:(lua_State *)L {
|
|
return [[HSapplication alloc] initWithNSRunningApplication:app withState:L];
|
|
}
|
|
|
|
+(HSapplication *)applicationForPID:(pid_t)pid withState:(lua_State *)L {
|
|
return [[HSapplication alloc] initWithPid:pid withState:L];
|
|
}
|
|
|
|
+(NSString *)nameForBundleID:(NSString *)bundleID {
|
|
NSBundle *app = [NSBundle bundleWithPath:[HSapplication pathForBundleID:bundleID]];
|
|
return [app objectForInfoDictionaryKey:(id)kCFBundleNameKey];
|
|
}
|
|
|
|
+(NSString *)pathForBundleID:(NSString *)bundleID {
|
|
return [[NSWorkspace sharedWorkspace] absolutePathForAppBundleWithIdentifier:bundleID];
|
|
}
|
|
|
|
+(NSDictionary *)infoForBundleID:(NSString *)bundleID {
|
|
NSWorkspace *ws = [NSWorkspace sharedWorkspace];
|
|
NSString *appPath = [ws absolutePathForAppBundleWithIdentifier:bundleID];
|
|
return [HSapplication infoForBundlePath:appPath];
|
|
}
|
|
|
|
+(NSDictionary *)infoForBundlePath:(NSString *)bundlePath {
|
|
NSDictionary *appInfo = nil;
|
|
|
|
NSBundle *app = [NSBundle bundleWithPath:bundlePath];
|
|
if (app) {
|
|
appInfo = app.infoDictionary;
|
|
}
|
|
return appInfo;
|
|
}
|
|
|
|
+(NSArray<HSapplication *>*)runningApplicationsWithState:(lua_State *)L {
|
|
NSMutableArray<HSapplication *> *apps = [[NSMutableArray alloc] init];
|
|
|
|
for (NSRunningApplication* runningApp in [[NSWorkspace sharedWorkspace] runningApplications]) {
|
|
HSapplication *app = [HSapplication applicationForNSRunningApplication:runningApp withState:L];
|
|
if (app) {
|
|
[apps addObject:app];
|
|
}
|
|
}
|
|
|
|
return (NSArray *)[apps copy];
|
|
}
|
|
|
|
+(NSArray<HSapplication *>*)applicationsForBundleID:(NSString *)bundleID withState:(lua_State *)L {
|
|
NSMutableArray<HSapplication *> *apps = [[NSMutableArray alloc] init];
|
|
|
|
for (NSRunningApplication* runningApp in [NSRunningApplication runningApplicationsWithBundleIdentifier:bundleID]) {
|
|
HSapplication *app = [HSapplication applicationForNSRunningApplication:runningApp withState:L];
|
|
if (app) {
|
|
[apps addObject:app];
|
|
}
|
|
}
|
|
|
|
return apps;
|
|
}
|
|
|
|
+(BOOL)launchByName:(NSString *)name {
|
|
return [[NSWorkspace sharedWorkspace] launchApplication:name];
|
|
}
|
|
|
|
+(BOOL)launchByBundleID:(NSString *)bundleID {
|
|
return [[NSWorkspace sharedWorkspace] launchAppWithBundleIdentifier:bundleID
|
|
options:NSWorkspaceLaunchDefault
|
|
additionalEventParamDescriptor:nil
|
|
launchIdentifier:NULL];
|
|
}
|
|
|
|
#pragma mark - Custom getter/setter methods
|
|
-(void)setHidden:(BOOL)shouldHide {
|
|
AXUIElementSetAttributeValue(self.elementRef, (CFStringRef)NSAccessibilityHiddenAttribute, shouldHide ? kCFBooleanTrue : kCFBooleanFalse);
|
|
}
|
|
|
|
-(BOOL)isHidden {
|
|
CFBooleanRef _isHidden;
|
|
NSNumber* isHidden = @NO;
|
|
AXError result = AXUIElementCopyAttributeValue(self.elementRef,
|
|
(CFStringRef)NSAccessibilityHiddenAttribute,
|
|
(CFTypeRef *)&_isHidden);
|
|
if (result == kAXErrorSuccess) {
|
|
isHidden = (__bridge_transfer NSNumber*)_isHidden;
|
|
}
|
|
return isHidden.boolValue;
|
|
}
|
|
|
|
#pragma mark - Instance initialisers
|
|
-(HSapplication *)initWithPid:(pid_t)pid withState:(lua_State *)L {
|
|
LuaSkin *skin = [LuaSkin sharedWithState:L];
|
|
|
|
NSRunningApplication *runningApp = [NSRunningApplication runningApplicationWithProcessIdentifier:pid];
|
|
if (!runningApp) {
|
|
[skin logError:[NSString stringWithFormat:@"Unable to fetch NSRunningApplication for pid: %d", pid]];
|
|
return nil;
|
|
}
|
|
|
|
return [self initWithNSRunningApplication:runningApp withState:L];
|
|
}
|
|
|
|
-(HSapplication *)initWithNSRunningApplication:(NSRunningApplication *)app withState:(lua_State *)L {
|
|
LuaSkin *skin = [LuaSkin sharedWithState:L];
|
|
|
|
if (!app) {
|
|
[skin logError:@"HSapplication::initWithNSRunningApplication called with invalid application"];
|
|
return nil;
|
|
}
|
|
|
|
AXUIElementRef appRef = AXUIElementCreateApplication(app.processIdentifier);
|
|
if (!appRef) {
|
|
[skin logError:[NSString stringWithFormat:@"Unable to fetch AXUIElementRef for application: %@", app.localizedName]];
|
|
return nil;
|
|
}
|
|
|
|
self = [super init];
|
|
if (self) {
|
|
_pid = app.processIdentifier;
|
|
_elementRef = appRef; // no retain required because of AXUIElementCreateApplication above
|
|
_runningApp = app;
|
|
_uiElement = [[HSuielement alloc] initWithElementRef:_elementRef];
|
|
_selfRefCount = 0;
|
|
} else {
|
|
CFRelease(appRef);
|
|
}
|
|
return self;
|
|
}
|
|
|
|
#pragma mark - Instance destructor
|
|
-(void)dealloc {
|
|
if (_elementRef) CFRelease(_elementRef) ;
|
|
_elementRef = NULL ;
|
|
}
|
|
|
|
#pragma mark - Instance methods
|
|
-(NSArray<HSwindow *>*)allWindows {
|
|
NSMutableArray<HSwindow *> *allWindows = [[NSMutableArray alloc] init];
|
|
CFArrayRef windows;
|
|
AXError result = AXUIElementCopyAttributeValues(self.elementRef, kAXWindowsAttribute, 0, 100, &windows);
|
|
if (result == kAXErrorSuccess) {
|
|
CFIndex windowCount = CFArrayGetCount(windows);
|
|
allWindows = [[NSMutableArray alloc] initWithCapacity:windowCount];
|
|
for (NSInteger i = 0; i < windowCount; i++) {
|
|
AXUIElementRef win = CFArrayGetValueAtIndex(windows, i);
|
|
HSwindow *window = [[HSwindow alloc] initWithAXUIElementRef:win];
|
|
[allWindows addObject:window];
|
|
}
|
|
CFRelease(windows);
|
|
}
|
|
return allWindows;
|
|
}
|
|
|
|
-(HSwindow *)mainWindow {
|
|
HSwindow *mainWindow = nil;
|
|
CFTypeRef window;
|
|
if (AXUIElementCopyAttributeValue(self.elementRef, kAXMainWindowAttribute, &window) == kAXErrorSuccess) {
|
|
mainWindow = [[HSwindow alloc] initWithAXUIElementRef:window];
|
|
CFRelease(window);
|
|
}
|
|
return mainWindow;
|
|
}
|
|
|
|
-(HSwindow *)focusedWindow {
|
|
HSwindow *focusedWindow = nil;
|
|
CFTypeRef window;
|
|
if (AXUIElementCopyAttributeValue(self.elementRef, kAXFocusedWindowAttribute, &window) == kAXErrorSuccess) {
|
|
focusedWindow = [[HSwindow alloc] initWithAXUIElementRef:window];
|
|
CFRelease(window);
|
|
}
|
|
return focusedWindow;
|
|
}
|
|
|
|
-(BOOL)activate:(BOOL)allWindows {
|
|
return [self.runningApp activateWithOptions:NSApplicationActivateIgnoringOtherApps | (allWindows ? NSApplicationActivateAllWindows : 0)];
|
|
}
|
|
|
|
-(BOOL)isResponsive {
|
|
// Define some private API we need
|
|
typedef int CGSConnectionID;
|
|
CG_EXTERN CGSConnectionID CGSMainConnectionID(void);
|
|
bool CGSEventIsAppUnresponsive(CGSConnectionID cid, const ProcessSerialNumber *psn);
|
|
// End of private API definitions
|
|
|
|
ProcessSerialNumber psn;
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
GetProcessForPID(self.pid, &psn);
|
|
#pragma clang diagnostic pop
|
|
|
|
CGSConnectionID conn = CGSMainConnectionID();
|
|
return !CGSEventIsAppUnresponsive(conn, &psn);
|
|
}
|
|
|
|
-(BOOL)isRunningWithState:(lua_State *)L {
|
|
// FIXME: Figure out why we can't use NSRunningApplication.terminated here - it always seems to say NO
|
|
//BOOL isTerminated = self.runningApp.terminated;
|
|
HSapplication *test = [HSapplication applicationForPID:self.runningApp.processIdentifier withState:(lua_State *)L];
|
|
return (test != nil);
|
|
}
|
|
|
|
-(BOOL)setFrontmost:(BOOL)allWindows {
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
ProcessSerialNumber psn;
|
|
GetProcessForPID(self.pid, &psn);
|
|
return (SetFrontProcessWithOptions(&psn, allWindows ? 0 : kSetFrontProcessFrontWindowOnly) == noErr);
|
|
#pragma clang diagnostic pop
|
|
}
|
|
|
|
-(BOOL)isFrontmost {
|
|
CFTypeRef _isFrontmost;
|
|
NSNumber* isFrontmost = @NO;
|
|
|
|
if (kAXErrorSuccess == AXUIElementCopyAttributeValue(self.elementRef,
|
|
(CFStringRef)NSAccessibilityFrontmostAttribute,
|
|
&_isFrontmost)) {
|
|
isFrontmost = (__bridge_transfer NSNumber *)_isFrontmost;
|
|
// } else {
|
|
// [LuaSkin logError:[NSString stringWithFormat:@"Unable to fetch element attribute NSAccessibilityFrontmostAttribute for: %@", [self.runningApp localizedName]]];
|
|
}
|
|
|
|
//[LuaSkin logBreadcrumb:[NSString stringWithFormat:@"FRONTMOST: %@:%@:%@", self.title, isFrontmost, AXIsProcessTrusted() ? @"YES" : @"NO"]];
|
|
|
|
return isFrontmost.boolValue;
|
|
}
|
|
|
|
-(NSString *)title {
|
|
return self.runningApp.localizedName;
|
|
}
|
|
|
|
-(NSString *)bundleID {
|
|
return self.runningApp.bundleIdentifier;
|
|
}
|
|
|
|
-(NSString *)path {
|
|
return [NSBundle bundleWithURL:self.runningApp.bundleURL].bundlePath;
|
|
}
|
|
|
|
-(void)kill {
|
|
[self.runningApp terminate];
|
|
}
|
|
|
|
-(void)kill9 {
|
|
[self.runningApp forceTerminate];
|
|
}
|
|
|
|
-(int)kind {
|
|
int kind = 1;
|
|
switch (self.runningApp.activationPolicy) {
|
|
case NSApplicationActivationPolicyAccessory:
|
|
kind = 0;
|
|
break;
|
|
case NSApplicationActivationPolicyProhibited:
|
|
kind = -1;
|
|
break;
|
|
default: break;
|
|
}
|
|
return kind;
|
|
}
|
|
|
|
@end
|
|
|
|
#pragma mark - HSuielement implementation
|
|
|
|
@implementation HSuielement
|
|
|
|
#pragma mark - Class methods
|
|
+(HSuielement *)focusedElement {
|
|
HSuielement *focused = nil;
|
|
AXUIElementRef focusedElement;
|
|
AXUIElementRef systemWide = AXUIElementCreateSystemWide();
|
|
|
|
AXError error = AXUIElementCopyAttributeValue(systemWide, kAXFocusedUIElementAttribute, (CFTypeRef *)&focusedElement);
|
|
CFRelease(systemWide);
|
|
|
|
if (error == kAXErrorSuccess) {
|
|
focused = [[HSuielement alloc] initWithElementRef:focusedElement];
|
|
CFRelease(focusedElement) ;
|
|
}
|
|
|
|
return focused;
|
|
}
|
|
|
|
#pragma mark - Instance initialiser
|
|
-(HSuielement *)initWithElementRef:(AXUIElementRef)elementRef {
|
|
self = [super init];
|
|
if (self) {
|
|
_elementRef = CFRetain(elementRef);
|
|
_selfRefCount = 0;
|
|
}
|
|
return self;
|
|
}
|
|
|
|
#pragma mark - Instance destructor
|
|
-(void)dealloc {
|
|
if (_elementRef) CFRelease(_elementRef) ;
|
|
_elementRef = NULL ;
|
|
}
|
|
|
|
#pragma mark - Instance methods
|
|
-(id)newWatcherAtIndex:(int)callbackRefIndex withUserdataAtIndex:(int)userDataRefIndex withLuaState:(lua_State *)L {
|
|
LuaSkin *skin = [LuaSkin sharedWithState:L];
|
|
|
|
int callbackRef = [skin luaRef:LUA_REGISTRYINDEX atIndex:callbackRefIndex];
|
|
|
|
int userDataRef = LUA_REFNIL;
|
|
if (lua_type(L, userDataRefIndex) != LUA_TNONE) {
|
|
userDataRef = [skin luaRef:LUA_REGISTRYINDEX atIndex:userDataRefIndex];
|
|
}
|
|
|
|
HSuielementWatcher *watcher = [[HSuielementWatcher alloc] initWithElement:self
|
|
callbackRef:(int)callbackRef
|
|
userdataRef:(int)userDataRef];
|
|
watcher.lsCanary = [skin createGCCanary];
|
|
return watcher;
|
|
}
|
|
|
|
-(id)getElementProperty:(NSString *)property withDefaultValue:(id)defaultValue {
|
|
CFTypeRef value;
|
|
if (AXUIElementCopyAttributeValue(self.elementRef, (__bridge CFStringRef)property, &value) == kAXErrorSuccess) {
|
|
return CFBridgingRelease(value);
|
|
}
|
|
return defaultValue;
|
|
}
|
|
|
|
-(BOOL)isApplication {
|
|
return [self.role isEqualToString:(__bridge NSString *)kAXApplicationRole];
|
|
}
|
|
|
|
-(BOOL)isWindow {
|
|
return [self isWindow:self.role];
|
|
}
|
|
|
|
-(BOOL)isWindow:(NSString *)role {
|
|
// Most windows have a role of kAXWindowRole, but some apps are weird (e.g. Emacs) so we also do a duck-typing test for an expected window attribute
|
|
return ([role isEqualToString:(__bridge NSString *)kAXWindowRole] || [self getElementProperty:NSAccessibilityMinimizedAttribute withDefaultValue:nil]);
|
|
}
|
|
|
|
-(NSString *)getRole {
|
|
return [self getElementProperty:NSAccessibilityRoleAttribute withDefaultValue:@""];
|
|
}
|
|
|
|
-(NSString *)getSelectedText {
|
|
NSString *selectedText = nil;
|
|
AXValueRef _selectedText = NULL;
|
|
if (AXUIElementCopyAttributeValue(self.elementRef, kAXSelectedTextAttribute, (CFTypeRef *)&_selectedText) == kAXErrorSuccess) {
|
|
selectedText = (__bridge_transfer NSString *)_selectedText;
|
|
}
|
|
return selectedText;
|
|
}
|
|
|
|
@end
|
|
|
|
#pragma mark - HSuielementWatcher implementation
|
|
|
|
#pragma mark - Helper functions
|
|
|
|
static void watcher_observer_callback(AXObserverRef observer __unused, AXUIElementRef element,
|
|
CFStringRef notificationName, void* contextData) {
|
|
HSuielementWatcher *watcher = (__bridge HSuielementWatcher *)contextData;
|
|
LuaSkin *skin = [LuaSkin sharedWithState:NULL];
|
|
[skin checkGCCanary:watcher.lsCanary];
|
|
_lua_stackguard_entry(skin.L);
|
|
|
|
|
|
[skin pushLuaRef:watcher.refTable ref:watcher.handlerRef]; // Callback function
|
|
|
|
HSuielement *elementObj = [[HSuielement alloc] initWithElementRef:element];
|
|
id pushObj = elementObj;
|
|
if (elementObj.isWindow) {
|
|
pushObj = [[HSwindow alloc] initWithAXUIElementRef:element];
|
|
} else if ([elementObj.role isEqualToString:(__bridge NSString *)kAXApplicationRole]) {
|
|
pid_t pid;
|
|
AXUIElementGetPid(element, &pid);
|
|
pushObj = [[HSapplication alloc] initWithPid:pid withState:skin.L];
|
|
} else {
|
|
// This isn't a window or an application, so we'll send it as an hs.uielement object
|
|
pushObj = elementObj;
|
|
}
|
|
|
|
[skin pushNSObject:pushObj]; // Parameter 1: element
|
|
lua_pushstring(skin.L, CFStringGetCStringPtr(notificationName, kCFStringEncodingASCII)); // Parameter 2: event
|
|
[skin pushLuaRef:watcher.refTable ref:watcher.watcherRef]; // Parameter 3: watcher
|
|
if (watcher.userDataRef == LUA_NOREF || watcher.userDataRef == LUA_REFNIL) {
|
|
lua_pushnil(skin.L);
|
|
} else {
|
|
[skin pushLuaRef:watcher.refTable ref:watcher.userDataRef]; // Parameter 4: userData
|
|
}
|
|
|
|
if (![skin protectedCallAndTraceback:4 nresults:0]) {
|
|
const char *errorMsg = lua_tostring(skin.L, -1);
|
|
[skin logError:[NSString stringWithUTF8String:errorMsg]];
|
|
lua_pop(skin.L, 1); // remove error message
|
|
}
|
|
|
|
_lua_stackguard_exit(skin.L);
|
|
return;
|
|
}
|
|
|
|
@implementation HSuielementWatcher
|
|
|
|
#pragma mark - Instance initialiser
|
|
// NOTE THAT THE LUA REF ARGUMENTS MUST BE ON LUA_REGISTRYINDEX AND NOT SOME OTHER REFTABLE
|
|
-(HSuielementWatcher *)initWithElement:(HSuielement *)element callbackRef:(int)callbackRef userdataRef:(int)userdataRef{
|
|
self = [super init];
|
|
if (self) {
|
|
_refTable = LUA_REGISTRYINDEX;
|
|
_elementRef = CFRetain(element.elementRef);
|
|
_selfRefCount = 0;
|
|
_handlerRef = callbackRef;
|
|
_userDataRef = userdataRef;
|
|
_watcherRef = LUA_NOREF;
|
|
_running = NO;
|
|
_watchDestroyed = NO;
|
|
AXUIElementGetPid(_elementRef, &_pid);
|
|
}
|
|
return self;
|
|
}
|
|
|
|
#pragma mark - Instance destructor
|
|
-(void)dealloc {
|
|
if (_elementRef) CFRelease(_elementRef) ;
|
|
_elementRef = NULL ;
|
|
}
|
|
|
|
#pragma mark - Instance methods
|
|
|
|
-(void)start:(NSArray <NSString *>*)events withState:(lua_State *)L {
|
|
LuaSkin *skin = [LuaSkin sharedWithState:L];
|
|
if (self.running) {
|
|
return;
|
|
}
|
|
|
|
// Create our observer
|
|
AXObserverRef observer = NULL;
|
|
AXError err = AXObserverCreate(self.pid, watcher_observer_callback, &observer);
|
|
if (err != kAXErrorSuccess) {
|
|
[skin logBreadcrumb:[NSString stringWithFormat:@"AXObserverCreate error: %d", (int)err]];
|
|
return;
|
|
}
|
|
|
|
// Add specified events to the observer
|
|
for (NSString *event in events) {
|
|
AXObserverAddNotification(observer, self.elementRef, (__bridge CFStringRef)event, (__bridge void *)self);
|
|
}
|
|
|
|
self.observer = observer;
|
|
self.running = YES;
|
|
|
|
// Begin observing events
|
|
CFRunLoopAddSource([[NSRunLoop currentRunLoop] getCFRunLoop],
|
|
AXObserverGetRunLoopSource(observer),
|
|
kCFRunLoopDefaultMode);
|
|
}
|
|
|
|
-(void)stop {
|
|
if (!self.running) {
|
|
return;
|
|
}
|
|
|
|
CFRunLoopRemoveSource([[NSRunLoop currentRunLoop] getCFRunLoop],
|
|
AXObserverGetRunLoopSource(self.observer),
|
|
kCFRunLoopDefaultMode);
|
|
CFRelease(self.observer);
|
|
self.running = NO;
|
|
}
|
|
@end
|
|
|
|
#pragma mark - HSwindow implementation
|
|
|
|
#pragma mark - Helper functions
|
|
static AXUIElementRef system_wide_element() {
|
|
static AXUIElementRef element;
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{
|
|
element = AXUIElementCreateSystemWide();
|
|
});
|
|
return element;
|
|
}
|
|
|
|
static AXUIElementRef get_window_tabs(AXUIElementRef win) {
|
|
AXUIElementRef tabs = NULL;
|
|
|
|
CFArrayRef children = NULL;
|
|
if (AXUIElementCopyAttributeValues(win, kAXChildrenAttribute, 0, 100, &children) != noErr) {
|
|
goto cleanup;
|
|
}
|
|
CFIndex count = CFArrayGetCount(children);
|
|
|
|
CFTypeRef typeRef;
|
|
for (CFIndex i = 0; i < count; ++i) {
|
|
AXUIElementRef child = CFArrayGetValueAtIndex(children, i);
|
|
if(AXUIElementCopyAttributeValue(child, kAXRoleAttribute, &typeRef) != noErr) goto cleanup;
|
|
CFStringRef role = (CFStringRef)typeRef;
|
|
BOOL correctRole = kCFCompareEqualTo == CFStringCompare(role, kAXTabGroupRole, 0);
|
|
CFRelease(role);
|
|
if (correctRole) {
|
|
tabs = child;
|
|
CFRetain(tabs);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Safari 14 puts them into an AXGroup, not an AXTabsGroup
|
|
if (tabs == NULL) {
|
|
for (CFIndex i = 0; i < count; ++i) {
|
|
AXUIElementRef child = CFArrayGetValueAtIndex(children, i);
|
|
if(AXUIElementCopyAttributeValue(child, kAXRoleAttribute, &typeRef) != noErr) goto cleanup;
|
|
CFStringRef role = (CFStringRef)typeRef;
|
|
BOOL correctRole = kCFCompareEqualTo == CFStringCompare(role, kAXGroupRole, 0);
|
|
CFRelease(role);
|
|
if (correctRole) {
|
|
CFArrayRef attributeNames = NULL ;
|
|
if (AXUIElementCopyAttributeNames(child, &attributeNames) != noErr) goto cleanup ;
|
|
if (CFArrayContainsValue(attributeNames, CFRangeMake(0, CFArrayGetCount(attributeNames)), kAXTabsAttribute)) {
|
|
tabs = child;
|
|
CFRetain(tabs);
|
|
CFRelease(attributeNames) ;
|
|
break;
|
|
}
|
|
CFRelease(attributeNames) ;
|
|
}
|
|
}
|
|
}
|
|
|
|
cleanup:
|
|
if (children) CFRelease(children);
|
|
|
|
return tabs;
|
|
}
|
|
|
|
@implementation HSwindow
|
|
|
|
#pragma mark - Class methods
|
|
+(NSArray<NSNumber *>*)orderedWindowIDs {
|
|
NSMutableArray *windowIDs = [[NSMutableArray alloc] init];
|
|
CFArrayRef wins = CGWindowListCreate(kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements, kCGNullWindowID);
|
|
|
|
if (wins) {
|
|
CFArrayRef windowDescs = CGWindowListCreateDescriptionFromArray(wins);
|
|
windowIDs = [[NSMutableArray alloc] initWithCapacity:CFArrayGetCount(wins)];
|
|
for (CFIndex i = 0; i < CFArrayGetCount(wins); i++) {
|
|
CFDictionaryRef dict = CFArrayGetValueAtIndex(windowDescs, i);
|
|
CFNumberRef winid = CFDictionaryGetValue(dict, kCGWindowNumber);
|
|
[windowIDs addObject:(__bridge NSNumber *)winid];
|
|
}
|
|
CFRelease(wins);
|
|
CFRelease(windowDescs);
|
|
} else {
|
|
[LuaSkin logBreadcrumb:@"hs.window._orderedwinids CGWindowListCreate returned NULL"] ;
|
|
}
|
|
return windowIDs;
|
|
}
|
|
|
|
+(NSImage *)snapshotForID:(int)windowID keepTransparency:(BOOL)keepTransparency {
|
|
NSImage *image = nil;
|
|
CGWindowImageOption makeOpaque = keepTransparency ? kCGWindowImageDefault : kCGWindowImageShouldBeOpaque;
|
|
CGRect windowRect = CGRectNull;
|
|
CGImageRef windowImage = CGWindowListCreateImage(windowRect, kCGWindowListOptionIncludingWindow, windowID, kCGWindowImageBoundsIgnoreFraming | makeOpaque);
|
|
|
|
if (windowImage) {
|
|
image = [[NSImage alloc] initWithCGImage:windowImage size:windowRect.size];
|
|
CFRelease(windowImage);
|
|
}
|
|
return image;
|
|
}
|
|
|
|
+(HSwindow *)focusedWindow {
|
|
HSwindow *window = nil;
|
|
CFTypeRef app;
|
|
AXUIElementCopyAttributeValue(system_wide_element(), kAXFocusedApplicationAttribute, &app);
|
|
|
|
if (app) {
|
|
CFTypeRef win;
|
|
AXError result = AXUIElementCopyAttributeValue(app, (CFStringRef)NSAccessibilityFocusedWindowAttribute, &win);
|
|
|
|
CFRelease(app);
|
|
|
|
if (result == kAXErrorSuccess) {
|
|
window = [[HSwindow alloc] initWithAXUIElementRef:win];
|
|
CFRelease(win);
|
|
}
|
|
}
|
|
return window;
|
|
|
|
}
|
|
|
|
#pragma mark - Initialiser
|
|
-(HSwindow *)initWithAXUIElementRef:(AXUIElementRef)winRef {
|
|
self = [super init];
|
|
if (self) {
|
|
CFRetain(winRef);
|
|
_elementRef = winRef; // retained above
|
|
_selfRefCount = 0;
|
|
|
|
pid_t pid;
|
|
if (AXUIElementGetPid(winRef, &pid) == kAXErrorSuccess) {
|
|
_pid = pid;
|
|
}
|
|
|
|
CGWindowID winID;
|
|
AXError err = _AXUIElementGetWindow(winRef, &winID);
|
|
if (!err) {
|
|
_winID = winID;
|
|
}
|
|
|
|
_uiElement = [[HSuielement alloc] initWithElementRef:_elementRef];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
#pragma mark - Destructor
|
|
-(void)dealloc {
|
|
if (_elementRef) CFRelease(_elementRef) ;
|
|
_elementRef = NULL ;
|
|
}
|
|
|
|
#pragma mark - Instance methods
|
|
-(id)getWindowProperty:(NSString *)property withDefaultValue:(id)defaultValue {
|
|
CFTypeRef value;
|
|
if (AXUIElementCopyAttributeValue(self.elementRef, (__bridge CFStringRef)property, &value) == kAXErrorSuccess) {
|
|
return CFBridgingRelease(value);
|
|
}
|
|
return defaultValue;
|
|
}
|
|
|
|
-(BOOL)setWindowProperty:(NSString *)property withValue:(id)value {
|
|
BOOL result = NO;
|
|
if ([value isKindOfClass:NSNumber.class]) {
|
|
result = (AXUIElementSetAttributeValue(self.elementRef, (__bridge CFStringRef)property, (__bridge CFTypeRef)value) == kAXErrorSuccess);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
-(NSString *)title {
|
|
return [self getWindowProperty:NSAccessibilityTitleAttribute withDefaultValue:@""];
|
|
}
|
|
|
|
-(NSString *)subRole {
|
|
return [self getWindowProperty:NSAccessibilitySubroleAttribute withDefaultValue:@""];
|
|
}
|
|
|
|
-(NSString *)role {
|
|
return [self getWindowProperty:NSAccessibilityRoleAttribute withDefaultValue:@""];
|
|
}
|
|
|
|
-(BOOL)isStandard {
|
|
return [self.subRole isEqualToString:(NSString *)kAXStandardWindowSubrole];
|
|
}
|
|
|
|
// FIXME: Can getTopLeft/setTopLeft be converted to use/augment getWindowProperty/setWindowProperty?
|
|
-(NSPoint)getTopLeft {
|
|
CGPoint topLeft = CGPointZero;
|
|
CFTypeRef positionStorage;
|
|
if (AXUIElementCopyAttributeValue(self.elementRef, (CFStringRef)NSAccessibilityPositionAttribute, &positionStorage) == kAXErrorSuccess) {
|
|
if (!AXValueGetValue(positionStorage, kAXValueCGPointType, (void *)&topLeft)) {
|
|
topLeft = CGPointZero;
|
|
}
|
|
CFRelease(positionStorage);
|
|
}
|
|
return NSMakePoint(topLeft.x, topLeft.y);
|
|
}
|
|
|
|
-(void)setTopLeft:(NSPoint)topLeft {
|
|
CFTypeRef positionStorage = (CFTypeRef)(AXValueCreate(kAXValueCGPointType, (const void *)&topLeft));
|
|
AXUIElementSetAttributeValue(self.elementRef, (CFStringRef)NSAccessibilityPositionAttribute, positionStorage);
|
|
if (positionStorage) {
|
|
CFRelease(positionStorage);
|
|
}
|
|
}
|
|
|
|
-(NSSize)getSize {
|
|
CGSize size = CGSizeZero;
|
|
CFTypeRef sizeStorage;
|
|
if (AXUIElementCopyAttributeValue(self.elementRef, (CFStringRef)NSAccessibilitySizeAttribute, &sizeStorage) == kAXErrorSuccess) {
|
|
if (!AXValueGetValue(sizeStorage, kAXValueCGSizeType, (void *)&size)) {
|
|
size = CGSizeZero;
|
|
}
|
|
CFRelease(sizeStorage);
|
|
}
|
|
return NSMakeSize(size.width, size.height);
|
|
}
|
|
|
|
-(void)setSize:(NSSize)size {
|
|
CFTypeRef sizeStorage = (CFTypeRef)(AXValueCreate(kAXValueCGSizeType, (const void *)&size));
|
|
AXUIElementSetAttributeValue(self.elementRef, (CFStringRef)NSAccessibilitySizeAttribute, sizeStorage);
|
|
if (sizeStorage) {
|
|
CFRelease(sizeStorage);
|
|
}
|
|
}
|
|
|
|
-(BOOL)pushButton:(CFStringRef)buttonId {
|
|
BOOL worked = NO;
|
|
AXUIElementRef button = NULL;
|
|
if (AXUIElementCopyAttributeValue(self.elementRef, buttonId, (CFTypeRef*)&button) == noErr) {
|
|
if (AXUIElementPerformAction(button, kAXPressAction) == noErr) {
|
|
worked = YES;
|
|
}
|
|
CFRelease(button);
|
|
}
|
|
return worked;
|
|
}
|
|
|
|
-(void)toggleZoom {
|
|
[self pushButton:kAXZoomButtonAttribute];
|
|
}
|
|
|
|
-(NSRect)getZoomButtonRect {
|
|
NSRect rect = NSZeroRect;
|
|
AXUIElementRef button = nil;
|
|
CFTypeRef pointRef, sizeRef;
|
|
CGPoint point;
|
|
CGSize size;
|
|
|
|
if (AXUIElementCopyAttributeValue(self.elementRef, kAXZoomButtonAttribute, (CFTypeRef*)&button) == noErr) {
|
|
if ((AXUIElementCopyAttributeValue(button, kAXPositionAttribute, &pointRef) == noErr) && (AXUIElementCopyAttributeValue(button, kAXSizeAttribute, &sizeRef) == noErr)) {
|
|
if (AXValueGetValue(pointRef, kAXValueCGPointType, &point) && AXValueGetValue(sizeRef, kAXValueCGSizeType, &size)) {
|
|
rect = NSMakeRect(point.x, point.y, size.width, size.height);
|
|
}
|
|
}
|
|
CFRelease(button);
|
|
}
|
|
return rect;
|
|
}
|
|
|
|
-(BOOL)close {
|
|
return [self pushButton:kAXCloseButtonAttribute];
|
|
}
|
|
|
|
-(int)getTabCount {
|
|
CFIndex count = 0;
|
|
AXUIElementRef tabs = get_window_tabs(self.elementRef);
|
|
if (tabs) {
|
|
AXUIElementGetAttributeValueCount(tabs, kAXTabsAttribute, &count);
|
|
CFRelease(tabs);
|
|
}
|
|
return (int)count;
|
|
}
|
|
|
|
-(BOOL)focusTab:(int)index {
|
|
BOOL worked = NO;
|
|
CFArrayRef children = NULL;
|
|
AXUIElementRef tab = NULL;
|
|
|
|
AXUIElementRef tabs = get_window_tabs(self.elementRef);
|
|
if(tabs == NULL) goto cleanup;
|
|
|
|
if(AXUIElementCopyAttributeValues(tabs, kAXTabsAttribute, 0, 100, &children) != noErr) goto cleanup;
|
|
CFIndex count = CFArrayGetCount(children);
|
|
|
|
CFIndex i = index;
|
|
if(i > count || i <= 0) {
|
|
i = count - 1;
|
|
} else {
|
|
i = i - 1 ; // adjust because lua style indexes start at 1
|
|
}
|
|
tab = CFArrayGetValueAtIndex(children, i);
|
|
|
|
if (AXUIElementPerformAction(tab, kAXPressAction) != noErr) goto cleanup;
|
|
|
|
worked = YES;
|
|
cleanup:
|
|
if (tabs) CFRelease(tabs);
|
|
if (children) CFRelease(children);
|
|
|
|
return worked;
|
|
}
|
|
|
|
-(void)setFullscreen:(BOOL)fullscreen {
|
|
AXUIElementSetAttributeValue(self.elementRef, CFSTR("AXFullScreen"), fullscreen ? kCFBooleanTrue : kCFBooleanFalse);
|
|
}
|
|
|
|
-(BOOL)isFullscreen {
|
|
BOOL fullscreen = NO;
|
|
CFBooleanRef _fullscreen = kCFBooleanFalse;
|
|
if (AXUIElementCopyAttributeValue(self.elementRef, CFSTR("AXFullScreen"), (CFTypeRef*)&_fullscreen) == noErr) {
|
|
fullscreen = CFBooleanGetValue(_fullscreen);
|
|
CFRelease(_fullscreen);
|
|
}
|
|
return fullscreen;
|
|
}
|
|
|
|
-(BOOL)isMinimized {
|
|
NSNumber *minimized = [self getWindowProperty:NSAccessibilityMinimizedAttribute withDefaultValue:@(NO)];
|
|
return minimized.boolValue;
|
|
}
|
|
|
|
-(void)setMinimized:(BOOL)minimize {
|
|
[self setWindowProperty:NSAccessibilityMinimizedAttribute withValue:@(minimize)];
|
|
}
|
|
|
|
-(void)becomeMain {
|
|
[self setWindowProperty:NSAccessibilityMainAttribute withValue:@(YES)];
|
|
}
|
|
|
|
-(void)raise {
|
|
AXUIElementPerformAction(self.elementRef, kAXRaiseAction);
|
|
}
|
|
|
|
-(NSImage *)snapshot:(BOOL)keepTransparency {
|
|
NSImage *image = nil;
|
|
CGWindowID windowID;
|
|
if (_AXUIElementGetWindow(self.elementRef, &windowID) == kAXErrorSuccess) {
|
|
image = [HSwindow snapshotForID:windowID keepTransparency:keepTransparency];
|
|
}
|
|
return image;
|
|
}
|
|
@end
|