# -*- coding: utf-8 -*- # Copyright © 2012-2017 marmuta # # This file is part of Onboard. # # Onboard is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # Onboard is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ GTK specific keyboard class """ from __future__ import division, print_function, unicode_literals import time import logging _logger = logging.getLogger(__name__) from Onboard.Version import require_gi_versions require_gi_versions() try: from gi.repository import Atspi except ImportError as e: _logger.warning("Atspi typelib missing, auto-show unavailable") from Onboard.utils import Rect, EventSource, Process, unicode_str from Onboard.Timer import Timer # Config Singleton from Onboard.Config import Config config = Config() class CachedAccessible: def __init__(self, accessible): self._accessible = accessible self._state = {} # cache of various accessible properties # Use "==" for object identity tests instead of "is". def __eq__(self, other): return other is not None and self._accessible is other._accessible def __ne__(self, other): return other is None or self._accessible is not other._accessible def get_state(self): """ All cached state of the accessible """ return self._state def get_all_state(self): """ Return _state filled with all kinds of properties, for easy printint as debug output in TextContext. """ self.get_role() self.get_role_name() self.get_name() self.get_state_set() self.get_id() self.get_attributes() self.get_interfaces() self.get_description() self.get_pid() self.get_process_name() self.get_toolkit_name() self.get_toolkit_version() self.get_editable_text_iface() self.get_editable_text_iface() self.get_app_name() self.get_app_description() self.get_extents() self.get_frame() self.get_frame_extents() self.is_urlbar() self.is_byobu() return self._state # ### Cached, exception-safe accessor functions ### def get_role(self): return self._get_value("role", self._accessible.get_role) def get_role_name(self): return self._get_value("role-name", self._accessible.get_role_name) def get_name(self): return self._get_value("name", self._accessible.get_name) def invalidate_state_set(self): self.invalidate("state-set") def get_state_set(self): return self._get_value("state-set", self._accessible.get_state_set) def get_id(self): return self._get_value("id", self._accessible.get_id) def get_attributes(self): return self._get_value("attributes", self._accessible.get_attributes, {}) def get_interfaces(self): return self._get_value("interfaces", self._accessible.get_interfaces, []) def get_description(self): return self._get_value("description", self._accessible.get_description) def get_pid(self): return self._get_value("pid", self._accessible.get_process_id) def get_process_name(self): pid = self.get_pid() if pid != -1: return self._get_value_noex( "process-name", lambda : Process.get_process_name(pid)) return None def get_toolkit_name(self): return self._get_value("toolkit-name", self._accessible.get_toolkit_name) def get_toolkit_version(self): return self._get_value("toolkit-version", self._accessible.get_toolkit_version) def get_editable_text_iface(self): return self._get_value("editable-text-iface", self._accessible.get_editable_text_iface) def get_app_name(self): def func(): app = self._accessible.get_application() return app.get_name() return self._get_value("app-name", func, "") def get_app_description(self): def func(): app = self._accessible.get_application() return app.get_description() return self._get_value("app-description", func, "") def invalidate_extents(self): self.invalidate("extents") def get_extents(self): """ Screen rect after scaling. """ scale = config.window_scaling_factor if scale != 1.0: # Only Gtk-3 widgets return scaled coordinates, all others, # including Gtk-2 apps like firefox, clawsmail and Qt-apps, # apparently don't. if self.is_toolkit_gtk3(): scale = 1.0 else: scale = 1.0 / config.window_scaling_factor def func(): ext = self._accessible.get_extents(Atspi.CoordType.SCREEN) return Rect(ext.x * scale, ext.y * scale, ext.width * scale, ext.height * scale) return self._get_value("extents", func, Rect()) def get_frame(self): def func(): frame = self._get_accessible_frame(self._accessible) if frame: return CachedAccessible(frame) return None return self._get_value_noex("frame", func) def get_frame_extents(self): def func(): frame = self.get_frame() if frame: return frame.get_extents() return Rect() return self._get_value_noex("frame_extents", func) @staticmethod def _get_accessible_frame(accessible): """ Accessible of the top level window to which accessible belongs. """ frame = None _logger.atspi("_get_accessible_frame(): searching for top level:") try: parent = accessible while True: parent = parent.get_parent() if not parent: break role = parent.get_role() _logger.atspi("parent: {}".format(role)) if role == Atspi.Role.FRAME or \ role == Atspi.Role.DIALOG or \ role == Atspi.Role.WINDOW or \ role == Atspi.Role.NOTIFICATION: frame = parent break # private exception gi._glib.GError when # right clicking onboards unity2d launcher (Precise) except Exception as ex: _logger.atspi("Invalid accessible," " failed to get top level accessible: " + unicode_str(ex)) return frame def is_urlbar(self): """ Is this a (most likely firefox') URL bar? """ def func(): attributes = self.get_attributes() return bool(attributes and "urlbar" in attributes.get("class", "")) return self._get_value_noex("is_urlbar", func) def is_byobu(self): """ Is this possibly byobu running in a terminal? """ def func(): description = self.get_description() return bool(description and "byobu" in description.lower()) return self._get_value_noex("is_byobu", func) def _get_value(self, name, func, default=None): """ Return cached return value of func(). """ value = self._state.get(name) if value is None: try: value = func() except Exception as ex: # private exception gi._glib.GError _logger.info("CachedAccessible._get_value({}): " "invalid accessible, failed to read state: " .format(name) + unicode_str(ex)) value = default self._state[name] = value return value def _get_value_noex(self, name, func): """ Return cached return value of func(). """ value = self._state.get(name) if value is None: value = func() self._state[name] = value return value def invalidate(self, name): """ Force re-reading property from the accessible. May cause a D-Bus round-trip on the next read-attempt. """ try: del self._state[name] except KeyError: pass # ### uncached, but still exception safe functions ### def get_selection(self, selection_num=0): selection = None try: sel = self._accessible.get_selection(selection_num) # Gtk-2 applications return 0,0 when there is no selection. # Gtk-3 applications return caret positions in that case. # LibreOffice Writer in Vivid initially returns -1,-1 when there # is no selection, later the caret position. start = sel.start_offset end = sel.end_offset if start > 0 and \ end > 0 and \ start <= end: selection = (sel.start_offset, sel.end_offset) except Exception as ex: # Private exception gi._glib.GErro _logger.info("CachedAccessible.get_selection(): " + unicode_str(ex)) return selection def set_caret_offset(self, offset): try: self._accessible.set_caret_offset(offset) except Exception as ex: # Private exception gi._glib.GErro _logger.info("CachedAccessible.set_caret_offset(): " + unicode_str(ex)) def insert_text(self, position, text): try: return self._accessible.insert_text(position, text, -1) except Exception as ex: # Private exception gi._glib.GErro _logger.info("CachedAccessible.insert_text(): " + unicode_str(ex)) return False def delete_text(self, start_pos, end_pos): try: return self._accessible.delete_text(start_pos, end_pos) except Exception as ex: # Private exception gi._glib.GErro _logger.info("CachedAccessible.delete_text(): " + unicode_str(ex)) return False # ### uncached, raising exceptions ### def get_caret_offset(self): try: offset = self._accessible.get_caret_offset() except Exception as ex: # Private exception gi._glib.GErro _logger.info("CachedAccessible.get_caret_offset(): " + unicode_str(ex)) raise ex return offset def get_character_count(self): try: count = self._accessible.get_character_count() except Exception as ex: # Private exception gi._glib.GErro _logger.info("CachedAccessible.get_character_count(): " + unicode_str(ex)) raise ex return count def get_text_at_offset(self, offset, boundary_type): try: text = self._accessible.get_text_at_offset(offset, boundary_type) except Exception as ex: # Private exception gi._glib.GErro _logger.info("CachedAccessible.get_text_at_offset(): " + unicode_str(ex)) raise ex return text def get_text_before_offset(self, offset, boundary_type): try: text = self._accessible.get_text_before_offset(offset, boundary_type) except Exception as ex: # Private exception gi._glib.GErro _logger.info("CachedAccessible.get_text_before_offset(): " + unicode_str(ex)) raise ex return text def get_text(self, begin, end): """ Text of the given accessible, no caching """ try: text = Atspi.Text.get_text(self._accessible, begin, end) # private exception gi._glib.GError: timeout from dbind # with web search in firefox. except Exception as ex: _logger.atspi("CachedAccessible.get_text(): " + unicode_str(ex)) raise ex return text # ### Higher level functions ### def is_focused(self, invalidate=False): if invalidate: # re-read properties? self.invalidate_state_set() state_set = self.get_state_set() if state_set is not None: return state_set.contains(Atspi.StateType.FOCUSED) return False def is_editable(self): """ Is this an accessible onboard should be shown for? """ role = self.get_role() state_set = self.get_state_set() if state_set is not None: if role in [Atspi.Role.TEXT, Atspi.Role.TERMINAL, Atspi.Role.DATE_EDITOR, Atspi.Role.PASSWORD_TEXT, Atspi.Role.EDITBAR, Atspi.Role.ENTRY, Atspi.Role.DOCUMENT_TEXT, Atspi.Role.DOCUMENT_FRAME, Atspi.Role.DOCUMENT_EMAIL, Atspi.Role.SPIN_BUTTON, Atspi.Role.COMBO_BOX, Atspi.Role.DATE_EDITOR, Atspi.Role.PARAGRAPH, # LibreOffice Writer Atspi.Role.HEADER, Atspi.Role.FOOTER, ]: if role in [Atspi.Role.TERMINAL] or \ (state_set is not None and state_set.contains(Atspi.StateType.EDITABLE)): return True return False def is_not_focus_stealing(self): """ Is this accessible unlikely to steal the focus from a previously focused editable accessible? """ role = self.get_role() state_set = self.get_state_set() if state_set is not None: # Mainly firefox elements after the workaround # for firefox 50. if role in [Atspi.Role.DOCUMENT_FRAME, Atspi.Role.LINK, ] \ and state_set is not None and \ not state_set.contains(Atspi.StateType.EDITABLE): return True return False def is_single_line(self): """ Is accessible a single line text entry? """ state_set = self.get_state_set() return state_set and state_set.contains(Atspi.StateType.SINGLE_LINE) def is_toolkit_gtk3(self): """ Are the accessible attributes from a gtk3 widget? """ attributes = self.get_attributes() return attributes and \ "toolkit" in attributes and attributes["toolkit"] == "gtk" def get_character_extents(self, accessible, offset): """ Screen rect of the character at offset """ try: rect = self._get_character_extents(offset) except Exception as ex: # private exception gi._glib.GError when # right clicking onboards unity2d launcher (Precise) _logger.atspi("Invalid accessible," " failed to get character extents: " + unicode_str(ex)) rect = Rect() return rect def _get_character_extents(self, offset): """ Screen rect of the character at offset of the accessible, little caching and exception handling. """ scale = config.window_scaling_factor if scale != 1.0: # Only Gtk-3 widgets return scaled coordinates, all others, # including Gtk-2 apps like firefox, clawsmail and Qt-apps, # apparently don't. if self.is_toolkit_gtk3(): scale = 1.0 else: scale = 1.0 / config.window_scaling_factor ext = self._accessible.get_character_extents(offset, Atspi.CoordType.SCREEN) # x, y = ext.x + ext.width / 2, ext.y + ext.height / 2 # offset_control = self._accessible.get_offset_at_point(x, y, # Atspi.CoordType.SCREEN) # print(offset, offset_control) return Rect(ext.x * scale, ext.y * scale, ext.width * scale, ext.height * scale) class AsyncEvent: """ Decouple AT-SPI events from D-Bus calls to reduce the risk for deadlocks. """ def __init__(self, **kwargs): self.__dict__.update(kwargs) self._kwargs = kwargs def __repr__(self): return type(self).__name__ + "(" + \ ", ".join(str(key) + "=" + repr(val) for key, val in self._kwargs.items()) \ + ")" class AtspiStateTracker(EventSource): """ Keeps track of the currently active accessible by listening to AT-SPI focus events. """ _focus_event_names = ("text-entry-activated",) _text_event_names = ("text-changed", "text-caret-moved") _key_stroke_event_names = ("key-pressed",) _async_event_names = ("async-focus-changed", "async-text-changed", "async-text-caret-moved") _event_names = (_async_event_names + _focus_event_names + _text_event_names + _key_stroke_event_names) _focus_listeners_registered = False _keystroke_listeners_registered = False _text_listeners_registered = False _keystroke_listener = None # asynchronously accessible members _focused_accessible = None # last focused editable accessible _focused_pid = None # pid of last focused editable accessible _active_accessible = None # currently active editable accessible _active_accessible_activation_time = 0.0 # time since focus received _last_active_accessible = None _poll_unity_timer = Timer() def __new__(cls, *args, **kwargs): """ Singleton magic. """ if not hasattr(cls, "self"): cls.self = object.__new__(cls, *args, **kwargs) cls.self.construct() return cls.self def __init__(self): """ Called multiple times, don't use this. """ pass def construct(self): """ Singleton constructor, runs only once. """ EventSource.__init__(self, self._event_names) self._frozen = False def cleanup(self): EventSource.cleanup(self) self._register_atspi_listeners(False) def connect(self, event_name, callback): EventSource.connect(self, event_name, callback) self._update_listeners() def disconnect(self, event_name, callback): had_listeners = self.has_listeners(self._event_names) EventSource.disconnect(self, event_name, callback) self._update_listeners() # help debugging disconnecting events on exit if had_listeners and not self.has_listeners(self._event_names): _logger.info("all listeners disconnected") def _update_listeners(self): register = self.has_listeners(self._focus_event_names) self._register_atspi_focus_listeners(register) register = self.has_listeners(self._text_event_names) self._register_atspi_text_listeners(register) register = self.has_listeners(self._key_stroke_event_names) self._register_atspi_keystroke_listeners(register) def _register_atspi_listeners(self, register): self._register_atspi_focus_listeners(register) self._register_atspi_text_listeners(register) self._register_atspi_keystroke_listeners(register) def _register_atspi_focus_listeners(self, register): if "Atspi" not in globals(): return if self._focus_listeners_registered != register: if register: self.atspi_connect("_listener_focus", "focus", self._on_atspi_global_focus) self.atspi_connect("_listener_object_focus", "object:state-changed:focused", self._on_atspi_object_focus) # private asynchronous events for name in self._async_event_names: handler = "_on_" + name.replace("-", "_") EventSource.connect(self, name, getattr(self, handler)) else: self._poll_unity_timer.stop() self.atspi_disconnect("_listener_focus", "focus") self.atspi_disconnect("_listener_object_focus", "object:state-changed:focused") for name in self._async_event_names: handler = "_on_" + name.replace("-", "_") EventSource.disconnect(self, name, getattr(self, handler)) self._focus_listeners_registered = register def _register_atspi_text_listeners(self, register): if "Atspi" not in globals(): return if self._text_listeners_registered != register: if register: self.atspi_connect("_listener_text_changed", "object:text-changed:insert", self._on_atspi_text_changed) self.atspi_connect("_listener_text_changed", "object:text-changed:delete", self._on_atspi_text_changed) self.atspi_connect("_listener_text_caret_moved", "object:text-caret-moved", self._on_atspi_text_caret_moved) else: self.atspi_disconnect("_listener_text_changed", "object:text-changed:insert") self.atspi_disconnect("_listener_text_changed", "object:text-changed:delete") self.atspi_disconnect("_listener_text_caret_moved", "object:text-caret-moved") self._text_listeners_registered = register def _register_atspi_keystroke_listeners(self, register): if "Atspi" not in globals(): return if self._keystroke_listeners_registered != register: modifier_masks = range(16) if register: if not self._keystroke_listener: self._keystroke_listener = \ Atspi.DeviceListener.new(self._on_atspi_keystroke, None) for modifier_mask in modifier_masks: Atspi.register_keystroke_listener( self._keystroke_listener, None, # key set, None=all modifier_mask, Atspi.KeyEventType.PRESSED, Atspi.KeyListenerSyncType.SYNCHRONOUS) else: # Apparently any single deregister call will turn off # all the other registered modifier_masks too. Since # deregistering takes extremely long (~2.5s for 16 calls) # seize the opportunity and just pick a single arbitrary # mask (Quantal). modifier_masks = [2] for modifier_mask in modifier_masks: Atspi.deregister_keystroke_listener( self._keystroke_listener, None, # key set, None=all modifier_mask, Atspi.KeyEventType.PRESSED) self._keystroke_listeners_registered = register def atspi_connect(self, attribute, event, callback): """ Start listening to an AT-SPI event. Creates a new event listener for each event, since this seems to be the only way to allow reliable deregistering of events. """ if hasattr(self, attribute): listener = getattr(self, attribute) else: listener = None if listener is None: listener = Atspi.EventListener.new(callback, None) setattr(self, attribute, listener) listener.register(event) def atspi_disconnect(self, attribute, event): """ Stop listening to AT-SPI event. """ listener = getattr(self, attribute) listener.deregister(event) def freeze(self): """ Freeze AT-SPI message processing, e.g. while displaying a dialog or popoup menu. """ self._register_atspi_listeners(False) self._frozen = True def thaw(self): """ Resume AT-SPI message processing. """ self._update_listeners() self._frozen = False def emit_async(self, event_name, *args, **kwargs): if not self._frozen: EventSource.emit_async(self, event_name, *args, **kwargs) def _get_cached_accessible(self, accessible): return CachedAccessible(accessible) \ if accessible else None # ######### synchronous handlers ######### # def _on_atspi_global_focus(self, event, user_data): self._on_atspi_focus(event, True) def _on_atspi_object_focus(self, event, user_data): self._on_atspi_focus(event) def _on_atspi_focus(self, event, focus_received=False): focused = (bool(focus_received) or bool(event.detail1)) # received focus? ae = AsyncEvent(accessible=self._get_cached_accessible(event.source), focused=focused) self.emit_async("async-focus-changed", ae) def _on_atspi_text_changed(self, event, user_data): # print("_on_atspi_text_changed", event.detail1, event.detail2, # event.source, event.type, event.type.endswith("delete")) ae = AsyncEvent(accessible=self._get_cached_accessible(event.source), type=event.type, pos=event.detail1, length=event.detail2) self.emit_async("async-text-changed", ae) return False def _on_atspi_text_caret_moved(self, event, user_data): # print("_on_atspi_text_caret_moved", event.detail1, event.detail2, # event.source, event.type, event.source.get_name(), # event.source.get_role()) ae = AsyncEvent(accessible=self._get_cached_accessible(event.source), caret=event.detail1) self.emit_async("async-text-caret-moved", ae) return False def _on_atspi_keystroke(self, event, user_data): if event.type == Atspi.EventType.KEY_PRESSED_EVENT: _logger.atspi("key-stroke {} {} {} {}" .format(event.modifiers, event.hw_code, event.id, event.is_text)) # keysym = event.id # What is this? Not an XK_ keysym apparently. ae = AsyncEvent(hw_code=event.hw_code, modifiers=event.modifiers) self.emit_async("key-pressed", ae) return False # don't consume event # ######### asynchronous handlers ######### # def _on_async_focus_changed(self, event): accessible = event.accessible focused = event.focused # Don't access the accessible while frozen. This leads to deadlocks # while displaying Onboard's own dialogs/popup menu's. if self._frozen: return self._log_accessible(accessible, focused) if not accessible: return app_name = accessible.get_app_name().lower() if app_name == "unity": self._handle_focus_changed_unity(event) else: self._handle_focus_changed_apps(event) def _handle_focus_changed_apps(self, event): """ Focus change in regular applications """ accessible = event.accessible focused = event.focused # Since Trusty, focus events no longer come reliably in a # predictable order. -> Store the last editable accessible # so we can pick it over later focused non-editable ones. # Helps to keep the keyboard open in presence of popup selections # e.g. in GNOME's file dialog and in Unity Dash. if self._focused_accessible == accessible: if not focused: self._focused_accessible = None else: pid = accessible.get_pid() if focused: self._poll_unity_timer.stop() if accessible.is_editable(): self._focused_accessible = accessible self._focused_pid = pid # Static accessible, i.e. something that cannot # accidentally steal the focus from an editable # accessible. e.g. firefox ATSPI_ROLE_DOCUMENT_FRAME? elif accessible.is_not_focus_stealing(): self._focused_accessible = None self._focused_pid = None else: # Wily: attempt to hide when unity dash closes # (there's no focus lost event). # Also check duration since last activation to # skip out of order focus events (firefox # ATSPI_ROLE_DOCUMENT_FRAME) for a short while # after opening dash. now = time.time() if focused and \ now - self._active_accessible_activation_time > .5: if self._focused_pid != pid: self._focused_accessible = None _logger.atspi("Dropping accessible due to " "pid change: {} != {} " .format(self._focused_pid, pid)) # Has the previously focused accessible lost the focus? active_accessible = self._focused_accessible if active_accessible and \ not active_accessible.is_focused(True): # Zesty: Firefox 50+ loses focus of the URL entry after # typing just a few letters and focuses a completion # menu item instead. Let's pretend the accessible is # still focused in that case. is_firefox_completion = \ self._focused_accessible.is_urlbar() and \ accessible.get_role() == Atspi.Role.MENU_ITEM if not is_firefox_completion: active_accessible = None self._set_active_accessible(active_accessible) def _handle_focus_changed_unity(self, event): """ Focus change in Unity Dash """ accessible = event.accessible focused = event.focused # Wily: prevent random icons, buttons and toolbars # in unity dash from hiding Onboard. Somehow hovering # over those buttons silently drops the focus from the # text entry. Let's pretend the buttons don't exist # and keep the previously saved text entry active. # Zesty: Don't fight lost focus events anymore, only # react to focus events when the text entry gains focus. if focused and \ accessible.is_editable(): self._focused_accessible = accessible self._set_active_accessible(accessible) # For hiding we poll Dash's toplevel accessible def _poll_unity_dash(): frame = accessible.get_frame() state_set = frame.get_state_set() _logger.debug( "polling unity dash state_set: {}" .format(AtspiStateType.to_strings(state_set))) if not state_set or \ not state_set.contains(Atspi.StateType.ACTIVE): self._focused_accessible = None self._set_active_accessible(None) return False return True # Only ever start polling if Dash is "ACTIVE". # The state_set might change in the future and the # keyboard better fail to auto-hide than to never show. frame = accessible.get_frame() state_set = frame.get_state_set() _logger.debug( "dash focused, state_set: {}" .format(AtspiStateType.to_strings(state_set))) if state_set and \ state_set.contains(Atspi.StateType.ACTIVE): self._poll_unity_timer.start(0.5, _poll_unity_dash) def _set_active_accessible(self, accessible): if self._active_accessible != accessible: self._active_accessible = accessible if self._active_accessible or \ self._last_active_accessible: # notify listeners self.emit("text-entry-activated", self._active_accessible) self._last_active_accessible = self._active_accessible self._active_accessible_activation_time = time.time() def _on_async_text_changed(self, event): if event.accessible == self._active_accessible: type = event.type insert = type.endswith(("insert", "insert:system")) delete = type.endswith(("delete", "delete:system")) # print(event.accessible.get_id(), type, insert) if insert or delete: event.insert = insert self.emit("text-changed", event) else: _logger.warning("_on_async_text_changed: " "unknown event type '{}'" .format(event.type)) def _on_async_text_caret_moved(self, event): if event.accessible == self._active_accessible: self.emit("text-caret-moved", event) def _log_accessible(self, accessible, focused): if _logger.isEnabledFor(_logger.LEVEL_ATSPI): msg = "AT-SPI focus event: focused={}, ".format(focused) msg += "accessible={}, ".format(accessible) if accessible: name = accessible.get_name() role = accessible.get_role() role_name = accessible.get_role_name() state_set = accessible.get_state_set() states = state_set.states editable = state_set.contains(Atspi.StateType.EDITABLE) \ if state_set else None extents = accessible.get_extents() msg += "name={name}, role={role}({role_name}), " \ "editable={editable}, states={states}, " \ "extents={extents}]" \ .format(accessible=accessible, name=repr(name), role=role.value_name if role else role, role_name=repr(role_name), editable=editable, states=states, extents=extents ) _logger.atspi(msg) class AtspiStateType: states = ['ACTIVE', 'ANIMATED', 'ARMED', 'BUSY', 'CHECKED', 'COLLAPSED', 'DEFUNCT', 'EDITABLE', 'ENABLED', 'EXPANDABLE', 'EXPANDED', 'FOCUSABLE', 'FOCUSED', 'HAS_TOOLTIP', 'HORIZONTAL', 'ICONIFIED', 'INDETERMINATE', 'INVALID', 'INVALID_ENTRY', 'IS_DEFAULT', 'LAST_DEFINED', 'MANAGES_DESCENDANTS', 'MODAL', 'MULTISELECTABLE', 'MULTI_LINE', 'OPAQUE', 'PRESSED', 'REQUIRED', 'RESIZABLE', 'SELECTABLE', 'SELECTABLE_TEXT', 'SELECTED', 'SENSITIVE', 'SHOWING', 'SINGLE_LINE', 'STALE', 'SUPPORTS_AUTOCOMPLETION', 'TRANSIENT', 'TRUNCATED', 'VERTICAL', 'VISIBLE', 'VISITED', ] @staticmethod def to_strings(state_set): result = [] if state_set is not None: for s in AtspiStateType.states: if state_set.contains(getattr(Atspi.StateType, s)): result.append(s) return result