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 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
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)
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
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)
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)
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
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 read_context(self, accessible): return "", "", 0, TextSpan(), False, 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
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
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