├── .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 |
13 |
21 |
25 |
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 |
--------------------------------------------------------------------------------