From e9fbeea5f8a7ae295ac995053fe48d9889dafed3 Mon Sep 17 00:00:00 2001 From: Davee Nguyen Date: Tue, 19 Mar 2019 19:32:30 -0700 Subject: [PATCH] refactor(a11y): reorder class methods --- atomacos/AXClasses.py | 6 +- atomacos/a11y.py | 333 ++++++++++++++++++++---------------------- 2 files changed, 163 insertions(+), 176 deletions(-) diff --git a/atomacos/AXClasses.py b/atomacos/AXClasses.py index b98d2fa..354c233 100755 --- a/atomacos/AXClasses.py +++ b/atomacos/AXClasses.py @@ -103,7 +103,7 @@ def launchAppByBundleId(bundleID): # NSWorkspaceLaunchAllowingClassicStartup does nothing on any # modern system that doesn't have the classic environment installed. # Encountered a bug when passing 0 for no options on 10.6 PyObjC. - NativeUIElement.launch_app_by_bundle_id(bundleID) + a11y.launch_app_by_bundle_id(bundleID) @staticmethod def launchAppByBundlePath(bundlePath, arguments=None): @@ -111,7 +111,7 @@ def launchAppByBundlePath(bundlePath, arguments=None): Return True if succeed. """ - return NativeUIElement.launch_app_by_bundle_path(bundlePath, arguments) + return a11y.launch_app_by_bundle_path(bundlePath, arguments) @staticmethod def terminateAppByBundleId(bundleID): @@ -120,7 +120,7 @@ def terminateAppByBundleId(bundleID): Return True if succeed. """ - return NativeUIElement.terminate_app_by_bundle_id(bundleID) + return a11y.terminate_app_by_bundle_id(bundleID) @classmethod def set_systemwide_timeout(cls, timeout=0.0): diff --git a/atomacos/a11y.py b/atomacos/a11y.py index e12f024..b97a0b5 100644 --- a/atomacos/a11y.py +++ b/atomacos/a11y.py @@ -41,7 +41,6 @@ def __init__(self, ref=None): self.converter = converter.Converter(self.__class__) def __repr__(self): - """Build a descriptive string for UIElements.""" c = repr(self.__class__).partition("")[0] _attributes = self.ax_attributes @@ -63,6 +62,20 @@ def __repr__(self): return "<%s %s %s>" % (c, role, title) + def __eq__(self, other): + if not isinstance(other, type(self)): + return False + if self.ref is None and other.ref is None: + return True + + if self.ref is None or other.ref is None: + return False + + return CFEqual(self.ref, other.ref) + + def __ne__(self, other): + return not self.__eq__(other) + def __getattr__(self, item): if item in self.ax_attributes: return self._get_ax_attribute(item) @@ -95,98 +108,6 @@ def __dir__(self): + dir(super(AXUIElement, self)) # not working in python 2 ) - def _get_ax_attribute(self, item): - """Get the value of the the specified attribute""" - if item in self.ax_attributes: - try: - attr_value = PAXUIElementCopyAttributeValue(self.ref, item) - return self.converter.convert_value(attr_value) - except AXErrorNoValue: - if item == "AXChildren": - return [] - return None - - raise AttributeError("'%s' object has no attribute '%s'" % (type(self), item)) - - def _set_ax_attribute(self, name, value): - """ - Set the specified attribute to the specified value - """ - settable = PAXUIElementIsAttributeSettable(self.ref, name) - - if not settable: - raise AXErrorUnsupported("Attribute is not settable") - - PAXUIElementSetAttributeValue(self.ref, name, value) - - def _activate(self): - """Activate the application (bringing menus and windows forward).""" - ra = AppKit.NSRunningApplication - app = ra.runningApplicationWithProcessIdentifier_(self.pid) - # NSApplicationActivateAllWindows | NSApplicationActivateIgnoringOtherApps - # == 3 - PyObjC in 10.6 does not expose these constants though so I have - # to use the int instead of the symbolic names - app.activateWithOptions_(3) - - @property - def ax_attributes(self): - """ - Get a list of attributes available on the AXUIElement - """ - try: - names = PAXUIElementCopyAttributeNames(self.ref) - return list(names) - except AXError: - return [] - - @property - def ax_actions(self): - """ - Get a list of actions available on the AXUIElement - """ - try: - names = PAXUIElementCopyActionNames(self.ref) - return list(names) - except AXError: - return [] - - def _perform_ax_actions(self, name): - PAXUIElementPerformAction(self.ref, name) - - @property - def bundle_id(self): - """Return the bundle ID of the application.""" - ra = AppKit.NSRunningApplication - app = ra.runningApplicationWithProcessIdentifier_(self.pid) - return app.bundleIdentifier() - - @property - def pid(self): - pid = PAXUIElementGetPid(self.ref) - return pid - - @classmethod - def from_pid(cls, pid): - """ - Get an AXUIElement reference to the application by specified PID. - """ - app_ref = AXUIElementCreateApplication(pid) - - if app_ref is None: - raise AXErrorUnsupported("Error getting app ref") - - return cls(ref=app_ref) - - @classmethod - def systemwide(cls): - """Get an AXUIElement reference for the system accessibility object.""" - app_ref = AXUIElementCreateSystemWide() - - if app_ref is None: - raise AXErrorUnsupported("Error getting a11y object") - - return cls(ref=app_ref) - @classmethod def from_bundle_id(cls, bundle_id): """ @@ -220,14 +141,24 @@ def from_localized_name(cls, name): raise ValueError("Specified application not found in running apps.") @classmethod - def frontmost(cls): - """Get the current frontmost application. + def from_pid(cls, pid): + """ + Creates an instance with the AXUIElementRef for the application with + the specified process ID. + """ + app_ref = AXUIElementCreateApplication(pid) + + if app_ref is None: + raise AXErrorUnsupported("Error getting app ref") + + return cls(ref=app_ref) - Raise a ValueError exception if no GUI applications are found. + @classmethod + def frontmost(cls): """ - # Refresh the runningApplications list - apps = get_running_apps() - for app in apps: + Creates an instance with the AXUIElementRef for the frontmost application. + """ + for app in get_running_apps(): pid = app.processIdentifier() ref = cls.from_pid(pid) try: @@ -248,33 +179,61 @@ def frontmost(cls): raise ValueError("No GUI application found.") @classmethod - def with_window(cls): - """Get a random app that has windows. + def systemwide(cls): + """ + Creates an instance with the AXUIElementRef for the system-wide + accessibility object. + """ + app_ref = AXUIElementCreateSystemWide() + + if app_ref is None: + raise AXErrorUnsupported("Error getting a11y object") - Raise a ValueError exception if no GUI applications are found. + return cls(ref=app_ref) + + @classmethod + def with_window(cls): """ - # Refresh the runningApplications list - apps = get_running_apps() - for app in apps: + Creates an instance with the AXUIElementRef for a random application + that has windows. + """ + for app in get_running_apps(): pid = app.processIdentifier() ref = cls.from_pid(pid) if hasattr(ref, "windows") and len(ref.windows()) > 0: return ref raise ValueError("No GUI application found.") - def __eq__(self, other): - if not isinstance(other, type(self)): - return False - if self.ref is None and other.ref is None: - return True + @property + def ax_actions(self): + """Gets the list of actions available on the AXUIElement""" + try: + names = PAXUIElementCopyActionNames(self.ref) + return list(names) + except AXError: + return [] - if self.ref is None or other.ref is None: - return False + @property + def ax_attributes(self): + """Gets the list of attributes available on the AXUIElement""" + try: + names = PAXUIElementCopyAttributeNames(self.ref) + return list(names) + except AXError: + return [] - return CFEqual(self.ref, other.ref) + @property + def bundle_id(self): + """Gets the AXUIElement's bundle identifier""" + ra = AppKit.NSRunningApplication + app = ra.runningApplicationWithProcessIdentifier_(self.pid) + return app.bundleIdentifier() - def __ne__(self, other): - return not self.__eq__(other) + @property + def pid(self): + """Gets the AXUIElement's process ID""" + pid = PAXUIElementGetPid(self.ref) + return pid def get_element_at_position(self, x, y): if self.ref is None: @@ -289,60 +248,13 @@ def get_element_at_position(self, x, y): return self.__class__(element) - @staticmethod - def launch_app_by_bundle_id(bundle_id): - """Launch the application with the specified bundle ID""" - # NSWorkspaceLaunchAllowingClassicStartup does nothing on any - # modern system that doesn't have the classic environment installed. - # Encountered a bug when passing 0 for no options on 10.6 PyObjC. - ws = AppKit.NSWorkspace.sharedWorkspace() - # Sorry about the length of the following line - r = ws.launchAppWithBundleIdentifier_options_additionalEventParamDescriptor_launchIdentifier_( # noqa: B950 - bundle_id, - AppKit.NSWorkspaceLaunchAllowingClassicStartup, - AppKit.NSAppleEventDescriptor.nullDescriptor(), - None, - ) - # On 10.6, this returns a tuple - first element bool result, second is - # a number. Let's use the bool result. - if not r[0]: - raise RuntimeError("Error launching specified application. %s" % str(r)) - - @staticmethod - def launch_app_by_bundle_path(bundle_path, arguments=None): - """Launch app with a given bundle path. - - Return True if succeed. + def set_timeout(self, timeout): """ - if arguments is None: - arguments = [] - - bundleUrl = AppKit.NSURL.fileURLWithPath_(bundle_path) - workspace = AppKit.NSWorkspace.sharedWorkspace() - configuration = {AppKit.NSWorkspaceLaunchConfigurationArguments: arguments} - - return workspace.launchApplicationAtURL_options_configuration_error_( - bundleUrl, - AppKit.NSWorkspaceLaunchAllowingClassicStartup, - configuration, - None, - ) - - @staticmethod - def terminate_app_by_bundle_id(bundle_id): - """Terminate app with a given bundle ID. - Requires 10.6. + Sets the timeout value used in the accessibility API - Return True if succeed. + Args: + timeout: timeout in seconds """ - ra = AppKit.NSRunningApplication - appList = ra.runningApplicationsWithBundleIdentifier_(bundle_id) - if appList: - app = appList[0] - return app and app.terminate() - return False - - def set_timeout(self, timeout): if self.ref is None: raise AXErrorUnsupported( "Operation not supported on null element references" @@ -353,24 +265,99 @@ def set_timeout(self, timeout): except AXErrorIllegalArgument: raise ValueError("Accessibility timeout values must be non-negative") + def _activate(self): + """Activates the application (bringing menus and windows forward)""" + ra = AppKit.NSRunningApplication + app = ra.runningApplicationWithProcessIdentifier_(self.pid) + # NSApplicationActivateAllWindows | NSApplicationActivateIgnoringOtherApps + # == 3 - PyObjC in 10.6 does not expose these constants though so I have + # to use the int instead of the symbolic names + app.activateWithOptions_(3) -def get_frontmost_pid(): - """Return the PID of the application in the foreground.""" - frontmost_app = NSWorkspace.sharedWorkspace().frontmostApplication() - pid = frontmost_app.processIdentifier() - return pid + def _get_ax_attribute(self, item): + """Gets the value of the the specified attribute""" + if item in self.ax_attributes: + try: + attr_value = PAXUIElementCopyAttributeValue(self.ref, item) + return self.converter.convert_value(attr_value) + except AXErrorNoValue: + if item == "AXChildren": + return [] + return None + + raise AttributeError("'%s' object has no attribute '%s'" % (type(self), item)) + + def _set_ax_attribute(self, name, value): + """Sets the specified attribute to the specified value""" + settable = PAXUIElementIsAttributeSettable(self.ref, name) + + if not settable: + raise AXErrorUnsupported("Attribute is not settable") + + PAXUIElementSetAttributeValue(self.ref, name, value) + + def _perform_ax_actions(self, name): + """Performs specified action on the AXUIElementRef""" + PAXUIElementPerformAction(self.ref, name) def axenabled(): - """Return the status of accessibility on the system.""" + """Return the status of accessibility on the system""" return AXIsProcessTrusted() +def get_frontmost_pid(): + """Return the process ID of the application in the foreground""" + frontmost_app = NSWorkspace.sharedWorkspace().frontmostApplication() + pid = frontmost_app.processIdentifier() + return pid + + def get_running_apps(): - """Get a list of the running applications.""" + """Get a list of the running applications""" AppHelper.callLater(1, AppHelper.stopEventLoop) AppHelper.runConsoleEventLoop() # Get a list of running applications ws = AppKit.NSWorkspace.sharedWorkspace() apps = ws.runningApplications() return apps + + +def launch_app_by_bundle_id(bundle_id): + # NSWorkspaceLaunchAllowingClassicStartup does nothing on any + # modern system that doesn't have the classic environment installed. + # Encountered a bug when passing 0 for no options on 10.6 PyObjC. + ws = AppKit.NSWorkspace.sharedWorkspace() + + r = ws.launchAppWithBundleIdentifier_options_additionalEventParamDescriptor_launchIdentifier_( # noqa: B950 + bundle_id, + AppKit.NSWorkspaceLaunchAllowingClassicStartup, + AppKit.NSAppleEventDescriptor.nullDescriptor(), + None, + ) + # On 10.6, this returns a tuple - first element bool result, second is + # a number. Let's use the bool result. + if not r[0]: + raise RuntimeError("Error launching specified application. %s" % str(r)) + + +def launch_app_by_bundle_path(bundle_path, arguments=None): + if arguments is None: + arguments = [] + + bundleUrl = AppKit.NSURL.fileURLWithPath_(bundle_path) + workspace = AppKit.NSWorkspace.sharedWorkspace() + configuration = {AppKit.NSWorkspaceLaunchConfigurationArguments: arguments} + + return workspace.launchApplicationAtURL_options_configuration_error_( + bundleUrl, AppKit.NSWorkspaceLaunchAllowingClassicStartup, configuration, None + ) + + +def terminate_app_by_bundle_id(bundle_id): + ra = AppKit.NSRunningApplication + appList = ra.runningApplicationsWithBundleIdentifier_(bundle_id) + if appList: + app = appList[0] + return app and app.terminate() + return False