├── .gitignore ├── .gitmodules ├── BBLAccessibility.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── BBLAccessibility.xcscmblueprint └── xcshareddata │ └── xcschemes │ ├── BBLAccessibility.xcscheme │ ├── BBLAccessibilityApp.xcscheme │ ├── TabInference.xcscheme │ ├── WindowCoordinator.xcscheme │ └── WindowCoordinatorDemo.xcscheme ├── BBLAccessibility ├── AccessibilityHelper.swift ├── AccessibilityInfo-ext.swift ├── AccessibilityInfo.h ├── AccessibilityInfo.m ├── BBLAccessibility.h ├── BBLAccessibilityPublisher-ext.swift ├── BBLAccessibilityPublisher.h ├── BBLAccessibilityPublisher.m ├── Info.plist ├── Silica-ext.h ├── Silica-ext.m ├── Silica-ext.swift └── logging.h ├── BBLAccessibilityApp ├── AppDelegate.swift ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── BBLAccessibilityApp-Bridging-Header.h ├── Base.lproj │ └── MainMenu.xib └── Info.plist ├── BBLAccessibilityAppUITests ├── BBLAccessibilityAppUITests.swift └── Info.plist ├── Cartfile ├── Cartfile.resolved ├── README.md ├── ResizeWindowsDemo └── Base.lproj │ └── Main.storyboard ├── TabInference ├── Info.plist ├── TabInference.h └── TabInference.swift ├── TabInferenceTests ├── Info.plist └── TabInferenceTests.swift ├── WindowCoordinator ├── Info.plist ├── WindowCoordinator.h └── WindowCoordinator.swift ├── WindowCoordinatorDemo ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ └── Main.storyboard ├── ContentView.swift ├── Info.plist ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json └── WindowCoordinatorDemo.entitlements ├── lab └── NMAccessibility │ ├── .gitignore │ ├── NMAccessibility │ ├── Info.plist │ └── NMAccessibility.h │ ├── NMTest001.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── NMTest001.xcscmblueprint │ └── xcshareddata │ │ └── xcschemes │ │ ├── NMAccessibility.xcscheme │ │ └── NMTest001.xcscheme │ ├── NMTest001 │ ├── NMTest001-Info.plist │ ├── NMTest001-Prefix.pch │ ├── TestAppDelegate.h │ ├── TestAppDelegate.m │ ├── TestWindowController.h │ ├── TestWindowController.m │ ├── WebKitSystemInterface.h │ ├── Window.xib │ ├── en.lproj │ │ ├── Credits.rtf │ │ ├── InfoPlist.strings │ │ └── MainMenu.xib │ └── main.m │ └── NMUIElement │ ├── NMUIElement.h │ └── NMUIElement.m └── withPrior.swift /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | BBLAccessibility.xcodeproj/project.xcworkspace/xcuserdata 3 | BBLAccessibility.xcworkspace/xcuserdata 4 | NMAccessibility/NMTest001.xcodeproj/project.xcworkspace/xcuserdata 5 | NMAccessibility/NMTest001.xcodeproj/xcuserdata 6 | BBLAccessibility.xcodeproj/xcuserdata/ 7 | Carthage 8 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Silica"] 2 | path = Silica 3 | url = git@github.com:bigbearlabs/Silica.git 4 | update = rebase 5 | -------------------------------------------------------------------------------- /BBLAccessibility.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /BBLAccessibility.xcodeproj/project.xcworkspace/xcshareddata/BBLAccessibility.xcscmblueprint: -------------------------------------------------------------------------------- 1 | { 2 | "DVTSourceControlWorkspaceBlueprintPrimaryRemoteRepositoryKey" : "0187FD8E7B296CEAF7520105D6EDD775240C7EBA", 3 | "DVTSourceControlWorkspaceBlueprintWorkingCopyRepositoryLocationsKey" : { 4 | 5 | }, 6 | "DVTSourceControlWorkspaceBlueprintWorkingCopyStatesKey" : { 7 | "06E7CE2D0ECFC9E9F3EE80734E82F3C3A4072C4C" : 9223372036854775807, 8 | "BE91C9C5ADF3ADEE2969E31AF581A0C779CC0273" : 0, 9 | "0187FD8E7B296CEAF7520105D6EDD775240C7EBA" : 9223372036854775807, 10 | "6C71DF8D4EAED84A4BB92018D54642F26E11361D" : 9223372036854775807 11 | }, 12 | "DVTSourceControlWorkspaceBlueprintIdentifierKey" : "D0D44E5A-6457-4982-AB80-D87C2193550D", 13 | "DVTSourceControlWorkspaceBlueprintWorkingCopyPathsKey" : { 14 | "06E7CE2D0ECFC9E9F3EE80734E82F3C3A4072C4C" : "..", 15 | "BE91C9C5ADF3ADEE2969E31AF581A0C779CC0273" : "BBLAccessibility\/Silica\/", 16 | "0187FD8E7B296CEAF7520105D6EDD775240C7EBA" : "BBLAccessibility\/", 17 | "6C71DF8D4EAED84A4BB92018D54642F26E11361D" : "" 18 | }, 19 | "DVTSourceControlWorkspaceBlueprintNameKey" : "BBLAccessibility", 20 | "DVTSourceControlWorkspaceBlueprintVersion" : 204, 21 | "DVTSourceControlWorkspaceBlueprintRelativePathToProjectKey" : "BBLAccessibility.xcodeproj", 22 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoriesKey" : [ 23 | { 24 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "github.com:bigbearlabs\/BBLAccessibility.git", 25 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 26 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "0187FD8E7B296CEAF7520105D6EDD775240C7EBA" 27 | }, 28 | { 29 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "bitbucket.org:bigbearlabs\/contexter-helper.git", 30 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 31 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "06E7CE2D0ECFC9E9F3EE80734E82F3C3A4072C4C" 32 | }, 33 | { 34 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "bitbucket.org:bigbearlabs\/contexter.git", 35 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 36 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "6C71DF8D4EAED84A4BB92018D54642F26E11361D" 37 | }, 38 | { 39 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "github.com:bigbearlabs\/Silica.git", 40 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 41 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "BE91C9C5ADF3ADEE2969E31AF581A0C779CC0273" 42 | } 43 | ] 44 | } -------------------------------------------------------------------------------- /BBLAccessibility.xcodeproj/xcshareddata/xcschemes/BBLAccessibility.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 51 | 52 | 53 | 54 | 55 | 56 | 66 | 67 | 73 | 74 | 75 | 76 | 82 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /BBLAccessibility.xcodeproj/xcshareddata/xcschemes/BBLAccessibilityApp.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 42 | 48 | 49 | 50 | 51 | 52 | 62 | 64 | 70 | 71 | 72 | 73 | 79 | 81 | 87 | 88 | 89 | 90 | 92 | 93 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /BBLAccessibility.xcodeproj/xcshareddata/xcschemes/TabInference.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 54 | 60 | 61 | 67 | 68 | 69 | 70 | 72 | 73 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /BBLAccessibility.xcodeproj/xcshareddata/xcschemes/WindowCoordinator.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 57 | 58 | 59 | 60 | 62 | 63 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /BBLAccessibility.xcodeproj/xcshareddata/xcschemes/WindowCoordinatorDemo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /BBLAccessibility/AccessibilityHelper.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import ApplicationServices 3 | import BBLBasics 4 | 5 | 6 | 7 | open class AccessibilityHelper { 8 | 9 | var lastOnlyQueue: LastOnlyQueue? 10 | 11 | public init() {} 12 | 13 | 14 | @discardableResult 15 | open func queryAccessibilityPermission( 16 | systemPromptIfNotFound: Bool = true, 17 | onPermissionFound: () -> Void = {}, 18 | onPermissionReceived: @escaping () -> Void = {}, 19 | onPollFindsNoPermission: @escaping () -> Void = {}) -> Bool { 20 | 21 | // first check if we have the perm. 22 | let isOriginallyPermissioned = AXIsProcessTrustedWithOptions(nil) 23 | if isOriginallyPermissioned { 24 | onPermissionFound() 25 | return true 26 | } 27 | 28 | // real world situation 1. 29 | // no perm, so we prompt once, then poll. 30 | 31 | guard systemPromptIfNotFound else { 32 | return false 33 | } 34 | 35 | // 1. no perm + prompt option -> will fail, prompt. 36 | let promptOptionKey = kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String 37 | let options = [ 38 | promptOptionKey: true 39 | ] 40 | _ = AXIsProcessTrustedWithOptions(options as CFDictionary) 41 | 42 | // sparsely and repeatedly check for perm. 43 | let shouldPoll = true 44 | self.lastOnlyQueue = LastOnlyQueue() 45 | self.lastOnlyQueue!.pollingAsync { [unowned self] in 46 | 47 | let isPermissioned = AXIsProcessTrustedWithOptions(nil) 48 | 49 | // since we got sent to a polling queue, we stop it after one invocation. 50 | if !shouldPoll || isPermissioned { 51 | self.lastOnlyQueue!.pollStop() 52 | self.lastOnlyQueue = nil 53 | } 54 | 55 | if isPermissioned { 56 | // we obtained the perm in the recursive call chain. 57 | onPermissionReceived() 58 | } 59 | else { 60 | onPollFindsNoPermission() 61 | } 62 | } 63 | 64 | return false 65 | } 66 | 67 | 68 | open func showSystemAxRequestDialog() { 69 | let promptOptionKey = kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String 70 | let options = [ 71 | promptOptionKey: true 72 | ] as CFDictionary 73 | 74 | let isPermissioned = AXIsProcessTrustedWithOptions(options) 75 | 76 | if isPermissioned { 77 | // fatalError("invalid call -- can't show system ax request dialog when we already have perms") 78 | } 79 | 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /BBLAccessibility/AccessibilityInfo-ext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccessibilityInfo-ext.swift 3 | // BBLAccessibility 4 | // 5 | // Created by ilo on 30/06/2017. 6 | // Copyright © 2017 Big Bear Labs. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import BBLBasics 11 | 12 | 13 | public struct AxApplication { 14 | 15 | let siApp: SIApplication 16 | 17 | public init(bundleId: String) { 18 | self.init(siApplication: SIApplication.application(bundleId: bundleId)!) 19 | // improve: reuse siapp dictionary in the publisher when we can. 20 | } 21 | 22 | init(siApplication: SIApplication) { 23 | self.siApp = siApplication 24 | } 25 | 26 | public func focus(windowNumber: CGWindowID) { 27 | print("focusing window \(windowNumber)") 28 | let matches = self.siApp.uncachedWindows.filter { 29 | $0.windowID == windowNumber 30 | } 31 | 32 | if let match = matches.first { 33 | match.focusOnlyThisWindow() 34 | } 35 | } 36 | 37 | public static var focused: AxApplication? { 38 | if let app = SIApplication.focused() { 39 | return self.init(siApplication: app) 40 | } 41 | return nil 42 | } 43 | 44 | public var pid: pid_t { 45 | return self.siApp.processIdentifier() 46 | } 47 | } 48 | 49 | 50 | extension AccessibilityInfo { 51 | 52 | public var buttonGroupRect: CGRect? { 53 | guard let window = self.windowElement else { 54 | // there's no window!? 55 | 56 | return nil 57 | } 58 | 59 | var revealFrame = window.closeButton?.frame() 60 | if let minimiseFrame = window.minimiseButton?.frame() { 61 | revealFrame = revealFrame?.union(minimiseFrame) 62 | } 63 | if let zoomFrame = window.zoomButton?.frame() { 64 | revealFrame = revealFrame?.union(zoomFrame) 65 | } 66 | 67 | return revealFrame?.toCocoaFrame() 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /BBLAccessibility/AccessibilityInfo.h: -------------------------------------------------------------------------------- 1 | // 2 | // AccessibilityInfo.h 3 | // BBLAccessibility 4 | // 5 | // Created by ilo on 18.11.16. 6 | // Copyright © 2016 Big Bear Labs. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | #import "Silica-ext.h" 12 | 13 | @interface AccessibilityInfo : NSObject 14 | 15 | 16 | @property(readonly) NSString* _Nonnull axNotification; 17 | 18 | @property(readonly) NSString* _Nullable appName; 19 | @property(readonly) pid_t pid; 20 | 21 | @property(readonly) NSString* _Nullable role; 22 | @property(readonly) NSString* _Nullable windowRole; 23 | @property(readonly) NSString* _Nullable windowSubrole; 24 | 25 | @property(readonly) NSString* _Nullable windowTitle; 26 | @property(readonly) NSString* _Nonnull windowId; 27 | @property(readonly) NSRect windowRect; 28 | 29 | @property(readonly) NSString* _Nullable selectedText; 30 | @property(readonly) NSRect selectionBounds; 31 | 32 | @property(readonly) NSString* _Nullable text; 33 | 34 | @property(readonly) SIWindow* _Nullable windowElement; 35 | 36 | -(nonnull instancetype)initWithAppElement:(nonnull SIApplication*)element 37 | axNotification:(nonnull CFStringRef)axNotification 38 | ; 39 | 40 | -(nonnull instancetype)initWithAppElement:(nonnull SIApplication*)appElement 41 | focusedElement:(nullable SIAccessibilityElement*)element 42 | axNotification:(nonnull CFStringRef)axNotification 43 | ; 44 | 45 | @end 46 | -------------------------------------------------------------------------------- /BBLAccessibility/AccessibilityInfo.m: -------------------------------------------------------------------------------- 1 | // 2 | // AccessibilityInfo.m 3 | // BBLAccessibility 4 | // 5 | // Created by ilo on 18.11.16. 6 | // Copyright © 2016 Big Bear Labs. All rights reserved. 7 | // 8 | 9 | #import "AccessibilityInfo.h" 10 | 11 | 12 | @implementation AccessibilityInfo 13 | { 14 | SIAccessibilityElement* _focusedElement; 15 | } 16 | 17 | 18 | -(instancetype)initWithAppElement:(SIApplication*)app axNotification:(CFStringRef)axNotification; 19 | { 20 | return [self initWithAppElement:app 21 | focusedElement:nil 22 | axNotification:axNotification]; 23 | } 24 | 25 | 26 | // TODO keep reference of element and maybe even the notif responsible for creation, to better keep track of the context of ax events. 27 | /// not thread-safe -- caller must ensure thread confinement. 28 | -(instancetype)initWithAppElement:(SIApplication*)appElement 29 | focusedElement:(SIAccessibilityElement*)focusedElement 30 | axNotification:(CFStringRef)axNotification 31 | { 32 | self = [super init]; 33 | if (self) { 34 | _axNotification = [(__bridge NSString*)axNotification copy]; 35 | 36 | _appName = appElement.title; 37 | _pid = appElement.processIdentifier; 38 | 39 | _focusedElement = focusedElement; 40 | _role = focusedElement.role; 41 | 42 | if (focusedElement != nil) { 43 | _windowElement = [SIWindow windowForElement:focusedElement]; 44 | } 45 | if (_windowElement == nil || _windowElement.windowID == kCGNullWindowID) { 46 | _windowElement = appElement.focusedWindow; 47 | } 48 | 49 | _windowTitle = _windowElement.title; 50 | _windowId = @(_windowElement.windowID).stringValue; 51 | _windowRect = _windowElement.frame; 52 | _windowRole = _windowElement.role; 53 | _windowSubrole = _windowElement.subrole; 54 | 55 | } 56 | 57 | return self; 58 | } 59 | 60 | 61 | -(NSString*) text { 62 | return _focusedElement.text; 63 | } 64 | 65 | -(NSString*)selectedText { 66 | return _focusedElement.selectedText; 67 | } 68 | -(NSRect)selectionBounds { 69 | return _focusedElement.selectionBounds; 70 | } 71 | 72 | -(NSString *)description { 73 | NSObject* selectedText = self.selectedText; 74 | NSUInteger selectedTextHash = selectedText.hash; 75 | NSUInteger selectionBoundsHash = self.selectedText != nil ? 76 | @(self.selectionBounds).hash 77 | : 0; 78 | 79 | return [NSString stringWithFormat: 80 | @"ax: %@, app: %@, pid: %@, title: %@, windowId: %@, windowRect: %@, selectedTextHash: %@, selectionBoundsHash: %@, role: %@, windowRole: %@, windowSubrole: %@", 81 | _axNotification, _appName, @(_pid), _windowTitle, _windowId, [NSValue valueWithRect:_windowRect], @(selectedTextHash), @(selectionBoundsHash), _role, _windowRole, _windowSubrole 82 | ]; 83 | } 84 | 85 | 86 | - (BOOL)isEqual:(id)other 87 | { 88 | if (other == nil) { 89 | return NO; 90 | } 91 | if (other == self) { 92 | return YES; 93 | } 94 | 95 | AccessibilityInfo* theOther = (AccessibilityInfo*) other; 96 | return 97 | [_axNotification isEqual:theOther.axNotification] 98 | && NSEqualRects(_windowRect, theOther.windowRect) 99 | && [_focusedElement isEqual:theOther->_focusedElement] 100 | // should cover bundle id, pid 101 | && [_role isEqual:theOther.role] 102 | && [_windowElement isEqual:theOther.windowElement] 103 | // should cover window id, window role, window subrole 104 | && [_windowTitle isEqual:theOther.windowTitle]; 105 | } 106 | 107 | - (NSUInteger)hash 108 | { 109 | return @[ 110 | _axNotification, 111 | @(_windowRect), 112 | _focusedElement, 113 | _role, 114 | _windowElement, 115 | _windowTitle 116 | ].hash; 117 | } 118 | 119 | 120 | @end 121 | -------------------------------------------------------------------------------- /BBLAccessibility/BBLAccessibility.h: -------------------------------------------------------------------------------- 1 | // 2 | // BBLAccessibility.h 3 | // BBLAccessibility 4 | // 5 | // Created by ilo on 15/04/2016. 6 | // Copyright © 2016 Big Bear Labs. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for BBLAccessibility. 12 | FOUNDATION_EXPORT double BBLAccessibilityVersionNumber; 13 | 14 | //! Project version string for BBLAccessibility. 15 | FOUNDATION_EXPORT const unsigned char BBLAccessibilityVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | #import 21 | #import 22 | 23 | -------------------------------------------------------------------------------- /BBLAccessibility/BBLAccessibilityPublisher-ext.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import AppKit 3 | 4 | 5 | 6 | public typealias AxNotification = String 7 | 8 | 9 | 10 | public extension BBLAccessibilityPublisher { 11 | 12 | @objc 13 | var axNotificationsToObserve: [AxNotification] { 14 | [ 15 | kAXApplicationActivatedNotification, 16 | kAXApplicationDeactivatedNotification, 17 | 18 | kAXApplicationShownNotification, 19 | kAXApplicationHiddenNotification, 20 | 21 | kAXWindowCreatedNotification, 22 | 23 | kAXMainWindowChangedNotification, 24 | kAXFocusedWindowChangedNotification, 25 | 26 | kAXWindowMovedNotification, 27 | kAXWindowResizedNotification, 28 | kAXTitleChangedNotification, 29 | "AXFocusedTabChanged", 30 | 31 | kAXWindowMiniaturizedNotification, 32 | kAXWindowDeminiaturizedNotification, 33 | 34 | // kAXUIElementDestroyedNotification, // obsreved for individual windows. 35 | 36 | kAXFocusedUIElementChangedNotification, 37 | 38 | // kAXSelectedTextChangedNotification 39 | ] 40 | } 41 | 42 | } 43 | 44 | 45 | public extension BBLAccessibilityPublisher { 46 | 47 | func windows(pid: pid_t, completionHandler: @escaping ([SIWindow]) -> Void) { 48 | siQuery(pid: pid) { siApp in 49 | completionHandler( 50 | siApp?.uncachedWindows 51 | ?? [] 52 | ) 53 | } 54 | } 55 | 56 | func focusedWindow(pid: pid_t, completionHandler: @escaping (SIWindow?) -> Void) { 57 | siQuery(pid: pid) { siApp in 58 | completionHandler(siApp?.focusedWindow()) 59 | } 60 | } 61 | 62 | func focusedWindow(completionHandler: @escaping (SIWindow?) -> Void) { 63 | guard let frontmostApp = NSWorkspace.shared.frontmostApplication 64 | else { 65 | completionHandler(nil) 66 | return 67 | } 68 | 69 | focusedWindow(pid: frontmostApp.processIdentifier, completionHandler: completionHandler) 70 | } 71 | 72 | 73 | // MARK: - 74 | 75 | /** 76 | @callback siAppHandler: 77 | - @param siAppilcation nil when pid is not subject to ax queries (e.g. due to #shouldObserve) 78 | 79 | */ 80 | // FIXME will get stuck with e.g. Archive Utility expanding an Xcode xip, so shouldn't call on main thread. 81 | // WORKAROUND aggressively short timeout to reduce impact of blocks from AX API calls. 82 | func siQuery( 83 | pid: pid_t, 84 | timeout: TimeInterval = 1, 85 | completionHandler: @escaping (SIApplication?) -> Void 86 | ) { 87 | if let siApp = self.appElement(forProcessIdentifier: pid) { 88 | execAsyncSynchronising(onPid: siApp.processIdentifier()) { 89 | completionHandler(siApp) 90 | } 91 | } else { 92 | completionHandler(nil) 93 | } 94 | } 95 | 96 | // MARK: - 97 | 98 | // NOTE #execAsyncSynchronising causes thread explosion due to its use of global concurrent queues. 99 | // this freaks us out every now and then when we see 100+ threads. 100 | // 101 | // But after considering many alternatives, this is still considered the best trade-off to make as of now, since: 102 | // - it has the least impact in the face of a blocked thread when making ax queries (e.g. Expander unzipping Xcode) 103 | // - the only occasions where thread explosion occurs are: a) on app launch where we need all ax subscriptions -- this is very thread-heavy, and b) space changes bring forth a lot of new apps that need to be queried -- this is moderately thread-heavy. 104 | // 105 | // in the future we may be able to reimplement this method to work on swift async operations + precise cancellations. 106 | // but it's still unclear whether such an approach would mitigate issues when the ax query blocks and doesn't return, which in principle can happen with any locked process currently running. 107 | // so, revisit when thread explosion becomes a more material issue. 108 | // // temp swift-impl to investigate cause of abandoned memory 109 | // @objc 110 | // func execAsyncSynchronising(onObject: NSObject, block: @escaping () -> Void) { 111 | // IMPL! 112 | // } 113 | } 114 | 115 | // MARK: - ax observation of newly launched apps 116 | 117 | public extension BBLAccessibilityPublisher { 118 | 119 | @objc 120 | func observeLaunch(_ handler: @escaping (_ app: NSRunningApplication) -> Void) { 121 | handleLaunchSubscription = NSWorkspace.shared.notificationCenter.publisher(for: NSWorkspace.didLaunchApplicationNotification, object: nil) 122 | .map { notif in 123 | guard let app = notif.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication 124 | else { fatalError() } 125 | 126 | return app 127 | } 128 | .sink { app in 129 | handler(app) 130 | } 131 | } 132 | 133 | @objc 134 | func unobserveLaunch() { 135 | handleLaunchSubscription = nil 136 | } 137 | 138 | @objc 139 | func observeTerminate(_ handler: @escaping (_ app: NSRunningApplication) -> Void) { 140 | handleTerminateSubscription = NSWorkspace.shared.notificationCenter.publisher(for: NSWorkspace.didTerminateApplicationNotification, object: nil) 141 | .map { notif in 142 | guard let app = notif.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication 143 | else { fatalError() } 144 | 145 | return app 146 | } 147 | .sink { app in 148 | handler(app) 149 | } 150 | } 151 | 152 | @objc 153 | func unobserveTerminate() { 154 | handleTerminateSubscription = nil 155 | } 156 | 157 | } 158 | 159 | 160 | var handleLaunchSubscription: Any? 161 | var handleTerminateSubscription: Any? 162 | 163 | 164 | enum AppEvent { 165 | case launched(NSRunningApplication) 166 | case terminated(NSRunningApplication) 167 | } 168 | 169 | 170 | let appEventPublisher = PassthroughSubject() 171 | 172 | 173 | public actor RunningApplicationsBookkeeper { 174 | 175 | var finishedLaunchingSubsByPid: [pid_t : Any] = [:] 176 | 177 | public var runningApplications = NSWorkspace.shared.runningApplications { 178 | didSet { 179 | let newApps = Set(runningApplications).subtracting(oldValue) 180 | .filter { 181 | // filter out the terminated ones 182 | !$0.isTerminated 183 | // occasionally we get a corrupt instance with pid -1. 184 | && $0.processIdentifier > 0 185 | } 186 | 187 | let newAppSubs = Dictionary(uniqueKeysWithValues: newApps.map { app in 188 | ( 189 | app.processIdentifier, 190 | ( 191 | app, 192 | app.publisher(for: \.isFinishedLaunching, options: [.initial, .new]) 193 | .filter { $0 == true } 194 | .sink { isFinishedLaunching in 195 | appEventPublisher.send(.launched(app)) 196 | self.finishedLaunchingSubsByPid.removeValue(forKey: app.processIdentifier) 197 | } 198 | ) 199 | ) 200 | }) 201 | 202 | 203 | self.finishedLaunchingSubsByPid.merge(newAppSubs) { $1 } 204 | // most app will signal readiness via isFinishedLaunching. 205 | // what of apps that never do? warn after delay? 206 | 207 | let terminatedApps = Set(oldValue).subtracting(runningApplications) 208 | for app in terminatedApps { 209 | appEventPublisher.send(.terminated(app)) 210 | self.finishedLaunchingSubsByPid.removeValue(forKey: app.processIdentifier) 211 | } 212 | } 213 | } 214 | 215 | } 216 | 217 | // TODO re-implement the async dispatch method that avoids current issues: 218 | // - global concurrent queue thread proliferation resulting in a crash as thread count hits 256 (possibly happening when any one of the critical section blocks 219 | // 220 | // DEFERRED wait until swift actors arrive (xcode 13). 221 | 222 | //public extension BBLAccessibilityPublisher { 223 | // 224 | // @objc 225 | // func execAsyncSynchronisingOnObject(_ object: Any, block: () -> Void) { 226 | // 227 | // } 228 | // 229 | //} 230 | 231 | -------------------------------------------------------------------------------- /BBLAccessibility/BBLAccessibilityPublisher.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import "AccessibilityInfo.h" 4 | 5 | @interface BBLAccessibilityPublisher : NSObject 6 | 7 | NS_ASSUME_NONNULL_BEGIN 8 | 9 | @property(readonly) NSArray* applicationsToObserve; 10 | 11 | //@property(readonly,copy)NSDictionary* observedAppsByPid; 12 | 13 | @property(readonly,copy) NSDictionary* accessibilityInfosByPid; // a growing dict of ax infos. 14 | 15 | //@property(readonly) pid_t frontmostProcessIdentifier; 16 | 17 | @property(readwrite,copy) NSMutableDictionary* observedAppsByPid; 18 | 19 | @property(readonly,copy,atomic,nonnull) NSDictionary* bundleIdsByPid; // cache bundle ids as the processes come and go, to avoid hot path to NSRunningApplication.bundleIdentifier / its backing LS function (which showed up a few times as suspicious) 20 | 21 | -(BOOL)shouldObserveApplication: (NSRunningApplication*)application; 22 | 23 | -(void) observeAxEvents; 24 | 25 | -(void) unobserveAxEvents; 26 | 27 | 28 | -(NSArray*) observeAxEventsForApplication:(NSRunningApplication*)application; 29 | 30 | -(void) unobserveAxEventsForApplication:(NSRunningApplication*)app; 31 | 32 | 33 | //-(void) onApplicationActivated:(SIAccessibilityElement*)element; 34 | // 35 | //-(void) onFocusedElementChanged:(SIAccessibilityElement*)element; 36 | // 37 | //-(void) onFocusedWindowChanged:(SIWindow*)window; 38 | // 39 | // 40 | //-(void) onWindowCreated:(SIWindow*)window; 41 | // 42 | //-(void) onWindowMinimised:(SIWindow*)window; 43 | // 44 | //-(void) onWindowUnminimised:(SIWindow*)window; 45 | // 46 | //-(void) onWindowMoved:(SIWindow*)window; 47 | // 48 | //-(void) onWindowResized:(SIWindow*)window; 49 | // 50 | // 51 | //-(void) onTitleChanged:(SIWindow*)window; 52 | // 53 | //-(void) onTextSelectionChanged:(SIAccessibilityElement*)element; 54 | // 55 | //-(void) onElementDestroyed:(SIAccessibilityElement*)element; 56 | 57 | 58 | -(AccessibilityInfo*) accessibilityInfoForElement:(SIAccessibilityElement*)siElement axNotification:(CFStringRef)axNotification; 59 | 60 | //-(void) updateAccessibilityInfoForElement:(SIAccessibilityElement*)siElement axNotification:(CFStringRef)axNotification; 61 | -(void) updateAccessibilityInfoForElement:(SIAccessibilityElement*)siElement axNotification:(CFStringRef)axNotification forceUpdate:(BOOL)forceUpdate; 62 | 63 | 64 | -(SIWindow*) keyWindowForApplication:(SIApplication*) application; 65 | 66 | @property(readonly) AccessibilityInfo* _Nullable focusedWindowAccessibilityInfo; 67 | 68 | //-(NSArray*) windowsForPid:(pid_t)pid; 69 | 70 | 71 | -(SIApplication* _Nullable) appElementForProcessIdentifier:(pid_t)processIdentifier; 72 | 73 | 74 | -(void) execAsyncSynchronisingOnPid:(pid_t)pid block:(void(^)(void))block; 75 | 76 | //-(void) execAsyncSynchronisingOnObject:(id)object block:(void(^)(void))block; 77 | 78 | 79 | -(void) handleAxObservationResults:(NSArray*) axResults forRunningApplication:(NSRunningApplication*) application; 80 | 81 | 82 | // notif center registration 83 | -(void) observeInternalNotification; 84 | -(void) unobserveInternalNotification; 85 | 86 | 87 | NS_ASSUME_NONNULL_END 88 | 89 | @end 90 | 91 | -------------------------------------------------------------------------------- /BBLAccessibility/BBLAccessibilityPublisher.m: -------------------------------------------------------------------------------- 1 | #import "BBLAccessibilityPublisher.h" 2 | #import 3 | #import 4 | #import "logging.h" 5 | #import 6 | 7 | // FIXME some performance problems with: 8 | // console.app (too frequent notifs for title change) 9 | // xcode.app (frequent ax event vomits) 10 | 11 | @interface BBLAccessibilityPublisher () 12 | @property(readwrite,copy) NSDictionary* accessibilityInfosByPid; 13 | @end 14 | 15 | 16 | 17 | @implementation BBLAccessibilityPublisher 18 | { 19 | // control load of concurrent queue. 20 | dispatch_semaphore_t semaphore; 21 | dispatch_queue_t serialQueue; 22 | 23 | id notificationCenterObserverToken; 24 | } 25 | 26 | - (instancetype)init 27 | { 28 | self = [super init]; 29 | if (self) { 30 | _accessibilityInfosByPid = [@{} mutableCopy]; 31 | 32 | _observedAppsByPid = [@{} mutableCopy]; 33 | 34 | serialQueue = dispatch_queue_create( 35 | "BBLAccessiblityPublisher-serial", 36 | dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INTERACTIVE, 0)); 37 | 38 | NSUInteger processorCount = NSProcessInfo.processInfo.processorCount; 39 | semaphore = dispatch_semaphore_create(processorCount); 40 | } 41 | return self; 42 | } 43 | 44 | - (void)dealloc 45 | { 46 | } 47 | 48 | 49 | #pragma mark - 50 | 51 | -(NSArray*) applicationsToObserve { 52 | NSMutableArray* apps = @[].mutableCopy; 53 | for (NSRunningApplication* app in [[NSWorkspace sharedWorkspace] runningApplications]) { 54 | if ([self shouldObserveApplication:app]) { 55 | [apps addObject:app]; 56 | } 57 | } 58 | return apps; 59 | } 60 | 61 | -(BOOL)shouldObserveApplication: (NSRunningApplication*)application { 62 | return true; 63 | } 64 | 65 | #pragma mark - 66 | 67 | -(void) observeInternalNotification { 68 | __weak BBLAccessibilityPublisher* blockSelf = self; 69 | notificationCenterObserverToken = [NSNotificationCenter.defaultCenter addObserverForName:AX_EVENT_NOTIFICATION object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) { 70 | 71 | // __log("!!notif: %@", note); 72 | 73 | SIAXNotificationData* axData = note.userInfo[AX_EVENT_NOTIFICATION_DATA]; 74 | 75 | [blockSelf invokeHandlerForAxNotificationData:axData]; 76 | }]; 77 | } 78 | 79 | -(void) unobserveInternalNotification { 80 | if (notificationCenterObserverToken) { 81 | [NSNotificationCenter.defaultCenter removeObserver:notificationCenterObserverToken]; 82 | } 83 | } 84 | 85 | -(void) invokeHandlerForAxNotificationData:(SIAXNotificationData*) axData { 86 | CFStringRef notification = axData.axNotification; 87 | SIAccessibilityElement* siElement = axData.siElement; 88 | 89 | // [self execAsyncSynchronisingOn:siApp block:^{ 90 | [self updateAccessibilityInfoForElement:siElement axNotification:notification]; 91 | // }]; 92 | } 93 | 94 | -(void) observeAxEvents { 95 | [self observeInternalNotification]; 96 | 97 | __weak BBLAccessibilityPublisher* blockSelf = self; 98 | 99 | // observe ax of newly launched apps. 100 | [self observeLaunch: ^(NSRunningApplication* _Nonnull app) { 101 | if ([blockSelf shouldObserveApplication:app]) { 102 | 103 | NSArray* axResults = [blockSelf observeAxEventsForApplication:app]; 104 | 105 | [blockSelf handleAxObservationResults:axResults forRunningApplication:app]; 106 | 107 | // // ensure ax info doesn't lag after new windows. 108 | // SIWindow* window = [SIApplication applicationWithRunningApplication:app].focusedWindow; 109 | 110 | // SIAXNotificationHandler handler = [blockSelf handlersByNotificationTypes][(__bridge NSString*)kAXFocusedWindowChangedNotification]; 111 | // handler(window); 112 | 113 | // check if app is active, manually issue ax notif. 114 | if ([SIApplication.focusedApplication.runningApplication isEqualTo:app]) { 115 | [blockSelf updateAccessibilityInfoForApplication:app axNotification:kAXApplicationActivatedNotification]; 116 | } 117 | 118 | __log("%@ launched, ax observations added", app); 119 | 120 | } else { 121 | __log("👺 %@ not in ax observation scope", app); 122 | } 123 | 124 | }]; 125 | 126 | // clean up on terminated apps. 127 | [self observeTerminate: ^(NSRunningApplication* _Nonnull app) { 128 | 129 | if ([blockSelf shouldObserveApplication:app]) { 130 | 131 | [blockSelf unobserveAxEventsForApplication:app]; 132 | 133 | NSMutableDictionary* axInfos = blockSelf.accessibilityInfosByPid.mutableCopy; 134 | [axInfos removeObjectForKey:@(app.processIdentifier)]; 135 | 136 | __log("%@ terminated, ax observations removed", app); 137 | } 138 | }]; 139 | 140 | // observe all current apps. 141 | // NOTE it still takes a while for the notifs to actually invoke the handlers. at least with concurrent set up we don't hog the main thread as badly as before. 142 | for (NSRunningApplication* app in self.applicationsToObserve) { 143 | NSArray* axResults = [self observeAxEventsForApplication:app]; 144 | [self handleAxObservationResults: axResults forRunningApplication:app]; 145 | } 146 | 147 | __log("%@ is watching the windows", self); 148 | } 149 | 150 | -(void) unobserveAxEvents { 151 | 152 | @synchronized(_observedAppsByPid) { 153 | for (SIApplication* app in _observedAppsByPid.allValues) { 154 | [self unobserveAxEventsForApplication:app.runningApplication]; 155 | } 156 | } 157 | 158 | [self unobserveTerminate]; 159 | 160 | [self unobserveLaunch]; 161 | 162 | [self unobserveInternalNotification]; 163 | 164 | __log("%@ is no longer watching the windows", self); 165 | } 166 | 167 | 168 | // TODO return errors for further processing by callers. 169 | -(NSArray*) observeAxEventsForApplication:(NSRunningApplication*)application { 170 | SIApplication* siApp = [SIApplication applicationWithRunningApplication:application]; 171 | 172 | __log("%@ registering observation for app %@", self, application); 173 | 174 | NSMutableArray* observationFailures = @[].mutableCopy; 175 | 176 | for (NSString* notification in [self axNotificationsToObserve]) { 177 | 178 | AXError observeResult = [siApp observeAxNotification:(__bridge CFStringRef)notification withElement:siApp]; 179 | if (observeResult != kAXErrorSuccess) { 180 | [observationFailures addObject:@(observeResult)]; 181 | } 182 | } 183 | 184 | if (observationFailures.count > 0) { 185 | __log("👺 %@: ax observation failed with codes: %@", siApp, [[[NSSet setWithArray:observationFailures] allObjects] componentsJoinedByString:@", "]); 186 | return observationFailures; 187 | } 188 | 189 | // in order for the notifications to work, we must retain the SIApplication. 190 | @synchronized(_observedAppsByPid) { 191 | _observedAppsByPid[@(application.processIdentifier)] = siApp; 192 | } 193 | 194 | return @[]; 195 | } 196 | 197 | -(void) unobserveAxEventsForApplication:(NSRunningApplication*)application { 198 | 199 | @synchronized(_observedAppsByPid) { 200 | 201 | NSNumber* pid = @(application.processIdentifier); 202 | SIApplication* siApp = _observedAppsByPid[pid]; 203 | if (siApp == nil) { 204 | __log("%@ %@ was not being observed.", application.bundleIdentifier, pid); 205 | return; 206 | } 207 | 208 | for (NSString* notification in [self axNotificationsToObserve]) { 209 | [siApp unobserveNotification:(__bridge CFStringRef)notification withElement:siApp]; 210 | } 211 | 212 | [_observedAppsByPid removeObjectForKey:pid]; 213 | 214 | __log("%@ deregistered observation for app %@", self, application); 215 | } 216 | } 217 | 218 | 219 | #pragma mark - 220 | 221 | -(AccessibilityInfo*) accessibilityInfoForElement:(SIAccessibilityElement*)siElement axNotification:(CFStringRef)axNotification { 222 | 223 | // ensure we can reference the app for this element. 224 | id appElement = [self appElementForProcessIdentifier:siElement.processIdentifier]; 225 | if (appElement == nil) { 226 | return nil; 227 | } 228 | 229 | // * case: element is an SIApplication. 230 | if ([siElement.role isEqual:(NSString*)kAXApplicationRole]) { 231 | return [[AccessibilityInfo alloc] initWithAppElement:appElement axNotification:axNotification]; 232 | } 233 | 234 | SIAccessibilityElement* focusedElement = siElement.focusedElement; 235 | 236 | // * case: no focused element. 237 | if (focusedElement == nil) { 238 | return [[AccessibilityInfo alloc] initWithAppElement:appElement focusedElement:siElement axNotification:axNotification]; 239 | } 240 | 241 | // * default case. 242 | return [[AccessibilityInfo alloc] initWithAppElement:appElement focusedElement:focusedElement axNotification:axNotification]; 243 | } 244 | 245 | -(SIApplication*) appElementForProcessIdentifier:(pid_t)processIdentifier { 246 | @synchronized(_observedAppsByPid) { 247 | return _observedAppsByPid[@(processIdentifier)]; 248 | } 249 | } 250 | 251 | -(void) updateAccessibilityInfoForApplication:(NSRunningApplication*)runningApplication 252 | axNotification:(CFStringRef)axNotification 253 | { 254 | SIApplication* app = [SIApplication applicationWithRunningApplication:runningApplication]; 255 | SIWindow* window = app.focusedWindow; 256 | if (window) { 257 | [self updateAccessibilityInfoForElement:window axNotification:axNotification]; 258 | } 259 | } 260 | 261 | -(void) updateAccessibilityInfoForElement:(SIAccessibilityElement*)siElement 262 | axNotification:(CFStringRef)axNotification 263 | { 264 | [self updateAccessibilityInfoForElement:siElement axNotification:axNotification forceUpdate:NO]; 265 | } 266 | 267 | 268 | -(void) updateAccessibilityInfoForElement:(SIAccessibilityElement*)siElement 269 | axNotification:(CFStringRef)axNotification 270 | forceUpdate:(BOOL)forceUpdate 271 | { 272 | } 273 | 274 | 275 | #pragma mark - handlers 276 | 277 | //-(void) onApplicationActivated:(SIAccessibilityElement*)element { 278 | // _frontmostProcessIdentifier = element.processIdentifier; 279 | // __log("app activated: %@", element); 280 | //} 281 | // 282 | //-(void) onApplicationDeactivated:(SIAccessibilityElement*)element { 283 | // _frontmostProcessIdentifier = [SIApplication focusedApplication].processIdentifier; // ?? too slow? 284 | // __log("app deactivated: %@", element); 285 | //} 286 | // 287 | //-(void) onFocusedWindowChanged:(SIWindow*)window { 288 | // _frontmostProcessIdentifier = window.processIdentifier; 289 | // __log("focused window: %@", window); 290 | //} 291 | // 292 | //-(void) onFocusedElementChanged:(SIAccessibilityElement*)element { 293 | // __log("focused element: %@", element); 294 | //} 295 | // 296 | //-(void) onWindowCreated:(SIWindow*)window { 297 | // __log("new window: %@", window); // NOTE title may not be available yet. 298 | //} 299 | // 300 | //-(void) onTitleChanged:(SIWindow*)window { 301 | // __log("title changed: %@", window); 302 | //} 303 | // 304 | //-(void) onWindowMinimised:(SIWindow*)window { 305 | // __log("window minimised: %@",window); // NOTE title may not be available yet. 306 | //} 307 | // 308 | //-(void) onWindowUnminimised:(SIWindow*)window { 309 | // __log("window unminimised: %@",window); // NOTE title may not be available yet. 310 | //} 311 | // 312 | //-(void) onWindowMoved:(SIWindow*)window { 313 | // __log("window moved: %@",window); // NOTE title may not be available yet. 314 | //} 315 | // 316 | //-(void) onWindowResized:(SIWindow*)window { 317 | // __log("window resized: %@",window); // NOTE title may not be available yet. 318 | //} 319 | // 320 | //-(void) onTextSelectionChanged:(SIAccessibilityElement*)element { 321 | // __log("text selection changed on element: %@. selection: %@", element, element.selectedText); 322 | //} 323 | // 324 | //-(void) onElementDestroyed:(SIAccessibilityElement*)element { 325 | // __log("element destroyed: %@", element); 326 | //} 327 | 328 | 329 | -(AccessibilityInfo*) focusedWindowAccessibilityInfo { 330 | SIApplication* app = [SIApplication focusedApplication]; 331 | SIWindow* window = [app focusedWindow]; 332 | return [[AccessibilityInfo alloc] initWithAppElement:app focusedElement:window axNotification:kAXFocusedWindowChangedNotification]; 333 | } 334 | 335 | #pragma mark - util 336 | 337 | -(SIWindow*) keyWindowForApplication:(SIApplication*) application { 338 | for (SIWindow* window in application.windows) { 339 | if (window.isVisible 340 | && !window.isSheet) { 341 | return window; 342 | } 343 | } 344 | 345 | @throw [NSException exceptionWithName:@"invalid-state" reason:@"no suitable window to return as key" userInfo:nil]; 346 | } 347 | 348 | -(void) execAsyncSynchronisingOnPid:(pid_t)pid block:(void(^)(void))block { 349 | SIApplication* application = nil; 350 | @synchronized(_observedAppsByPid) { 351 | application = _observedAppsByPid[@(pid)]; 352 | } 353 | if (application == nil) { 354 | // @throw [[NSException alloc] initWithName:@"app-not-observed" reason:nil userInfo:@{@"pid": pid}]; 355 | 356 | // retrieve running app, sync on it. 357 | application = [SIApplication applicationForProcessIdentifier:pid]; 358 | // if (![self shouldObserveApplication:application]) { 359 | // 360 | // } 361 | } 362 | 363 | // NOTE if app for pid not observed, we will not be synchronising! 364 | 365 | [self execAsyncSynchronisingOnObject:application block:block]; 366 | } 367 | 368 | 369 | /// asynchronously execute on global concurrent queue, synchronised on object to avoid deadlocks. 370 | /// FIXME can result in temp thread explosion. 371 | -(void) execAsyncSynchronisingOnObject:(id)object block:(void(^)(void))block { 372 | 373 | // use a semaphore to avoid excessive thread spawning if the code path leading to the global 374 | // concurrent queue gets hot. 375 | // do it asyncly to avoid blocking calling thread. 376 | __weak dispatch_semaphore_t _semaphore = semaphore; 377 | dispatch_async(serialQueue, ^{ 378 | 379 | dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{ 380 | @synchronized(object) { 381 | block(); 382 | } 383 | dispatch_semaphore_signal(_semaphore); 384 | }); 385 | 386 | dispatch_semaphore_wait(_semaphore, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC)); 387 | }); 388 | } 389 | 390 | 391 | -(void) handleAxObservationResults:(NSArray*) axResults forRunningApplication:(NSRunningApplication*) application { 392 | // override. 393 | } 394 | 395 | 396 | @end 397 | 398 | 399 | 400 | // MARK: - 401 | 402 | @interface StateBasedBBLAccessibilityPublisher: BBLAccessibilityPublisher 403 | @end 404 | 405 | 406 | @implementation StateBasedBBLAccessibilityPublisher 407 | 408 | -(void) updateAccessibilityInfoForElement:(SIAccessibilityElement*)siElement 409 | axNotification:(CFStringRef)axNotification 410 | forceUpdate:(BOOL)forceUpdate 411 | { 412 | 413 | // * case: text selection handling special cases. 414 | if (CFEqual(axNotification, kAXSelectedTextChangedNotification)) { 415 | 416 | // NOTE some apps, e.g. iterm, seem to fail to notify observers properly. 417 | // FIXME investigate why not working with Notes.app 418 | // INVESTIGATE sierra + safari: notifies only for some windows. 419 | // during investigation we saw that inspecting with Prefab UI Browser 'wakes up' the windows such that they send out notifications only after inspection. 420 | NSString* selectedText = siElement.selectedText; 421 | if (selectedText == nil) { 422 | selectedText = @""; 423 | } 424 | 425 | // guard: xcode spams us with notifs even when no text has changed, so only notify when value has changed. 426 | id previousSelectedText = self.accessibilityInfosByPid[@(siElement.processIdentifier)].selectedText; // FIXME synchronise access. 427 | if (previousSelectedText == nil) { 428 | previousSelectedText = @""; 429 | } 430 | 431 | if ( selectedText == previousSelectedText 432 | || 433 | [selectedText isEqual:previousSelectedText]) { 434 | // no need to update. 435 | return; 436 | } 437 | } 438 | 439 | // * updated the published property. 440 | 441 | // dispatch to a queue, to avoid spins if ax query of the target app takes a long time. 442 | pid_t pid = siElement.processIdentifier; 443 | __weak BBLAccessibilityPublisher* blockSelf = self; 444 | [self execAsyncSynchronisingOnPid:pid block:^{ 445 | @autoreleasepool { 446 | id axInfo = [blockSelf accessibilityInfoForElement:siElement axNotification:axNotification]; 447 | 448 | // synchronise state access on main queue. 449 | // this restricts usage of this class on the main thread! 450 | dispatch_async(dispatch_get_main_queue(), ^{ 451 | NSDictionary* accessibilityInfosByPid = blockSelf.accessibilityInfosByPid; 452 | 453 | if (forceUpdate 454 | || ![accessibilityInfosByPid[@(pid)] isEqual:axInfo]) { 455 | 456 | // __log("update ax info dict with: %@", siElement); 457 | 458 | NSMutableDictionary* updatedAccessibilityInfosByPid = accessibilityInfosByPid.mutableCopy; 459 | updatedAccessibilityInfosByPid[@(pid)] = axInfo; 460 | 461 | blockSelf.accessibilityInfosByPid = updatedAccessibilityInfosByPid; 462 | } 463 | }); 464 | } 465 | }]; 466 | } 467 | 468 | @end 469 | -------------------------------------------------------------------------------- /BBLAccessibility/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSHumanReadableCopyright 24 | Copyright © 2016 Big Bear Labs. All rights reserved. 25 | NSPrincipalClass 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /BBLAccessibility/Silica-ext.h: -------------------------------------------------------------------------------- 1 | // 2 | // Silica-ext.h 3 | // BBLAccessibility 4 | // 5 | // Created by ilo on 19/11/2016. 6 | // Copyright © 2016 Big Bear Labs. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | 13 | /// Retrieving selected text. 14 | @interface SIAccessibilityElement (TextSelection) 15 | 16 | -(NSString*) selectedText; 17 | 18 | -(NSRect) selectionBounds; 19 | 20 | @end 21 | 22 | 23 | /// Retrieving text content. 24 | @interface SIAccessibilityElement (Text) 25 | 26 | -(NSString*) text; 27 | 28 | -(BOOL) isTextContainerComponent; 29 | @end 30 | -------------------------------------------------------------------------------- /BBLAccessibility/Silica-ext.m: -------------------------------------------------------------------------------- 1 | // 2 | // Silica-ext.m 3 | // BBLAccessibility 4 | // 5 | // Created by ilo on 19/11/2016. 6 | // Copyright © 2016 Big Bear Labs. All rights reserved. 7 | // 8 | 9 | #import "Silica-ext.h" 10 | 11 | @implementation SIAccessibilityElement (TextSelection) 12 | 13 | 14 | -(NSString*) selectedText { 15 | if (self.isWebArea) { 16 | return [self selectedTextForWebArea]; 17 | } 18 | else if (self.isTextContainerComponent) { 19 | id selectedText = [self stringForKey:kAXSelectedTextAttribute]; 20 | return selectedText; 21 | } 22 | return nil; 23 | } 24 | 25 | 26 | 27 | -(NSRect) selectionBounds { 28 | if (self.isWebArea) { 29 | return [self selectionBoundsForWebArea]; 30 | } 31 | 32 | CGRect result = NSZeroRect; 33 | 34 | CFTypeRef selectedRangeValue = NULL; 35 | CFTypeRef selectionBoundsValue = NULL; 36 | 37 | // query selected text range. 38 | if (AXUIElementCopyAttributeValue(self.axElementRef, kAXSelectedTextRangeAttribute, (CFTypeRef *)&selectedRangeValue) == kAXErrorSuccess) { 39 | 40 | // query bounds of range. 41 | if (AXUIElementCopyParameterizedAttributeValue(self.axElementRef, kAXBoundsForRangeParameterizedAttribute, selectedRangeValue, (CFTypeRef *)&selectionBoundsValue) == kAXErrorSuccess) { 42 | 43 | // get value out. 44 | AXValueGetValue(selectionBoundsValue, kAXValueCGRectType, &result); 45 | } 46 | 47 | else { 48 | // couldn't query bounds of range. 49 | } 50 | } 51 | else { 52 | // CASE Preview.app: AXGroup doesn't have the selectedTextRange, but its child AXStaticText does. 53 | if ([self.role isEqual:(__bridge NSString*)kAXGroupRole]) { 54 | NSArray* children = self.children; 55 | if (children.count > 0) { 56 | AXUIElementRef staticText = (__bridge AXUIElementRef) children[0]; 57 | SIAccessibilityElement* staticTextElem = [[SIAccessibilityElement alloc] initWithAXElement:staticText]; 58 | result = staticTextElem.selectionBounds; 59 | } 60 | } 61 | 62 | else { 63 | NSLog(@"query for selection range failed on %@", self.debugDescription); 64 | result = NSZeroRect; 65 | } 66 | } 67 | 68 | // NSLog(@"bounds: %@", [NSValue valueWithRect:rect]); 69 | 70 | if (NSIsEmptyRect(result)) { 71 | // @throw [NSException exceptionWithName:@"AXQueryFailedException" reason:[NSString stringWithFormat:@"couldn't retrieve bounds for selected text on element %@", self] userInfo:nil]; 72 | } 73 | 74 | if (selectedRangeValue) CFRelease(selectedRangeValue); 75 | if (selectionBoundsValue) CFRelease(selectionBoundsValue); 76 | 77 | return NSRectFromCGRect(result); 78 | } 79 | 80 | 81 | #pragma mark 82 | 83 | - (NSString*) selectedTextForWebArea { 84 | CFTypeRef range = NULL; 85 | AXUIElementCopyAttributeValue(self.axElementRef, CFSTR("AXSelectedTextMarkerRange"), &range); 86 | 87 | if (range == nil) { 88 | // no selected range, return nil. 89 | return nil; 90 | } 91 | 92 | CFTypeRef val = NULL; 93 | AXError err = AXUIElementCopyParameterizedAttributeValue(self.axElementRef, CFSTR("AXStringForTextMarkerRange"), range, &val); 94 | 95 | if (range) CFRelease(range); 96 | 97 | if (err == kAXErrorSuccess) { 98 | return (NSString*)CFBridgingRelease(val); 99 | } 100 | else { 101 | NSLog(@"err AXStringForTextMarkerRange: %d", (int)err); 102 | if (val) CFRelease(val); 103 | return nil; 104 | } 105 | } 106 | 107 | 108 | -(NSRect) selectionBoundsForWebArea { 109 | // guard against empty selected text. 110 | if ([self selectedTextForWebArea].length == 0) { 111 | return NSZeroRect; 112 | } 113 | 114 | NSRect result = NSZeroRect; 115 | 116 | AXValueRef selectedRangeValue = NULL; 117 | AXValueRef selectionBoundsValue = NULL; 118 | 119 | // query web area selected text range. 120 | if (AXUIElementCopyAttributeValue(self.axElementRef, CFSTR("AXSelectedTextMarkerRange"), (CFTypeRef *)&selectedRangeValue) == kAXErrorSuccess) { 121 | 122 | // query bounds of web area selected text range. 123 | if (AXUIElementCopyParameterizedAttributeValue(self.axElementRef, CFSTR("AXBoundsForTextMarkerRange"), selectedRangeValue, (CFTypeRef *)&selectionBoundsValue) == kAXErrorSuccess) { 124 | AXValueGetValue(selectionBoundsValue, kAXValueCGRectType, &result); 125 | } 126 | else { 127 | // query for bounds failed. 128 | } 129 | } 130 | 131 | if (selectedRangeValue) CFRelease(selectedRangeValue); 132 | if (selectionBoundsValue) CFRelease(selectionBoundsValue); 133 | 134 | return result; 135 | 136 | } 137 | 138 | 139 | -(BOOL) isWebArea { 140 | // if i have a AXWebArea role (undeclared constant!), i am a web area. 141 | // TODO see if should test the subrole instead? 142 | return [self.role isEqual:@"AXWebArea"]; 143 | } 144 | 145 | @end 146 | 147 | 148 | 149 | #pragma mark 150 | 151 | @implementation SIAccessibilityElement (Text) 152 | 153 | -(NSString*) text { 154 | if ([self isTextContainerComponent]) { 155 | id text = [self stringForKey:kAXValueAttribute]; 156 | return text; 157 | } 158 | return nil; 159 | } 160 | 161 | 162 | 163 | -(BOOL) isTextContainerComponent { 164 | return 165 | [self.role isEqual:(NSString*)kAXTextAreaRole] 166 | || [self.role isEqual:(NSString*)kAXTextFieldRole] 167 | || [self.role isEqual:(NSString*)kAXStaticTextRole] 168 | ; 169 | } 170 | 171 | @end 172 | -------------------------------------------------------------------------------- /BBLAccessibility/Silica-ext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Silica-ext.swift 3 | // BBLAccessibility 4 | // 5 | // Created by ilo on 27/06/2017. 6 | // Copyright © 2017 Big Bear Labs. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Silica 11 | 12 | 13 | // NOTE window tabs: 14 | // an SIWindow acquired prior has correct isVisible status even after being turned into an inactive tab! 15 | 16 | 17 | public extension SIApplication { 18 | 19 | class func application(bundleId: String) -> SIApplication? { 20 | if let app = NSRunningApplication.runningApplications(withBundleIdentifier: bundleId).last { 21 | return SIApplication(runningApplication: app) 22 | } 23 | else { 24 | return nil 25 | } 26 | } 27 | 28 | 29 | var mainWindow: SIWindow? { 30 | if let mainWindowElement = self.forKey(kAXMainWindowAttribute as CFString) { 31 | return SIWindow(axElement: mainWindowElement.axElementRef) 32 | } 33 | return nil 34 | } 35 | 36 | var uncachedWindows: [SIWindow] { 37 | self.dropWindowsCache() 38 | return self.windows 39 | } 40 | 41 | } 42 | 43 | 44 | public extension SIWindow { 45 | 46 | class func `for`(windowNumber: UInt32) -> SIWindow? { 47 | 48 | guard let app = NSRunningApplication.application(windowNumber: windowNumber) else { return nil } 49 | let siApp = SIApplication(forProcessIdentifier: app.processIdentifier) 50 | 51 | // NOTE -25204 was caused by sandbox settings applied to default app template since xcode 11.3 } 52 | let siWindow = siApp.uncachedWindows.first(where: {$0.windowID == windowNumber}) 53 | if siWindow != nil { 54 | return siWindow 55 | } 56 | 57 | 58 | // fallback in cases where reading the windows ax attribute results in an error. 59 | // e.g. 60 | // 2023-01-28 11:50:59.043679+0900 Zen[43434:11034094] pid: 1388, AXApplication/(null) file:///System/Library/CoreServices/Finder.app/: AXError -25201 getting AXWindows 61 | 62 | guard let children = siApp.children() as? [AXUIElement] 63 | else { return nil } 64 | 65 | for child in children { 66 | if SIAccessibilityElement(axElement: child).role() == kAXWindowRole { 67 | let siWindow = SIWindow(axElement: child) 68 | if siWindow.windowID == windowNumber { 69 | return siWindow 70 | } 71 | } 72 | } 73 | 74 | return nil 75 | } 76 | 77 | 78 | var isMain: Bool { 79 | self.bool(forKey: kAXMainAttribute as CFString) 80 | } 81 | 82 | } 83 | 84 | extension SIWindow { 85 | open override var debugDescription: String { 86 | let minDesc = isWindowMinimized() ? " min'ed" : "" 87 | let visDesc = isVisible() ? "" : " notVisible" 88 | let tabsDesc = tabGroup.map { 89 | " \($0.tabs.count) tabs" 90 | } ?? "" 91 | return self.description 92 | + tabsDesc 93 | + minDesc 94 | + visDesc 95 | 96 | } 97 | } 98 | 99 | public extension SIAccessibilityElement { 100 | var roleDescription: String? { 101 | self.string(forKey: kAXRoleDescriptionAttribute as CFString) 102 | } 103 | } 104 | 105 | 106 | // MARK: - buttons 107 | 108 | public extension SIWindow { 109 | 110 | var closeButton: SIAccessibilityElement? { 111 | return self.forKey(kAXCloseButtonAttribute as CFString) 112 | } 113 | 114 | var minimiseButton: SIAccessibilityElement? { 115 | return self.forKey(kAXMinimizeButtonSubrole as CFString) 116 | } 117 | 118 | var zoomButton: SIAccessibilityElement? { 119 | return self.forKey(kAXZoomButtonAttribute as CFString) 120 | } 121 | 122 | 123 | var childrenInNavigationOrder: [SIAccessibilityElement] { 124 | if let childrenAxRefs = self.array(forKey: "AXChildrenInNavigationOrder" as CFString) as? [AXUIElement] { 125 | let siElements = childrenAxRefs.map { 126 | SIAccessibilityElement(axElement: $0) 127 | } 128 | return siElements 129 | } 130 | return [] 131 | } 132 | 133 | var tabGroup: SITabGroup? { 134 | let navChildren = self.childrenInNavigationOrder 135 | if let tabGroupElem = navChildren.first(where: { 136 | $0.role() == kAXTabGroupRole 137 | }) { 138 | return SITabGroup(axElement: tabGroupElem.axElementRef) 139 | } 140 | return nil 141 | } 142 | } 143 | 144 | 145 | // MARK: - tab groups 146 | 147 | public class SITabGroup: SIAccessibilityElement { 148 | 149 | public var tabs: [Tab] { 150 | guard let children = self.children() as? [AXUIElement] else { 151 | fatalError() 152 | } 153 | 154 | let tabElements = children 155 | .map { axElem in 156 | return SIAccessibilityElement(axElement: axElem) 157 | } 158 | .filter { 159 | $0.roleDescription == "tab" 160 | } 161 | 162 | return tabElements.map { 163 | Tab( 164 | title: $0.title() ?? "<>", 165 | isSelected: $0.bool(forKey: kAXValueAttribute as CFString), 166 | pid: $0.processIdentifier() 167 | ) 168 | } 169 | } 170 | 171 | public var window: SIWindow { 172 | SIWindow.init(for: self) 173 | } 174 | 175 | public struct Tab: Equatable, CustomStringConvertible { 176 | public let title: String 177 | public let isSelected: Bool 178 | 179 | public let pid: pid_t 180 | 181 | public init (title: String, isSelected: Bool, pid: pid_t) { 182 | self.title = title 183 | self.isSelected = isSelected 184 | self.pid = pid 185 | } 186 | 187 | public var description: String { 188 | "Tab(\(title))" 189 | } 190 | } 191 | 192 | } 193 | 194 | 195 | // MARK: - 196 | 197 | extension NSRunningApplication { 198 | 199 | class func application(windowNumber: UInt32) -> NSRunningApplication? { 200 | if let dicts = (CGWindowListCopyWindowInfo([.optionAll], kCGNullWindowID) as? [NSDictionary]) { 201 | if let matching = dicts.first(where: { $0[kCGWindowNumber] as? NSNumber == NSNumber(value: windowNumber) }) { 202 | let pid = Int32(truncating: matching[kCGWindowOwnerPID] as! NSNumber) 203 | return NSRunningApplication(processIdentifier: pid) 204 | } 205 | } 206 | return nil 207 | } 208 | 209 | } 210 | 211 | 212 | 213 | // MARK: - window focusing hack 214 | 215 | extension SIWindow { 216 | 217 | /// messy indeed. 218 | /// https://github.com/ianyh/Amethyst/blob/4d0e820beb25f4bdb89088326a470a9132d89ccb/Amethyst/Model/Window.swift#L16 219 | public func focusBetter() { 220 | let pid = self.processIdentifier() 221 | var wid = self.windowID 222 | var psn = ProcessSerialNumber() 223 | let status = GetProcessForPID(pid, &psn) 224 | 225 | guard status == noErr else { 226 | return 227 | } 228 | 229 | var cgStatus = _SLPSSetFrontProcessWithOptions(&psn, wid, SLPSMode.userGenerated.rawValue) 230 | 231 | guard cgStatus == .success else { 232 | return 233 | } 234 | 235 | for byte in [0x01, 0x02] { 236 | var bytes = [UInt8](repeating: 0, count: 0xf8) 237 | bytes[0x04] = 0xF8 238 | bytes[0x08] = UInt8(byte) 239 | bytes[0x3a] = 0x10 240 | memcpy(&bytes[0x3c], &wid, MemoryLayout.size) 241 | memset(&bytes[0x20], 0xFF, 0x10) 242 | cgStatus = bytes.withUnsafeMutableBufferPointer { pointer in 243 | return SLPSPostEventRecordTo(&psn, &pointer.baseAddress!.pointee) 244 | } 245 | guard cgStatus == .success else { 246 | return 247 | } 248 | } 249 | 250 | // guard super.focus() else { 251 | // return false 252 | // } 253 | self.focusOnlyThisWindow() 254 | 255 | // guard UserConfiguration.shared.mouseFollowsFocus() else { 256 | // return true 257 | // } 258 | let mouseFollowsFocus = false 259 | if mouseFollowsFocus { 260 | let windowFrame = self.frame() 261 | let mouseCursorPoint = NSPoint(x: windowFrame.midX, y: windowFrame.midY) 262 | guard let mouseMoveEvent = CGEvent(mouseEventSource: nil, mouseType: .mouseMoved, mouseCursorPosition: mouseCursorPoint, mouseButton: .left) else { 263 | return 264 | } 265 | mouseMoveEvent.flags = CGEventFlags(rawValue: 0) 266 | mouseMoveEvent.post(tap: CGEventTapLocation.cghidEventTap) 267 | } 268 | 269 | return 270 | } 271 | } 272 | 273 | //// focuses the front process 274 | //// * macOS 10.12+ 275 | @_silgen_name("_SLPSSetFrontProcessWithOptions") @discardableResult 276 | func _SLPSSetFrontProcessWithOptions(_ psn: inout ProcessSerialNumber, _ wid: CGWindowID, _ mode: SLPSMode.RawValue) -> CGError 277 | 278 | enum SLPSMode: UInt32 { 279 | case allWindows = 0x100 280 | case userGenerated = 0x200 281 | case noWindows = 0x400 282 | } 283 | 284 | // returns the psn for a given pid 285 | // * macOS 10.9-10.15 (officially removed in 10.9, but available as a private API still) 286 | @_silgen_name("GetProcessForPID") @discardableResult 287 | func GetProcessForPID(_ pid: pid_t, _ psn: inout ProcessSerialNumber) -> OSStatus 288 | 289 | @_silgen_name("SLPSPostEventRecordTo") @discardableResult 290 | func SLPSPostEventRecordTo(_ psn: inout ProcessSerialNumber, _ bytes: inout UInt8) -> CGError 291 | 292 | -------------------------------------------------------------------------------- /BBLAccessibility/logging.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | #ifndef logging_h 5 | #define logging_h 6 | 7 | 8 | // MARK: - constants 9 | 10 | #define BBL_LOG_AX // DEBUG enable trace-level logging of AX. 11 | 12 | 13 | // MARK: - 14 | 15 | #ifndef BBL_LOG_AX 16 | 17 | // opt4. silent. 18 | #define __log(...) ; 19 | 20 | #else 21 | 22 | #ifdef __AVAILABILITY_INTERNAL__MAC_10_12 23 | 24 | // opt1. logging based on unified logging API. 25 | // requires 10.12 or higher. 26 | 27 | #define __log(...) os_log_info(OS_LOG_DEFAULT, __VA_ARGS__); 28 | 29 | #else 30 | // // opt2. 31 | // // uncomment to perform primitive logging. 32 | // #define __log(...) NSLog(@__VA_ARGS__); 33 | #endif 34 | 35 | // // opt3. 36 | // // impl the log convension in terms of pre 'os_log' api. 37 | // #define __log(...) asl_NSLog(NULL, NULL, ASL_LEVEL_INFO, __VA_ARGS__); 38 | // 39 | // // credit: Peter Hosey. 40 | // #define asl_NSLog(client, msg, level, format, ...) asl_log(client, msg, level, "%s", [[NSString stringWithFormat:@format, ##__VA_ARGS__] UTF8String]) 41 | 42 | #endif 43 | 44 | #endif /* logging_h */ 45 | 46 | 47 | -------------------------------------------------------------------------------- /BBLAccessibilityApp/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // BBLAccessibilityApp 4 | // 5 | // Created by ilo on 15/04/2016. 6 | // Copyright © 2016 Big Bear Labs. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import BBLAccessibility 11 | import Silica 12 | import ApplicationServices 13 | 14 | @NSApplicationMain 15 | class AppDelegate: NSObject, NSApplicationDelegate { 16 | 17 | @IBOutlet weak var window: NSWindow! 18 | 19 | var axPublisher: BBLAccessibilityPublisher! 20 | 21 | var siApp: SIApplication! 22 | 23 | 24 | var observation: Any? 25 | 26 | func applicationDidFinishLaunching(_ aNotification: Notification) { 27 | print("AXIsProcessTrusted: #\(AXIsProcessTrusted())") 28 | 29 | self.axPublisher = AXPublisher() 30 | 31 | self.observation = axPublisher.observe(\.accessibilityInfosByPid, options: [.initial, .new]) { (o, c) in 32 | print("ax updated: \(c.newValue)") 33 | } 34 | 35 | 36 | // DispatchQueue.global().async { 37 | if let finder = NSRunningApplication.runningApplications(withBundleIdentifier: "com.apple.finder").last { 38 | 39 | self.siApp = SIApplication(runningApplication: finder) 40 | 41 | // // PoC Silica basic usage. receive data via axPublisher.accessibilityInfosByPid 42 | // self.siApp.observeNotification(kAXApplicationActivatedNotification as CFString, with: self.siApp) 43 | 44 | // // PoC Silica coarse-grain interface. 45 | // watcher.watchNotifications(forApp: finder) 46 | } 47 | 48 | // PoC watch windows. 49 | axPublisher!.observeAxEvents() 50 | 51 | } 52 | 53 | func applicationWillTerminate(_ aNotification: Notification) { 54 | // Insert code here to tear down your application 55 | } 56 | 57 | 58 | // PoC requesting perms. 59 | @IBAction 60 | func action_showAxRequestDialog(_ sender: AnyObject) { 61 | AccessibilityHelper().showSystemAxRequestDialog() 62 | } 63 | } 64 | 65 | 66 | 67 | class AXPublisher: BBLAccessibilityPublisher { 68 | 69 | 70 | override var applicationsToObserve: [NSRunningApplication] { 71 | get { 72 | let pid = NSRunningApplication.current.processIdentifier 73 | let excludedBundleIds = self.excludedBundleIds 74 | let excludedNames = self.excludedNames 75 | 76 | return super.applicationsToObserve.filter { runningApplication in 77 | if 78 | // don't observe this app. 79 | runningApplication.processIdentifier == pid 80 | // exclude all agent apps. 81 | || runningApplication.isAgent() 82 | // exclude everything that ends with '.xpc'. 83 | || ( 84 | runningApplication.bundleURL?.absoluteString.hasSuffix(".xpc") 85 | || runningApplication.bundleURL?.absoluteString.hasSuffix(".xpc/") 86 | ) { 87 | return false 88 | } 89 | 90 | if let bundleId = runningApplication.bundleIdentifier { 91 | if excludedBundleIds.contains(bundleId) { 92 | return false 93 | } 94 | } 95 | 96 | if let appUrl = runningApplication.executableURL { 97 | let filename = appUrl.absoluteString.components(separatedBy: "/").last! 98 | if excludedNames.contains(filename) { 99 | return false 100 | } 101 | } 102 | 103 | return true 104 | } 105 | } 106 | } 107 | 108 | 109 | var excludedNames: [String] { 110 | // return (NSApp.default(forKey: .axpublisher_excluded_names) as? String ?? "").components(separatedBy: ",") 111 | return [ 112 | //"AirPlayUIAgent.app" 113 | // Siri.app 114 | // SiriNCService.xpc 115 | ] 116 | } 117 | var excludedBundleIds: [String] { 118 | // return (NSApp.default(forKey: .axpublisher_excluded_bundleids) as? String ?? "").components(separatedBy: ",") 119 | // + 120 | // // always exclude my own bundle id. 121 | // [ NSApp.bundleIdentifier ] 122 | return [] 123 | } 124 | 125 | } 126 | -------------------------------------------------------------------------------- /BBLAccessibilityApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "size" : "16x16", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "size" : "16x16", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "size" : "32x32", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "size" : "32x32", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "size" : "128x128", 26 | "scale" : "1x" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "size" : "128x128", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "size" : "256x256", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "size" : "256x256", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "size" : "512x512", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "size" : "512x512", 51 | "scale" : "2x" 52 | } 53 | ], 54 | "info" : { 55 | "version" : 1, 56 | "author" : "xcode" 57 | } 58 | } -------------------------------------------------------------------------------- /BBLAccessibilityApp/BBLAccessibilityApp-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | -------------------------------------------------------------------------------- /BBLAccessibilityApp/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSMinimumSystemVersion 26 | $(MACOSX_DEPLOYMENT_TARGET) 27 | NSHumanReadableCopyright 28 | Copyright © 2016 Big Bear Labs. All rights reserved. 29 | NSMainNibFile 30 | MainMenu 31 | NSPrincipalClass 32 | NSApplication 33 | 34 | 35 | -------------------------------------------------------------------------------- /BBLAccessibilityAppUITests/BBLAccessibilityAppUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BBLAccessibilityAppUITests.swift 3 | // BBLAccessibilityAppUITests 4 | // 5 | // Created by ilo on 15/04/2016. 6 | // Copyright © 2016 Big Bear Labs. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class BBLAccessibilityAppUITests: XCTestCase { 12 | 13 | override func setUp() { 14 | super.setUp() 15 | 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | 18 | // In UI tests it is usually best to stop immediately when a failure occurs. 19 | continueAfterFailure = false 20 | // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. 21 | XCUIApplication().launch() 22 | 23 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 24 | } 25 | 26 | override func tearDown() { 27 | // Put teardown code here. This method is called after the invocation of each test method in the class. 28 | super.tearDown() 29 | } 30 | 31 | func testExample() { 32 | // Use recording to get started writing UI tests. 33 | // Use XCTAssert and related functions to verify your tests produce the correct results. 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /BBLAccessibilityAppUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /Cartfile: -------------------------------------------------------------------------------- 1 | git "git@github.com:bigbearlabs/BBLBasics.git" "master" 2 | -------------------------------------------------------------------------------- /Cartfile.resolved: -------------------------------------------------------------------------------- 1 | github "bigbearlabs/BBLBasics" "08e1dded741e347484ee9962956fb1205036bbae" 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BBLAccessibilityApp 2 | 3 | ## Consolidated API for macOS Accessibility 4 | 5 | Fronts accessibility features from existing repos such as [Silica] and [NMTest001]. 6 | 7 | - [x] Getting information on windows for all running apps -- using Silica. 8 | - [x] Getting information on the selected text -- using NMAccessibility. 9 | - [x] Managing Accessibility (`AXUIElement`) observations. 10 | 11 | This is for an app I'm building that manages working contexts on OS X. I wouldn't be able to make such a tool without valuable source that was open to explore, so I thought I'd share my findings with everyone. 12 | 13 | In the queue is refinements moved over from currently private code in the following areas. Please get in touch if you need expedited access for these features. 14 | 15 | - [x] Getting full text content. 16 | - [ ] Repositioning a window to another screen or space 17 | 18 | 19 | Your pull requests and suggestions are welcome. 20 | 21 | [Silica]: https://github.com/ianyh/Silica 22 | [NMTest001]: https://github.com/invariant/NMTest001/tree/master/NMTest001 23 | 24 | 31 | -------------------------------------------------------------------------------- /TabInference/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSHumanReadableCopyright 22 | Copyright © 2021 Big Bear Labs. All rights reserved. 23 | 24 | 25 | -------------------------------------------------------------------------------- /TabInference/TabInference.h: -------------------------------------------------------------------------------- 1 | // 2 | // TabInference.h 3 | // TabInference 4 | // 5 | // Created by ilo on 11/07/2021. 6 | // Copyright © 2021 Big Bear Labs. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for TabInference. 12 | FOUNDATION_EXPORT double TabInferenceVersionNumber; 13 | 14 | //! Project version string for TabInference. 15 | FOUNDATION_EXPORT const unsigned char TabInferenceVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /TabInference/TabInference.swift: -------------------------------------------------------------------------------- 1 | import BBLAccessibility 2 | 3 | 4 | 5 | enum InferTabResult { 6 | 7 | // - for n tabs, 8 | // - there exist exactly n cg infos unambiguously matching (app, title): no duplicate titles in 'title space' 9 | // - or: exactly n cg infos unambiguously matching (app, title, frame) 10 | // - or: currently 2 tabs, 1 cg info previously visible, matching (title, frame) 11 | case conclusive(matches: [Match]) 12 | 13 | case partial(matches: [Match]) 14 | 15 | struct Match { 16 | let tabs: [SITabGroup.Tab] 17 | let cgInfos: [CGWindowInfo] 18 | } 19 | } 20 | 21 | func inferTabs(tabs: [SITabGroup.Tab]) -> InferTabResult { 22 | fatalError() 23 | } 24 | // 25 | //if let tabs = tabs { 26 | // inferTabs(tabs: tabs) 27 | //} 28 | 29 | 30 | -------------------------------------------------------------------------------- /TabInferenceTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /TabInferenceTests/TabInferenceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabInferenceTests.swift 3 | // TabInferenceTests 4 | // 5 | // Created by ilo on 11/07/2021. 6 | // Copyright © 2021 Big Bear Labs. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import TabInference 11 | import BBLAccessibility 12 | 13 | 14 | 15 | class TabInferenceTests: XCTestCase { 16 | 17 | let inferrer = Inferrer() 18 | 19 | override func setUpWithError() throws { 20 | // Put setup code here. This method is called before the invocation of each test method in the class. 21 | } 22 | 23 | override func tearDownWithError() throws { 24 | // Put teardown code here. This method is called after the invocation of each test method in the class. 25 | } 26 | 27 | func test_inferForUnambiguousTitles() throws { 28 | let infosAfterNewTab = [ 29 | CGWindowInfo(pid: 1, windowNumber: "1", title: "a", isVisible: false, frame: frameA), 30 | CGWindowInfo(pid: 1, windowNumber: "2", title: "b", isVisible: true, frame: frameB), 31 | ] 32 | 33 | let tabsAfterNewTab = [ 34 | SITabGroup.Tab(title: "a", isSelected: false, pid: 1), 35 | SITabGroup.Tab(title: "b", isSelected: true, pid: 1), 36 | ] 37 | 38 | XCTAssertEqual( 39 | inferrer.inferForUnambiguousTitles( 40 | tabs: tabsAfterNewTab, 41 | cgInfos: infosAfterNewTab 42 | ), 43 | .conclusive(matches: zip(infosAfterNewTab, tabsAfterNewTab).map { 44 | Match(tab: $0.1, cgInfo: $0.0) 45 | }) 46 | ) 47 | 48 | let infosWithUnmatchingTitles = [ 49 | CGWindowInfo(pid: 1, windowNumber: "1", title: "a", isVisible: false, frame: frameA), 50 | CGWindowInfo(pid: 1, windowNumber: "2", title: "c", isVisible: true, frame: frameB), 51 | ] 52 | 53 | XCTAssertEqual( 54 | inferrer.inferForUnambiguousTitles(tabs: tabsAfterNewTab, cgInfos: infosWithUnmatchingTitles), 55 | .none 56 | ) 57 | 58 | 59 | let tabsWithAmbiguousTitles = [ 60 | SITabGroup.Tab(title: "a", isSelected: false, pid: 1), 61 | SITabGroup.Tab(title: "a", isSelected: true, pid: 1), 62 | ] 63 | let infosWithAmbiguousTitles = [ 64 | CGWindowInfo(pid: 1, windowNumber: "1", title: "a", isVisible: false, frame: frameA), 65 | CGWindowInfo(pid: 1, windowNumber: "2", title: "a", isVisible: true, frame: frameB), 66 | ] 67 | 68 | XCTAssertEqual( 69 | inferrer.inferForUnambiguousTitles(tabs: tabsWithAmbiguousTitles, cgInfos: infosWithAmbiguousTitles), 70 | .none 71 | ) 72 | 73 | let infosWithAdditionalAmbiguousTitles = [ 74 | CGWindowInfo(pid: 1, windowNumber: "1", title: "a", isVisible: false, frame: frameA), 75 | CGWindowInfo(pid: 1, windowNumber: "2", title: "b", isVisible: true, frame: frameB), 76 | CGWindowInfo(pid: 1, windowNumber: "3", title: "a", isVisible: false, frame: frameB), 77 | ] 78 | 79 | XCTAssertEqual( 80 | inferrer.inferForUnambiguousTitles(tabs: tabsAfterNewTab, cgInfos: infosWithAdditionalAmbiguousTitles), 81 | .none 82 | ) 83 | 84 | } 85 | 86 | func testXX() { 87 | let tabsWithAmbiguousTitles = [ 88 | SITabGroup.Tab(title: "a", isSelected: false, pid: 1), 89 | SITabGroup.Tab(title: "a", isSelected: true, pid: 1), 90 | ] 91 | let infosWithAmbiguousTitles = [ 92 | CGWindowInfo(pid: 1, windowNumber: "1", title: "a", isVisible: false, frame: frameA), 93 | CGWindowInfo(pid: 1, windowNumber: "2", title: "a", isVisible: true, frame: frameA), 94 | ] 95 | 96 | XCTAssertEqual( 97 | inferrer.inferForTabProperties(tabs: tabsWithAmbiguousTitles, cgInfos: infosWithAmbiguousTitles), 98 | .conclusive(matches: tabsWithAmbiguousTitles.enumerated().map { i, tab in 99 | Match(tab: tab, cgInfo: infosWithAmbiguousTitles[i]) 100 | }) 101 | ) 102 | 103 | let infosWithAdditionalAmbiguous = [ 104 | CGWindowInfo(pid: 1, windowNumber: "1", title: "a", isVisible: false, frame: frameA), 105 | CGWindowInfo(pid: 1, windowNumber: "2", title: "a", isVisible: true, frame: frameA), 106 | CGWindowInfo(pid: 1, windowNumber: "3", title: "a", isVisible: false, frame: frameA), 107 | ] 108 | 109 | XCTAssertEqual( 110 | inferrer.inferForTabProperties(tabs: tabsWithAmbiguousTitles, cgInfos: infosWithAdditionalAmbiguous), 111 | .none 112 | ) 113 | 114 | 115 | } 116 | 117 | func testX() { 118 | let baselineInfos = [ 119 | CGWindowInfo(pid: 1, windowNumber: "1", title: "a", isVisible: true, frame: frameA), 120 | ] 121 | 122 | // HOW in calling context, how to bookkeep this history of infos? 123 | 124 | } 125 | } 126 | 127 | struct Inferrer { 128 | func inferForUnambiguousTitles(tabs: [SITabGroup.Tab], cgInfos: [CGWindowInfo]) -> InferTabResult { 129 | assert(tabs.map { $0.pid }.uniqueValues == [tabs[0].pid]) 130 | let pid = tabs[0].pid 131 | let infosForPid = cgInfos.filter { $0.pid == pid } 132 | 133 | func titleSpaceContainsNoDuplicates(_ titles: [String]) -> Bool { 134 | Set(titles).count == titles.count 135 | } 136 | 137 | if titleSpaceContainsNoDuplicates(tabs.map { $0.title }) { 138 | let sortedTabs = tabs.sorted { $0.title < $1.title } 139 | let sortedInfos = infosForPid.sorted { $0.title < $1.title } 140 | if sortedTabs.map({ $0.title }) == sortedInfos.map({ $0.title }) { 141 | return .conclusive( 142 | matches: zip(sortedTabs, sortedInfos).map { 143 | Match(tab: $0.0, cgInfo: $0.1) 144 | } 145 | ) 146 | } 147 | } 148 | return .none 149 | } 150 | 151 | func inferForTabProperties(tabs: [SITabGroup.Tab], cgInfos: [CGWindowInfo]) -> InferTabResult { 152 | let focusedTabs = tabs.filter { $0.isSelected } 153 | assert(focusedTabs.count == 1) 154 | 155 | // same count 156 | if tabs.count == cgInfos.count { 157 | // all frames are the same 158 | if cgInfos.map { $0.frame }.uniqueValues.count == 1 { 159 | 160 | let sortedTabs = tabs.sorted { $0.title < $1.title } 161 | let sortedInfos = cgInfos.sorted { $0.title < $1.title } 162 | 163 | let nonmatches = zip(sortedTabs, sortedInfos).filter { 164 | let (tab, info) = $0 165 | return tab.title != info.title 166 | || tab.isSelected != info.isVisible 167 | } 168 | 169 | if nonmatches.isEmpty { 170 | return .conclusive(matches: zip(sortedTabs, sortedInfos).map { 171 | Match(tab: $0.0, cgInfo: $0.1) 172 | }) 173 | } 174 | } 175 | } 176 | 177 | return .none 178 | } 179 | 180 | } 181 | 182 | 183 | let frameA = CGRect(x: 0, y: 0, width: 100, height: 100) 184 | let frameB = CGRect(x: 10, y: 10, width: 100, height: 100) 185 | 186 | enum InferTabResult: Equatable { 187 | 188 | // - for n tabs, 189 | // - there exist exactly n cg infos unambiguously matching (app, title): no duplicate titles in 'title space' 190 | // - or: exactly n cg infos unambiguously matching (app, title, frame) 191 | // - or: currently 2 tabs, 1 cg info previously visible, matching (title, frame) 192 | case conclusive(matches: [Match]) 193 | 194 | case partial(matches: [Match]) 195 | 196 | case none 197 | 198 | } 199 | 200 | struct Match: Equatable { 201 | let tab: SITabGroup.Tab 202 | let cgInfo: CGWindowInfo 203 | } 204 | -------------------------------------------------------------------------------- /WindowCoordinator/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSHumanReadableCopyright 22 | Copyright © 2020 Big Bear Labs. All rights reserved. 23 | 24 | 25 | -------------------------------------------------------------------------------- /WindowCoordinator/WindowCoordinator.h: -------------------------------------------------------------------------------- 1 | // 2 | // WindowCoordinator.h 3 | // WindowCoordinator 4 | // 5 | // Created by ilo on 15/06/2020. 6 | // Copyright © 2020 Big Bear Labs. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for WindowCoordinator. 12 | FOUNDATION_EXPORT double WindowCoordinatorVersionNumber; 13 | 14 | //! Project version string for WindowCoordinator. 15 | FOUNDATION_EXPORT const unsigned char WindowCoordinatorVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /WindowCoordinator/WindowCoordinator.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Silica 3 | import BBLBasics 4 | import BBLAccessibility 5 | import OrderedCollections 6 | 7 | 8 | public class WindowCoordinator { 9 | 10 | public init() {} 11 | 12 | public func position( 13 | framesByWindowNumber: OrderedDictionary, 14 | raise: Bool = false, 15 | activate windowNumberToFocus: UInt32? = nil, 16 | queue: DispatchQueue = coordinatorQueue 17 | ) { 18 | 19 | coordinatorQueue.async { 20 | 21 | for (windowNumber, frame) in framesByWindowNumber.reversed() { 22 | 23 | if let window = SIWindow.for(windowNumber: windowNumber) { 24 | if frame == .zero { 25 | print("👺 window \(windowNumber) is given a zero frame; will not set.") 26 | } 27 | else if window.frame() != frame { 28 | window.setFrame(frame) 29 | } 30 | 31 | if raise { 32 | if windowNumberToFocus != nil 33 | && windowNumber == windowNumberToFocus { 34 | // don't raise since we will focus later 35 | } else { 36 | self.raise(windowNumber: windowNumber) 37 | } 38 | } 39 | } 40 | } 41 | 42 | if let n = windowNumberToFocus { 43 | 44 | self.focus(windowNumber: n) 45 | } 46 | 47 | } 48 | 49 | // investigating cases where call to this method didn't seem to position correctly 50 | #if DEBUG 51 | coordinatorQueue.asyncAfter(deadline: .now() + 1) { 52 | let widTargetCurrentTuple = framesByWindowNumber.compactMap { wid, targetFrame in 53 | SIWindow.for(windowNumber: wid).flatMap { window in 54 | let actualFrame = window.frame() 55 | return (wid, targetFrame, actualFrame) 56 | } 57 | } 58 | 59 | let targetActualDiscrepencies = widTargetCurrentTuple.filter { wid, targetFrame, actualFrame in 60 | targetFrame != actualFrame 61 | } 62 | 63 | if targetActualDiscrepencies.count > 0 { 64 | 65 | } 66 | } 67 | #endif 68 | } 69 | 70 | public func focus(windowNumber: UInt32) { 71 | guard let window = SIWindow.for(windowNumber: windowNumber) 72 | else { return } 73 | 74 | window.focusBetter() 75 | } 76 | 77 | public func raise(windowNumber: UInt32) { 78 | if let w = SIWindow.for(windowNumber: windowNumber) { 79 | w.raise() 80 | } 81 | } 82 | 83 | // MARK: - 84 | 85 | public func frame(windowNumber: UInt32) -> CGRect? { 86 | return SIWindow.for(windowNumber: windowNumber)?.frame() 87 | } 88 | 89 | } 90 | 91 | 92 | public let coordinatorQueue = DispatchQueue.global(qos: .userInteractive) 93 | -------------------------------------------------------------------------------- /WindowCoordinatorDemo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // WindowCoordinatorDemo 4 | // 5 | // Created by ilo on 15/06/2020. 6 | // Copyright © 2020 Big Bear Labs. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import SwiftUI 11 | 12 | @NSApplicationMain 13 | class AppDelegate: NSObject, NSApplicationDelegate { 14 | 15 | var window: NSWindow! 16 | 17 | 18 | func applicationDidFinishLaunching(_ aNotification: Notification) { 19 | // Create the SwiftUI view that provides the window contents. 20 | let contentView = ContentView() 21 | 22 | // Create the window and set the content view. 23 | window = NSWindow( 24 | contentRect: NSRect(x: 0, y: 0, width: 480, height: 300), 25 | styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], 26 | backing: .buffered, defer: false) 27 | window.center() 28 | window.setFrameAutosaveName("Main Window") 29 | window.contentView = NSHostingView(rootView: contentView) 30 | window.makeKeyAndOrderFront(nil) 31 | } 32 | 33 | func applicationWillTerminate(_ aNotification: Notification) { 34 | // Insert code here to tear down your application 35 | } 36 | 37 | 38 | } 39 | 40 | -------------------------------------------------------------------------------- /WindowCoordinatorDemo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "size" : "16x16", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "size" : "16x16", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "size" : "32x32", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "size" : "32x32", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "size" : "128x128", 26 | "scale" : "1x" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "size" : "128x128", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "size" : "256x256", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "size" : "256x256", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "size" : "512x512", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "size" : "512x512", 51 | "scale" : "2x" 52 | } 53 | ], 54 | "info" : { 55 | "version" : 1, 56 | "author" : "xcode" 57 | } 58 | } -------------------------------------------------------------------------------- /WindowCoordinatorDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /WindowCoordinatorDemo/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // ResizeWindowsDemo 4 | // 5 | // Created by ilo on 10/06/2020. 6 | // Copyright © 2020 Big Bear Labs. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import WindowCoordinator 11 | 12 | 13 | 14 | struct ContentView: View { 15 | 16 | let windowCoordinator = WindowCoordinator() 17 | 18 | @State var targetWindowIdString = "" 19 | 20 | var body: some View { 21 | VStack { 22 | 23 | TextField("target window id", text: $targetWindowIdString, onCommit: {} 24 | ) 25 | Button("positionAsMainLayoutElement") { 26 | if let windowNumber = UInt32(self.targetWindowIdString) { 27 | self.windowCoordinator.positionAsMainLayoutElement(windowNumber: windowNumber) 28 | } 29 | } 30 | } 31 | } 32 | 33 | } 34 | 35 | 36 | struct ContentView_Previews: PreviewProvider { 37 | static var previews: some View { 38 | ContentView() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /WindowCoordinatorDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | Copyright © 2020 Big Bear Labs. All rights reserved. 27 | NSMainStoryboardFile 28 | Main 29 | NSPrincipalClass 30 | NSApplication 31 | NSSupportsAutomaticTermination 32 | 33 | NSSupportsSuddenTermination 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /WindowCoordinatorDemo/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /WindowCoordinatorDemo/WindowCoordinatorDemo.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /lab/NMAccessibility/.gitignore: -------------------------------------------------------------------------------- 1 | build* 2 | .build* 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /lab/NMAccessibility/NMAccessibility/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /lab/NMAccessibility/NMAccessibility/NMAccessibility.h: -------------------------------------------------------------------------------- 1 | // 2 | // NMAccessibility.h 3 | // NMAccessibility 4 | // 5 | // Created by ilo on 10/04/2016. 6 | // 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for NMAccessibility. 12 | FOUNDATION_EXPORT double NMAccessibilityVersionNumber; 13 | 14 | //! Project version string for NMAccessibility. 15 | FOUNDATION_EXPORT const unsigned char NMAccessibilityVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | #import -------------------------------------------------------------------------------- /lab/NMAccessibility/NMTest001.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 47; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 013D8CAD143C85A100AC2A43 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 013D8CAC143C85A100AC2A43 /* Cocoa.framework */; }; 11 | 013D8CB7143C85A100AC2A43 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 013D8CB5143C85A100AC2A43 /* InfoPlist.strings */; }; 12 | 013D8CB9143C85A100AC2A43 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 013D8CB8143C85A100AC2A43 /* main.m */; }; 13 | 013D8CBD143C85A100AC2A43 /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 013D8CBB143C85A100AC2A43 /* Credits.rtf */; }; 14 | 013D8CC0143C85A100AC2A43 /* TestAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 013D8CBF143C85A100AC2A43 /* TestAppDelegate.m */; }; 15 | 013D8CC3143C85A200AC2A43 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 013D8CC1143C85A200AC2A43 /* MainMenu.xib */; }; 16 | 013D8CEC143CA34000AC2A43 /* TestWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 013D8CEB143CA34000AC2A43 /* TestWindowController.m */; }; 17 | 013D8CEE143CA41200AC2A43 /* Window.xib in Resources */ = {isa = PBXBuildFile; fileRef = 013D8CED143CA41200AC2A43 /* Window.xib */; }; 18 | 5148948D1C091352005F43AB /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5148948C1C091352005F43AB /* WebKit.framework */; }; 19 | 51AC6CF51CE66D3B0078C2EE /* NMUIElement.m in Sources */ = {isa = PBXBuildFile; fileRef = 013D8CD9143C8B5100AC2A43 /* NMUIElement.m */; }; 20 | 51B8F2201DB509B100E79EEC /* NMAccessibility.h in Headers */ = {isa = PBXBuildFile; fileRef = 51AC6CEF1CE66D340078C2EE /* NMAccessibility.h */; settings = {ATTRIBUTES = (Public, ); }; }; 21 | 51B8F2211DB50A4000E79EEC /* NMUIElement.h in Headers */ = {isa = PBXBuildFile; fileRef = 013D8CD8143C8B5100AC2A43 /* NMUIElement.h */; settings = {ATTRIBUTES = (Public, ); }; }; 22 | /* End PBXBuildFile section */ 23 | 24 | /* Begin PBXFileReference section */ 25 | 013D8CA8143C85A100AC2A43 /* NMTest001.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NMTest001.app; sourceTree = BUILT_PRODUCTS_DIR; }; 26 | 013D8CAC143C85A100AC2A43 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; 27 | 013D8CAF143C85A100AC2A43 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; 28 | 013D8CB0143C85A100AC2A43 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = System/Library/Frameworks/CoreData.framework; sourceTree = SDKROOT; }; 29 | 013D8CB1143C85A100AC2A43 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 30 | 013D8CB4143C85A100AC2A43 /* NMTest001-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "NMTest001-Info.plist"; sourceTree = ""; }; 31 | 013D8CB6143C85A100AC2A43 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 32 | 013D8CB8143C85A100AC2A43 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 33 | 013D8CBA143C85A100AC2A43 /* NMTest001-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NMTest001-Prefix.pch"; sourceTree = ""; }; 34 | 013D8CBC143C85A100AC2A43 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; name = en; path = en.lproj/Credits.rtf; sourceTree = ""; }; 35 | 013D8CBE143C85A100AC2A43 /* TestAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TestAppDelegate.h; sourceTree = ""; }; 36 | 013D8CBF143C85A100AC2A43 /* TestAppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TestAppDelegate.m; sourceTree = ""; }; 37 | 013D8CC2143C85A200AC2A43 /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/MainMenu.xib; sourceTree = ""; }; 38 | 013D8CD8143C8B5100AC2A43 /* NMUIElement.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NMUIElement.h; sourceTree = ""; }; 39 | 013D8CD9143C8B5100AC2A43 /* NMUIElement.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NMUIElement.m; sourceTree = ""; }; 40 | 013D8CEA143CA34000AC2A43 /* TestWindowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TestWindowController.h; sourceTree = ""; }; 41 | 013D8CEB143CA34000AC2A43 /* TestWindowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TestWindowController.m; sourceTree = ""; }; 42 | 013D8CED143CA41200AC2A43 /* Window.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = Window.xib; sourceTree = ""; }; 43 | 5148948B1C090F0D005F43AB /* WebKitSystemInterface.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WebKitSystemInterface.h; sourceTree = ""; }; 44 | 5148948C1C091352005F43AB /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; 45 | 51AC6CED1CE66D340078C2EE /* NMAccessibility.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = NMAccessibility.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 46 | 51AC6CEF1CE66D340078C2EE /* NMAccessibility.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NMAccessibility.h; sourceTree = ""; }; 47 | 51AC6CF11CE66D350078C2EE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 48 | /* End PBXFileReference section */ 49 | 50 | /* Begin PBXFrameworksBuildPhase section */ 51 | 013D8CA5143C85A100AC2A43 /* Frameworks */ = { 52 | isa = PBXFrameworksBuildPhase; 53 | buildActionMask = 2147483647; 54 | files = ( 55 | 5148948D1C091352005F43AB /* WebKit.framework in Frameworks */, 56 | 013D8CAD143C85A100AC2A43 /* Cocoa.framework in Frameworks */, 57 | ); 58 | runOnlyForDeploymentPostprocessing = 0; 59 | }; 60 | 51AC6CE91CE66D340078C2EE /* Frameworks */ = { 61 | isa = PBXFrameworksBuildPhase; 62 | buildActionMask = 2147483647; 63 | files = ( 64 | ); 65 | runOnlyForDeploymentPostprocessing = 0; 66 | }; 67 | /* End PBXFrameworksBuildPhase section */ 68 | 69 | /* Begin PBXGroup section */ 70 | 013D8C9D143C85A100AC2A43 = { 71 | isa = PBXGroup; 72 | children = ( 73 | 013D8CB2143C85A100AC2A43 /* TestApp */, 74 | 51100F001CC15CB20095B985 /* NMUIElement */, 75 | 51AC6CEE1CE66D340078C2EE /* NMAccessibility */, 76 | 013D8CAB143C85A100AC2A43 /* Frameworks */, 77 | 013D8CA9143C85A100AC2A43 /* Products */, 78 | ); 79 | sourceTree = ""; 80 | }; 81 | 013D8CA9143C85A100AC2A43 /* Products */ = { 82 | isa = PBXGroup; 83 | children = ( 84 | 013D8CA8143C85A100AC2A43 /* NMTest001.app */, 85 | 51AC6CED1CE66D340078C2EE /* NMAccessibility.framework */, 86 | ); 87 | name = Products; 88 | sourceTree = ""; 89 | }; 90 | 013D8CAB143C85A100AC2A43 /* Frameworks */ = { 91 | isa = PBXGroup; 92 | children = ( 93 | 5148948C1C091352005F43AB /* WebKit.framework */, 94 | 013D8CAC143C85A100AC2A43 /* Cocoa.framework */, 95 | 013D8CAE143C85A100AC2A43 /* Other Frameworks */, 96 | ); 97 | name = Frameworks; 98 | sourceTree = ""; 99 | }; 100 | 013D8CAE143C85A100AC2A43 /* Other Frameworks */ = { 101 | isa = PBXGroup; 102 | children = ( 103 | 013D8CAF143C85A100AC2A43 /* AppKit.framework */, 104 | 013D8CB0143C85A100AC2A43 /* CoreData.framework */, 105 | 013D8CB1143C85A100AC2A43 /* Foundation.framework */, 106 | ); 107 | name = "Other Frameworks"; 108 | sourceTree = ""; 109 | }; 110 | 013D8CB2143C85A100AC2A43 /* TestApp */ = { 111 | isa = PBXGroup; 112 | children = ( 113 | 013D8CBE143C85A100AC2A43 /* TestAppDelegate.h */, 114 | 013D8CBF143C85A100AC2A43 /* TestAppDelegate.m */, 115 | 013D8CEA143CA34000AC2A43 /* TestWindowController.h */, 116 | 013D8CEB143CA34000AC2A43 /* TestWindowController.m */, 117 | 5148948B1C090F0D005F43AB /* WebKitSystemInterface.h */, 118 | 013D8CC1143C85A200AC2A43 /* MainMenu.xib */, 119 | 013D8CED143CA41200AC2A43 /* Window.xib */, 120 | 013D8CB3143C85A100AC2A43 /* Supporting Files */, 121 | ); 122 | name = TestApp; 123 | path = NMTest001; 124 | sourceTree = ""; 125 | }; 126 | 013D8CB3143C85A100AC2A43 /* Supporting Files */ = { 127 | isa = PBXGroup; 128 | children = ( 129 | 013D8CB4143C85A100AC2A43 /* NMTest001-Info.plist */, 130 | 013D8CB5143C85A100AC2A43 /* InfoPlist.strings */, 131 | 013D8CB8143C85A100AC2A43 /* main.m */, 132 | 013D8CBA143C85A100AC2A43 /* NMTest001-Prefix.pch */, 133 | 013D8CBB143C85A100AC2A43 /* Credits.rtf */, 134 | ); 135 | name = "Supporting Files"; 136 | sourceTree = ""; 137 | }; 138 | 51100F001CC15CB20095B985 /* NMUIElement */ = { 139 | isa = PBXGroup; 140 | children = ( 141 | 013D8CD8143C8B5100AC2A43 /* NMUIElement.h */, 142 | 013D8CD9143C8B5100AC2A43 /* NMUIElement.m */, 143 | ); 144 | path = NMUIElement; 145 | sourceTree = ""; 146 | }; 147 | 51AC6CEE1CE66D340078C2EE /* NMAccessibility */ = { 148 | isa = PBXGroup; 149 | children = ( 150 | 51AC6CEF1CE66D340078C2EE /* NMAccessibility.h */, 151 | 51AC6CF11CE66D350078C2EE /* Info.plist */, 152 | ); 153 | path = NMAccessibility; 154 | sourceTree = ""; 155 | }; 156 | /* End PBXGroup section */ 157 | 158 | /* Begin PBXHeadersBuildPhase section */ 159 | 51AC6CEA1CE66D340078C2EE /* Headers */ = { 160 | isa = PBXHeadersBuildPhase; 161 | buildActionMask = 2147483647; 162 | files = ( 163 | 51B8F2211DB50A4000E79EEC /* NMUIElement.h in Headers */, 164 | 51B8F2201DB509B100E79EEC /* NMAccessibility.h in Headers */, 165 | ); 166 | runOnlyForDeploymentPostprocessing = 0; 167 | }; 168 | /* End PBXHeadersBuildPhase section */ 169 | 170 | /* Begin PBXNativeTarget section */ 171 | 013D8CA7143C85A100AC2A43 /* NMTest001 */ = { 172 | isa = PBXNativeTarget; 173 | buildConfigurationList = 013D8CC6143C85A200AC2A43 /* Build configuration list for PBXNativeTarget "NMTest001" */; 174 | buildPhases = ( 175 | 013D8CA4143C85A100AC2A43 /* Sources */, 176 | 013D8CA5143C85A100AC2A43 /* Frameworks */, 177 | 013D8CA6143C85A100AC2A43 /* Resources */, 178 | ); 179 | buildRules = ( 180 | ); 181 | dependencies = ( 182 | ); 183 | name = NMTest001; 184 | productName = NMTest001; 185 | productReference = 013D8CA8143C85A100AC2A43 /* NMTest001.app */; 186 | productType = "com.apple.product-type.application"; 187 | }; 188 | 51AC6CEC1CE66D340078C2EE /* NMAccessibility */ = { 189 | isa = PBXNativeTarget; 190 | buildConfigurationList = 51AC6CF21CE66D350078C2EE /* Build configuration list for PBXNativeTarget "NMAccessibility" */; 191 | buildPhases = ( 192 | 51AC6CE81CE66D340078C2EE /* Sources */, 193 | 51AC6CE91CE66D340078C2EE /* Frameworks */, 194 | 51AC6CEA1CE66D340078C2EE /* Headers */, 195 | 51AC6CEB1CE66D340078C2EE /* Resources */, 196 | ); 197 | buildRules = ( 198 | ); 199 | dependencies = ( 200 | ); 201 | name = NMAccessibility; 202 | productName = NMAccessibility; 203 | productReference = 51AC6CED1CE66D340078C2EE /* NMAccessibility.framework */; 204 | productType = "com.apple.product-type.framework"; 205 | }; 206 | /* End PBXNativeTarget section */ 207 | 208 | /* Begin PBXProject section */ 209 | 013D8C9F143C85A100AC2A43 /* Project object */ = { 210 | isa = PBXProject; 211 | attributes = { 212 | LastUpgradeCheck = 0900; 213 | TargetAttributes = { 214 | 51AC6CEC1CE66D340078C2EE = { 215 | CreatedOnToolsVersion = 7.3.1; 216 | }; 217 | }; 218 | }; 219 | buildConfigurationList = 013D8CA2143C85A100AC2A43 /* Build configuration list for PBXProject "NMTest001" */; 220 | compatibilityVersion = "Xcode 6.3"; 221 | developmentRegion = English; 222 | hasScannedForEncodings = 0; 223 | knownRegions = ( 224 | en, 225 | ); 226 | mainGroup = 013D8C9D143C85A100AC2A43; 227 | productRefGroup = 013D8CA9143C85A100AC2A43 /* Products */; 228 | projectDirPath = ""; 229 | projectRoot = ""; 230 | targets = ( 231 | 51AC6CEC1CE66D340078C2EE /* NMAccessibility */, 232 | 013D8CA7143C85A100AC2A43 /* NMTest001 */, 233 | ); 234 | }; 235 | /* End PBXProject section */ 236 | 237 | /* Begin PBXResourcesBuildPhase section */ 238 | 013D8CA6143C85A100AC2A43 /* Resources */ = { 239 | isa = PBXResourcesBuildPhase; 240 | buildActionMask = 2147483647; 241 | files = ( 242 | 013D8CB7143C85A100AC2A43 /* InfoPlist.strings in Resources */, 243 | 013D8CBD143C85A100AC2A43 /* Credits.rtf in Resources */, 244 | 013D8CC3143C85A200AC2A43 /* MainMenu.xib in Resources */, 245 | 013D8CEE143CA41200AC2A43 /* Window.xib in Resources */, 246 | ); 247 | runOnlyForDeploymentPostprocessing = 0; 248 | }; 249 | 51AC6CEB1CE66D340078C2EE /* Resources */ = { 250 | isa = PBXResourcesBuildPhase; 251 | buildActionMask = 2147483647; 252 | files = ( 253 | ); 254 | runOnlyForDeploymentPostprocessing = 0; 255 | }; 256 | /* End PBXResourcesBuildPhase section */ 257 | 258 | /* Begin PBXSourcesBuildPhase section */ 259 | 013D8CA4143C85A100AC2A43 /* Sources */ = { 260 | isa = PBXSourcesBuildPhase; 261 | buildActionMask = 2147483647; 262 | files = ( 263 | 013D8CB9143C85A100AC2A43 /* main.m in Sources */, 264 | 013D8CC0143C85A100AC2A43 /* TestAppDelegate.m in Sources */, 265 | 013D8CEC143CA34000AC2A43 /* TestWindowController.m in Sources */, 266 | ); 267 | runOnlyForDeploymentPostprocessing = 0; 268 | }; 269 | 51AC6CE81CE66D340078C2EE /* Sources */ = { 270 | isa = PBXSourcesBuildPhase; 271 | buildActionMask = 2147483647; 272 | files = ( 273 | 51AC6CF51CE66D3B0078C2EE /* NMUIElement.m in Sources */, 274 | ); 275 | runOnlyForDeploymentPostprocessing = 0; 276 | }; 277 | /* End PBXSourcesBuildPhase section */ 278 | 279 | /* Begin PBXVariantGroup section */ 280 | 013D8CB5143C85A100AC2A43 /* InfoPlist.strings */ = { 281 | isa = PBXVariantGroup; 282 | children = ( 283 | 013D8CB6143C85A100AC2A43 /* en */, 284 | ); 285 | name = InfoPlist.strings; 286 | sourceTree = ""; 287 | }; 288 | 013D8CBB143C85A100AC2A43 /* Credits.rtf */ = { 289 | isa = PBXVariantGroup; 290 | children = ( 291 | 013D8CBC143C85A100AC2A43 /* en */, 292 | ); 293 | name = Credits.rtf; 294 | sourceTree = ""; 295 | }; 296 | 013D8CC1143C85A200AC2A43 /* MainMenu.xib */ = { 297 | isa = PBXVariantGroup; 298 | children = ( 299 | 013D8CC2143C85A200AC2A43 /* en */, 300 | ); 301 | name = MainMenu.xib; 302 | sourceTree = ""; 303 | }; 304 | /* End PBXVariantGroup section */ 305 | 306 | /* Begin XCBuildConfiguration section */ 307 | 013D8CC4143C85A200AC2A43 /* Debug */ = { 308 | isa = XCBuildConfiguration; 309 | buildSettings = { 310 | ALWAYS_SEARCH_USER_PATHS = NO; 311 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 312 | CLANG_WARN_BOOL_CONVERSION = YES; 313 | CLANG_WARN_COMMA = YES; 314 | CLANG_WARN_CONSTANT_CONVERSION = YES; 315 | CLANG_WARN_EMPTY_BODY = YES; 316 | CLANG_WARN_ENUM_CONVERSION = YES; 317 | CLANG_WARN_INFINITE_RECURSION = YES; 318 | CLANG_WARN_INT_CONVERSION = YES; 319 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 320 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 321 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 322 | CLANG_WARN_STRICT_PROTOTYPES = YES; 323 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 324 | CLANG_WARN_UNREACHABLE_CODE = YES; 325 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 326 | COPY_PHASE_STRIP = NO; 327 | ENABLE_STRICT_OBJC_MSGSEND = YES; 328 | ENABLE_TESTABILITY = YES; 329 | GCC_C_LANGUAGE_STANDARD = gnu99; 330 | GCC_DYNAMIC_NO_PIC = NO; 331 | GCC_ENABLE_OBJC_EXCEPTIONS = YES; 332 | GCC_NO_COMMON_BLOCKS = YES; 333 | GCC_OPTIMIZATION_LEVEL = 0; 334 | GCC_PREPROCESSOR_DEFINITIONS = ( 335 | "DEBUG=1", 336 | "$(inherited)", 337 | ); 338 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 339 | GCC_VERSION = com.apple.compilers.llvm.clang.1_0; 340 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 341 | GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; 342 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 343 | GCC_WARN_UNDECLARED_SELECTOR = YES; 344 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 345 | GCC_WARN_UNUSED_FUNCTION = YES; 346 | GCC_WARN_UNUSED_VARIABLE = YES; 347 | MACOSX_DEPLOYMENT_TARGET = 10.7; 348 | ONLY_ACTIVE_ARCH = YES; 349 | SDKROOT = macosx; 350 | SYMROOT = build; 351 | }; 352 | name = Debug; 353 | }; 354 | 013D8CC5143C85A200AC2A43 /* Release */ = { 355 | isa = XCBuildConfiguration; 356 | buildSettings = { 357 | ALWAYS_SEARCH_USER_PATHS = NO; 358 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 359 | CLANG_WARN_BOOL_CONVERSION = YES; 360 | CLANG_WARN_COMMA = YES; 361 | CLANG_WARN_CONSTANT_CONVERSION = YES; 362 | CLANG_WARN_EMPTY_BODY = YES; 363 | CLANG_WARN_ENUM_CONVERSION = YES; 364 | CLANG_WARN_INFINITE_RECURSION = YES; 365 | CLANG_WARN_INT_CONVERSION = YES; 366 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 367 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 368 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 369 | CLANG_WARN_STRICT_PROTOTYPES = YES; 370 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 371 | CLANG_WARN_UNREACHABLE_CODE = YES; 372 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 373 | COPY_PHASE_STRIP = YES; 374 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 375 | ENABLE_STRICT_OBJC_MSGSEND = YES; 376 | GCC_C_LANGUAGE_STANDARD = gnu99; 377 | GCC_ENABLE_OBJC_EXCEPTIONS = YES; 378 | GCC_NO_COMMON_BLOCKS = YES; 379 | GCC_VERSION = com.apple.compilers.llvm.clang.1_0; 380 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 381 | GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; 382 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 383 | GCC_WARN_UNDECLARED_SELECTOR = YES; 384 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 385 | GCC_WARN_UNUSED_FUNCTION = YES; 386 | GCC_WARN_UNUSED_VARIABLE = YES; 387 | MACOSX_DEPLOYMENT_TARGET = 10.7; 388 | SDKROOT = macosx; 389 | SYMROOT = build; 390 | }; 391 | name = Release; 392 | }; 393 | 013D8CC7143C85A200AC2A43 /* Debug */ = { 394 | isa = XCBuildConfiguration; 395 | buildSettings = { 396 | CLANG_ENABLE_OBJC_ARC = YES; 397 | CODE_SIGN_IDENTITY = "-"; 398 | COMBINE_HIDPI_IMAGES = YES; 399 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 400 | GCC_PREFIX_HEADER = "NMTest001/NMTest001-Prefix.pch"; 401 | INFOPLIST_FILE = "NMTest001/NMTest001-Info.plist"; 402 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 403 | MACOSX_DEPLOYMENT_TARGET = 10.6; 404 | PRODUCT_BUNDLE_IDENTIFIER = "com.pilotmoon.${PRODUCT_NAME:rfc1034identifier}"; 405 | PRODUCT_NAME = "$(TARGET_NAME)"; 406 | WRAPPER_EXTENSION = app; 407 | }; 408 | name = Debug; 409 | }; 410 | 013D8CC8143C85A200AC2A43 /* Release */ = { 411 | isa = XCBuildConfiguration; 412 | buildSettings = { 413 | CLANG_ENABLE_OBJC_ARC = YES; 414 | CODE_SIGN_IDENTITY = "-"; 415 | COMBINE_HIDPI_IMAGES = YES; 416 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 417 | GCC_PREFIX_HEADER = "NMTest001/NMTest001-Prefix.pch"; 418 | INFOPLIST_FILE = "NMTest001/NMTest001-Info.plist"; 419 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 420 | MACOSX_DEPLOYMENT_TARGET = 10.6; 421 | PRODUCT_BUNDLE_IDENTIFIER = "com.pilotmoon.${PRODUCT_NAME:rfc1034identifier}"; 422 | PRODUCT_NAME = "$(TARGET_NAME)"; 423 | WRAPPER_EXTENSION = app; 424 | }; 425 | name = Release; 426 | }; 427 | 51AC6CF31CE66D350078C2EE /* Debug */ = { 428 | isa = XCBuildConfiguration; 429 | buildSettings = { 430 | CLANG_ANALYZER_NONNULL = YES; 431 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 432 | CLANG_CXX_LIBRARY = "libc++"; 433 | CLANG_ENABLE_MODULES = YES; 434 | CLANG_ENABLE_OBJC_ARC = YES; 435 | CLANG_WARN_BOOL_CONVERSION = YES; 436 | CLANG_WARN_CONSTANT_CONVERSION = YES; 437 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 438 | CLANG_WARN_EMPTY_BODY = YES; 439 | CLANG_WARN_ENUM_CONVERSION = YES; 440 | CLANG_WARN_INT_CONVERSION = YES; 441 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 442 | CLANG_WARN_UNREACHABLE_CODE = YES; 443 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 444 | COMBINE_HIDPI_IMAGES = YES; 445 | CURRENT_PROJECT_VERSION = 1; 446 | DEBUG_INFORMATION_FORMAT = dwarf; 447 | DEFINES_MODULE = YES; 448 | DYLIB_COMPATIBILITY_VERSION = 1; 449 | DYLIB_CURRENT_VERSION = 1; 450 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 451 | ENABLE_STRICT_OBJC_MSGSEND = YES; 452 | ENABLE_TESTABILITY = YES; 453 | FRAMEWORK_VERSION = A; 454 | GCC_NO_COMMON_BLOCKS = YES; 455 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 456 | GCC_WARN_UNDECLARED_SELECTOR = YES; 457 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 458 | GCC_WARN_UNUSED_FUNCTION = YES; 459 | INFOPLIST_FILE = NMAccessibility/Info.plist; 460 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 461 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; 462 | MACOSX_DEPLOYMENT_TARGET = 10.11; 463 | MTL_ENABLE_DEBUG_INFO = YES; 464 | PRODUCT_BUNDLE_IDENTIFIER = com.bigbearlabs.NMAccessibility; 465 | PRODUCT_NAME = "$(TARGET_NAME)"; 466 | SKIP_INSTALL = YES; 467 | VERSIONING_SYSTEM = "apple-generic"; 468 | VERSION_INFO_PREFIX = ""; 469 | }; 470 | name = Debug; 471 | }; 472 | 51AC6CF41CE66D350078C2EE /* Release */ = { 473 | isa = XCBuildConfiguration; 474 | buildSettings = { 475 | CLANG_ANALYZER_NONNULL = YES; 476 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 477 | CLANG_CXX_LIBRARY = "libc++"; 478 | CLANG_ENABLE_MODULES = YES; 479 | CLANG_ENABLE_OBJC_ARC = YES; 480 | CLANG_WARN_BOOL_CONVERSION = YES; 481 | CLANG_WARN_CONSTANT_CONVERSION = YES; 482 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 483 | CLANG_WARN_EMPTY_BODY = YES; 484 | CLANG_WARN_ENUM_CONVERSION = YES; 485 | CLANG_WARN_INT_CONVERSION = YES; 486 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 487 | CLANG_WARN_UNREACHABLE_CODE = YES; 488 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 489 | COMBINE_HIDPI_IMAGES = YES; 490 | COPY_PHASE_STRIP = NO; 491 | CURRENT_PROJECT_VERSION = 1; 492 | DEFINES_MODULE = YES; 493 | DYLIB_COMPATIBILITY_VERSION = 1; 494 | DYLIB_CURRENT_VERSION = 1; 495 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 496 | ENABLE_NS_ASSERTIONS = NO; 497 | ENABLE_STRICT_OBJC_MSGSEND = YES; 498 | FRAMEWORK_VERSION = A; 499 | GCC_NO_COMMON_BLOCKS = YES; 500 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 501 | GCC_WARN_UNDECLARED_SELECTOR = YES; 502 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 503 | GCC_WARN_UNUSED_FUNCTION = YES; 504 | INFOPLIST_FILE = NMAccessibility/Info.plist; 505 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 506 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; 507 | MACOSX_DEPLOYMENT_TARGET = 10.11; 508 | MTL_ENABLE_DEBUG_INFO = NO; 509 | PRODUCT_BUNDLE_IDENTIFIER = com.bigbearlabs.NMAccessibility; 510 | PRODUCT_NAME = "$(TARGET_NAME)"; 511 | SKIP_INSTALL = YES; 512 | VERSIONING_SYSTEM = "apple-generic"; 513 | VERSION_INFO_PREFIX = ""; 514 | }; 515 | name = Release; 516 | }; 517 | /* End XCBuildConfiguration section */ 518 | 519 | /* Begin XCConfigurationList section */ 520 | 013D8CA2143C85A100AC2A43 /* Build configuration list for PBXProject "NMTest001" */ = { 521 | isa = XCConfigurationList; 522 | buildConfigurations = ( 523 | 013D8CC4143C85A200AC2A43 /* Debug */, 524 | 013D8CC5143C85A200AC2A43 /* Release */, 525 | ); 526 | defaultConfigurationIsVisible = 0; 527 | defaultConfigurationName = Release; 528 | }; 529 | 013D8CC6143C85A200AC2A43 /* Build configuration list for PBXNativeTarget "NMTest001" */ = { 530 | isa = XCConfigurationList; 531 | buildConfigurations = ( 532 | 013D8CC7143C85A200AC2A43 /* Debug */, 533 | 013D8CC8143C85A200AC2A43 /* Release */, 534 | ); 535 | defaultConfigurationIsVisible = 0; 536 | defaultConfigurationName = Release; 537 | }; 538 | 51AC6CF21CE66D350078C2EE /* Build configuration list for PBXNativeTarget "NMAccessibility" */ = { 539 | isa = XCConfigurationList; 540 | buildConfigurations = ( 541 | 51AC6CF31CE66D350078C2EE /* Debug */, 542 | 51AC6CF41CE66D350078C2EE /* Release */, 543 | ); 544 | defaultConfigurationIsVisible = 0; 545 | defaultConfigurationName = Release; 546 | }; 547 | /* End XCConfigurationList section */ 548 | }; 549 | rootObject = 013D8C9F143C85A100AC2A43 /* Project object */; 550 | } 551 | -------------------------------------------------------------------------------- /lab/NMAccessibility/NMTest001.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /lab/NMAccessibility/NMTest001.xcodeproj/project.xcworkspace/xcshareddata/NMTest001.xcscmblueprint: -------------------------------------------------------------------------------- 1 | { 2 | "DVTSourceControlWorkspaceBlueprintPrimaryRemoteRepositoryKey" : "0187FD8E7B296CEAF7520105D6EDD775240C7EBA", 3 | "DVTSourceControlWorkspaceBlueprintWorkingCopyRepositoryLocationsKey" : { 4 | 5 | }, 6 | "DVTSourceControlWorkspaceBlueprintWorkingCopyStatesKey" : { 7 | "06E7CE2D0ECFC9E9F3EE80734E82F3C3A4072C4C" : 0, 8 | "6C71DF8D4EAED84A4BB92018D54642F26E11361D" : 0, 9 | "0187FD8E7B296CEAF7520105D6EDD775240C7EBA" : 0 10 | }, 11 | "DVTSourceControlWorkspaceBlueprintIdentifierKey" : "4703B6D5-771F-49A2-93B7-FBEF4C799C63", 12 | "DVTSourceControlWorkspaceBlueprintWorkingCopyPathsKey" : { 13 | "06E7CE2D0ECFC9E9F3EE80734E82F3C3A4072C4C" : "..", 14 | "6C71DF8D4EAED84A4BB92018D54642F26E11361D" : "..\/..", 15 | "0187FD8E7B296CEAF7520105D6EDD775240C7EBA" : "BBLAccessibility\/" 16 | }, 17 | "DVTSourceControlWorkspaceBlueprintNameKey" : "NMTest001", 18 | "DVTSourceControlWorkspaceBlueprintVersion" : 204, 19 | "DVTSourceControlWorkspaceBlueprintRelativePathToProjectKey" : "NMAccessibility\/NMTest001.xcodeproj", 20 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoriesKey" : [ 21 | { 22 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "github.com:bigbearlabs\/BBLAccessibility.git", 23 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 24 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "0187FD8E7B296CEAF7520105D6EDD775240C7EBA" 25 | }, 26 | { 27 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "bitbucket.org:bigbearlabs\/contexter-helper.git", 28 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 29 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "06E7CE2D0ECFC9E9F3EE80734E82F3C3A4072C4C" 30 | }, 31 | { 32 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "bitbucket.org:bigbearlabs\/contexter.git", 33 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 34 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "6C71DF8D4EAED84A4BB92018D54642F26E11361D" 35 | } 36 | ] 37 | } -------------------------------------------------------------------------------- /lab/NMAccessibility/NMTest001.xcodeproj/xcshareddata/xcschemes/NMAccessibility.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /lab/NMAccessibility/NMTest001.xcodeproj/xcshareddata/xcschemes/NMTest001.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /lab/NMAccessibility/NMTest001/NMTest001-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | ${PRODUCT_NAME} 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSMinimumSystemVersion 26 | ${MACOSX_DEPLOYMENT_TARGET} 27 | NSHumanReadableCopyright 28 | Copyright © 2011 __MyCompanyName__. All rights reserved. 29 | NSMainNibFile 30 | MainMenu 31 | NSPrincipalClass 32 | NSApplication 33 | 34 | 35 | -------------------------------------------------------------------------------- /lab/NMAccessibility/NMTest001/NMTest001-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header for all source files of the 'NMTest001' target in the 'NMTest001' project 3 | // 4 | 5 | #ifdef __OBJC__ 6 | #import 7 | #endif 8 | -------------------------------------------------------------------------------- /lab/NMAccessibility/NMTest001/TestAppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // NMTest001AppDelegate.h 3 | // NMTest001 4 | // 5 | // Created by Nick Moore on 05/10/2011. 6 | // Copyright 2011 __MyCompanyName__. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "TestWindowController.h" 11 | 12 | @interface TestAppDelegate : NSObject { 13 | NSMutableArray *windowControllers; 14 | pid_t prev_pid; 15 | } 16 | - (IBAction)createNewWindow:(id)sender; 17 | 18 | @end 19 | -------------------------------------------------------------------------------- /lab/NMAccessibility/NMTest001/TestAppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // NMTest001AppDelegate.m 3 | // NMTest001 4 | // 5 | // Created by Nick Moore on 05/10/2011. 6 | // Copyright 2011 __MyCompanyName__. All rights reserved. 7 | // 8 | 9 | #import "TestAppDelegate.h" 10 | 11 | typedef void (^NMAXObservationHandler)(void); 12 | 13 | 14 | 15 | @interface NMAXObservationCentre : NSObject 16 | 17 | +(instancetype) sharedInstance; 18 | 19 | -(void) observe:(CFStringRef)axNotification withHandler:(NMAXObservationHandler)handler; 20 | 21 | @end 22 | 23 | 24 | 25 | @implementation NMAXObservationCentre 26 | 27 | +(instancetype) sharedInstance { 28 | return nil; // stub 29 | } 30 | 31 | -(void) observe:(CFStringRef)axNotification withHandler:(NMAXObservationHandler)handler { 32 | } 33 | 34 | @end 35 | 36 | 37 | @interface TestAppDelegate () 38 | { 39 | NSTimer* timer; 40 | } 41 | @end 42 | 43 | @implementation TestAppDelegate 44 | 45 | - (IBAction)createNewWindow:(id)sender 46 | { 47 | TestWindowController *windowController=[[TestWindowController alloc] initWithWindowNibName:@"Window"]; 48 | [windowControllers addObject:windowController]; 49 | [windowController showWindow:self]; 50 | } 51 | 52 | - (void)applicationDidFinishLaunching:(NSNotification *)aNotification 53 | { 54 | windowControllers=[NSMutableArray array]; 55 | [self createNewWindow:self]; 56 | prev_pid=-1; 57 | 58 | 59 | // Insert code here to initialize your application 60 | [NSEvent addGlobalMonitorForEventsMatchingMask:NSLeftMouseDownMask|NSLeftMouseUpMask handler:^(NSEvent *event) { 61 | 62 | // get the UI Element at the mouse location 63 | CGEventRef eventRef = CGEventCreate(NULL); 64 | NSPoint point=NSPointFromCGPoint(CGEventGetLocation(eventRef)); 65 | CFRelease(eventRef); 66 | 67 | NMUIElement *const element=[NMUIElement elementAtLocation:point]; 68 | 69 | 70 | NSLog(@"report for element %@: %@", element, element.accessibilityInfo); 71 | NMUIElement* focusedElement = [NMUIElement focusedElement]; 72 | NSLog(@"report for focused element %@: %@", element, focusedElement.accessibilityInfo); 73 | 74 | }]; 75 | 76 | // periodically poll for info on first responder element. 77 | timer=[NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(timerRoutine) userInfo:nil repeats:YES]; 78 | 79 | // ALT: observe systemwide element for focuseduielement changes, observe focused element for selected text changes. 80 | } 81 | 82 | -(void)timerRoutine { 83 | NMUIElement* focusedElement = [NMUIElement focusedElement]; 84 | NSLog(@"focusedElement: %@ selectedText: %@ selectionBounds: %@", focusedElement, focusedElement.selectedText, focusedElement.selectionBounds); 85 | } 86 | 87 | - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender 88 | { 89 | return YES; 90 | } 91 | 92 | @end 93 | -------------------------------------------------------------------------------- /lab/NMAccessibility/NMTest001/TestWindowController.h: -------------------------------------------------------------------------------- 1 | // 2 | // TestWindowController.h 3 | // NMTest001 4 | // 5 | // Created by Nick Moore on 05/10/2011. 6 | // Copyright 2011 __MyCompanyName__. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "NMUIElement.h" 11 | 12 | @interface TestWindowController : NSWindowController { 13 | // internals 14 | NMUIElement *menuItem; 15 | NSTimer *timer; 16 | 17 | // ui 18 | NSString *__strong appDisplayName; 19 | NSString *__strong menuItemTitle; 20 | NSString *__strong foundMenuItemTitle; 21 | NSString *__strong foundMenuItemState; 22 | } 23 | 24 | @property (strong) NSString *appDisplayName; 25 | @property (strong) NSString *menuItemTitle; 26 | @property (strong) NSString *foundMenuItemTitle; 27 | @property (strong) NSString *foundMenuItemState; 28 | 29 | - (void)handleNewElement:(NMUIElement *)element; 30 | 31 | @end 32 | -------------------------------------------------------------------------------- /lab/NMAccessibility/NMTest001/TestWindowController.m: -------------------------------------------------------------------------------- 1 | // 2 | // TestWindowController.m 3 | // NMTest001 4 | // 5 | // Created by Nick Moore on 05/10/2011. 6 | // Copyright 2011 __MyCompanyName__. All rights reserved. 7 | // 8 | 9 | #import "TestWindowController.h" 10 | 11 | 12 | @implementation TestWindowController 13 | 14 | @synthesize appDisplayName, menuItemTitle, foundMenuItemTitle, foundMenuItemState; 15 | 16 | 17 | - (id)initWithWindow:(NSWindow *)window 18 | { 19 | self = [super initWithWindow:window]; 20 | if (self) { 21 | // Initialization code here. 22 | } 23 | 24 | return self; 25 | } 26 | 27 | - (NMUIElement *)findItemInMenuBar:(NMUIElement *)menuBar usingBlock:(BOOL (^)(NMUIElement *))block; 28 | { 29 | __block NMUIElement *result=nil; 30 | NSUInteger expectedMenu=3; // note: searching edit menu only 31 | NSUInteger expectedDepth=2; 32 | [[menuBar childAtIndex:expectedMenu] enumerateDescendentsToDepth:expectedDepth 33 | usingBlock:^(NMUIElement *element, NSUInteger depth, const NSUInteger *path, BOOL *stop) { 34 | if (depth==expectedDepth) 35 | { 36 | if (block(element)) 37 | { 38 | result=element; 39 | *stop=YES; 40 | } 41 | } 42 | }]; 43 | return result; 44 | } 45 | 46 | // called frequently to poll the menu item state and update the ui 47 | - (void)timerRoutine 48 | { 49 | NSString *titleString=[NSString stringWithFormat:@"no match for '%@'", self.menuItemTitle]; 50 | NSString *stateString=@"unknown"; 51 | 52 | if (menuItem) { 53 | titleString=[menuItem title]; 54 | stateString=[menuItem enabled]?@"+++ Enabled +++":@"--- Disabled ---"; 55 | } 56 | 57 | // update the UI 58 | self.foundMenuItemTitle=titleString; 59 | self.foundMenuItemState=stateString; 60 | } 61 | 62 | - (void)handleNewElement:(NMUIElement *)element 63 | { 64 | // find and save new menu bar 65 | NMUIElement *appElement=[element appElement]; 66 | NMUIElement *menuBar=[appElement menuBar]; 67 | menuItem=[self findItemInMenuBar:menuBar usingBlock:^(NMUIElement *element) { 68 | return [[element title] isEqualToString:self.menuItemTitle]; 69 | }]; 70 | 71 | // what it this app's name and pid 72 | self.appDisplayName=[NSString stringWithFormat:@"%@ (%i)", [appElement title],[appElement pid]]; 73 | [self timerRoutine]; 74 | } 75 | 76 | 77 | - (void)windowDidLoad 78 | { 79 | [super windowDidLoad]; 80 | [self.window setLevel:NSFloatingWindowLevel]; 81 | self.menuItemTitle=@"Copy"; 82 | self.appDisplayName=@"(click in an app window)"; 83 | 84 | timer=[NSTimer scheduledTimerWithTimeInterval:0.025 target:self selector:@selector(timerRoutine) userInfo:nil repeats:YES]; 85 | } 86 | 87 | - (void)showWindow:(id)sender 88 | { 89 | [self.window center]; 90 | [self.window makeKeyAndOrderFront:self]; 91 | } 92 | 93 | @end 94 | -------------------------------------------------------------------------------- /lab/NMAccessibility/NMTest001/WebKitSystemInterface.h: -------------------------------------------------------------------------------- 1 | /* 2 | WebKitSystemInterface.h 3 | Copyright (C) 2005, 2006, 2007 Apple Inc. All rights reserved. 4 | 5 | Public header file. 6 | */ 7 | 8 | #import 9 | #import 10 | 11 | @class QTMovie; 12 | 13 | #ifdef __cplusplus 14 | extern "C" { 15 | #endif 16 | 17 | typedef enum { 18 | WKCertificateParseResultSucceeded = 0, 19 | WKCertificateParseResultFailed = 1, 20 | WKCertificateParseResultPKCS7 = 2, 21 | } WKCertificateParseResult; 22 | 23 | CFStringRef WKCopyCFLocalizationPreferredName(CFStringRef localization); 24 | CFStringRef WKSignedPublicKeyAndChallengeString(unsigned keySize, CFStringRef challenge, CFStringRef keyDescription); 25 | WKCertificateParseResult WKAddCertificatesToKeychainFromData(const void *bytes, unsigned length); 26 | 27 | NSString *WKGetPreferredExtensionForMIMEType(NSString *type); 28 | NSArray *WKGetExtensionsForMIMEType(NSString *type); 29 | NSString *WKGetMIMETypeForExtension(NSString *extension); 30 | 31 | NSDate *WKGetNSURLResponseLastModifiedDate(NSURLResponse *response); 32 | NSTimeInterval WKGetNSURLResponseFreshnessLifetime(NSURLResponse *response); 33 | NSTimeInterval WKGetNSURLResponseCalculatedExpiration(NSURLResponse *response); 34 | BOOL WKGetNSURLResponseMustRevalidate(NSURLResponse *response); 35 | 36 | CFStringEncoding WKGetWebDefaultCFStringEncoding(void); 37 | 38 | float WKSecondsSinceLastInputEvent(void); 39 | CFStringRef WKPreferRGB32Key(void); 40 | 41 | void WKSetNSURLConnectionDefersCallbacks(NSURLConnection *connection, BOOL defers); 42 | float WKSecondsSinceLastInputEvent(void); 43 | 44 | void WKShowKeyAndMain(void); 45 | #ifndef __LP64__ 46 | OSStatus WKSyncWindowWithCGAfterMove(WindowRef); 47 | unsigned WKCarbonWindowMask(void); 48 | void *WKGetNativeWindowFromWindowRef(WindowRef); 49 | OSType WKCarbonWindowPropertyCreator(void); 50 | OSType WKCarbonWindowPropertyTag(void); 51 | #endif 52 | 53 | typedef id WKNSURLConnectionDelegateProxyPtr; 54 | 55 | WKNSURLConnectionDelegateProxyPtr WKCreateNSURLConnectionDelegateProxy(void); 56 | 57 | void WKDisableCGDeferredUpdates(void); 58 | 59 | Class WKNSURLProtocolClassForReqest(NSURLRequest *request); 60 | void WKSetNSURLRequestShouldContentSniff(NSMutableURLRequest *request, BOOL shouldContentSniff); 61 | 62 | unsigned WKGetNSAutoreleasePoolCount(void); 63 | 64 | NSString *WKMouseMovedNotification(void); 65 | void WKSetNSWindowShouldPostEventNotifications(NSWindow *window, BOOL post); 66 | 67 | CFTypeID WKGetAXTextMarkerTypeID(void); 68 | CFTypeID WKGetAXTextMarkerRangeTypeID(void); 69 | CFTypeRef WKCreateAXTextMarker(const void *bytes, size_t len); 70 | BOOL WKGetBytesFromAXTextMarker(CFTypeRef textMarker, void *bytes, size_t length); 71 | CFTypeRef WKCreateAXTextMarkerRange(CFTypeRef start, CFTypeRef end); 72 | CFTypeRef WKCopyAXTextMarkerRangeStart(CFTypeRef range); 73 | CFTypeRef WKCopyAXTextMarkerRangeEnd(CFTypeRef range); 74 | void WKAccessibilityHandleFocusChanged(void); 75 | AXUIElementRef WKCreateAXUIElementRef(id element); 76 | void WKUnregisterUniqueIdForElement(id element); 77 | 78 | BOOL WKFontSmoothingModeIsLCD(int mode); 79 | void WKSetUpFontCache(size_t s); 80 | 81 | void WKSignalCFReadStreamEnd(CFReadStreamRef stream); 82 | void WKSignalCFReadStreamHasBytes(CFReadStreamRef stream); 83 | void WKSignalCFReadStreamError(CFReadStreamRef stream, CFStreamError *error); 84 | 85 | CFReadStreamRef WKCreateCustomCFReadStream(void *(*formCreate)(CFReadStreamRef, void *), 86 | void (*formFinalize)(CFReadStreamRef, void *), 87 | Boolean (*formOpen)(CFReadStreamRef, CFStreamError *, Boolean *, void *), 88 | CFIndex (*formRead)(CFReadStreamRef, UInt8 *, CFIndex, CFStreamError *, Boolean *, void *), 89 | Boolean (*formCanRead)(CFReadStreamRef, void *), 90 | void (*formClose)(CFReadStreamRef, void *), 91 | void (*formSchedule)(CFReadStreamRef, CFRunLoopRef, CFStringRef, void *), 92 | void (*formUnschedule)(CFReadStreamRef, CFRunLoopRef, CFStringRef, void *), 93 | void *context); 94 | 95 | void WKDrawFocusRing(CGContextRef context, CGRect clipRect, CGColorRef color, int radius); 96 | // Ignore the context's clipping. 97 | // The CG context's current path is the focus ring's path. 98 | // A color of 0 means "use system focus ring color". 99 | // A radius of 0 means "use default focus ring radius". 100 | 101 | void WKSetDragImage(NSImage *image, NSPoint offset); 102 | 103 | void WKDrawBezeledTextFieldCell(NSRect, BOOL enabled); 104 | void WKDrawTextFieldCellFocusRing(NSTextFieldCell*, NSRect); 105 | void WKDrawBezeledTextArea(NSRect, BOOL enabled); 106 | void WKPopupMenu(NSMenu*, NSPoint location, float width, NSView*, int selectedItem, NSFont*); 107 | 108 | void WKSendUserChangeNotifications(void); 109 | #ifndef __LP64__ 110 | BOOL WKConvertNSEventToCarbonEvent(EventRecord *carbonEvent, NSEvent *cocoaEvent); 111 | void WKSendKeyEventToTSM(NSEvent *theEvent); 112 | void WKCallDrawingNotification(CGrafPtr port, Rect *bounds); 113 | #endif 114 | 115 | BOOL WKGetGlyphTransformedAdvances(CGFontRef, NSFont*, CGAffineTransform *m, ATSGlyphRef *glyph, CGSize *advance); 116 | CGFontRef WKGetCGFontFromNSFont(NSFont *font); 117 | void WKGetFontMetrics(CGFontRef font, int *ascent, int *descent, int *lineGap, unsigned *unitsPerEm); 118 | NSFont *WKGetFontInLanguageForRange(NSFont *font, NSString *string, NSRange range); 119 | NSFont *WKGetFontInLanguageForCharacter(NSFont *font, UniChar ch); 120 | void WKSetCGFontRenderingMode(CGContextRef cgContext, NSFont *font); 121 | ATSUFontID WKGetNSFontATSUFontId(NSFont *font); 122 | void WKReleaseStyleGroup(void *group); 123 | BOOL WKCGContextGetShouldSmoothFonts(CGContextRef cgContext); 124 | 125 | void WKSetPatternPhaseInUserSpace(CGContextRef, CGPoint); 126 | 127 | typedef void *WKGlyphVectorRef; 128 | OSStatus WKConvertCharToGlyphs(void *styleGroup, const UniChar *characters, unsigned numCharacters, WKGlyphVectorRef glyphs); 129 | OSStatus WKGetATSStyleGroup(ATSUStyle fontStyle, void **styleGroup); 130 | OSStatus WKInitializeGlyphVector(int count, WKGlyphVectorRef glyphs); 131 | void WKClearGlyphVector(WKGlyphVectorRef glyphs); 132 | 133 | int WKGetGlyphVectorNumGlyphs(WKGlyphVectorRef glyphVector); 134 | ATSLayoutRecord *WKGetGlyphVectorFirstRecord(WKGlyphVectorRef glyphVector); 135 | size_t WKGetGlyphVectorRecordSize(WKGlyphVectorRef glyphVector); 136 | ATSGlyphRef WKGetDefaultGlyphForChar(NSFont *font, UniChar c); 137 | 138 | #ifndef __LP64__ 139 | NSEvent *WKCreateNSEventWithCarbonEvent(EventRef eventRef); 140 | NSEvent *WKCreateNSEventWithCarbonMouseMoveEvent(EventRef inEvent, NSWindow *window); 141 | NSEvent *WKCreateNSEventWithCarbonClickEvent(EventRef inEvent, WindowRef windowRef); 142 | #endif 143 | 144 | CGContextRef WKNSWindowOverrideCGContext(NSWindow *, CGContextRef); 145 | void WKNSWindowRestoreCGContext(NSWindow *, CGContextRef); 146 | 147 | BOOL WKSupportsMultipartXMixedReplace(NSMutableURLRequest *request); 148 | NSString* WKPathFromFont(NSFont *font); 149 | 150 | BOOL WKCGContextIsBitmapContext(CGContextRef context); 151 | 152 | void WKGetWheelEventDeltas(NSEvent *, float *deltaX, float *deltaY, BOOL *continuous); 153 | 154 | BOOL WKAppVersionCheckLessThan(NSString *, int, double); 155 | 156 | int WKQTMovieDataRate(QTMovie* movie); 157 | float WKQTMovieMaxTimeLoaded(QTMovie* movie); 158 | 159 | CFStringRef WKCopyFoundationCacheDirectory(void); 160 | 161 | #ifdef __cplusplus 162 | } 163 | #endif 164 | -------------------------------------------------------------------------------- /lab/NMAccessibility/NMTest001/Window.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1070 5 | 11B2118 6 | 1617 7 | 1138.1 8 | 566.00 9 | 10 | com.apple.InterfaceBuilder.CocoaPlugin 11 | 1617 12 | 13 | 14 | YES 15 | NSTextField 16 | NSTextFieldCell 17 | NSWindowTemplate 18 | NSView 19 | NSCustomObject 20 | 21 | 22 | YES 23 | com.apple.InterfaceBuilder.CocoaPlugin 24 | 25 | 26 | YES 27 | 28 | YES 29 | 30 | 31 | 32 | 33 | YES 34 | 35 | TestWindowController 36 | 37 | 38 | FirstResponder 39 | 40 | 41 | NSApplication 42 | 43 | 44 | 3 45 | 2 46 | {{335, 390}, {323, 152}} 47 | 1954021376 48 | NMTest001 49 | NSWindow 50 | 51 | 52 | 53 | 54 | 256 55 | 56 | YES 57 | 58 | 59 | 268 60 | {{151, 70}, {289, 17}} 61 | 62 | 63 | 64 | _NS:3936 65 | YES 66 | 67 | 68288064 68 | 272630784 69 | Label 70 | 71 | LucidaGrande 72 | 13 73 | 1044 74 | 75 | _NS:3936 76 | 77 | 78 | 6 79 | System 80 | controlColor 81 | 82 | 3 83 | MC42NjY2NjY2NjY3AA 84 | 85 | 86 | 87 | 6 88 | System 89 | controlTextColor 90 | 91 | 3 92 | MAA 93 | 94 | 95 | 96 | 97 | 98 | 99 | 268 100 | {{155, 110}, {100, 22}} 101 | 102 | 103 | 104 | _NS:248 105 | YES 106 | 107 | -1804468671 108 | 272663552 109 | 110 | 111 | _NS:248 112 | 113 | YES 114 | 115 | 6 116 | System 117 | textBackgroundColor 118 | 119 | 3 120 | MQA 121 | 122 | 123 | 124 | 6 125 | System 126 | textColor 127 | 128 | 129 | 130 | 131 | 132 | 133 | 268 134 | {{82, 113}, {69, 17}} 135 | 136 | 137 | 138 | _NS:3936 139 | YES 140 | 141 | 68288064 142 | 71304192 143 | Item Title: 144 | 145 | _NS:3936 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 268 154 | {{35, 70}, {114, 17}} 155 | 156 | 157 | 158 | _NS:3936 159 | YES 160 | 161 | 68288064 162 | 71304192 163 | Last Clicked App: 164 | 165 | _NS:3936 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 268 174 | {{151, 45}, {289, 17}} 175 | 176 | 177 | 178 | _NS:3936 179 | YES 180 | 181 | 68288064 182 | 272630784 183 | Label 184 | 185 | _NS:3936 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 268 194 | {{30, 45}, {119, 17}} 195 | 196 | 197 | 198 | _NS:3936 199 | YES 200 | 201 | 68288064 202 | 71304192 203 | Found Menu Item: 204 | 205 | _NS:3936 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 268 214 | {{151, 20}, {289, 17}} 215 | 216 | 217 | 218 | _NS:3936 219 | YES 220 | 221 | 68288064 222 | 272630784 223 | Label 224 | 225 | _NS:3936 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 268 234 | {{109, 20}, {40, 17}} 235 | 236 | 237 | 238 | _NS:3936 239 | YES 240 | 241 | 68288064 242 | 71304192 243 | State: 244 | 245 | _NS:3936 246 | 247 | 248 | 249 | 250 | 251 | 252 | {323, 152} 253 | 254 | 255 | 256 | 257 | {{0, 0}, {1680, 1028}} 258 | {10000000000000, 10000000000000} 259 | YES 260 | 261 | 262 | 263 | 264 | YES 265 | 266 | 267 | delegate 268 | 269 | 270 | 271 | 17 272 | 273 | 274 | 275 | initialFirstResponder 276 | 277 | 278 | 279 | 18 280 | 281 | 282 | 283 | value: self.appDisplayName 284 | 285 | 286 | 287 | 288 | 289 | value: self.appDisplayName 290 | value 291 | self.appDisplayName 292 | 2 293 | 294 | 295 | 20 296 | 297 | 298 | 299 | value: self.foundMenuItemTitle 300 | 301 | 302 | 303 | 304 | 305 | value: self.foundMenuItemTitle 306 | value 307 | self.foundMenuItemTitle 308 | 2 309 | 310 | 311 | 21 312 | 313 | 314 | 315 | window 316 | 317 | 318 | 319 | 22 320 | 321 | 322 | 323 | value: self.foundMenuItemState 324 | 325 | 326 | 327 | 328 | 329 | value: self.foundMenuItemState 330 | value 331 | self.foundMenuItemState 332 | 2 333 | 334 | 335 | 28 336 | 337 | 338 | 339 | value: self.menuItemTitle 340 | 341 | 342 | 343 | 344 | 345 | value: self.menuItemTitle 346 | value 347 | self.menuItemTitle 348 | 349 | NSContinuouslyUpdatesValue 350 | 351 | 352 | 2 353 | 354 | 355 | 36 356 | 357 | 358 | 359 | 360 | YES 361 | 362 | 0 363 | 364 | 365 | 366 | 367 | 368 | -2 369 | 370 | 371 | File's Owner 372 | 373 | 374 | -1 375 | 376 | 377 | First Responder 378 | 379 | 380 | -3 381 | 382 | 383 | Application 384 | 385 | 386 | 3 387 | 388 | 389 | YES 390 | 391 | 392 | 393 | 394 | 395 | 4 396 | 397 | 398 | YES 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 5 412 | 413 | 414 | YES 415 | 416 | 417 | 418 | 419 | 420 | 6 421 | 422 | 423 | YES 424 | 425 | 426 | 427 | 428 | 429 | 7 430 | 431 | 432 | YES 433 | 434 | 435 | 436 | 437 | 438 | 8 439 | 440 | 441 | YES 442 | 443 | 444 | 445 | 446 | 447 | 9 448 | 449 | 450 | YES 451 | 452 | 453 | 454 | 455 | 456 | 10 457 | 458 | 459 | YES 460 | 461 | 462 | 463 | 464 | 465 | 11 466 | 467 | 468 | 469 | 470 | 12 471 | 472 | 473 | 474 | 475 | 13 476 | 477 | 478 | 479 | 480 | 14 481 | 482 | 483 | 484 | 485 | 15 486 | 487 | 488 | 489 | 490 | 16 491 | 492 | 493 | 494 | 495 | 23 496 | 497 | 498 | YES 499 | 500 | 501 | 502 | 503 | 504 | 24 505 | 506 | 507 | YES 508 | 509 | 510 | 511 | 512 | 513 | 25 514 | 515 | 516 | 517 | 518 | 26 519 | 520 | 521 | 522 | 523 | 524 | 525 | YES 526 | 527 | YES 528 | -1.IBPluginDependency 529 | -2.IBPluginDependency 530 | -3.IBPluginDependency 531 | 10.IBPluginDependency 532 | 11.IBPluginDependency 533 | 12.IBPluginDependency 534 | 13.IBPluginDependency 535 | 14.IBPluginDependency 536 | 15.IBPluginDependency 537 | 16.IBPluginDependency 538 | 23.IBPluginDependency 539 | 24.IBPluginDependency 540 | 25.IBPluginDependency 541 | 26.IBPluginDependency 542 | 3.IBPluginDependency 543 | 3.IBWindowTemplateEditedContentRect 544 | 3.NSWindowTemplate.visibleAtLaunch 545 | 4.IBPluginDependency 546 | 5.IBPluginDependency 547 | 6.IBPluginDependency 548 | 7.IBPluginDependency 549 | 8.IBPluginDependency 550 | 9.IBPluginDependency 551 | 552 | 553 | YES 554 | com.apple.InterfaceBuilder.CocoaPlugin 555 | com.apple.InterfaceBuilder.CocoaPlugin 556 | com.apple.InterfaceBuilder.CocoaPlugin 557 | com.apple.InterfaceBuilder.CocoaPlugin 558 | com.apple.InterfaceBuilder.CocoaPlugin 559 | com.apple.InterfaceBuilder.CocoaPlugin 560 | com.apple.InterfaceBuilder.CocoaPlugin 561 | com.apple.InterfaceBuilder.CocoaPlugin 562 | com.apple.InterfaceBuilder.CocoaPlugin 563 | com.apple.InterfaceBuilder.CocoaPlugin 564 | com.apple.InterfaceBuilder.CocoaPlugin 565 | com.apple.InterfaceBuilder.CocoaPlugin 566 | com.apple.InterfaceBuilder.CocoaPlugin 567 | com.apple.InterfaceBuilder.CocoaPlugin 568 | com.apple.InterfaceBuilder.CocoaPlugin 569 | {{380, 496}, {480, 360}} 570 | 571 | com.apple.InterfaceBuilder.CocoaPlugin 572 | com.apple.InterfaceBuilder.CocoaPlugin 573 | com.apple.InterfaceBuilder.CocoaPlugin 574 | com.apple.InterfaceBuilder.CocoaPlugin 575 | com.apple.InterfaceBuilder.CocoaPlugin 576 | com.apple.InterfaceBuilder.CocoaPlugin 577 | 578 | 579 | 580 | YES 581 | 582 | 583 | 584 | 585 | 586 | YES 587 | 588 | 589 | 590 | 591 | 37 592 | 593 | 594 | 595 | YES 596 | 597 | TestWindowController 598 | NSWindowController 599 | 600 | IBProjectSource 601 | ./Classes/TestWindowController.h 602 | 603 | 604 | 605 | 606 | 0 607 | IBCocoaFramework 608 | 609 | com.apple.InterfaceBuilder.CocoaPlugin.InterfaceBuilder3 610 | 611 | 612 | YES 613 | 3 614 | 615 | 616 | -------------------------------------------------------------------------------- /lab/NMAccessibility/NMTest001/en.lproj/Credits.rtf: -------------------------------------------------------------------------------- 1 | {\rtf0\ansi{\fonttbl\f0\fswiss Helvetica;} 2 | {\colortbl;\red255\green255\blue255;} 3 | \paperw9840\paperh8400 4 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural 5 | 6 | \f0\b\fs24 \cf0 Engineering: 7 | \b0 \ 8 | Some people\ 9 | \ 10 | 11 | \b Human Interface Design: 12 | \b0 \ 13 | Some other people\ 14 | \ 15 | 16 | \b Testing: 17 | \b0 \ 18 | Hopefully not nobody\ 19 | \ 20 | 21 | \b Documentation: 22 | \b0 \ 23 | Whoever\ 24 | \ 25 | 26 | \b With special thanks to: 27 | \b0 \ 28 | Mom\ 29 | } 30 | -------------------------------------------------------------------------------- /lab/NMAccessibility/NMTest001/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /lab/NMAccessibility/NMTest001/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // NMTest001 4 | // 5 | // Created by Nick Moore on 05/10/2011. 6 | // Copyright 2011 __MyCompanyName__. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | int main(int argc, char *argv[]) 12 | { 13 | return NSApplicationMain(argc, (const char **)argv); 14 | } 15 | -------------------------------------------------------------------------------- /lab/NMAccessibility/NMUIElement/NMUIElement.h: -------------------------------------------------------------------------------- 1 | // 2 | // NMUIElement.h 3 | // dc 4 | // 5 | // Created by Work on 20/07/2010. 6 | // Copyright 2010 Nicholas Moore. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #define NM_UIELEMENT_MAX_PATH_DEPTH 10 12 | 13 | @interface NMUIElement : NSObject { 14 | AXUIElementRef elementRef; 15 | } 16 | 17 | @property (readonly) NSDictionary* accessibilityInfo; 18 | 19 | @property (readonly) AXUIElementRef elementRef; 20 | 21 | @property (readonly) pid_t pid; 22 | 23 | @property (readonly) NSString *selectedText; 24 | @property (readonly) CGRect selectionBounds; 25 | 26 | @property (readonly) NSString *role; 27 | @property (readonly) NSString *subRole; 28 | @property (readonly) NSString *title; 29 | @property (readonly) NSString *menuCmdCharacter; 30 | @property (readonly) NSNumber *menuCmdKeycode; 31 | @property (readonly) NSNumber *menuCmdModifiers; 32 | @property (readonly) NSSize size; 33 | @property (readonly) NSPoint origin; 34 | 35 | @property (readonly) BOOL selected; 36 | @property (readonly) BOOL enabled; 37 | @property (readonly) BOOL main; 38 | @property (readonly) BOOL hasChildren; 39 | @property (readonly) BOOL hasSelectedChildren; 40 | 41 | @property (readonly) NSSet *allParentRoles; 42 | @property (readonly) NMUIElement *appElement; 43 | @property (readonly) NMUIElement *menuBarDirect; 44 | @property (readonly) NMUIElement *menuBar; 45 | @property (readonly) NMUIElement *parentElement; 46 | @property (readonly) NMUIElement *topLevelElement; 47 | @property (readonly) NMUIElement *windowElement; 48 | @property (readonly) NMUIElement *closeButtonElement; 49 | @property (readonly) NMUIElement *zoomButtonElement; 50 | @property (readonly) NMUIElement *minimizeButtonElement; 51 | @property (readonly) NMUIElement *toolbarButtonElement; 52 | 53 | @property (readonly) NSArray *actionNames; 54 | @property (readonly) NSArray *children; // children as array of ACUIElementRef 55 | @property (readonly) NSNumber *insertionPointLineNumber; 56 | @property (readonly) NSNumber *numberOfCharacters; 57 | 58 | + (NMUIElement *)elementAtLocation:(NSPoint)point; 59 | + (NMUIElement *)focusedElement; 60 | 61 | - (NMUIElement *)childAtIndex:(NSUInteger)index; 62 | - (id)initWithElement:(AXUIElementRef)element; 63 | - (void)performAction:(NSString *)name; 64 | - (NMUIElement *)findParentWithRole:(NSString *)role; 65 | - (NMUIElement *)topLevelMenuWithIndex:(NSUInteger)index; 66 | - (NMUIElement *)attributeNamed:(NSString *)name; 67 | 68 | - (void)enumerateDescendentsToDepth:(NSUInteger)depth 69 | usingBlock:(void (^)(NMUIElement *element, NSUInteger depth, const NSUInteger *path, BOOL *stop))block; // nested enumeration of all children 70 | 71 | 72 | + (CGWindowID)windowIdForElement:(AXUIElementRef)element; 73 | 74 | @end -------------------------------------------------------------------------------- /lab/NMAccessibility/NMUIElement/NMUIElement.m: -------------------------------------------------------------------------------- 1 | // 2 | // NMUIElement.m 3 | // dc 4 | // 5 | // Created by Work on 20/07/2010. 6 | // Copyright 2010 Nicholas Moore. All rights reserved. 7 | // 8 | 9 | #import "NMUIElement.h" 10 | 11 | extern AXError _AXUIElementGetWindow(AXUIElementRef, CGWindowID* out); 12 | 13 | 14 | static AXUIElementRef _systemWide = NULL; 15 | 16 | 17 | @implementation NMUIElement 18 | 19 | @synthesize elementRef; 20 | 21 | + (void)initialize 22 | { 23 | if (self == [NMUIElement class]) // standard check to prevent multiple runs 24 | { 25 | if (!_systemWide) { 26 | _systemWide = AXUIElementCreateSystemWide(); 27 | } 28 | } 29 | } 30 | 31 | + (NMUIElement *)elementAtLocation:(NSPoint)point 32 | { 33 | AXUIElementRef element=NULL; 34 | AXUIElementCopyElementAtPosition (_systemWide, point.x, point.y, &element); 35 | if (element) { 36 | id elem = [[NMUIElement alloc] initWithElement:element]; 37 | CFRelease(element); 38 | return elem; 39 | } else { 40 | return nil; 41 | } 42 | } 43 | 44 | + (NMUIElement *) focusedElement 45 | { 46 | AXUIElementRef result=NULL; 47 | AXUIElementCopyAttributeValue(_systemWide, kAXFocusedUIElementAttribute, (CFTypeRef *)&result); 48 | if (result) { 49 | id elem = [[NMUIElement alloc] initWithElement:result]; 50 | CFRelease(result); 51 | return elem; 52 | } else { 53 | NSLog(@"no focused element..."); 54 | return nil; 55 | } 56 | } 57 | 58 | 59 | - (id)initWithElement:(AXUIElementRef)element 60 | { 61 | if (!(self = [super init])) return nil; 62 | if(!element) return nil; 63 | CFRetain(element); 64 | elementRef=element; 65 | return self; 66 | } 67 | 68 | - (void)dealloc { 69 | if (elementRef) CFRelease(elementRef); 70 | } 71 | 72 | #pragma high-level 73 | 74 | -(NSDictionary*) accessibilityInfo { 75 | NSMutableDictionary* info = [@{} mutableCopy]; 76 | 77 | // // only retrieve certain info if contained in windows 78 | // if ([[[self windowElement] role] isEqualToString:(NSString *)kAXWindowRole]) 79 | // { 80 | NMUIElement *appElement=[self appElement]; 81 | 82 | // DISABLED finding the Copy menu item to show its status. 83 | // // find and save new menu bar 84 | // NMUIElement *menuBar=[appElement menuBar]; 85 | // menuItem=[self findItemInMenuBar:menuBar usingBlock:^(NMUIElement *element) { 86 | // return [[element title] isEqualToString:self.menuItemTitle]; 87 | // }]; 88 | 89 | // app-level info. 90 | info[@"appName"] = [appElement title]; 91 | info[@"pid"] = @([appElement pid]); 92 | // } 93 | 94 | // AX info. 95 | info[@"role"] = self.role; 96 | 97 | // window info. 98 | NMUIElement* window = self.windowElement; 99 | info[@"windowTitle"] = ([window.title length] == 0 ? nil : window.title); 100 | NSPoint origin = window.origin; 101 | NSSize size = window.size; 102 | NSRect windowRect = NSMakeRect(origin.x, origin.y, size.width, size.height); 103 | info[@"windowRect"] = [NSValue valueWithRect:windowRect]; 104 | 105 | // window id. 106 | CGWindowID windowId = [NMUIElement windowIdForElement:window.elementRef]; 107 | info[@"windowId"] = @(windowId); 108 | 109 | // selectedText, selectionBounds. 110 | NMUIElement* elementWithSelection = self.firstChildElementWithSelection; 111 | if (elementWithSelection) { 112 | info[@"selectedText"] = elementWithSelection.selectedText; 113 | info[@"selectionBounds"] = [NSValue valueWithRect:elementWithSelection.selectionBounds]; 114 | } 115 | 116 | 117 | // TODO to provide a more complete AX information: 118 | 119 | // (contexter) 120 | // PoC URL of resource represented by window. 121 | 122 | // (xform) 123 | // PoC Contents of content-containing control (e.g. TextView or web text area) 124 | 125 | // (webbuddy) 126 | // PoC mouseover'ed URL. 127 | 128 | return info; 129 | } 130 | 131 | -(NSString*) description { 132 | return [NSString stringWithFormat:@"%@: role: %@, actions: %@, parent: %@, children: %@", [super description], self.role, self.actionNames, self.parentElement, self.children]; 133 | } 134 | 135 | #pragma mark App Info 136 | 137 | - (pid_t)pid 138 | { 139 | pid_t result=-1; 140 | AXUIElementGetPid(elementRef, &result); 141 | return result; 142 | } 143 | 144 | #pragma mark Text Selection 145 | 146 | - (NSString *)selectedText 147 | { 148 | CFTypeRef result = NULL; 149 | AXUIElementCopyAttributeValue(elementRef, kAXSelectedTextAttribute, &result); 150 | if (result) { 151 | return (NSString*)CFBridgingRelease(result); 152 | } else { 153 | return self.selectedTextForWebArea; 154 | } 155 | } 156 | 157 | // selected text query specific for web views. 158 | - (NSString *)selectedTextForWebArea { 159 | CFTypeRef range = NULL; 160 | AXUIElementCopyAttributeValue(elementRef, CFSTR("AXSelectedTextMarkerRange"), &range); 161 | 162 | CFTypeRef val = NULL; 163 | AXError err = AXUIElementCopyParameterizedAttributeValue(self.elementRef, CFSTR("AXStringForTextMarkerRange"), range, &val); 164 | 165 | if (range) CFRelease(range); 166 | 167 | if (err == kAXErrorSuccess) { 168 | return (NSString*)CFBridgingRelease(val); 169 | } 170 | else { 171 | // NSLog(@"err AXStringForTextMarkerRange: %d", (int)err); 172 | return nil; 173 | } 174 | } 175 | 176 | -(CGRect) selectionBounds { 177 | // query selected text range. 178 | AXValueRef selectedRangeValue = NULL; 179 | AXError err = AXUIElementCopyAttributeValue(self.elementRef, kAXSelectedTextRangeAttribute, (CFTypeRef *)&selectedRangeValue); 180 | if (err != kAXErrorSuccess) { 181 | 182 | // query web area selected text range. 183 | err = AXUIElementCopyAttributeValue(elementRef, CFSTR("AXSelectedTextMarkerRange"), (CFTypeRef *)&selectedRangeValue); 184 | if (err != kAXErrorSuccess) { 185 | 186 | // CASE Preview.app: AXGroup doesn't have the selectedTextRange, but its child AXStaticText does. 187 | if ([self.role isEqualToString:(__bridge NSString*)kAXGroupRole]) { 188 | AXUIElementRef staticText = (__bridge AXUIElementRef)(self.children[0]); 189 | NMUIElement* staticTextElem = [[NMUIElement alloc] initWithElement:staticText]; 190 | CGRect result = staticTextElem.selectionBounds; 191 | return result; 192 | } 193 | 194 | else { 195 | NSLog(@"query for selection ranged failed on %@", self); 196 | NSLog(@"diagnosis: %@", self.description); 197 | return CGRectZero; 198 | } 199 | } 200 | } 201 | 202 | // query bounds of range. 203 | CGRect result = CGRectNull; 204 | AXValueRef selectionBoundsValue = NULL; 205 | if (AXUIElementCopyParameterizedAttributeValue(self.elementRef, kAXBoundsForRangeParameterizedAttribute, selectedRangeValue, (CFTypeRef *)&selectionBoundsValue) == kAXErrorSuccess) { 206 | // get value out 207 | AXValueGetValue(selectionBoundsValue, kAXValueCGRectType, &result); 208 | } 209 | 210 | else { 211 | // couldn't query bounds of range. 212 | 213 | // DEBUG 214 | // id names = [self parameterisedAttributeNames]; 215 | // NSLog(@"parameterised attribute names for %@: %@", self, names); 216 | 217 | // query bounds of web area selected text range. 218 | if (AXUIElementCopyParameterizedAttributeValue(self.elementRef, CFSTR("AXBoundsForTextMarkerRange"), selectedRangeValue, (CFTypeRef *)&selectionBoundsValue) == kAXErrorSuccess) { 219 | AXValueGetValue(selectionBoundsValue, kAXValueCGRectType, &result); 220 | } 221 | else { 222 | // all queries for bounds failed. 223 | } 224 | } 225 | 226 | CFRelease(selectedRangeValue); 227 | CFRelease(selectionBoundsValue); 228 | 229 | // NSLog(@"bounds: %@", [NSValue valueWithRect:rect]); 230 | 231 | if (CGRectIsEmpty(result)) { 232 | // @throw [NSException exceptionWithName:@"AXQueryFailedException" reason:[NSString stringWithFormat:@"couldn't retrieve bounds for selected text on element %@", self] userInfo:nil]; 233 | } 234 | 235 | return result; 236 | } 237 | 238 | - (NMUIElement*)firstChildElementWithSelection { 239 | NMUIElement* element = self; 240 | while (element) { 241 | id text = element.selectedText; 242 | 243 | if (text) { 244 | return element; 245 | } 246 | else { 247 | // walk up element tree. 248 | element = element.parentElement; 249 | } 250 | } 251 | 252 | // couldn't retrieve selected text. 253 | return nil; 254 | } 255 | 256 | #pragma mark Parent roles (including self) 257 | - (NSSet *)allParentRoles 258 | { 259 | NSMutableSet *result=[NSMutableSet set]; 260 | NMUIElement *p=self; 261 | 262 | while (p) 263 | { 264 | NSString *role=p.role; 265 | if (role) { 266 | [result addObject:role]; 267 | } 268 | p=p.parentElement; 269 | } 270 | return result; 271 | } 272 | 273 | - (NMUIElement *)findParentWithRole:(NSString *)role 274 | { 275 | NMUIElement *p=self; 276 | while (p) 277 | { 278 | if ([p.role isEqualToString:role]) { 279 | return p; 280 | } 281 | p=p.parentElement; 282 | } 283 | return nil; 284 | } 285 | 286 | # pragma mark String Attributes 287 | 288 | - (NSString *)role 289 | { 290 | CFTypeRef result; 291 | AXUIElementCopyAttributeValue(elementRef, kAXRoleAttribute, &result); 292 | return (NSString*) CFBridgingRelease((CFTypeRef)result); 293 | } 294 | 295 | - (NSString *)subRole 296 | { 297 | CFTypeRef result; 298 | AXUIElementCopyAttributeValue(elementRef, kAXSubroleAttribute, (CFTypeRef *)&result); 299 | return (NSString*) CFBridgingRelease((CFTypeRef)result); 300 | } 301 | 302 | - (NSString *)title 303 | { 304 | CFTypeRef result; 305 | AXUIElementCopyAttributeValue(elementRef, kAXTitleAttribute, (CFTypeRef *)&result); 306 | return (NSString*) CFBridgingRelease((CFTypeRef)result); 307 | } 308 | 309 | - (NSString *)menuCmdCharacter 310 | { 311 | CFTypeRef result; 312 | AXUIElementCopyAttributeValue(elementRef, kAXMenuItemCmdCharAttribute, (CFTypeRef *)&result); 313 | return (NSString*) CFBridgingRelease((CFTypeRef)result); 314 | } 315 | 316 | - (NSNumber *)menuCmdKeycode 317 | { 318 | CFTypeRef result; 319 | AXUIElementCopyAttributeValue(elementRef, kAXMenuItemCmdVirtualKeyAttribute, (CFTypeRef *)&result); 320 | return (NSNumber*) CFBridgingRelease((CFTypeRef)result); 321 | } 322 | 323 | - (NSNumber *)menuCmdModifiers 324 | { 325 | CFTypeRef result; 326 | AXUIElementCopyAttributeValue(elementRef, kAXMenuItemCmdModifiersAttribute, (CFTypeRef *)&result); 327 | return (NSNumber*)CFBridgingRelease( result); 328 | } 329 | 330 | #pragma mark Boolean Attributes 331 | 332 | - (BOOL)selected 333 | { 334 | CFBooleanRef result=NULL; 335 | AXUIElementCopyAttributeValue(elementRef, kAXSelectedAttribute, (CFTypeRef *)&result); 336 | return(result && CFBooleanGetValue(result)); 337 | } 338 | 339 | - (BOOL)enabled 340 | { 341 | CFBooleanRef result=NULL; 342 | AXUIElementCopyAttributeValue(elementRef, kAXEnabledAttribute, (CFTypeRef *)&result); 343 | return(result && CFBooleanGetValue(result)); 344 | } 345 | 346 | - (BOOL)main 347 | { 348 | CFBooleanRef result=NULL; 349 | AXUIElementCopyAttributeValue(elementRef, kAXMainAttribute, (CFTypeRef *)&result); 350 | return(result && CFBooleanGetValue(result)); 351 | } 352 | 353 | - (BOOL)hasChildren 354 | { 355 | CFArrayRef children=NULL; 356 | AXUIElementCopyAttributeValue(elementRef, kAXChildrenAttribute, (CFTypeRef *)&children); 357 | return(children && CFArrayGetCount(children)>0); 358 | } 359 | 360 | - (BOOL)hasSelectedChildren 361 | { 362 | CFArrayRef children=NULL; 363 | AXUIElementCopyAttributeValue(elementRef, kAXSelectedChildrenAttribute, (CFTypeRef *)&children); 364 | BOOL retval = (children && CFArrayGetCount(children)>0); 365 | if (children) CFRelease(children); 366 | return retval; 367 | } 368 | 369 | #pragma mark Window Attributes 370 | 371 | - (NSPoint)origin 372 | { 373 | CGPoint result=NSPointToCGPoint(NSZeroPoint); 374 | AXValueRef ref=NULL; 375 | AXUIElementCopyAttributeValue(elementRef, kAXPositionAttribute, (CFTypeRef *)&ref); 376 | if(ref) 377 | { 378 | AXValueGetValue(ref, kAXValueCGPointType, &result); 379 | CFRelease(ref); 380 | } 381 | return NSPointFromCGPoint(result); 382 | } 383 | 384 | - (NSSize)size 385 | { 386 | CGSize result=NSSizeToCGSize(NSZeroSize); 387 | AXValueRef ref=NULL; 388 | AXUIElementCopyAttributeValue(elementRef, kAXSizeAttribute, (CFTypeRef *)&ref); 389 | if(ref) 390 | { 391 | AXValueGetValue(ref, kAXValueCGSizeType, &result); 392 | CFRelease(ref); 393 | } 394 | return NSSizeFromCGSize(result); 395 | } 396 | 397 | - (NSNumber *)insertionPointLineNumber 398 | { 399 | CFTypeRef result=nil; 400 | AXUIElementCopyAttributeValue(elementRef, kAXInsertionPointLineNumberAttribute, (CFTypeRef *)&result); 401 | return (NSNumber*)CFBridgingRelease(result); 402 | } 403 | 404 | - (NSNumber *)numberOfCharacters 405 | { 406 | CFTypeRef result=nil; 407 | AXUIElementCopyAttributeValue(elementRef, kAXNumberOfCharactersAttribute, (CFTypeRef *)&result); 408 | return (NSNumber *)CFBridgingRelease(result); 409 | } 410 | 411 | #pragma mark Related Elements 412 | 413 | - (NMUIElement *)parentElement 414 | { 415 | AXUIElementRef result=NULL; 416 | AXUIElementCopyAttributeValue(elementRef, kAXParentAttribute, (CFTypeRef *)&result); 417 | id elem = [[NMUIElement alloc] initWithElement:result]; 418 | if (result) CFRelease(result); 419 | return elem; 420 | } 421 | 422 | - (NMUIElement *)topLevelElement 423 | { 424 | AXUIElementRef result=NULL; 425 | AXUIElementCopyAttributeValue(elementRef, kAXTopLevelUIElementAttribute, (CFTypeRef *)&result); 426 | id elem = [[NMUIElement alloc] initWithElement:result]; 427 | if (result) CFRelease(result); 428 | return elem; 429 | } 430 | 431 | - (NMUIElement *)windowElement 432 | { 433 | NMUIElement *result=nil; 434 | if ([self.role isEqualToString:(NSString *)kAXWindowRole]) 435 | { 436 | result=self; 437 | } 438 | else 439 | { 440 | NMUIElement *top=self.topLevelElement; 441 | if ([top.role isEqualToString:(NSString *)kAXWindowRole]) 442 | { 443 | result=top; 444 | } 445 | } 446 | return result; 447 | 448 | // // IT2 449 | // AXUIElementRef result=NULL; 450 | // AXUIElementCopyAttributeValue(elementRef, kAXWindowAttribute, (CFTypeRef *)&result); 451 | // id elem =[[NMUIElement alloc] initWithElement:result]; 452 | // if (result) { CFRelease(result); } 453 | // return elem; 454 | } 455 | 456 | - (NSArray *)children 457 | { 458 | CFTypeRef result; 459 | AXUIElementCopyAttributeValue(elementRef, kAXChildrenAttribute, &result); 460 | return (NSArray*) CFBridgingRelease(result); 461 | } 462 | 463 | - (NMUIElement *)appElement 464 | { 465 | id result=[self findParentWithRole:(NSString *)kAXApplicationRole]; 466 | return result; 467 | } 468 | 469 | - (NMUIElement *)menuBar 470 | { 471 | AXUIElementRef result=NULL; 472 | AXUIElementRef app=[[self findParentWithRole:(NSString *)kAXApplicationRole] elementRef]; 473 | if (app) { 474 | AXUIElementCopyAttributeValue(app, kAXMenuBarRole, (CFTypeRef *)&result); 475 | } 476 | id elem = [[NMUIElement alloc] initWithElement:result]; 477 | if (result) CFRelease(result); 478 | return elem; 479 | } 480 | 481 | - (NMUIElement *)menuBarDirect 482 | { 483 | AXUIElementRef result=NULL; 484 | AXUIElementCopyAttributeValue(elementRef, kAXMenuBarRole, (CFTypeRef *)&result); 485 | id elem =[[NMUIElement alloc] initWithElement:result]; 486 | if (result) { CFRelease(result); } 487 | return elem; 488 | } 489 | 490 | -(NMUIElement *)childAtIndex:(NSUInteger)index 491 | { 492 | NMUIElement *result=nil; 493 | CFTypeRef itemChildren; 494 | AXUIElementCopyAttributeValue(elementRef, kAXChildrenAttribute, (CFTypeRef *)&itemChildren); 495 | if (itemChildren&&[(__bridge NSArray*)itemChildren count]>index) { 496 | result=[[NMUIElement alloc] initWithElement:(AXUIElementRef)[(__bridge NSArray*)itemChildren objectAtIndex:index]]; 497 | } 498 | return result; 499 | } 500 | 501 | - (NMUIElement *)closeButtonElement 502 | { 503 | AXUIElementRef result=NULL; 504 | AXUIElementCopyAttributeValue(elementRef, kAXCloseButtonAttribute, (CFTypeRef *)&result); 505 | id elem =[[NMUIElement alloc] initWithElement:result]; 506 | if (result) { CFRelease(result); } 507 | return elem; 508 | } 509 | 510 | - (NMUIElement *)zoomButtonElement 511 | { 512 | AXUIElementRef result=NULL; 513 | AXUIElementCopyAttributeValue(elementRef, kAXZoomButtonAttribute, (CFTypeRef *)&result); 514 | id elem =[[NMUIElement alloc] initWithElement:result]; 515 | if (result) { CFRelease(result); } 516 | return elem; 517 | } 518 | 519 | - (NMUIElement *)minimizeButtonElement 520 | { 521 | AXUIElementRef result=NULL; 522 | AXUIElementCopyAttributeValue(elementRef, kAXMinimizeButtonAttribute, (CFTypeRef *)&result); 523 | id elem =[[NMUIElement alloc] initWithElement:result]; 524 | if (result) { CFRelease(result); } 525 | return elem; 526 | } 527 | 528 | - (NMUIElement *)toolbarButtonElement 529 | { 530 | AXUIElementRef result=NULL; 531 | AXUIElementCopyAttributeValue(elementRef, kAXToolbarButtonAttribute, (CFTypeRef *)&result); 532 | id elem =[[NMUIElement alloc] initWithElement:result]; 533 | if (result) { CFRelease(result); } 534 | return elem; 535 | } 536 | 537 | - (NMUIElement *)attributeNamed:(NSString *)name 538 | { 539 | AXUIElementRef result=NULL; 540 | AXUIElementCopyAttributeValue(elementRef, (__bridge CFTypeRef)name, (CFTypeRef *)&result); 541 | id elem =[[NMUIElement alloc] initWithElement:result]; 542 | if (result) { CFRelease(result); } 543 | return elem; 544 | } 545 | 546 | - (NSArray *)actionNames 547 | { 548 | CFArrayRef result; 549 | AXUIElementCopyActionNames(elementRef, &result); 550 | return (NSArray*)CFBridgingRelease(result); 551 | } 552 | 553 | - (void)performAction:(NSString *)name 554 | { 555 | AXUIElementPerformAction(elementRef, (__bridge CFStringRef)name); 556 | } 557 | 558 | - (NMUIElement *)topLevelMenuWithIndex:(NSUInteger)index 559 | { 560 | NMUIElement *result=nil; 561 | NMUIElement *menuBar=[self menuBar]; 562 | if (menuBar) { 563 | NSArray *menus=[menuBar children]; 564 | if ([menus count]>index) { 565 | result=[[NMUIElement alloc] initWithElement:(AXUIElementRef)[menus objectAtIndex:index]]; 566 | } 567 | } 568 | return result; 569 | } 570 | 571 | static void _enumerate(void (^block)(NMUIElement *element, NSUInteger depth, const NSUInteger *path, BOOL *stop), 572 | NMUIElement *element, BOOL *stop, NSUInteger depth, NSUInteger maxDepth, NSUInteger *path) 573 | { 574 | // check depth 575 | if (depth>maxDepth) { 576 | return; 577 | } 578 | 579 | // call the block 580 | block(element, depth, path, stop); 581 | 582 | // we are going one level deeper 583 | NSUInteger *pathLocation=path+depth++; 584 | 585 | // enumerate any children 586 | NSArray *children=(NSArray *)[element children]; 587 | if (children) { 588 | NSUInteger subChildIndex=0; 589 | for(id childRef in children) 590 | { 591 | if (*stop) { 592 | break; 593 | } 594 | NMUIElement *child=[[NMUIElement alloc] initWithElement:(AXUIElementRef)childRef]; 595 | *pathLocation=subChildIndex++; 596 | _enumerate(block, child, stop, depth, maxDepth, path); 597 | } 598 | } 599 | } 600 | 601 | - (void)enumerateDescendentsToDepth:(NSUInteger)maxDepth 602 | usingBlock:(void (^)(NMUIElement *element, NSUInteger depth, const NSUInteger *path, BOOL *stop))block; 603 | { 604 | __block BOOL stop=NO; 605 | __block NSUInteger path[NM_UIELEMENT_MAX_PATH_DEPTH]={0}; 606 | if (maxDepth>NM_UIELEMENT_MAX_PATH_DEPTH) { 607 | maxDepth=NM_UIELEMENT_MAX_PATH_DEPTH; 608 | } 609 | _enumerate(block, self, &stop, 0, maxDepth, path); 610 | } 611 | 612 | 613 | #pragma mark AX util methods 614 | 615 | + (CGWindowID)windowIdForElement:(AXUIElementRef)element { 616 | // IT1 using CG private API. 617 | CGWindowID out; 618 | _AXUIElementGetWindow(element, &out); 619 | return out; 620 | 621 | // IT2 for MAS compliance, consider replacing with a filtering op from CGWindowList. 622 | } 623 | 624 | + (NSArray*) windowIdsForPid:(pid_t)pid { 625 | AXUIElementRef app = AXUIElementCreateApplication(pid); 626 | CFTypeRef windows = NULL; 627 | AXError err = AXUIElementCopyAttributeValue(app, kAXWindowsAttribute, &windows); 628 | 629 | if (err) { 630 | NSLog(@"err getting windows: %i", err); 631 | } 632 | 633 | CFRelease(app); 634 | 635 | // pe_debug "elems: #{windows[0]}" 636 | 637 | NSMutableArray* ids = [@[] mutableCopy]; 638 | for (id windowRef in (NSArray*)CFBridgingRelease(windows)) { 639 | CGWindowID windowId = [self windowIdForElement:(__bridge AXUIElementRef)windowRef]; 640 | [ids addObject:@(windowId)]; 641 | } 642 | 643 | return ids; 644 | } 645 | 646 | -(NSArray*) parameterisedAttributeNames { 647 | CFArrayRef names = NULL; 648 | AXUIElementCopyParameterizedAttributeNames(self.elementRef, &names); 649 | return CFBridgingRelease(names); 650 | } 651 | 652 | @end 653 | 654 | -------------------------------------------------------------------------------- /withPrior.swift: -------------------------------------------------------------------------------- 1 | 2 | import Combine 3 | 4 | extension Publisher { 5 | 6 | /// Includes the current element as well as the previous element from the upstream publisher in a tuple where the previous element is optional. 7 | /// The first time the upstream publisher emits an element, the previous element will be `nil`. 8 | /// 9 | /// let range = (1...5) 10 | /// cancellable = range.publisher 11 | /// .withPrevious() 12 | /// .sink { print ("(\($0.previous), \($0.current))", terminator: " ") } 13 | /// // Prints: "(nil, 1) (Optional(1), 2) (Optional(2), 3) (Optional(3), 4) (Optional(4), 5) ". 14 | /// 15 | /// - Returns: A publisher of a tuple of the previous and current elements from the upstream publisher. 16 | func withPrior() -> AnyPublisher<(previous: Output?, current: Output), Failure> { 17 | scan(Optional<(Output?, Output)>.none) { ($0?.1, $1) } 18 | .compactMap { $0 } 19 | .eraseToAnyPublisher() 20 | } 21 | 22 | /// Includes the current element as well as the previous element from the upstream publisher in a tuple where the previous element is not optional. 23 | /// The first time the upstream publisher emits an element, the previous element will be the `initialPreviousValue`. 24 | /// 25 | /// let range = (1...5) 26 | /// cancellable = range.publisher 27 | /// .withPrevious(0) 28 | /// .sink { print ("(\($0.previous), \($0.current))", terminator: " ") } 29 | /// // Prints: "(0, 1) (1, 2) (2, 3) (3, 4) (4, 5) ". 30 | /// 31 | /// - Parameter initialPreviousValue: The initial value to use as the "previous" value when the upstream publisher emits for the first time. 32 | /// - Returns: A publisher of a tuple of the previous and current elements from the upstream publisher. 33 | func withPrior(_ initialPreviousValue: Output) -> AnyPublisher<(previous: Output, current: Output), Failure> { 34 | scan((initialPreviousValue, initialPreviousValue)) { ($0.1, $1) }.eraseToAnyPublisher() 35 | } 36 | } 37 | --------------------------------------------------------------------------------