[PATCH] Add NSAXSpy to help debugging OS X accessibility

Boris Dušek (via_Code_Review) gerrit at gerrit.libreoffice.org
Sun Jul 28 13:56:09 PDT 2013


Hi,

I would like you to review the following patch:

    https://gerrit.libreoffice.org/5160

To pull it, you can do:

    git pull ssh://gerrit.libreoffice.org:29418/dev-tools refs/changes/60/5160/1

Add NSAXSpy to help debugging OS X accessibility

This tool currently logs the AXAttributedString of the first AXTextArea
found in LibreOffice (suited for Writer), and logs all AXValueChanged
and AXSelectedTextChanged notifications of that AXTextArea and also all
AXFocusedUIElementChange notifications. It can be adjusted to do
the same thing for TextEdit so that one can learn from it
 how stuff should be implemented (TextEdit is basically
*the* reference implementation of AXTextArea).

Any adjustments (e.g. to observe TextEdit instead of LibreOffice)
need to be done in the source code of this tool.

Change-Id: Ia7f4dbed4924b8f5330726d378eda3601991f404
---
M .gitignore
A NSAXSpy/NSAXSpy.xcodeproj/project.pbxproj
A NSAXSpy/NSAXSpy.xcodeproj/project.xcworkspace/contents.xcworkspacedata
A NSAXSpy/NSAXSpy/main.m
4 files changed, 455 insertions(+), 1 deletion(-)



diff --git a/.gitignore b/.gitignore
index 9745f02..765cd01 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,4 @@
 *~
-*.orig
\ No newline at end of file
+*.orig
+NSAXSpy/NSAXSpy.xcodeproj/project.xcworkspace/xcuserdata/
+NSAXSpy/NSAXSpy.xcodeproj/xcuserdata/
diff --git a/NSAXSpy/NSAXSpy.xcodeproj/project.pbxproj b/NSAXSpy/NSAXSpy.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..392335a
--- /dev/null
+++ b/NSAXSpy/NSAXSpy.xcodeproj/project.pbxproj
@@ -0,0 +1,240 @@
+// !$*UTF8*$!
+{
+	archiveVersion = 1;
+	classes = {
+	};
+	objectVersion = 46;
+	objects = {
+
+/* Begin PBXBuildFile section */
+		6B6A880F17A5B49B00182C47 /* CoreFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6B6A880E17A5B49B00182C47 /* CoreFoundation.framework */; };
+		6B6A881217A5B49B00182C47 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 6B6A881117A5B49B00182C47 /* main.m */; };
+		6B6A881B17A5B4ED00182C47 /* ApplicationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6B6A881A17A5B4ED00182C47 /* ApplicationServices.framework */; };
+		6B6A881D17A5B4F600182C47 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6B6A881C17A5B4F600182C47 /* Cocoa.framework */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+		6B6A880917A5B49B00182C47 /* CopyFiles */ = {
+			isa = PBXCopyFilesBuildPhase;
+			buildActionMask = 2147483647;
+			dstPath = /usr/share/man/man1/;
+			dstSubfolderSpec = 0;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 1;
+		};
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+		6B6A880B17A5B49B00182C47 /* NSAXSpy */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = NSAXSpy; sourceTree = BUILT_PRODUCTS_DIR; };
+		6B6A880E17A5B49B00182C47 /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; };
+		6B6A881117A5B49B00182C47 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
+		6B6A881A17A5B4ED00182C47 /* ApplicationServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ApplicationServices.framework; path = System/Library/Frameworks/ApplicationServices.framework; sourceTree = SDKROOT; };
+		6B6A881C17A5B4F600182C47 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+		6B6A880817A5B49B00182C47 /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				6B6A881D17A5B4F600182C47 /* Cocoa.framework in Frameworks */,
+				6B6A881B17A5B4ED00182C47 /* ApplicationServices.framework in Frameworks */,
+				6B6A880F17A5B49B00182C47 /* CoreFoundation.framework in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+		6B6A880217A5B49B00182C47 = {
+			isa = PBXGroup;
+			children = (
+				6B6A881017A5B49B00182C47 /* NSAXSpy */,
+				6B6A880D17A5B49B00182C47 /* Frameworks */,
+				6B6A880C17A5B49B00182C47 /* Products */,
+			);
+			sourceTree = "<group>";
+		};
+		6B6A880C17A5B49B00182C47 /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				6B6A880B17A5B49B00182C47 /* NSAXSpy */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+		6B6A880D17A5B49B00182C47 /* Frameworks */ = {
+			isa = PBXGroup;
+			children = (
+				6B6A881C17A5B4F600182C47 /* Cocoa.framework */,
+				6B6A881A17A5B4ED00182C47 /* ApplicationServices.framework */,
+				6B6A880E17A5B49B00182C47 /* CoreFoundation.framework */,
+			);
+			name = Frameworks;
+			sourceTree = "<group>";
+		};
+		6B6A881017A5B49B00182C47 /* NSAXSpy */ = {
+			isa = PBXGroup;
+			children = (
+				6B6A881117A5B49B00182C47 /* main.m */,
+			);
+			path = NSAXSpy;
+			sourceTree = "<group>";
+		};
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+		6B6A880A17A5B49B00182C47 /* NSAXSpy */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 6B6A881717A5B49B00182C47 /* Build configuration list for PBXNativeTarget "NSAXSpy" */;
+			buildPhases = (
+				6B6A880717A5B49B00182C47 /* Sources */,
+				6B6A880817A5B49B00182C47 /* Frameworks */,
+				6B6A880917A5B49B00182C47 /* CopyFiles */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+			);
+			name = NSAXSpy;
+			productName = NSAXSpy;
+			productReference = 6B6A880B17A5B49B00182C47 /* NSAXSpy */;
+			productType = "com.apple.product-type.tool";
+		};
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+		6B6A880317A5B49B00182C47 /* Project object */ = {
+			isa = PBXProject;
+			attributes = {
+				LastUpgradeCheck = 0460;
+				ORGANIZATIONNAME = LibreOffice;
+			};
+			buildConfigurationList = 6B6A880617A5B49B00182C47 /* Build configuration list for PBXProject "NSAXSpy" */;
+			compatibilityVersion = "Xcode 3.2";
+			developmentRegion = English;
+			hasScannedForEncodings = 0;
+			knownRegions = (
+				en,
+			);
+			mainGroup = 6B6A880217A5B49B00182C47;
+			productRefGroup = 6B6A880C17A5B49B00182C47 /* Products */;
+			projectDirPath = "";
+			projectRoot = "";
+			targets = (
+				6B6A880A17A5B49B00182C47 /* NSAXSpy */,
+			);
+		};
+/* End PBXProject section */
+
+/* Begin PBXSourcesBuildPhase section */
+		6B6A880717A5B49B00182C47 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				6B6A881217A5B49B00182C47 /* main.m in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXSourcesBuildPhase section */
+
+/* Begin XCBuildConfiguration section */
+		6B6A881517A5B49B00182C47 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				ARCHS = "$(ARCHS_STANDARD_64_BIT)";
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				COPY_PHASE_STRIP = NO;
+				GCC_C_LANGUAGE_STANDARD = gnu99;
+				GCC_DYNAMIC_NO_PIC = NO;
+				GCC_ENABLE_OBJC_EXCEPTIONS = YES;
+				GCC_OPTIMIZATION_LEVEL = 0;
+				GCC_PREPROCESSOR_DEFINITIONS = (
+					"DEBUG=1",
+					"$(inherited)",
+				);
+				GCC_SYMBOLS_PRIVATE_EXTERN = NO;
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				MACOSX_DEPLOYMENT_TARGET = 10.8;
+				ONLY_ACTIVE_ARCH = YES;
+				SDKROOT = macosx;
+			};
+			name = Debug;
+		};
+		6B6A881617A5B49B00182C47 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				ARCHS = "$(ARCHS_STANDARD_64_BIT)";
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				COPY_PHASE_STRIP = YES;
+				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+				GCC_C_LANGUAGE_STANDARD = gnu99;
+				GCC_ENABLE_OBJC_EXCEPTIONS = YES;
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				MACOSX_DEPLOYMENT_TARGET = 10.8;
+				SDKROOT = macosx;
+			};
+			name = Release;
+		};
+		6B6A881817A5B49B00182C47 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				PRODUCT_NAME = "$(TARGET_NAME)";
+			};
+			name = Debug;
+		};
+		6B6A881917A5B49B00182C47 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				PRODUCT_NAME = "$(TARGET_NAME)";
+			};
+			name = Release;
+		};
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+		6B6A880617A5B49B00182C47 /* Build configuration list for PBXProject "NSAXSpy" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				6B6A881517A5B49B00182C47 /* Debug */,
+				6B6A881617A5B49B00182C47 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		6B6A881717A5B49B00182C47 /* Build configuration list for PBXNativeTarget "NSAXSpy" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				6B6A881817A5B49B00182C47 /* Debug */,
+				6B6A881917A5B49B00182C47 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+/* End XCConfigurationList section */
+	};
+	rootObject = 6B6A880317A5B49B00182C47 /* Project object */;
+}
diff --git a/NSAXSpy/NSAXSpy.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/NSAXSpy/NSAXSpy.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..7e82a16
--- /dev/null
+++ b/NSAXSpy/NSAXSpy.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+   version = "1.0">
+   <FileRef
+      location = "self:NSAXSpy.xcodeproj">
+   </FileRef>
+</Workspace>
diff --git a/NSAXSpy/NSAXSpy/main.m b/NSAXSpy/NSAXSpy/main.m
new file mode 100644
index 0000000..d504d65
--- /dev/null
+++ b/NSAXSpy/NSAXSpy/main.m
@@ -0,0 +1,205 @@
+//
+//  main.m
+//  AXText
+//
+//  Created by Boris Dušek on 14.10.12.
+//  Copyright (c) 2012 Boris Dušek. All rights reserved.
+//
+//This tool currently logs the AXAttributedString of the first AXTextArea
+//found in LibreOffice (suited for Writer), and logs all AXValueChanged
+//and AXSelectedTextChanged notifications of that AXTextArea and also all
+//AXFocusedUIElementChange notifications. It can be adjusted to do
+//the same thing for TextEdit so that one can learn from it
+// how stuff should be implemented (TextEdit is basically
+//*the* reference implementation of AXTextArea).
+//
+//Any adjustments (e.g. to observe TextEdit instead of LibreOffice)
+//need to be done in the source code of this tool.
+
+#import <ApplicationServices/ApplicationServices.h>
+#import <Cocoa/Cocoa.h>
+
+#define AX_CALL_(function, ...) if (kAXErrorSuccess != (err = function(__VA_ARGS__))) {\
+NSLog(@"Failed AX Call %s with return value %d", #function, err);\
+} else
+#define AX_CALL(type, var, result_type, function, ...) \
+type var;\
+AX_CALL_(function, __VA_ARGS__, ((result_type *)&var))
+#define AX_VALUE(element, type, var, attr) AX_CALL(type, var, CFTypeRef, AXUIElementCopyAttributeValue, (element), kAX##attr##Attribute)
+#define AX_PVALUE(element, type, var, attr, parameter) AX_CALL(type, var, CFTypeRef, AXUIElementCopyParameterizedAttributeValue, element, kAX##attr##ParameterizedAttribute, parameter)
+#define AX_CHILD(element, role, title, child) AX_CALL(AXUIElementRef, (child), AXUIElementRef, findChildOfRoleAndTitle, (element), kAX##role##Role, (title))
+#define AX_APPLICATION(appname, appvar, callback, observer) AX_CALL(AXUIElementRef, appvar, AXUIElementRef, findApplication, appname, callback, observer)
+#define AX_OBSERVE(observer, element, notification) AX_CALL_(AXObserverAddNotification, observer, element, kAX##notification##Notification, NULL)
+#define AX_PARENT(child, parent) AX_VALUE(child, AXUIElementRef, parent, Parent)
+
+static AXError findChildOfRoleAndTitle(AXUIElementRef element, CFStringRef role, NSString* title, AXUIElementRef *child)
+{
+    AXError err = kAXErrorSuccess;
+    AX_VALUE(element, CFArrayRef, children, Children) {
+        for (size_t i = 0; i < CFArrayGetCount(children); ++i) {
+            AXUIElementRef candidate = CFArrayGetValueAtIndex(children, i);
+            AX_VALUE(candidate, CFStringRef, actualRole, Role) {
+                if (CFEqual(role, actualRole)) {
+                    if (title) {
+                        AX_VALUE(candidate, CFStringRef, actualTitle, Title) {
+                            if (CFEqual((__bridge CFStringRef)title, actualTitle)) {
+                                *child = candidate;
+                                return err;
+                            }
+                        }
+                    } else {
+                        *child = candidate;
+                        return err;
+                    }
+                }
+            }
+        }
+    }
+    err = kAXErrorFailure;
+    return err;
+}
+
+static AXError findApplication(NSString *title, AXObserverCallback callback, AXObserverRef *observer, AXUIElementRef *application) {
+    AXError err = kAXErrorSuccess;
+    for (NSRunningApplication *app in [[NSWorkspace sharedWorkspace] runningApplications]) {
+        if ([app.localizedName isEqualToString:title]) {
+            *application = AXUIElementCreateApplication([app processIdentifier]);
+            err = (*application) ? kAXErrorSuccess: kAXErrorFailure;
+            if ((kAXErrorSuccess == err) && observer) {
+                AX_CALL_(AXObserverCreate, [app processIdentifier], callback, observer) {
+                    AX_OBSERVE(*observer, *application, FocusedUIElementChanged) {
+                    }
+                }
+            }
+            break;
+        }
+        err = kAXErrorFailure;
+    }
+    return err;
+}
+
+
+
+static void axLoggingObserverCallback(AXObserverRef observer, AXUIElementRef element, CFStringRef notification, void *refcon) {
+    AXError err = kAXErrorSuccess;
+    AX_VALUE(element, CFStringRef, role, Role) {
+        NSLog(@"%@ (%@): %@", element, role, notification);
+    }
+}
+
+
+
+static AXError findTextEditTextComponent(AXObserverCallback callback, AXObserverRef *observer, AXUIElementRef *component) {
+    AXError err = kAXErrorSuccess;
+    AX_APPLICATION(@"TextEdit", TextEdit, callback, observer) {
+        AX_CHILD(TextEdit, Window, 0, window) {
+            AX_CHILD(window, ScrollArea, 0, scrollArea) {
+                AX_CHILD(scrollArea, TextArea, 0, textArea) {
+                    *component = textArea;
+                }
+            }
+        }
+    }
+    return err;
+}
+
+static AXError findTextMateTextComponent(AXObserverCallback callback, AXObserverRef *observer, AXUIElementRef *component) {
+    AXError err = kAXErrorSuccess;
+    AX_APPLICATION(@"TextMate", TextMate, callback, observer) {
+        AX_CHILD(TextMate, Window, 0, window) {
+            AX_CHILD(window, ScrollArea, 0, scrollArea) {
+                AX_CHILD(scrollArea, TextArea, 0, textArea) {
+                    *component = textArea;
+                }
+            }
+        }
+    }
+    return err;
+}
+
+static AXError findLibreOfficeTextComponent(AXObserverCallback callback, AXObserverRef *observer, AXUIElementRef *component) {
+    AXError err = kAXErrorSuccess;
+    AX_APPLICATION(@"LibreOffice", LibreOffice, callback, observer) {
+        AX_CHILD(LibreOffice, Window, 0, window) {
+            AX_CHILD(window, ScrollArea, 0, scrollArea) {
+                AX_CHILD(scrollArea, Group, 0, group) {
+                    AX_CHILD(group, TextArea, 0, textArea) {
+                        *component = textArea;
+                    }
+                }
+            }
+        }
+    }
+    return err;
+}
+
+static AXError reportOnAXTextArea(AXUIElementRef textArea) {
+    AXError err = kAXErrorSuccess;
+    AX_VALUE(textArea, CFNumberRef, length, NumberOfCharacters) {
+        CFRange all = CFRangeMake(0, 0);
+        CFNumberGetValue(length, kCFNumberCFIndexType, &all.length);
+        AXValueRef allValue = AXValueCreate(kAXValueCFRangeType, &all);
+        AX_PVALUE(textArea, CFAttributedStringRef, attrString, AttributedStringForRange, allValue) {
+            NSLog(@"Attributed String = \"%@\"", attrString);
+        }
+    }
+    return err;
+}
+
+static AXError reportOnAXTextArea_Bounds(AXUIElementRef textArea) {
+    AXError err = kAXErrorSuccess;
+    CFRange all = CFRangeMake(30, 6);
+    AXValueRef allValue = AXValueCreate(kAXValueCFRangeType, &all);
+    AX_PVALUE(textArea, CFAttributedStringRef, attrString, AttributedStringForRange, allValue) {
+        NSLog(@"Attributed String = \"%@\"", attrString);
+        AX_PVALUE(textArea, CFAttributedStringRef, bounds, BoundsForRange, allValue) {
+            NSLog(@"Bounds = %@", bounds);
+        }
+    }
+    return err;
+}
+
+static AXError registerTextNotifications(AXObserverRef observer, AXUIElementRef element) {
+    AXError err = kAXErrorSuccess;
+    AX_OBSERVE(observer, element, SelectedTextChanged) {
+        AX_OBSERVE(observer, element, ValueChanged) {
+        }
+    }
+    return err;
+}
+
+static AXError registerTextNotificationsCascade(AXObserverRef observer, AXUIElementRef element) {
+    AXError err = kAXErrorSuccess;
+    while (kAXErrorSuccess == err) {
+        AX_CALL_(registerTextNotifications, observer, element) {
+            AX_VALUE(element, CFStringRef, role, Role) {
+                if (CFEqual(kAXApplicationRole, role)) {
+                    break;
+                }
+                AX_PARENT(element, elementParent) {
+                    element = elementParent;
+                }
+            }
+        }
+    }
+    return err;
+}
+
+
+int main(int argc, char **argv)
+{
+    AXError err;
+    AXUIElementRef textArea;
+    AXObserverRef observer;
+    AX_CALL_(findLibreOfficeTextComponent/*findTextEditTextComponent*/, axLoggingObserverCallback, &observer, &textArea) {
+        AX_CALL_(reportOnAXTextArea, textArea) {
+            AX_CALL_(registerTextNotifications, observer, textArea) {
+                CFRunLoopSourceRef axNotificationSource = AXObserverGetRunLoopSource(observer);
+                CFRunLoopRef runLoop = CFRunLoopGetMain();
+                CFRunLoopAddSource(runLoop, axNotificationSource, kCFRunLoopDefaultMode);
+                CFRunLoopRun();
+            }
+        }
+    }
+    return kAXErrorSuccess == err;
+}
\ No newline at end of file

-- 
To view, visit https://gerrit.libreoffice.org/5160
To unsubscribe, visit https://gerrit.libreoffice.org/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: Ia7f4dbed4924b8f5330726d378eda3601991f404
Gerrit-PatchSet: 1
Gerrit-Project: dev-tools
Gerrit-Branch: master
Gerrit-Owner: Boris Dušek <me at dusek.me>



More information about the LibreOffice mailing list