Esempio n. 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
Esempio n. 2
0
    def read_context(self, accessible, offset=None):
        """
        Extract prediction context from the accessible
        """
        if offset is None:
            try:
                offset = accessible.get_caret_offset()
            except Exception as ex:  # Private exception gi._glib.GError
                # when gedit became unresponsive.
                _logger.info("DomainTerminal.read_context(): " +
                             unicode_str(ex))
                return None

        context_lines, prompt_length, line, line_start, line_caret = \
            self._get_text_after_prompt(accessible, offset)

        if prompt_length:
            begin_of_text = True
            begin_of_text_offset = line_start
        else:
            begin_of_text = False
            begin_of_text_offset = None

        context = "".join(context_lines)
        before_line = "".join(context_lines[:-1])
        selection_span = TextSpan(offset, 0, before_line + line,
                                  line_start - len(before_line))

        result = (context, line, line_caret, selection_span, begin_of_text,
                  begin_of_text_offset)
        return result
Esempio n. 3
0
    def read_context(self, accessible):
        """ Extract prediction context from the accessible """

        # get caret position from selection
        selection = None
        try:
            sel = accessible.get_selection(0)
            # 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("DomainGenericText.read_context(), selection: " \
                         + unicode_str(ex))

        # get text around the caret position
        try:
            if selection is None:
                offset = accessible.get_caret_offset()
                selection = (offset, offset)
            r = accessible.get_text_at_offset(
                selection[0], Atspi.TextBoundaryType.LINE_START)
            count = accessible.get_character_count()
        except Exception as ex:  # Private exception gi._glib.GError when
            # gedit became unresponsive.
            _logger.info("DomainGenericText.read_context(), text: " \
                         + unicode_str(ex))
            return None

        line = unicode_str(r.content).replace("\n", "")
        line_caret = max(selection[0] - r.start_offset, 0)

        begin = max(selection[0] - 256, 0)
        end = min(selection[0] + 100, count)
        try:
            text = Atspi.Text.get_text(accessible, begin, end)
        except Exception as ex:  # Private exception gi._glib.GError when
            # gedit became unresponsive.
            _logger.info("DomainGenericText.read_context(), text2: " \
                         + unicode_str(ex))
            return None

        text = unicode_str(text)

        # Not all text may be available for large selections. We only need the
        # part before the begin of the selection/caret.
        selection_span = TextSpan(selection[0], selection[1] - selection[0],
                                  text, begin)
        context = text[:selection[0] - begin]
        begin_of_text = begin == 0
        begin_of_text_offset = 0

        return (context, line, line_caret, selection_span, begin_of_text,
                begin_of_text_offset)
Esempio n. 4
0
    def read_context(self, accessible, offset=None):
        """
        Extract prediction context from the accessible
        """
        if offset is None:
            try:
                offset = accessible.get_caret_offset()
            except Exception as ex:  # Private exception gi._glib.GError when
                # gedit became unresponsive.
                _logger.info("DomainTerminal.read_context(): " \
                             + unicode_str(ex))
                return None

        # remove prompt from the current or previous lines
        context, context_start, line, line_start, line_cursor = \
                self._read_after_prompt(accessible, offset)
        if context_start:
            begin_of_text = True
            begin_of_text_offset = line_start
        else:
            begin_of_text = False
            begin_of_text_offset = None

        # remove newlines
        context = context.replace("\n", "")

        #cursor_span = TextSpan(offset, 0, text, begin)
        cursor_span = TextSpan(offset, 0, line, line_start)

        result = (context, line, line_cursor, cursor_span, begin_of_text,
                  begin_of_text_offset)
        return result
Esempio n. 5
0
    def read_context(self, accessible):
        """ Extract prediction context from the accessible """
        try:
            offset = accessible.get_caret_offset()
            r = accessible.get_text_at_offset(
                offset, Atspi.TextBoundaryType.LINE_START)
            count = accessible.get_character_count()
        except Exception as ex:  # Private exception gi._glib.GError when
            # gedit became unresponsive.
            _logger.info("DomainGenericText.read_context(): " \
                         + unicode_str(ex))
            return None

        line = unicode_str(r.content).replace("\n", "")
        line_cursor = max(offset - r.start_offset, 0)

        begin = max(offset - 256, 0)
        end = min(offset + 100, count)
        text = Atspi.Text.get_text(accessible, begin, end)

        text = unicode_str(text)

        cursor_span = TextSpan(offset, 0, text, begin)
        context = text[:offset - begin]
        begin_of_text = begin == 0
        begin_of_text_offset = 0

        return (context, line, line_cursor, cursor_span, begin_of_text,
                begin_of_text_offset)
Esempio n. 6
0
    def read_context(self, keyboard, accessible):
        """ Extract prediction context from the accessible """

        # get caret position from selection
        selection = accessible.get_selection()

        # get text around the caret position
        try:
            count = accessible.get_character_count()

            if selection is None:
                offset = accessible.get_caret_offset()

                # In Zesty, firefox 50.1 often returns caret position -1
                # when typing into the urlbar. Assume we are at the end
                # of the text when that happens.
                if offset < 0:
                    _logger.warning("DomainGenericText.read_context(): "
                                    "Atspi.Text.get_caret_offset() "
                                    "returned invalid {}. "
                                    "Pretending the cursor is at the end "
                                    "of the text at offset {}."
                                    .format(offset, count))
                    offset = count

                selection = (offset, offset)

            r = accessible.get_text_at_offset(
                selection[0], Atspi.TextBoundaryType.LINE_START)
        except Exception as ex:     # Private exception gi._glib.GError when
                                    # gedit became unresponsive.
            _logger.info("DomainGenericText.read_context(), text: " +
                         unicode_str(ex))
            return None

        line = unicode_str(r.content).replace("\n", "")
        line_caret = max(selection[0] - r.start_offset, 0)

        begin = max(selection[0] - 256, 0)
        end   = min(selection[0] + 100, count)
        try:
            text = accessible.get_text(begin, end)
        except Exception as ex:     # Private exception gi._glib.GError when
                                    # gedit became unresponsive.
            _logger.info("DomainGenericText.read_context(), text2: " +
                         unicode_str(ex))
            return None

        text = unicode_str(text)

        # Not all text may be available for large selections. We only need the
        # part before the begin of the selection/caret.
        selection_span = TextSpan(selection[0], selection[1] - selection[0],
                                  text, begin)
        context = text[:selection[0] - begin]
        begin_of_text = begin == 0
        begin_of_text_offset = 0

        return (context, line, line_caret, selection_span,
                begin_of_text, begin_of_text_offset)
Esempio n. 7
0
    def _record_text_change(self, pos, length, insert):
        accessible = self._accessible

        insertion_span = None
        char_count = None
        if accessible:
            try:
                char_count = accessible.get_character_count()
            except:     # gi._glib.GError: The application no longer exists
                        # when closing a tab in gnome-terminal.
                char_count = None

        if char_count is not None:
            # record the change
            spans_to_update = []

            if insert:
                if self._entering_text and \
                   self.can_record_insertion(accessible, pos, length):
                    if self._wp.is_typing() or length < 30:
                        # Remember all of the insertion, might have been
                        # a pressed snippet or wordlist button.
                        include_length = -1
                    else:
                        # Remember only the first few characters.
                        # Large inserts can be paste, reload or scroll
                        # operations. Only learn the first word of these.
                        include_length = 2

                    # simple span for current insertion
                    begin = max(pos - 100, 0)
                    end = min(pos + length + 100, char_count)
                    text = self._state_tracker.get_accessible_text(accessible,
                                                                   begin, end)
                    if text is not None:
                        insertion_span = TextSpan(pos, length, text, begin)
                else:
                    # Remember nothing, just update existing spans.
                    include_length = None

                spans_to_update = self._changes.insert(pos, length,
                                                       include_length)

            else:
                spans_to_update = self._changes.delete(pos, length,
                                                       self._entering_text)

            # update text of all modified spans
            for span in spans_to_update:
                # Get some more text around the span to hopefully
                # include whole words at beginning and end.
                begin = max(span.begin() - 100, 0)
                end = min(span.end() + 100, char_count)
                span.text = Atspi.Text.get_text(accessible, begin, end)
                span.text_pos = begin

        self._text_changed = True

        return insertion_span
Esempio n. 8
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_cursor = 0
        self._span_at_cursor = TextSpan()
        self._begin_of_text = False  # context starts at begin of text?
        self._begin_of_text_offset = None  # offset of text begin

        self._last_context = None
        self._last_line = None

        self._update_context_timer = Timer()
Esempio n. 9
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_cursor = 0
        self._span_at_cursor = TextSpan()
        self._begin_of_text = False        # context starts at begin of text?
        self._begin_of_text_offset = None  # offset of text begin

        self._last_context = None
        self._last_line = None

        self._update_context_timer = Timer()
Esempio n. 10
0
 def read_context(self, accessible):
     return "", "", 0, TextSpan(), False, 0
Esempio n. 11
0
class AtspiTextContext(TextContext):
    """
    Keep track of the current text context with AT-SPI
    """

    _state_tracker = AtspiStateTracker()

    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_cursor = 0
        self._span_at_cursor = TextSpan()
        self._begin_of_text = False  # context starts at begin of text?
        self._begin_of_text_offset = None  # offset of text begin

        self._last_context = None
        self._last_line = None

        self._update_context_timer = Timer()

    def cleanup(self):
        self._register_atspi_listeners(False)

    def enable(self, enable):
        self._register_atspi_listeners(enable)

    def get_text_domain(self):
        return self._text_domain

    def get_context(self):
        """
        Returns the predictions context, i.e. some range of
        text before the cursor position.
        """
        if self._accessible is None:
            return ""

        # Don't update suggestions in scrolling terminals
        if self._entering_text or \
           not self._text_changed or \
           self.can_suggest_before_typing():
            return self._context

        return ""

    def get_bot_context(self):
        """
        Returns the predictions context with
        begin of text marker (at text begin).
        """
        context = ""
        if self._accessible:
            context = self.get_context()

            # prepend domain specific begin-of-text marker
            if self._begin_of_text:
                marker = self.get_text_begin_marker()
                if marker:
                    context = marker + " " + context

        return context

    def get_line(self):
        return self._line \
               if self._accessible else ""

    def get_line_cursor_pos(self):
        return self._line_cursor \
               if self._accessible else 0

    def get_line_past_cursor(self):
        return self._line[self._line_cursor:] \
               if self._accessible else ""

    def get_span_at_cursor(self):
        return self._span_at_cursor \
               if self._accessible else None

    def get_cursor(self):
        return self._span_at_cursor.begin() \
               if self._accessible else 0

    def get_text_begin_marker(self):
        domain = self.get_text_domain()
        if domain:
            return domain.get_text_begin_marker()
        return ""

    def can_record_insertion(self, accessible, pos, length):
        domain = self.get_text_domain()
        if domain:
            return domain.can_record_insertion(accessible, pos, length)
        return True

    def can_suggest_before_typing(self):
        domain = self.get_text_domain()
        if domain:
            return domain.can_suggest_before_typing()
        return True

    def get_begin_of_text_offset(self):
        return self._begin_of_text_offset \
               if self._accessible else None

    def get_changes(self):
        return self._changes

    def has_changes(self):
        """ Are there any changes to learn? """
        return not self._changes.is_empty()

    def clear_changes(self):
        self._changes.clear()

    def can_insert_text(self):
        """
        Can delete or insert text into the accessible?
        """
        #return False # support for inserting is spotty: not in firefox, terminal
        return bool(self._accessible) and self._can_insert_text

    def delete_text(self, offset, length=1):
        """ Delete directly, without going through faking key presses. """
        self._accessible.delete_text(offset, offset + length)

    def delete_text_before_cursor(self, length=1):
        """ Delete directly, without going through faking key presses. """
        offset = self._accessible.get_caret_offset()
        self.delete_text(offset - length, length)

    def insert_text(self, offset, text):
        """
        Insert directly, without going through faking key presses.
        """
        self._accessible.insert_text(offset, text, -1)

    def insert_text_at_cursor(self, text):
        """
        Insert directly, without going through faking key presses.
        Fails for terminal and firefox, unfortunately.
        """
        offset = self._accessible.get_caret_offset()
        self.insert_text(offset, text)

    def _register_atspi_listeners(self, register=True):
        st = self._state_tracker
        if register:
            st.connect("text-entry-activated", self._on_text_entry_activated)
            st.connect("text-changed", self._on_text_changed)
            st.connect("text-caret-moved", self._on_text_caret_moved)
            #st.connect("key-pressed", self._on_atspi_key_pressed)
        else:
            st.disconnect("text-entry-activated",
                          self._on_text_entry_activated)
            st.disconnect("text-changed", self._on_text_changed)
            st.disconnect("text-caret-moved", self._on_text_caret_moved)
            #st.disconnect("key-pressed", self._on_atspi_key_pressed)

    def get_accessible_capabilities(accessible, **kwargs):
        can_insert_text = False
        attributes = kwargs.get("attributes", {})
        interfaces = kwargs.get("interfaces", [])

        if accessible:

            # Can insert text via Atspi?
            # Advantages: - faster, no individual key presses
            #             - full trouble-free insertion of all unicode characters
            if "EditableText" in interfaces:
                # Support for atspi text insertion is spotty.
                # Firefox, LibreOffice Writer, gnome-terminal don't support it,
                # even if they claim to implement the EditableText interface.

                # Allow direct text insertion by gtk widgets
                if "toolkit" in attributes and attributes["toolkit"] == "gtk":
                    can_insert_text = True

        return can_insert_text

    def _on_text_entry_activated(self, accessible):
        # old text_domain still valid here
        self._wp.on_text_entry_deactivated()

        #print("_on_text_entry_activated", accessible)
        # keep track of the active accessible asynchronously
        self._accessible = accessible
        self._entering_text = False
        self._text_changed = False

        # select text domain matching this accessible
        state = self._state_tracker.get_state() \
                if self._accessible else {}
        self._text_domain = self._text_domains.find_match(**state)
        self._text_domain.init_domain()

        # determine capabilities of this accessible
        self._can_insert_text = self.get_accessible_capabilities(**state)

        # log accessible info
        if _logger.isEnabledFor(logging.DEBUG):
            log = _logger.debug
            log("-" * 70)
            log("Accessible focused: ")
            if self._accessible:
                state = self._state_tracker.get_state()
                for key, value in sorted(state.items()):
                    msg = str(key) + "="
                    if key == "state-set":
                        msg += repr(AtspiStateType.to_strings(value))
                    else:
                        msg += str(value)
                    log(msg)
                log("text_domain: {}".format(self._text_domain))
                log("can_insert_text: {}".format(self._can_insert_text))
                log("")
            else:
                log("None")
                log("")

        self._update_context()

        self._wp.on_text_entry_activated()

    def _on_text_changed(self, event):
        insertion_span = self._record_text_change(event.pos, event.length,
                                                  event.insert)
        # synchrounously notify of text insertion
        if insertion_span:
            try:
                cursor_offset = self._accessible.get_caret_offset()
            except:  # gi._glib.GError
                pass
            else:
                self._wp.on_text_inserted(insertion_span, cursor_offset)

        self._update_context()

    def _on_text_caret_moved(self, event):
        self._update_context()

    def _on_atspi_key_pressed(self, event):
        """ disabled, Francesco didn't receive any AT-SPI key-strokes. """
        keycode = event.hw_code  # uh oh, only keycodes...
        # hopefully "c" doesn't move around a lot.
        modifiers = event.modifiers
        #self._handle_key_press(keycode, modifiers)

    def on_onboard_typing(self, key, mod_mask):
        if key.is_text_changing():
            keycode = 0
            if key.is_return():
                keycode = KeyCode.KP_Enter
            else:
                label = key.get_label()
                if label == "C" or label == "c":
                    keycode = KeyCode.C

            self._handle_key_press(keycode, mod_mask)

    def _handle_key_press(self, keycode, modifiers):
        if self._accessible:
            domain = self.get_text_domain()
            if domain:
                self._entering_text, end_of_editing = \
                        domain.handle_key_press(keycode, modifiers)

                if end_of_editing == True:
                    self._wp.commit_changes()
                elif end_of_editing == False:
                    self._wp.discard_changes()

    def _record_text_change(self, pos, length, insert):
        accessible = self._accessible

        insertion_span = None
        char_count = None
        if accessible:
            try:
                char_count = accessible.get_character_count()
            except:  # gi._glib.GError: The application no longer exists
                # when closing a tab in gnome-terminal.
                char_count = None

        if not char_count is None:
            # record the change
            spans_to_update = []
            if insert:
                #print("insert", pos, length)

                if self._entering_text and \
                   self.can_record_insertion(accessible, pos, length):
                    if self._wp.is_typing() or length < 30:
                        # Remember all of the insertion, might have been
                        # a pressed snippet or wordlist button.
                        include_length = -1
                    else:
                        # Remember only the first few characters.
                        # Large inserts can be paste, reload or scroll
                        # operations. Only learn the first word of these.
                        include_length = 2

                    # simple span for current insertion
                    begin = max(pos - 100, 0)
                    end = min(pos + length + 100, char_count)
                    text = Atspi.Text.get_text(accessible, begin, end)
                    insertion_span = TextSpan(pos, length, text, begin)
                else:
                    # Remember nothing, just update existing spans.
                    include_length = None

                spans_to_update = self._changes.insert(pos, length,
                                                       include_length)

            else:
                #print("delete", pos, length)
                spans_to_update = self._changes.delete(pos, length,
                                                       self._entering_text)

            # update text of the modified spans
            for span in spans_to_update:
                # Get some more text around the span to hopefully
                # include whole words at beginning and end.
                begin = max(span.begin() - 100, 0)
                end = min(span.end() + 100, char_count)
                span.text = Atspi.Text.get_text(accessible, begin, end)
                span.text_pos = begin

        #print(self._changes)

        self._text_changed = True

        return insertion_span

    def _update_context(self):
        self._update_context_timer.start(0.01, self.on_text_context_changed)

    def on_text_context_changed(self):
        result = self._text_domain.read_context(self._accessible)
        if not result is None:
            (self._context, self._line, self._line_cursor,
             self._span_at_cursor, self._begin_of_text,
             self._begin_of_text_offset) = result

            context = self.get_bot_context(
            )  # make sure to include bot-markers
            if self._last_context != context or \
               self._last_line != self._line:
                self._last_context = context
                self._last_line = self._line
                self._wp.on_text_context_changed()

        return False
Esempio n. 12
0
class AtspiTextContext(TextContext):
    """
    Keep track of the current text context with AT-SPI
    """

    _state_tracker = AtspiStateTracker()

    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_cursor = 0
        self._span_at_cursor = TextSpan()
        self._begin_of_text = False        # context starts at begin of text?
        self._begin_of_text_offset = None  # offset of text begin

        self._last_context = None
        self._last_line = None

        self._update_context_timer = Timer()

    def cleanup(self):
        self._register_atspi_listeners(False)

    def enable(self, enable):
        self._register_atspi_listeners(enable)

    def get_text_domain(self):
        return self._text_domain

    def get_context(self):
        """
        Returns the predictions context, i.e. some range of
        text before the cursor position.
        """
        if self._accessible is None:
            return ""

        # Don't update suggestions in scrolling terminals
        if self._entering_text or \
           not self._text_changed or \
           self.can_suggest_before_typing():
            return self._context

        return ""

    def get_bot_context(self):
        """
        Returns the predictions context with
        begin of text marker (at text begin).
        """
        context = ""
        if self._accessible:
            context = self.get_context()

            # prepend domain specific begin-of-text marker
            if self._begin_of_text:
                marker = self.get_text_begin_marker()
                if marker:
                    context = marker + " " + context

        return context

    def get_line(self):
        return self._line \
               if self._accessible else ""

    def get_line_cursor_pos(self):
        return self._line_cursor \
               if self._accessible else 0

    def get_line_past_cursor(self):
        return self._line[self._line_cursor:] \
               if self._accessible else ""

    def get_span_at_cursor(self):
        return self._span_at_cursor \
               if self._accessible else None

    def get_cursor(self):
        return self._span_at_cursor.begin() \
               if self._accessible else 0

    def get_text_begin_marker(self):
        domain = self.get_text_domain()
        if domain:
            return domain.get_text_begin_marker()
        return ""

    def can_record_insertion(self, accessible, pos, length):
        domain = self.get_text_domain()
        if domain:
            return domain.can_record_insertion(accessible, pos, length)
        return True

    def can_suggest_before_typing(self):
        domain = self.get_text_domain()
        if domain:
            return domain.can_suggest_before_typing()
        return True

    def get_begin_of_text_offset(self):
        return self._begin_of_text_offset \
               if self._accessible else None

    def get_changes(self):
        return self._changes

    def has_changes(self):
        """ Are there any changes to learn? """
        return not self._changes.is_empty()

    def clear_changes(self):
        self._changes.clear()

    def can_insert_text(self):
        """
        Can delete or insert text into the accessible?
        """
        #return False # support for inserting is spotty: not in firefox, terminal
        return bool(self._accessible) and self._can_insert_text

    def delete_text(self, offset, length = 1):
        """ Delete directly, without going through faking key presses. """
        self._accessible.delete_text(offset, offset + length)

    def delete_text_before_cursor(self, length = 1):
        """ Delete directly, without going through faking key presses. """
        offset = self._accessible.get_caret_offset()
        self.delete_text(offset - length, length)

    def insert_text(self, offset, text):
        """
        Insert directly, without going through faking key presses.
        """
        self._accessible.insert_text(offset, text, -1)

    def insert_text_at_cursor(self, text):
        """
        Insert directly, without going through faking key presses.
        Fails for terminal and firefox, unfortunately.
        """
        offset = self._accessible.get_caret_offset()
        self.insert_text(offset, text)

    def _register_atspi_listeners(self, register = True):
        st = self._state_tracker
        if register:
            st.connect("text-entry-activated", self._on_text_entry_activated)
            st.connect("text-changed", self._on_text_changed)
            st.connect("text-caret-moved", self._on_text_caret_moved)
            #st.connect("key-pressed", self._on_atspi_key_pressed)
        else:
            st.disconnect("text-entry-activated", self._on_text_entry_activated)
            st.disconnect("text-changed", self._on_text_changed)
            st.disconnect("text-caret-moved", self._on_text_caret_moved)
            #st.disconnect("key-pressed", self._on_atspi_key_pressed)

    def get_accessible_capabilities(accessible, **kwargs):
        can_insert_text = False
        attributes = kwargs.get("attributes", {})
        interfaces = kwargs.get("interfaces", [])

        if accessible:

            # Can insert text via Atspi?
            # Advantages: - faster, no individual key presses
            #             - full trouble-free insertion of all unicode characters
            if "EditableText" in interfaces:
                # Support for atspi text insertion is spotty.
                # Firefox, LibreOffice Writer, gnome-terminal don't support it,
                # even if they claim to implement the EditableText interface.

                # Allow direct text insertion by gtk widgets
                if "toolkit" in attributes and attributes["toolkit"] == "gtk":
                   can_insert_text = True

        return can_insert_text

    def _on_text_entry_activated(self, accessible):
        # old text_domain still valid here
        self._wp.on_text_entry_deactivated()

        #print("_on_text_entry_activated", accessible)
        # keep track of the active accessible asynchronously
        self._accessible = accessible
        self._entering_text = False
        self._text_changed = False

        # select text domain matching this accessible
        state = self._state_tracker.get_state() \
                if self._accessible else {}
        self._text_domain = self._text_domains.find_match(**state)
        self._text_domain.init_domain()

        # determine capabilities of this accessible
        self._can_insert_text = self.get_accessible_capabilities(**state)

        # log accessible info
        if _logger.isEnabledFor(logging.DEBUG):
            log = _logger.debug
            log("-"*70)
            log("Accessible focused: ")
            if self._accessible:
                state = self._state_tracker.get_state()
                for key, value in sorted(state.items()):
                    msg = str(key) + "="
                    if key == "state-set":
                        msg += repr(AtspiStateType.to_strings(value))
                    else:
                        msg += str(value)
                    log(msg)
                log("text_domain: {}".format(self._text_domain))
                log("can_insert_text: {}".format(self._can_insert_text))
                log("")
            else:
                log("None")
                log("")

        self._update_context()

        self._wp.on_text_entry_activated()

    def _on_text_changed(self, event):
        insertion_span = self._record_text_change(event.pos,
                                                  event.length,
                                                  event.insert)
        # synchrounously notify of text insertion
        if insertion_span:
            try:
                cursor_offset = self._accessible.get_caret_offset()
            except: # gi._glib.GError
                pass
            else:
                self._wp.on_text_inserted(insertion_span, cursor_offset)

        self._update_context()

    def _on_text_caret_moved(self, event):
        self._update_context()

    def _on_atspi_key_pressed(self, event):
        """ disabled, Francesco didn't receive any AT-SPI key-strokes. """
        keycode = event.hw_code # uh oh, only keycodes...
                                # hopefully "c" doesn't move around a lot.
        modifiers = event.modifiers
        #self._handle_key_press(keycode, modifiers)

    def on_onboard_typing(self, key, mod_mask):
        if key.is_text_changing():
            keycode = 0
            if key.is_return():
                keycode = KeyCode.KP_Enter 
            else:
                label = key.get_label()
                if label == "C" or label == "c":
                    keycode = KeyCode.C 

            self._handle_key_press(keycode, mod_mask)

    def _handle_key_press(self, keycode, modifiers):
        if self._accessible:
            domain = self.get_text_domain()
            if domain:
                self._entering_text, end_of_editing = \
                        domain.handle_key_press(keycode, modifiers)

                if end_of_editing == True:
                    self._wp.commit_changes()
                elif end_of_editing == False:
                    self._wp.discard_changes()
 
    def _record_text_change(self, pos, length, insert):
        accessible = self._accessible

        insertion_span = None
        char_count = None
        if accessible:
            try:
                char_count = accessible.get_character_count()
            except: # gi._glib.GError: The application no longer exists
                    # when closing a tab in gnome-terminal.
                char_count = None

        if not char_count is None:
            # record the change
            spans_to_update = []
            if insert:
                #print("insert", pos, length)

                if self._entering_text and \
                   self.can_record_insertion(accessible, pos, length):
                    if self._wp.is_typing() or length < 30:
                        # Remember all of the insertion, might have been
                        # a pressed snippet or wordlist button.
                        include_length = -1
                    else:
                        # Remember only the first few characters.
                        # Large inserts can be paste, reload or scroll
                        # operations. Only learn the first word of these.
                        include_length = 2

                    # simple span for current insertion
                    begin = max(pos - 100, 0)
                    end = min(pos+length + 100, char_count)
                    text = Atspi.Text.get_text(accessible, begin, end)
                    insertion_span = TextSpan(pos, length, text, begin)
                else:
                    # Remember nothing, just update existing spans.
                    include_length = None

                spans_to_update = self._changes.insert(pos, length,
                                                      include_length)

            else:
                #print("delete", pos, length)
                spans_to_update = self._changes.delete(pos, length,
                                                       self._entering_text)

            # update text of the modified spans
            for span in spans_to_update:
                # Get some more text around the span to hopefully
                # include whole words at beginning and end.
                begin = max(span.begin() - 100, 0)
                end = min(span.end() + 100, char_count)
                span.text = Atspi.Text.get_text(accessible, begin, end)
                span.text_pos = begin

           #print(self._changes)

        self._text_changed = True

        return insertion_span

    def _update_context(self):
        self._update_context_timer.start(0.01, self.on_text_context_changed)

    def on_text_context_changed(self):
        result = self._text_domain.read_context(self._accessible)
        if not result is None:
            (self._context,
             self._line,
             self._line_cursor,
             self._span_at_cursor,
             self._begin_of_text,
             self._begin_of_text_offset) = result

            context = self.get_bot_context() # make sure to include bot-markers
            if self._last_context != context or \
               self._last_line != self._line:
                self._last_context = context
                self._last_line    = self._line
                self._wp.on_text_context_changed()

        return False
Esempio n. 13
0
class AtspiTextContext(TextContext):
    """
    Keep track of the current text context with AT-SPI
    """

    _state_tracker = AtspiStateTracker()

    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

    def cleanup(self):
        self._register_atspi_listeners(False)

    def enable(self, enable):
        self._register_atspi_listeners(enable)

    def get_text_domain(self):
        return self._text_domain

    def set_pending_separator(self, separator_span=None):
        """ Remember this separator span for later insertion. """
        if self._pending_separator_span is not separator_span:
            self._pending_separator_span = separator_span

    def get_pending_separator(self):
        """ Return current pending separator span or None """
        return self._pending_separator_span

    def get_context(self):
        """
        Returns the predictions context, i.e. some range of
        text before the caret position.
        """
        if self._accessible is None:
            return ""

        # Don't update suggestions in scrolling terminals
        if self._entering_text or \
           not self._text_changed or \
           self.can_suggest_before_typing():
            return self._context

        return ""

    def get_bot_context(self):
        """
        Returns the predictions context with
        begin of text marker (at text begin).
        """
        context = ""
        if self._accessible:
            context = self.get_context()

            # prepend domain specific begin-of-text marker
            if self._begin_of_text:
                marker = self.get_text_begin_marker()
                if marker:
                    context = marker + " " + context

        return context

    def get_pending_bot_context(self):
        """
        Context including bot marker and pending separator.
        """
        context = self.get_bot_context()
        if self._pending_separator_span is not None:
            context += self._pending_separator_span.get_span_text()
        return context

    def get_line(self):
        return self._line \
               if self._accessible else ""

    def get_line_caret_pos(self):
        return self._line_caret \
               if self._accessible else 0

    def get_line_past_caret(self):
        return self._line[self._line_caret:] \
               if self._accessible else ""

    def get_selection_span(self):
        return self._selection_span \
               if self._accessible else None

    def get_span_at_caret(self):
        if not self._accessible:
            return None

        span = self._selection_span.copy()
        span.length = 0
        return span

    def get_caret(self):
        return self._selection_span.begin() \
            if self._accessible else 0

    def get_character_extents(self, offset):
        accessible = self._accessible
        if accessible:
            return accessible.get_character_extents(offset)
        else:
            return None

    def get_text_begin_marker(self):
        domain = self.get_text_domain()
        if domain:
            return domain.get_text_begin_marker()
        return ""

    def can_record_insertion(self, accessible, pos, length):
        domain = self.get_text_domain()
        if domain:
            return domain.can_record_insertion(accessible, pos, length)
        return True

    def can_suggest_before_typing(self):
        domain = self.get_text_domain()
        if domain:
            return domain.can_suggest_before_typing()
        return True

    def can_auto_punctuate(self):
        domain = self.get_text_domain()
        if domain:
            return domain.can_auto_punctuate(self._begin_of_text)
        return False

    def get_begin_of_text_offset(self):
        return self._begin_of_text_offset \
               if self._accessible else None

    def get_changes(self):
        return self._changes

    def has_changes(self):
        """ Are there any changes to learn? """
        return not self._changes.is_empty()

    def clear_changes(self):
        self._changes.clear()

    def can_insert_text(self):
        """
        Can delete or insert text into the accessible?
        """
        # support for inserting is spotty: not in firefox, terminal
        return bool(self._accessible) and self._can_insert_text

    def delete_text(self, offset, length=1):
        """ Delete directly, without going through faking key presses. """
        self._accessible.delete_text(offset, offset + length)

    def delete_text_before_caret(self, length=1):
        """ Delete directly, without going through faking key presses. """
        try:
            caret_offset = self._accessible.get_caret_offset()
        except Exception as ex:  # Private exception gi._glib.GError when
            _logger.info("TextContext.delete_text_before_caret(): " +
                         unicode_str(ex))
            return

        self.delete_text(caret_offset - length, length)

    def insert_text(self, offset, text):
        """
        Insert directly, without going through faking key presses.
        """
        self._accessible.insert_text(offset, text)

        # Move the caret after insertion if the accessible itself
        # hasn't done so already. This assumes the insertion begins at
        # the current caret position, which always happens to be the case
        # currently.
        # Only the nautilus rename text entry appears to need this.
        offset_before = offset
        try:
            offset_after = self._accessible.get_caret_offset()
        except Exception as ex:  # Private exception gi._glib.GError when
            _logger.info("TextContext.insert_text(): " + unicode_str(ex))
            return

        if text and offset_before == offset_after:
            self._accessible.set_caret_offset(offset_before + len(text))

    def insert_text_at_caret(self, text):
        """
        Insert directly, without going through faking key presses.
        Fails for terminal and firefox, unfortunately.
        """
        try:
            caret_offset = self._accessible.get_caret_offset()
        except Exception as ex:  # Private exception gi._glib.GError when
            _logger.info("TextContext.insert_text_at_caret(): " +
                         unicode_str(ex))
            return

        self.insert_text(caret_offset, text)

    def _register_atspi_listeners(self, register=True):
        st = self._state_tracker
        if register:
            st.connect("text-entry-activated", self._on_text_entry_activated)
            st.connect("text-changed", self._on_text_changed)
            st.connect("text-caret-moved", self._on_text_caret_moved)
            # st.connect("key-pressed", self._on_atspi_key_pressed)
        else:
            st.disconnect("text-entry-activated",
                          self._on_text_entry_activated)
            st.disconnect("text-changed", self._on_text_changed)
            st.disconnect("text-caret-moved", self._on_text_caret_moved)
            # st.disconnect("key-pressed", self._on_atspi_key_pressed)

    def get_accessible_capabilities(self, accessible):
        can_insert_text = False

        if accessible:

            # Can insert text via Atspi?
            # Advantages:
            # - faster, no individual key presses
            # - trouble-free insertion of all unicode characters
            if "EditableText" in accessible.get_interfaces():
                # Support for atspi text insertion is spotty.
                # Firefox, LibreOffice Writer, gnome-terminal don't support it,
                # even if they claim to implement the EditableText interface.

                # Allow direct text insertion for gtk widgets
                if accessible.is_toolkit_gtk3():
                    can_insert_text = True

        return can_insert_text

    def _on_text_entry_activated(self, accessible):
        # old text_domain still valid here
        self._wp.on_text_entry_deactivated()

        # keep track of the active accessible asynchronously
        self._accessible = accessible
        self._entering_text = False
        self._text_changed = False

        # make sure state is filled with essential entries
        if accessible:
            accessible.get_role()
            accessible.get_attributes()
            accessible.get_interfaces()
            accessible.is_urlbar()
            state = accessible.get_state()
        else:
            state = {}

        # select text domain matching this accessible
        self._text_domain = self._text_domains.find_match(**state)
        self._text_domain.init_domain()

        # determine capabilities of this accessible
        self._can_insert_text = \
            self.get_accessible_capabilities(accessible)

        # log accessible info
        if _logger.isEnabledFor(_logger.LEVEL_ATSPI):
            log = _logger.atspi
            log("-" * 70)
            log("Accessible focused: ")
            indent = " " * 4
            if accessible:
                state = accessible.get_all_state()
                for key, value in sorted(state.items()):
                    msg = str(key) + "="
                    if key == "state-set":
                        msg += repr(AtspiStateType.to_strings(value))
                    elif hasattr(value, "value_name"):  # e.g. role
                        msg += value.value_name
                    else:
                        msg += repr(value)
                    log(indent + msg)
                log(indent + "text_domain: {}".format(
                    self._text_domain and type(self._text_domain).__name__))
                log(indent +
                    "can_insert_text: {}".format(self._can_insert_text))
            else:
                log(indent + "None")

        self._update_context()

        self._wp.on_text_entry_activated()

    def _on_text_changed(self, event):
        insertion_span = self._record_text_change(event.pos, event.length,
                                                  event.insert)
        # synchronously notify of text insertion
        if insertion_span:
            try:
                caret_offset = self._accessible.get_caret_offset()
            except Exception as ex:  # Private exception gi._glib.GError when
                _logger.info("TextContext._on_text_changed(): " +
                             unicode_str(ex))
            else:
                self._wp.on_text_inserted(insertion_span, caret_offset)

        self._last_text_change_time = time.time()
        self._update_context()

    def _on_text_caret_moved(self, event):
        self._last_caret_move_time = time.time()
        self._last_caret_move_position = event.caret
        self._update_context()
        self._wp.on_text_caret_moved()

    def _on_atspi_key_pressed(self, event):
        """ disabled, Francesco didn't receive any AT-SPI key-strokes. """
        # keycode = event.hw_code # uh oh, only keycodes...
        #                         # hopefully "c" doesn't move around a lot.
        # modifiers = event.modifiers
        # self._handle_key_press(keycode, modifiers)

    def on_onboard_typing(self, key, mod_mask):
        if key.is_text_changing():
            keycode = 0
            if key.is_return():
                keycode = KeyCode.KP_Enter
            else:
                label = key.get_label()
                if label == "C" or label == "c":
                    keycode = KeyCode.C

            self._handle_key_press(keycode, mod_mask)

    def _handle_key_press(self, keycode, modifiers):
        if self._accessible:
            domain = self.get_text_domain()
            if domain:
                self._entering_text, end_of_editing = \
                    domain.handle_key_press(keycode, modifiers)

                if end_of_editing is True:
                    self._wp.commit_changes()
                elif end_of_editing is False:
                    self._wp.discard_changes()

    def _record_text_change(self, pos, length, insert):
        accessible = self._accessible

        insertion_span = None
        char_count = None
        if accessible:
            try:
                char_count = accessible.get_character_count()
            except:  # gi._glib.GError: The application no longer exists
                # when closing a tab in gnome-terminal.
                char_count = None

        if char_count is not None:
            # record the change
            spans_to_update = []

            if insert:
                if self._entering_text and \
                   self.can_record_insertion(accessible, pos, length):
                    if self._wp.is_typing() or length < 30:
                        # Remember all of the insertion, might have been
                        # a pressed snippet or wordlist button.
                        include_length = -1
                    else:
                        # Remember only the first few characters.
                        # Large inserts can be paste, reload or scroll
                        # operations. Only learn the first word of these.
                        include_length = 2

                    # simple span for current insertion
                    begin = max(pos - 100, 0)
                    end = min(pos + length + 100, char_count)
                    try:
                        text = accessible.get_text(begin, end)
                    except Exception as ex:
                        _logger.info("TextContext._record_text_change() 1: " +
                                     unicode_str(ex))
                    else:
                        insertion_span = TextSpan(pos, length, text, begin)
                else:
                    # Remember nothing, just update existing spans.
                    include_length = None

                spans_to_update = self._changes.insert(pos, length,
                                                       include_length)

            else:
                spans_to_update = self._changes.delete(pos, length,
                                                       self._entering_text)

            # update text of all modified spans
            for span in spans_to_update:
                # Get some more text around the span to hopefully
                # include whole words at beginning and end.
                begin = max(span.begin() - 100, 0)
                end = min(span.end() + 100, char_count)
                try:
                    span.text = accessible.get_text(begin, end)
                except Exception as ex:
                    _logger.info("TextContext._record_text_change() 2: " +
                                 unicode_str(ex))
                    span.text = ""
                span.text_pos = begin

        self._text_changed = True

        return insertion_span

    def set_update_context_delay(self, delay):
        self._update_context_delay = delay

    def reset_update_context_delay(self):
        self._update_context_delay = self._update_context_delay_normal

    def _update_context(self):
        self._update_context_timer.start(self._update_context_delay,
                                         self.on_text_context_changed)

    def on_text_context_changed(self):
        # Clear pending separator when the user clicked to move
        # the cursor away from the separator position.
        if self._pending_separator_span:
            # Lone caret movement, no recent text change?
            if self._last_caret_move_time - self._last_text_change_time > 1.0:
                # Away from the separator?
                if self._last_caret_move_position != \
                   self._pending_separator_span.begin():
                    self.set_pending_separator(None)

        result = self._text_domain.read_context(self._wp, self._accessible)
        if result is not None:
            (self._context, self._line, self._line_caret, self._selection_span,
             self._begin_of_text, self._begin_of_text_offset) = result

            # make sure to include bot-markers and pending separator
            context = self.get_pending_bot_context()
            change_detected = (self._last_context != context
                               or self._last_line != self._line)
            if change_detected:
                self._last_context = context
                self._last_line = self._line

            self._wp.on_text_context_changed(change_detected)

        return False