示例#1
0
    def __init__(self, wp):
        self._wp = wp
        self._accessible = None
        self._can_insert_text = False

        self._text_domains = TextDomains()
        self._text_domain = self._text_domains.get_nop_domain()

        self._changes = TextChanges()
        self._entering_text = False
        self._text_changed = False

        self._context = ""
        self._line = ""
        self._line_caret = 0
        self._selection_span = TextSpan()
        self._begin_of_text = False        # context starts at begin of text?
        self._begin_of_text_offset = None  # offset of text begin

        self._pending_separator_span = None
        self._last_text_change_time = 0
        self._last_caret_move_time = 0
        self._last_caret_move_position = 0

        self._last_context = None
        self._last_line = None

        self._update_context_timer = Timer()
        self._update_context_delay_normal = 0.01
        self._update_context_delay = self._update_context_delay_normal
示例#2
0
    def on_screen_size_changed(self, screen):
        """ detect screen rotation (tablets)"""

        # Give the screen time to settle, the window manager
        # may block the move to previously invalid positions and
        # when docked, the slide animation may be drowned out by all
        # the action in other processes.
        Timer(1.5, self.on_screen_size_changed_delayed, screen)
示例#3
0
    def __init__(self):
        WindowRectTracker.__init__(self)
        self._screen_orientation = None
        self._save_position_timer = Timer()

        # init detection of screen "rotation"
        screen = self.get_screen()
        screen.connect('size-changed', self.on_screen_size_changed)
示例#4
0
    def on_input_sequence_end(self, sequence):
        key = sequence.active_key
        if key:
            keyboard = self.keyboard
            keyboard.key_up(key, self, sequence)

        if key and \
           not self._drag_selected:
            Timer(config.UNPRESS_DELAY, self.close_window)
        else:
            self.close_window()
示例#5
0
    def __init__(self, keyboard, notify_done_callback):
        self._layout = None
        self._notify_done_callback = notify_done_callback
        self._drag_selected = False  # grazed by the pointer?

        KeyboardPopup.__init__(self)
        LayoutView.__init__(self, keyboard)
        TouchInput.__init__(self)

        self.connect("destroy", self._on_destroy_event)

        self._close_timer = Timer()
        self.start_close_timer()
示例#6
0
    def __init__(self, redraw_callback, activate_callback):
        super(ScanMode, self).__init__()

        logger.debug("ScanMode.__init__()")
        """ Activation timer instance """
        self._activation_timer = Timer()
        """ Counter for key flash animation """
        self._flash = 0
        """ Callback for key redraws """
        self._redraw_callback = redraw_callback
        """ Callback for key activation """
        self._activate_callback = activate_callback
        """ A Chunker instance """
        self.chunker = None
示例#7
0
    def init(self):
        self.keyboard_state = None
        self._last_mod_mask = 0
        self.vk_timer = None
        self.reset_vk()
        self._connections = []
        self._window = None
        self.status_icon = None
        self.service_keyboard = None
        self._reload_layout_timer = Timer()

        # finish config initialization
        config.init()

        # Optionally wait a little before proceeding.
        # When the system starts up, the docking strut can be unreliable
        # on Compiz (Yakkety) and the keyboard may end up at unexpected initial
        # positions. Delaying the startup when launched by
        # onboard-autostart.desktop helps to prevent this.
        delay = config.startup_delay
        if delay:
            Timer(delay, self._init_delayed)
        else:
            self._init_delayed()
示例#8
0
    def __init__(self):
        InputEventSource.__init__(self)

        self._input_sequences = {}

        self._touch_events_enabled = False
        self._multi_touch_enabled = False
        self._gestures_enabled = False

        self._last_event_was_touch = False
        self._last_sequence_time = 0

        self._gesture = NO_GESTURE
        self._gesture_begin_point = (0, 0)
        self._gesture_begin_time = 0
        self._gesture_detected = False
        self._gesture_cancelled = False
        self._num_tap_sequences = 0
        self._gesture_timer = Timer()
示例#9
0
 def _on_dwell_begin_timer(self):
     self._dwell_progress.start_dwelling()
     self._dwell_timer = Timer(0.025, self._on_dwell_timer)
     return False
示例#10
0
 def _start_dwelling(self):
     self._stop_dwelling()
     self._dwell_begin_timer = Timer(1.5, self._on_dwell_begin_timer)
     self._no_more_dwelling = True
示例#11
0
    def init(self):
        self.keyboard_state = None
        self.vk_timer = None
        self.reset_vk()
        self._connections = []
        self._window = None
        self.status_icon = None
        self.service_keyboard = None
        self._reload_layout_timer = Timer()

        # finish config initialization
        config.init()

        # Release pressed keys when onboard is killed.
        # Don't keep enter key stuck when being killed by lightdm.
        self._osk_util = osk.Util()
        self._osk_util.set_unix_signal_handler(signal.SIGTERM, self.on_sigterm)
        self._osk_util.set_unix_signal_handler(signal.SIGINT, self.on_sigint)

        sys.path.append(os.path.join(config.install_dir, 'scripts'))

        # Create the central keyboard model
        self.keyboard = Keyboard(self)

        # Create the initial keyboard widget
        # Care for toolkit independence only once there is another
        # supported one besides GTK.
        self.keyboard_widget = KeyboardWidget(self.keyboard)

        # create the main window
        if config.xid_mode:  # XEmbed mode for gnome-screensaver?
            # no icp, don't flash the icon palette in lightdm

            self._window = KbdPlugWindow(self.keyboard_widget)

            # write xid to stdout
            sys.stdout.write('%d\n' % self._window.get_id())
            sys.stdout.flush()
        else:
            icp = IconPalette(self.keyboard)
            icp.set_layout_view(self.keyboard_widget)
            icp.connect("activated", self._on_icon_palette_acticated)
            self.do_connect(icp.get_menu(), "quit-onboard",
                            lambda x: self.do_quit_onboard())

            self._window = KbdWindow(self.keyboard_widget, icp)
            self.do_connect(self._window, "quit-onboard",
                            lambda x: self.do_quit_onboard())

        # config.xid_mode = True
        self._window.application = self
        # need this to access screen properties
        config.main_window = self._window

        # load the initial layout
        _logger.info("Loading initial layout")
        self.reload_layout()

        # Handle command line options x, y, size after window creation
        # because the rotation code needs the window's screen.
        if not config.xid_mode:
            rect = self._window.get_rect().copy()
            options = config.options
            if options.size:
                size = options.size.split("x")
                rect.w = int(size[0])
                rect.h = int(size[1])
            if options.x is not None:
                rect.x = options.x
            if options.y is not None:
                rect.y = options.y

            # Make sure the keyboard fits on screen
            rect = self._window.limit_size(rect)

            if rect != self._window.get_rect():
                orientation = self._window.get_screen_orientation()
                self._window.write_window_rect(orientation, rect)
                self._window.restore_window_rect()  # move/resize early

        # export dbus service
        if not config.xid_mode and \
           "dbus" in globals():
            self.service_keyboard = ServiceOnboardKeyboard(self)

        # show/hide the window
        self.keyboard_widget.set_startup_visibility()

        # keep keyboard window and icon palette on top of dash
        self._keep_windows_on_top()

        # connect notifications for keyboard map and group changes
        self.keymap = Gdk.Keymap.get_default()
        # map changes
        self.do_connect(self.keymap, "keys-changed", self.cb_keys_changed)
        self.do_connect(self.keymap, "state-changed", self.cb_state_changed)
        # group changes
        Gdk.event_handler_set(cb_any_event, self)

        # connect config notifications here to keep config from holding
        # references to keyboard objects.
        once = CallOnce(50).enqueue  # delay callbacks by 50ms
        reload_layout = lambda x: once(self.reload_layout_and_present)
        update_ui = lambda x: once(self._update_ui)
        update_ui_no_resize = lambda x: once(self._update_ui_no_resize)
        update_transparency = \
            lambda x: once(self.keyboard_widget.update_transparency)
        update_inactive_transparency = \
            lambda x: once(self.keyboard_widget.update_inactive_transparency)

        # general
        config.auto_show.enabled_notify_add(
            lambda x: self.keyboard.update_auto_show())
        config.auto_show.hide_on_key_press_notify_add(
            lambda x: self.keyboard.update_auto_hide())

        # keyboard
        config.keyboard.key_synth_notify_add(reload_layout)
        config.keyboard.input_event_source_notify_add(
            lambda x: self.keyboard.update_input_event_source())
        config.keyboard.touch_input_notify_add(
            lambda x: self.keyboard.update_touch_input_mode())
        config.keyboard.show_secondary_labels_notify_add(update_ui)

        # window
        config.window.window_state_sticky_notify_add(
            lambda x: self._window.update_sticky_state())
        config.window.window_decoration_notify_add(
            self._on_window_options_changed)
        config.window.force_to_top_notify_add(self._on_window_options_changed)
        config.window.keep_aspect_ratio_notify_add(update_ui)

        config.window.transparency_notify_add(update_transparency)
        config.window.background_transparency_notify_add(update_transparency)
        config.window.transparent_background_notify_add(update_ui)
        config.window.enable_inactive_transparency_notify_add(
            update_transparency)
        config.window.inactive_transparency_notify_add(
            update_inactive_transparency)
        config.window.docking_notify_add(self._update_docking)

        # layout
        config.layout_filename_notify_add(reload_layout)

        # theme
        # config.gdi.gtk_theme_notify_add(self.on_gtk_theme_changed)
        config.theme_notify_add(self.on_theme_changed)
        config.key_label_font_notify_add(reload_layout)
        config.key_label_overrides_notify_add(reload_layout)
        config.theme_settings.color_scheme_filename_notify_add(reload_layout)
        config.theme_settings.key_label_font_notify_add(reload_layout)
        config.theme_settings.key_label_overrides_notify_add(reload_layout)
        config.theme_settings.theme_attributes_notify_add(update_ui)

        # snippets
        config.snippets_notify_add(reload_layout)

        # word suggestions
        config.word_suggestions.show_context_line_notify_add(update_ui)
        config.word_suggestions.enabled_notify_add(
            lambda x: self.keyboard.on_word_suggestions_enabled(x))
        config.word_suggestions.auto_learn_notify_add(update_ui_no_resize)
        config.typing_assistance.active_language_notify_add(lambda x: \
                                 self.keyboard.on_active_lang_id_changed())
        config.typing_assistance.spell_check_backend_notify_add(lambda x: \
                                 self.keyboard.on_spell_checker_changed())
        config.typing_assistance.auto_capitalization_notify_add(lambda x: \
                                 self.keyboard.on_word_suggestions_enabled(x))
        config.word_suggestions.spelling_suggestions_enabled_notify_add(lambda x: \
                                 self.keyboard.on_spell_checker_changed())
        config.word_suggestions.delayed_word_separators_enabled_notify_add(lambda x: \
                                 self.keyboard.on_punctuator_changed())
        config.word_suggestions.wordlist_buttons_notify_add(
            update_ui_no_resize)

        # universal access
        config.scanner.enabled_notify_add(self.keyboard._on_scanner_enabled)
        config.window.window_handles_notify_add(
            self._on_window_handles_changed)

        # misc
        config.keyboard.show_click_buttons_notify_add(update_ui)
        config.lockdown.lockdown_notify_add(update_ui)
        if config.mousetweaks:
            config.mousetweaks.state_notify_add(update_ui_no_resize)

        # create status icon
        self.status_icon = Indicator()
        self.status_icon.set_keyboard(self.keyboard)
        self.do_connect(self.status_icon.get_menu(), "quit-onboard",
                        lambda x: self.do_quit_onboard())

        # Callbacks to use when icp or status icon is toggled
        config.show_status_icon_notify_add(self.show_hide_status_icon)
        config.icp.in_use_notify_add(self.cb_icp_in_use_toggled)

        self.show_hide_status_icon(config.show_status_icon)

        # Minimize to IconPalette if running under GDM
        if 'RUNNING_UNDER_GDM' in os.environ:
            _logger.info("RUNNING_UNDER_GDM set, turning on icon palette")
            config.icp.in_use = True
            _logger.info("RUNNING_UNDER_GDM set, turning off indicator")
            config.show_status_icon = False

            # For some reason the new values don't arrive in gsettings when
            # running the unit test "test_running_in_live_cd_environment".
            # -> Force gsettings to apply them, that seems to do the trick.
            config.icp.apply()
            config.apply()

        # unity-2d needs the skip-task-bar hint set before the first mapping.
        self.show_hide_taskbar()

        # Check gnome-screen-saver integration
        # onboard_xembed_enabled                False True     True      True
        # config.gss.embedded_keyboard_enabled  any   False    any       False
        # config.gss.embedded_keyboard_command  any   empty    !=onboard ==onboard
        # Action:                               nop   enable   Question1 Question2
        #                                             silently
        if not config.xid_mode and \
           config.onboard_xembed_enabled:

            # If it appears, that nothing has touched the gss keys before,
            # silently enable gss integration with onboard.
            if not config.gss.embedded_keyboard_enabled and \
               not config.gss.embedded_keyboard_command:
                config.enable_gss_embedding(True)

            # If onboard is configured to be embedded into the unlock screen
            # dialog, and the embedding command is different from onboard, ask
            # the user what to do
            elif not config.is_onboard_in_xembed_command_string():
                question = _(
                    "Onboard is configured to appear with the dialog to "
                    "unlock the screen; for example to dismiss the "
                    "password-protected screensaver.\n\n"
                    "However the system is not configured anymore to use "
                    "Onboard to unlock the screen. A possible reason can "
                    "be that another application configured the system to "
                    "use something else.\n\n"
                    "Would you like to reconfigure the system to show "
                    "Onboard when unlocking the screen?")
                _logger.warning("showing dialog: '{}'".format(question))
                reply = show_confirmation_dialog(question, self._window,
                                                 config.is_force_to_top())
                if reply == True:
                    config.enable_gss_embedding(True)
                else:
                    config.onboard_xembed_enabled = False
            else:
                if not config.gss.embedded_keyboard_enabled:
                    question = _(
                        "Onboard is configured to appear with the dialog "
                        "to unlock the screen; for example to dismiss "
                        "the password-protected screensaver.\n\n"
                        "However this function is disabled in the system.\n\n"
                        "Would you like to activate it?")
                    _logger.warning("showing dialog: '{}'".format(question))
                    reply = show_confirmation_dialog(question, self._window,
                                                     config.is_force_to_top())
                    if reply == True:
                        config.enable_gss_embedding(True)
                    else:
                        config.onboard_xembed_enabled = False

        # check if gnome accessibility is enabled for auto-show
        if (config.is_auto_show_enabled() or \
            config.are_word_suggestions_enabled()) and \
            not config.check_gnome_accessibility(self._window):
            config.auto_show.enabled = False
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)