# -*- coding: utf-8 -*- # Copyright © 2007 Martin Böhme # Copyright © 2007-2009 Chris Jones # Copyright © 2010 Francesco Fumanti # Copyright © 2012 Gerd Kohlberger # Copyright © 2009, 2011-2017 marmuta # # This file is part of Onboard. # # Onboard is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # Onboard is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from __future__ import division, print_function, unicode_literals import time import weakref import gc from contextlib import contextmanager from gi.repository import Gdk, GLib import logging _logger = logging.getLogger(__name__) from Onboard.Version import require_gi_versions require_gi_versions() try: from gi.repository import Atspi except ImportError as e: _logger.warning("Atspi typelib missing, at-spi key-synth unavailable") from Onboard import KeyCommon from Onboard.KeyCommon import StickyBehavior from Onboard.KeyboardPopups import TouchFeedback from Onboard.Sound import Sound from Onboard.ClickSimulator import (ClickSimulator, CSButtonMapper, CSFloatingSlave) from Onboard.Scanner import Scanner from Onboard.Timer import Timer, ProgressiveDelayTimer from Onboard.utils import (Modifiers, LABEL_MODIFIERS, parse_key_combination) from Onboard.definitions import (Handle, UIMask, KeySynthEnum, UINPUT_DEVICE_NAME) from Onboard.AutoShow import AutoShow from Onboard.AutoHide import AutoHide from Onboard.WordSuggestions import WordSuggestions from Onboard.canonical_equivalents import canonical_equivalents import Onboard.osk as osk try: from Onboard.utils import run_script, get_keysym_from_name, dictproperty except DeprecationWarning: pass from Onboard.Config import Config config = Config() class EventType: """ enum of event types for key press/release """ ( CLICK, DOUBLE_CLICK, DWELL, ) = range(3) class DockMode: """ enum dock mode """ ( FLOATING, BOTTOM, TOP, ) = range(3) class ModSource: """ enum of sources of modifier changes """ ( KEYBOARD, KEYSYNTH, ) = range(2) class UnpressTimers: """ Redraw keys unpressed after a short while. There are multiple timers to suppurt multi-touch. """ def __init__(self, keyboard): self._keyboard = keyboard self._timers = {} def start(self, key): timer = self._timers.get(key) if not timer: timer = Timer() self._timers[key] = timer timer.start(config.UNPRESS_DELAY, self.on_timer, key) def stop(self, key): timer = self._timers.get(key) if timer: timer.stop() del self._timers[key] def cancel_all(self): for key, timer in self._timers.items(): Timer.stop(timer) key.pressed = False def finish(self, key): timer = self._timers.get(key) if timer: timer.stop() self.unpress(key) def on_timer(self, key): self.unpress(key) self.stop(key) return False def unpress(self, key): if key.pressed: key.pressed = False self._keyboard.on_key_unpressed(key) class KeySynth(object): _last_press_time = 0 _suppress_keypress_delay = False @staticmethod @contextmanager def no_delay(): """ Temporarily disable the keypress delay. Do not nest. Mainly used for single key-strokes as there are far fewer calls for these than the bulk text insertion calls. """ KeySynth._suppress_keypress_delay = True yield None KeySynth._suppress_keypress_delay = False def _delay_keypress(self): """ Pause between multiple key-strokes. Firefox and Thunderbird may need this to not miss key-strokes. """ delay = config.keyboard.inter_key_stroke_delay if delay: # not just single presses? if not KeySynth._suppress_keypress_delay: elapsed = time.time() - KeySynth._last_press_time remaining = delay - elapsed if remaining > 0.0: time.sleep(remaining) KeySynth._last_press_time = time.time() class KeySynthVirtkey(KeySynth): """ Synthesize key strokes with python-virtkey """ def __init__(self, keyboard, vk): self._keyboard = keyboard self._vk = vk def cleanup(self): self._vk = None def press_unicode(self, char): _logger.debug("KeySynthVirtkey.press_unicode({})".format(repr(char))) if self._vk: keysym = self._vk.keysym_from_unicode(char) self.press_keysym(keysym) def release_unicode(self, char): _logger.debug("KeySynthVirtkey.release_unicode({})".format(repr(char))) if self._vk: keysym = self._vk.keysym_from_unicode(char) self.release_keysym(keysym) def press_keysym(self, keysym): _logger.debug("KeySynthVirtkey.press_keysym({})".format(keysym)) if self._vk: keycode, mod_mask = self._vk.keycode_from_keysym(keysym) # need modifiers for this keysym? if mod_mask: self._keyboard.lock_temporary_modifiers( ModSource.KEYSYNTH, mod_mask) self.press_keycode(keycode) def release_keysym(self, keysym): _logger.debug("KeySynthVirtkey.release_keysym({})".format(keysym)) if self._vk: keycode, mod_mask = self._vk.keycode_from_keysym(keysym) self.release_keycode(keycode) self._keyboard.unlock_temporary_modifiers(ModSource.KEYSYNTH) def press_keycode(self, keycode): _logger.debug("KeySynthVirtkey.press_keycode({})".format(keycode)) if self._vk: self._delay_keypress() self._vk.press_keycode(keycode) def release_keycode(self, keycode): _logger.debug("KeySynthVirtkey.release_keycode({})".format(keycode)) if self._vk: self._vk.release_keycode(keycode) def get_current_group(self): return self._vk.get_current_group() def lock_group(self, group): if self._vk: self._vk.lock_group(group) def lock_mod(self, mod_mask): if self._vk: self._vk.lock_mod(mod_mask) def unlock_mod(self, mod_mask): if self._vk: self._vk.unlock_mod(mod_mask) def press_key_string(self, keystr): """ Send key presses for all characters in a unicode string. """ keystr = keystr.replace("\\n", "\n") # for new lines in snippets if self._vk: # may be None in the last call before exiting for ch in keystr: if ch == "\b": # backspace? keysym = get_keysym_from_name("backspace") self.press_keysym(keysym) self.release_keysym(keysym) elif ch == "\n": # press_unicode("\n") fails in gedit. # -> explicitely send the key symbol instead keysym = get_keysym_from_name("return") self.press_keysym(keysym) self.release_keysym(keysym) else: # any other printable keys self.press_unicode(ch) self.release_unicode(ch) class KeySynthAtspi(KeySynthVirtkey): """ Synthesize key strokes with AT-SPI Not really useful anymore, as key generation there doesn't fit Onboard's requirements very well, e.g. there is no consistent separation between press and release events. Also some unexpected key sequences are not faithfully reproduced. """ def __init__(self, keyboard, vk): super(KeySynthAtspi, self).__init__(keyboard, vk) def press_keycode(self, keycode): if "Atspi" not in globals(): return self._delay_keypress() Atspi.generate_keyboard_event(keycode, "", Atspi.KeySynthType.PRESS) def release_keycode(self, keycode): if "Atspi" not in globals(): return Atspi.generate_keyboard_event(keycode, "", Atspi.KeySynthType.RELEASE) def press_key_string(self, string): if "Atspi" not in globals(): return Atspi.generate_keyboard_event(0, string, Atspi.KeySynthType.STRING) class TextChanger(): """ Abstract base class of TextChangers. """ def __init__(self, keyboard, vk): self.keyboard = keyboard self.vk = vk def cleanup(self): self.keyboard = None self.vk = None class TextChangerKeyStroke(TextChanger): """ Insert and delete text with key-strokes. - KeySynthVirtkey - KeySynthAtspi (not used by default) """ def __init__(self, keyboard, vk): TextChanger.__init__(self, keyboard, vk) self._key_synth_virtkey = KeySynthVirtkey(keyboard, vk) self._key_synth_atspi = KeySynthAtspi(keyboard, vk) self._update_key_synth() def _update_key_synth(self): key_synth_id = KeySynthEnum(config.keyboard.key_synth) if key_synth_id == KeySynthEnum.AUTO: key_synth_candidates = [ KeySynthEnum.XTEST, KeySynthEnum.UINPUT, KeySynthEnum.ATSPI] else: key_synth_candidates = [key_synth_id] _logger.debug("Key-synth candidates: {}" .format(key_synth_candidates)) key_synth_id = None key_synth = None vk = self.vk for id_ in key_synth_candidates: if id_ == KeySynthEnum.ATSPI: key_synth = self._key_synth_atspi key_synth_id = id_ break else: if not vk: _logger.debug("Key-synth '{}' unavailable: vk is None") else: key_synth = self._key_synth_virtkey try: if id_ == KeySynthEnum.XTEST: vk.select_backend(vk.BACKEND_XTEST) elif id_ == KeySynthEnum.UINPUT: vk.select_backend(vk.BACKEND_UINPUT, UINPUT_DEVICE_NAME) key_synth_id = id_ break except osk.error as ex: _logger.debug("Key-synth '{}' unavailable: {}" .format(id_, ex)) _logger.info("Using key-synth '{}'" .format(key_synth_id)) self._key_synth = key_synth def cleanup(self): # Somehow keyboard objects don't get released # when switching layouts, there are still # excess references/memory leaks somewhere. # We need to manually release virtkey references or # Xlib runs out of client connections after a couple # dozen layout switches. if self._key_synth_virtkey: self._key_synth_virtkey.cleanup() self._key_synth_virtkey = None if self._key_synth_atspi: self._key_synth_atspi.cleanup() self._key_synth_atspi = None TextChanger.cleanup(self) # KeySynth interface def press_unicode(self, char): self._key_synth.press_unicode(char) def release_unicode(self, char): self._key_synth.release_unicode(char) def press_keycode(self, keycode): self._key_synth.press_keycode(keycode) def release_keycode(self, keycode): self._key_synth.release_keycode(keycode) def press_keysym(self, keysym): self._key_synth.press_keysym(keysym) def release_keysym(self, keysym): self._key_synth.release_keysym(keysym) def get_current_group(self): return self._key_synth.get_current_group() def lock_group(self, group): self._key_synth.lock_group(group) def lock_mod(self, mod): self._key_synth.lock_mod(mod) def unlock_mod(self, mod): self._key_synth.unlock_mod(mod) # Higher-level functions def press_key_string(self, string): self._key_synth.press_key_string(string) def press_keysyms(self, key_name, count=1): """ Generate any number of full key-strokes for the given named key symbol. """ keysym = get_keysym_from_name(key_name) for i in range(count): self.press_keysym(keysym) self.release_keysym(keysym) def insert_string_at_caret(self, text): """ Insert text at the caret position. """ self._key_synth.press_key_string(text) def delete_at_caret(self): with self.keyboard.suppress_modifiers(): self.press_keysyms("backspace") class TextChangerDirectInsert(TextChanger): """ Insert and delete text by direct insertion/deletion. - Direct insertion/deletion via AtspiTextContext """ def __init__(self, keyboard, vk, tcks): TextChanger.__init__(self, keyboard, vk) self.text_changer_key_stroke = tcks delay, interval = vk.get_auto_repeat_rate() \ if vk else (500, 30) self._auto_repeat_delay = delay * 0.001 self._auto_repeat_interval = interval * 0.001 self._auto_repeat_delay_timer = Timer() self._auto_repeat_timer = Timer() _logger.debug("keyboard auto-repeat: delay {}, interval {}" .format(self._auto_repeat_delay, self._auto_repeat_interval)) def cleanup(self): self.stop_auto_repeat() TextChanger.cleanup(self) def get_text_context(self): return self.keyboard.text_context def _insert_unicode(self, char): text_context = self.get_text_context() if text_context: text_context.insert_text_at_caret(char) def _start_auto_repeat(self, char): self._auto_repeat_delay_timer.start(self._auto_repeat_delay, self._on_auto_repeat_delay_timer, char) def stop_auto_repeat(self): self._auto_repeat_delay_timer.stop() self._auto_repeat_timer.stop() def _on_auto_repeat_delay_timer(self, char): self._auto_repeat_timer.start(self._auto_repeat_interval, self._on_auto_repeat_timer, char) return False def _on_auto_repeat_timer(self, char): self._insert_unicode(char) return True # KeySynth interface def press_keycode(self, keycode): """ Use key-strokes because of dead keys, hot keys and editing keys. """ self.stop_auto_repeat() self.text_changer_key_stroke.press_keycode(keycode) def release_keycode(self, keycode): self.text_changer_key_stroke.release_keycode(keycode) def press_keysym(self, keysym): """ Use key-strokes because of dead keys, hot keys and editing keys. """ self.stop_auto_repeat() self.text_changer_key_stroke.press_keysym(keysym) def release_keysym(self, keysym): self.text_changer_key_stroke.release_keysym(keysym) def press_unicode(self, char): self._insert_unicode(char) self._start_auto_repeat(char) def release_unicode(self, char): self.stop_auto_repeat() def get_current_group(self): return self.text_changer_key_stroke.get_current_group() def lock_group(self, group): self.text_changer_key_stroke.lock_group(group) def lock_mod(self, mod): """ We still have to lock mods for pointer clicks with modifiers and hot-keys. """ self.text_changer_key_stroke.lock_mod(mod) def unlock_mod(self, mod): self.text_changer_key_stroke.unlock_mod(mod) # Higher-level functions def press_key_string(self, string): pass def press_keysyms(self, key_name, count=1): """ Generate any number of full key-strokes for the given named key symbol. """ self.text_changer_key_stroke.press_keysyms(key_name, count) def insert_string_at_caret(self, text): """ Insert text at the caret position. """ text_context = self.get_text_context() text = text.replace("\\n", "\n") text_context.insert_text_at_caret(text) def delete_at_caret(self): text_context = self.get_text_context() text_context.delete_text_before_caret(1) class Keyboard(WordSuggestions): """ Central keyboard model """ color_scheme = None _layer_locked = False _last_alt_key = None _alt_locked = False _click_sim = None LOCK_REASON_KEY_PRESSED = "key-pressed" # Properties # The number of pressed keys per modifier _mods = {1: 0, 2: 0, 4: 0, 8: 0, 16: 0, 32: 0, 64: 0, 128: 0} # Same to keep track of modifier changes triggered from the outside. # Doesn't include modifier changes caused by Onboard itself, so this is # not a complete representation of the modifier state. _external_mod_changes = {1: 0, 2: 0, 4: 0, 8: 0, 16: 0, 32: 0, 64: 0, 128: 0} def _get_mod(self, key): return self._mods[key] def _set_mod(self, key, value): self._mods[key] = value self.on_mods_changed() mods = dictproperty(_get_mod, _set_mod) def on_mods_changed(self): pass def get_mod_mask(self): """ Bit-mask of curently active modifers. """ return sum(mask for mask in (1 << bit for bit in range(8)) if self.mods[mask]) # bit mask of current modifiers @contextmanager def suppress_modifiers(self, modifiers=LABEL_MODIFIERS): """ Turn modifiers off temporarily. May be nested. """ self._push_and_clear_modifiers(modifiers) yield None self._pop_and_restore_modifiers() def _push_and_clear_modifiers(self, modifiers): mods = {mod : key for mod, key in self._mods.items() if mod & modifiers} self._suppress_modifiers_stack.append(mods) for mod, nkeys in mods.items(): if nkeys: self._mods[mod] = 0 self.get_text_changer().unlock_mod(mod) def _pop_and_restore_modifiers(self): mods = self._suppress_modifiers_stack.pop() for mod, nkeys in mods.items(): if nkeys: self._mods[mod] = nkeys self.get_text_changer().lock_mod(mod) # currently active layer def _get_active_layer_index(self): return config.active_layer_index def _set_active_layer_index(self, index): config.active_layer_index = index active_layer_index = property(_get_active_layer_index, _set_active_layer_index) def _get_active_layer(self): layers = self.get_layers() if not layers: return None index = self.active_layer_index if index < 0 or index >= len(layers): index = 0 return layers[index] def _set_active_layer(self, layer): index = 0 for i, layer in enumerate(self.get_layers()): if layer is layer: index = i break self.active_layer_index = index active_layer = property(_get_active_layer, _set_active_layer) def assure_valid_active_layer(self): """ Reset layer index if it is out of range. e.g. due to loading a layout with fewer panes. """ index = self.active_layer_index if index < 0 or index >= len(self.get_layers()): self.active_layer_index = 0 ################## def __init__(self, application): WordSuggestions.__init__(self) self._application = weakref.ref(application) self._pressed_key = None self._last_typing_time = 0 self._last_typed_was_separator = False self._temporary_modifiers = None self._locked_temporary_modifiers = {} self._suppress_modifiers_stack = [] self._capitalization_requested = False self.layout = None self.scanner = None self.button_controllers = {} self.editing_snippet = False self._layout_views = [] self._unpress_timers = UnpressTimers(self) self._touch_feedback = TouchFeedback() self._raise_timer = ProgressiveDelayTimer() self._auto_show = AutoShow(self) self._auto_show.enable(config.is_auto_show_enabled()) self._auto_hide = AutoHide(self) self._auto_hide.enable(config.is_auto_hide_enabled()) self.text_changer_key_stroke = None self.text_changer_direct_insert = None self._invalidated_ui = 0 self._pressed_keys = [] self._latched_sticky_keys = [] self._locked_sticky_keys = [] self._non_modifier_released = False self._disabled_keys = None self._pending_modifier_redraws = {} self._pending_modifier_redraws_timer = Timer() self._visibility_locked = False self._visibility_requested = None self.reset() def reset(self): """ init/reset on layout change """ WordSuggestions.reset(self) if self._auto_show: self._auto_show.reset() self.stop_raise_attempts() # Keep caps-lock state on layout change to prevent LP #1313176. # Otherwise, a caps press causes a layout change, cleanup # triggers another caps press that again causes a layout change, # and so on... # See OnboardGtk.reload_layout_delayed for the other part of the # puzzle for this bug report. self.ignore_capslock() # reset still latched and locked modifier keys on exit self.release_latched_sticky_keys() # NumLock is special. Keep its state on exit, except when # sticky_key_release_delay is set, then we assume to be # in kiosk mode and everything has to be cleaned up. release_all = bool(config.keyboard.sticky_key_release_delay) self.release_locked_sticky_keys(release_all) self.release_pressed_keys() self._pressed_keys = [] self._latched_sticky_keys = [] self._locked_sticky_keys = [] self._non_modifier_released = False self._disabled_keys = None self.layout = None self._pending_modifier_redraws_timer.stop() self._pending_modifier_redraws = {} self.unlock_visibility() def cleanup(self): """ final cleanup on exit """ self.reset() WordSuggestions.cleanup(self) if self._auto_show: self._auto_show.cleanup() self._auto_show = None if self._auto_hide: self._auto_hide.cleanup() self._auto_hide = None if self.text_changer_key_stroke: self.text_changer_key_stroke.cleanup() self.text_changer_key_stroke = None if self.text_changer_direct_insert: self.text_changer_direct_insert.cleanup() self.text_changer_direct_insert = None if self._click_sim: self._click_sim.cleanup() self._click_sim = None def get_application(self): return self._application() def register_view(self, layout_view): self._layout_views.append(layout_view) def deregister_view(self, layout_view): if layout_view in self._layout_views: self._layout_views.remove(layout_view) def get_main_view(self): layout_views = self._layout_views if layout_views: return layout_views[0] return None def is_visible(self): for view in self._layout_views: visible = view.is_visible() if visible is not None: return visible def set_visible(self, visible): self.unlock_visibility() # unlock frequenty in case of stuck keys self.update_auto_show_on_visibility_change(visible) if not visible: self.hide_touch_feedback() for view in self._layout_views: view.set_visible(visible) def toggle_visible(self): """ main method to show/hide onboard manually """ self.set_visible(not self.is_visible()) def request_visibility(self, visible): """ Request to change visibility when all keys have been released. """ if self._visibility_locked: self._visibility_requested = visible else: self.set_visible(visible) def request_visibility_toggle(self): if self._visibility_locked and \ self._visibility_requested is not None: visible = self._visibility_requested else: visible = self.is_visible() self.request_visibility(not visible) def lock_visibility(self): """ Lock all showing/hiding, but remember requests to do so. """ self._visibility_locked = True self.auto_show_lock(self.LOCK_REASON_KEY_PRESSED) def unlock_visibility(self): """ Unlock all showing/hiding. """ self._visibility_locked = False self._visibility_requested = None def unlock_and_apply_visibility(self): """ Unlock all showing/hiding and apply the last request to do so. """ if self._visibility_locked: visible = self._visibility_requested self.unlock_visibility() if visible is not None: self.set_visible(visible) # Unlock auto-show, and if the state has changed since locking, # transition to hide the keyboard. self.auto_show_unlock_and_apply_visibility( self.LOCK_REASON_KEY_PRESSED) def redraw(self, keys=None, invalidate=True): for view in self._layout_views: view.redraw(keys, invalidate) def process_updates(self): for view in self._layout_views: view.process_updates() def redraw_labels(self, invalidate=True): for view in self._layout_views: view.redraw_labels(invalidate) def has_input_sequences(self): for view in self._layout_views: if view.has_input_sequences(): return True return False def update_transparency(self): for view in self._layout_views: view.update_transparency() def update_input_event_source(self): """ Input event source changed, tell all views. """ for view in self._layout_views: view.update_input_event_source() self.update_click_sim() self.update_auto_hide() def update_touch_input_mode(self): """ Touch input mode has changed, tell all views. """ for view in self._layout_views: view.update_touch_input_mode() def update_click_sim(self): if config.is_event_source_xinput(): # XInput click simulator # Recommended, but requires the XInput event source. clicksim = CSFloatingSlave(self) # Fall back to button mapper if XInput 2.2 is unavaliable if not clicksim.is_valid(): _logger.warning("XInput click simulator CSFloatingSlave " "unavailable, " "falling back to CSButtonMapper.") clicksim = CSButtonMapper() else: # Button mapper # Works with any event source, but may fail on touch-screens. clicksim = CSButtonMapper() if self._click_sim: self._click_sim.cleanup() self._click_sim = clicksim self._click_sim.state_notify_add(self._on_click_sim_state_notify) _logger.info("using click simulator '{}'" .format(type(self._click_sim).__name__)) def _on_click_sim_state_notify(self, x): self.invalidate_context_ui() self.commit_ui_updates() def show_touch_handles(self, show, auto_hide=True): for view in self._layout_views: view.show_touch_handles(show, auto_hide) def set_layout(self, layout, color_scheme, vk): """ set or replace the current keyboard layout """ self.reset() self.set_virtkey(vk) self.layout = layout self.color_scheme = color_scheme self.on_layout_loaded() def on_layout_loaded(self): """ called when the layout has been loaded """ # hide all still visible feedback popups; keys have changed. self._touch_feedback.hide() self._connect_button_controllers() self.assure_valid_active_layer() WordSuggestions.on_layout_loaded(self) self.update_modifiers() self.update_scanner_enabled() # notify views for view in self._layout_views: view.on_layout_loaded() # redraw everything self.invalidate_ui() self.commit_ui_updates() def set_virtkey(self, vk): self._init_text_changers(vk) def _init_text_changers(self, vk): self.text_changer_key_stroke = \ TextChangerKeyStroke(self, vk) self.text_changer_direct_insert = \ TextChangerDirectInsert(self, vk, self.text_changer_key_stroke) def get_text_changer(self): text_context = self.text_context if text_context.can_insert_text(): return self.text_changer_direct_insert else: return self.text_changer_key_stroke def _connect_button_controllers(self): """ connect button controllers to button keys """ self.button_controllers = {} # connect button controllers to button keys types = {type.id : type for type in [BCMiddleClick, BCSingleClick, BCSecondaryClick, BCDoubleClick, BCDragClick, BCHoverClick, BCHide, BCShowClick, BCMove, BCBluetooth, BCTest, BCPreferences, BCQuit, BCExpandCorrections, BCPreviousPredictions, BCNextPredictions, BCPauseLearning, BCLanguage, BCStealthMode, BCAutoLearn, BCAutoPunctuation, BCInputline, ]} for key in self.layout.iter_global_keys(): if key.is_layer_button(): bc = BCLayer(self, key) bc.layer_index = key.get_layer_index() self.button_controllers[key] = bc else: type = types.get(key.id) if type: self.button_controllers[key] = type(self, key) def update_scanner_enabled(self): """ Enable keyboard scanning if it is enabled in gsettings. """ self.update_input_event_source() self.enable_scanner(config.scanner.enabled) def enable_scanner(self, enable): """ Enable keyboard scanning. """ if enable: if not self.scanner: self.scanner = Scanner(self._on_scanner_redraw, self._on_scanner_activate) if self.layout: self.scanner.update_layer(self.layout, self.active_layer) else: _logger.warning("Failed to update scanner. No layout.") else: if self.scanner: self.scanner.finalize() self.scanner = None def _on_scanner_enabled(self, enabled): """ Config callback for scanner.enabled changes. """ self.update_scanner_enabled() self.update_transparency() def _on_scanner_redraw(self, keys): """ Scanner callback for redraws. """ self.redraw(keys) def _on_scanner_activate(self, key): """ Scanner callback for key activation. """ self.key_down(key) self.key_up(key) def get_layers(self): if self.layout: return self.layout.get_layer_ids() return [] def iter_keys(self, group_name=None): """ iterate through all keys or all keys of a group """ if self.layout: return self.layout.iter_keys(group_name) else: return [] def _on_mods_changed(self): self.invalidate_context_ui() self.commit_ui_updates() def get_pressed_key(self): return self._pressed_key def set_currently_typing(self): """ Remember it was us that just typed text. """ self._last_typing_time = time.time() def is_typing(self): """ Is Onboard currently or was it just recently sending any text? """ key = self.get_pressed_key() return key and self._is_text_insertion_key(key) or \ time.time() - self._last_typing_time <= 0.5 def set_last_typed_was_separator(self, value): self._last_typed_was_separator = value def get_last_typed_was_separator(self): return self._last_typed_was_separator def _is_text_insertion_key(self, key): """ Does key actually insert any characters (not a navigation key)? """ return key and key.is_text_changing() def key_down(self, key, view=None, sequence=None, action=True): """ Press down on one of Onboard's key representations. This may be either an initial press, or a switch of the active_key due to dragging. """ self.on_any_key_down() self.stop_raise_attempts() if sequence: button = sequence.button event_type = sequence.event_type else: button = 1 event_type = EventType.CLICK # Stop garbage collection delays until key release. They might cause # unexpected key repeats on slow systems. if gc.isenabled(): gc.disable() if key and \ key.sensitive: # Stop hiding the keyboard until all keys have been released. self.lock_visibility() # stop timed redrawing for this key self._unpress_timers.stop(key) # announce temporary modifiers temp_mod_mask = 0 if config.keyboard.can_upper_case_on_button(button): temp_mod_mask = Modifiers.SHIFT self._set_temporary_modifiers(temp_mod_mask) self._update_temporary_key_label(key, temp_mod_mask) # mark key pressed key.pressed = True self.on_key_pressed(key, view, sequence, action) # Get drawing behind us now, so it can't delay processing key_up() # and cause unwanted key repeats on slow systems. self.redraw([key]) self.process_updates() # perform key action (not just dragging)? if action: self._do_key_down_action(key, view, button, event_type) # Make note that it was us who just sent text # (vs. at-spi update due to scrolling, physical typing, ...). # -> disables set_modifiers() for the case that virtkey # just locked temporary modifiers. if self._is_text_insertion_key(key): self.set_currently_typing() # remember as pressed key if key not in self._pressed_keys: self._pressed_keys.append(key) def key_up(self, key, view=None, sequence=None, action=True): """ Release one of Onboard's key representations. """ if sequence: button = sequence.button event_type = sequence.event_type else: button = 1 event_type = EventType.CLICK if key and \ key.sensitive: # Was the key nothing but pressed before? extend_pressed_state = key.is_pressed_only() # perform key action? # (not just dragging or canceled due to long press) if action: # If there was no down action yet (dragging), catch up now if not key.activated: self._do_key_down_action(key, view, button, event_type) self._do_key_up_action(key, view, button, event_type) # Skip context and button controller updates for the common # letter press to improve responsiveness on slow systems. if key.type == KeyCommon.BUTTON_TYPE: self.invalidate_context_ui() # no action but key was activated: must have been a long press elif key.activated: # switch to layer 0 after long pressing snippet buttons if key.type == KeyCommon.MACRO_TYPE: self.maybe_switch_to_first_layer(key) # Is the key still nothing but pressed? extend_pressed_state = (extend_pressed_state and key.is_pressed_only() and action) # Draw key unpressed to remove the visual feedback. if extend_pressed_state and \ not config.scanner.enabled: # Keep key pressed for a little longer for clear user feedback. self._unpress_timers.start(key) else: # Unpress now to avoid flickering of the # pressed color after key release. key.pressed = False self.on_key_unpressed(key) # no more actions left to finish key.activated = False # remove from list of pressed keys if key in self._pressed_keys: self._pressed_keys.remove(key) # Make note that it was us who just sent text # (vs. at-spi update due to scrolling, physical typing, ...). if self._is_text_insertion_key(key): self.set_currently_typing() # This key might have caused a completion popup to open, # e.g. the firefox URL-bar popup. # -> attempt to raise the keyboard over the popup # Disabled because only raising isn't enough for most # drop-downs. if False and \ action and \ config.is_force_to_top() and \ not self.has_focusable_gui() and \ not config.xid_mode: self.raise_ui_delayed() # Was this the final touch sequence? if not self.has_input_sequences(): self._non_modifier_released = False self._pressed_keys = [] self._pressed_key = None self.on_all_keys_up() gc.enable() # Allow hiding the keyboard again (LP #1648543). self.unlock_and_apply_visibility() # Process pending UI updates self.commit_ui_updates() def key_long_press(self, key, view=None, button=1): """ Long press of one of Onboard's key representations. """ long_pressed = False key_type = key.type if not config.xid_mode: # Is there a popup definition in the layout? sublayout = key.get_popup_layout() if sublayout: view.show_popup_layout(key, sublayout) long_pressed = True elif key_type == KeyCommon.BUTTON_TYPE: # Buttons decide for themselves what is to happen. controller = self.button_controllers.get(key) if controller: controller.long_press(view, button) elif key.is_prediction_key(): view.show_prediction_menu(key, button) long_pressed = True elif key_type == KeyCommon.MACRO_TYPE: snippet_id = int(key.code) self._edit_snippet(view, snippet_id) long_pressed = True else: # All other keys get hard-coded long press menus # (where available). action = self.get_key_action(key) if action == KeyCommon.DELAYED_STROKE_ACTION and \ not key.is_word_suggestion(): label = key.get_label() alternatives = self.find_canonical_equivalents(label) if alternatives: self._touch_feedback.hide(key) view.show_popup_alternative_chars(key, alternatives) long_pressed = True if long_pressed: key.activated = True # no more drag selection return long_pressed def _do_key_down_action(self, key, view, button, event_type): # generate key-stroke action = self.get_key_action(key) can_send_key = ((not key.sticky or not key.active) and not action == KeyCommon.DELAYED_STROKE_ACTION) if can_send_key: self.send_key_down(key, view, button, event_type) # Modifier keys may change multiple keys # -> redraw all dependent keys # no danger of key repeats due to delays # -> redraw asynchronously if can_send_key and key.is_modifier(): self.redraw_labels(False) if key.type == KeyCommon.BUTTON_TYPE: controller = self.button_controllers.get(key) if controller: key.activated = controller.is_activated_on_press() def _do_key_up_action(self, key, view, button, event_type): if key.sticky: # Multi-touch release? if key.is_modifier() and \ not self._can_cycle_modifiers(): can_send_key = True else: # single touch/click can_send_key = self.step_sticky_key(key, button, event_type) if can_send_key: self.send_key_up(key, view) if key.is_modifier(): self.redraw_labels(False) else: self._release_non_sticky_key(key, view, button, event_type) # Multi-touch: temporarily stop cycling modifiers if # a non-modifier key was pressed. This way we get both, # cycling latched and locked state with single presses # and press-only action for multi-touch modifer + key press. if not key.is_modifier(): self._non_modifier_released = True def send_key_down(self, key, view, button, event_type): if self.is_key_disabled(key): _logger.debug("send_key_down: " "rejecting blacklisted key action for '{}'" .format(key.id)) return modifier = key.modifier if modifier == Modifiers.ALT and \ self._is_alt_special(): self._last_alt_key = key else: action = self.get_key_action(key) if action != KeyCommon.DELAYED_STROKE_ACTION: WordSuggestions.on_before_key_press(self, key) self._maybe_send_alt_press_for_key(key, view, button, event_type) self._maybe_lock_temporary_modifiers_for_key(key) self.send_key_press(key, view, button, event_type) if action == KeyCommon.DOUBLE_STROKE_ACTION: # e.g. CAPS self.send_key_release(key, view, button, event_type) if modifier: self._do_lock_modifiers(modifier) # Update word suggestions on shift press. self.invalidate_context_ui() key.activated = True # modifiers set -> can't undo press anymore def send_key_up(self, key, view=None, button=1, event_type=EventType.CLICK): if self.is_key_disabled(key): _logger.debug("send_key_up: " "rejecting blacklisted key action for '{}'" .format(key.id)) return key_type = key.type modifier = key.modifier action = self.get_key_action(key) # Unlock most modifiers before key release, otherwise Compiz wall # plugin's viewport switcher window doesn't close after # Alt+Ctrl+Up/Down (LP: #1532254). if modifier and \ action != KeyCommon.DOUBLE_STROKE_ACTION: # not NumLock, CAPS self._do_unlock_modifiers(modifier) # Update word suggestions on shift unlatch or release. self.invalidate_context_ui() # generate key event(s) if modifier == Modifiers.ALT and \ self._is_alt_special(): pass else: if action == KeyCommon.DOUBLE_STROKE_ACTION or \ action == KeyCommon.DELAYED_STROKE_ACTION: WordSuggestions.on_before_key_press(self, key) self._maybe_send_alt_press_for_key(key, view, button, event_type) self._maybe_lock_temporary_modifiers_for_key(key) if key_type == KeyCommon.CHAR_TYPE: # allow direct text insertion by AT-SPI for char keys self.get_text_changer().insert_string_at_caret(key.code) else: self.send_key_press(key, view, button, event_type) self.send_key_release(key, view, button, event_type) else: self.send_key_release(key, view, button, event_type) # Unlock NumLock, CAPS, etc. after key events were sent, # else they are toggled right back on. if modifier and \ action == KeyCommon.DOUBLE_STROKE_ACTION: self._do_unlock_modifiers(modifier) # Update word suggestions on shift unlatch or release. self.invalidate_context_ui() self._maybe_unlock_temporary_modifiers() self._maybe_send_alt_release_for_key(key, view, button, event_type) # Check modifier counts for plausibility. # There might be a bug lurking that gets certain modifers stuck # with negative counts. Work around this and be verbose about it # so we can fix it eventually. # Seems fixed in 0.99, but keep the check just in case. # Happens again since 1.1.0 when using physical keyboards in # parallel with Onboard. Occasionally we fail to detect where a # modifier change originated from. for mod, nkeys in self._mods.items(): if nkeys < 0: _logger.warning("Negative count {} for modifier {}, reset." .format(self.mods[modifier], modifier)) self.mods[mod] = 0 # Reset this too, else unlatching won't happen until restart. self._external_mod_changes[mod] = 0 def _update_temporary_key_label(self, key, temp_mod_mask): """ update label for temporary modifiers """ mod_mask = self.get_mod_mask() temp_mod_mask |= mod_mask if key.mod_mask != temp_mod_mask: key.configure_label(temp_mod_mask) def _set_temporary_modifiers(self, mod_mask): """ Announce the intention to lock these modifiers on key-press. """ # only some single modifiers supported at this time if not mod_mask or \ mod_mask in (Modifiers.SHIFT, Modifiers.CAPS, Modifiers.CTRL, Modifiers.SUPER, Modifiers.ALTGR): self._temporary_modifiers = mod_mask def _maybe_lock_temporary_modifiers_for_key(self, key): """ Lock modifier before a single key-press """ modifier = self._temporary_modifiers if modifier and \ not key.modifier == modifier and \ not key.is_button(): self.lock_temporary_modifiers(ModSource.KEYBOARD, modifier) def _maybe_unlock_temporary_modifiers(self): """ Unlock modifier after a single key-press """ self.unlock_all_temporary_modifiers() def lock_temporary_modifiers(self, mod_source_id, mod_mask): """ Lock temporary modifiers """ stack = self._locked_temporary_modifiers.setdefault(mod_source_id, []) stack.append(mod_mask) _logger.debug("lock_temporary_modifiers({}, {}) {}" .format(mod_source_id, mod_mask, self._locked_temporary_modifiers)) self._do_lock_modifiers(mod_mask) def unlock_temporary_modifiers(self, mod_source_id): """ Unlock temporary modifiers """ stack = self._locked_temporary_modifiers.get(mod_source_id) if stack: mod_mask = stack.pop() _logger.debug("unlock_temporary_modifiers({}, {}) {}" .format(mod_source_id, mod_mask, self._locked_temporary_modifiers)) self._do_unlock_modifiers(mod_mask) def unlock_all_temporary_modifiers(self): """ Unlock all temporary modifiers """ if self._locked_temporary_modifiers: mod_counts = {} for mod_source_id, stack in \ self._locked_temporary_modifiers.items(): for mod_mask in stack: for mod_bit in (1 << bit for bit in range(8)): if mod_mask & mod_bit: mod_counts[mod_bit] = \ mod_counts.setdefault(mod_bit, 0) + 1 self._locked_temporary_modifiers = {} _logger.debug("unlock_all_temporary_modifiers() {}" .format(self._locked_temporary_modifiers)) self._do_unlock_modifier_counts(mod_counts) def _do_lock_modifiers(self, mod_mask): """ Lock modifiers and track their state. """ mods_to_lock = 0 for mod_bit in (1 << bit for bit in range(8)): if mod_mask & mod_bit: if not self.mods[mod_bit]: # Alt is special because it activates the # window manager's move mode. if mod_bit != Modifiers.ALT or \ not self._is_alt_special(): # not Alt? mods_to_lock |= mod_bit self.mods[mod_bit] += 1 if mods_to_lock: _logger.debug("_do_lock_modifiers({}) {} {}" .format(mod_mask, self._mods, mods_to_lock)) self.get_text_changer().lock_mod(mods_to_lock) def _do_unlock_modifiers(self, mod_mask): """ Unlock modifier in response to modifier releases. """ mod_counts = {} for mod_bit in (1 << bit for bit in range(8)): if mod_mask & mod_bit: mod_counts[mod_bit] = 1 if mod_counts: self._do_unlock_modifier_counts(mod_counts) def _do_unlock_modifier_counts(self, mod_counts): """ Unlock modifier in response to modifier releases. """ mods_to_unlock = 0 for mod_bit, count in mod_counts.items(): self.mods[mod_bit] -= count if not self.mods[mod_bit]: # Alt is special because it activates the # window manager's move mode. if mod_bit != Modifiers.ALT or \ not self._is_alt_special(): # not Alt? mods_to_unlock |= mod_bit if mods_to_unlock: _logger.debug("_do_unlock_modifier_counts({}) {} {}" .format(mod_counts, self._mods, mods_to_unlock)) self.get_text_changer().unlock_mod(mods_to_unlock) def _is_alt_special(self): """ Does the ALT key need special treatment due to it """ return not config.is_override_redirect() def _maybe_send_alt_press_for_key(self, key, view, button, event_type): """ handle delayed Alt press """ if self.mods[8] and \ self._is_alt_special() and \ not key.active and \ not key.type == KeyCommon.BUTTON_TYPE and \ not self.is_key_disabled(key): self.maybe_send_alt_press(view, button, event_type) def _maybe_send_alt_release_for_key(self, key, view, button, event_type): """ handle delayed Alt release """ if self._alt_locked: self.maybe_send_alt_release(view, button, event_type) def maybe_send_alt_press(self, view, button, event_type): if self.mods[8] and \ not self._alt_locked: self._alt_locked = True if self._last_alt_key: self.send_key_press(self._last_alt_key, view, button, event_type) self.get_text_changer().lock_mod(8) def maybe_send_alt_release(self, view, button, event_type): if self._alt_locked: self._alt_locked = False if self._last_alt_key: self.send_key_release(self._last_alt_key, view, button, event_type) self.get_text_changer().unlock_mod(8) def send_key_press(self, key, view, button, event_type): """ Actually generate a fake key press """ activated = True key_type = key.type if key_type == KeyCommon.KEYCODE_TYPE: with KeySynth.no_delay(): self.get_text_changer().press_keycode(key.code) elif key_type == KeyCommon.KEYSYM_TYPE: with KeySynth.no_delay(): self.get_text_changer().press_keysym(key.code) elif key_type == KeyCommon.CHAR_TYPE: if len(key.code) == 1: with KeySynth.no_delay(): self.get_text_changer().press_unicode(key.code) elif key_type == KeyCommon.KEYPRESS_NAME_TYPE: with KeySynth.no_delay(): self.get_text_changer().press_keysym( get_keysym_from_name(key.code)) elif key_type == KeyCommon.BUTTON_TYPE: activated = False controller = self.button_controllers.get(key) if controller: activated = controller.is_activated_on_press() controller.press(view, button, event_type) elif key_type == KeyCommon.MACRO_TYPE: activated = False elif key_type == KeyCommon.SCRIPT_TYPE: activated = False elif key_type == KeyCommon.WORD_TYPE: activated = False elif key_type == KeyCommon.CORRECTION_TYPE: activated = False key.activated = activated def send_key_release(self, key, view, button=1, event_type=EventType.CLICK): """ Actually generate a fake key release """ key_type = key.type if key_type == KeyCommon.CHAR_TYPE: if len(key.code) == 1: self.get_text_changer().release_unicode(key.code) else: self.get_text_changer().insert_string_at_caret(key.code) elif key_type == KeyCommon.KEYSYM_TYPE: self.get_text_changer().release_keysym(key.code) elif key_type == KeyCommon.KEYPRESS_NAME_TYPE: self.get_text_changer().release_keysym( get_keysym_from_name(key.code)) elif key_type == KeyCommon.KEYCODE_TYPE: self.get_text_changer().release_keycode(key.code) elif key_type == KeyCommon.BUTTON_TYPE: controller = self.button_controllers.get(key) if controller: controller.release(view, button, event_type) elif key_type == KeyCommon.MACRO_TYPE: snippet_id = int(key.code) if self.insert_snippet(snippet_id): pass # Block dialog in xembed mode. # Don't allow to open multiple dialogs in force-to-top mode. else: self._edit_snippet(view, snippet_id) elif key_type == KeyCommon.SCRIPT_TYPE: if not config.xid_mode: # block settings dialog in xembed mode if key.code: run_script(key.code) def _edit_snippet(self, view, snippet_id): if not config.xid_mode and \ not self.editing_snippet and \ view: view.show_snippets_dialog(snippet_id) self.editing_snippet = True def insert_snippet(self, snippet_id): mlabel, mString = config.snippets.get(snippet_id, (None, None)) if mString: self.get_text_changer().insert_string_at_caret(mString) return True return False def _release_non_sticky_key(self, key, view, button, event_type): # Request capitalization before keys are unlatched, so we can # prevent modifiers from toggling more than once and confuse # set_modifiers(). WordSuggestions.on_before_key_release(self, key) # release key self.send_key_up(key, view, button, event_type) # Don't release latched modifiers for click buttons yet, # keep them unchanged until the actual click happens. # -> allow clicks with modifiers if not key.is_layer_button() and \ not (key.type == KeyCommon.BUTTON_TYPE and key.is_click_type_key()) and \ key not in self.get_text_displays(): # Don't release SHIFT if we're going to enable # capitalization anyway. except_keys = None if self._capitalization_requested: except_keys = [key for key in self._latched_sticky_keys if key.modifier == Modifiers.SHIFT] # release latched modifiers self.release_latched_sticky_keys(only_unpressed=True, except_keys=except_keys) # undo temporary suppression of the text display WordSuggestions.show_input_line_on_key_release(self, key) self.set_last_typed_was_separator(key.is_separator()) # Insert words on button release to avoid having the wordlist # change between button press and release. # Make sure latched modifiers have been released, else they will # affect the whole inserted string. WordSuggestions.send_key_up(self, key, button, event_type) # switch to layer 0 on (almost) any key release self.maybe_switch_to_first_layer(key) # punctuation assistance and collapse corrections WordSuggestions.on_after_key_release(self, key) # capitalization requested by punctuator? if self._capitalization_requested: self._capitalization_requested = False if not self.mods[Modifiers.SHIFT]: # SHIFT not active yet? self._enter_caps_mode() def request_capitalization(self, capitalize): """ Request entering upper-caps mode after next key-release. """ self._capitalization_requested = capitalize def _enter_caps_mode(self): """ Do what has to be done so that the next pressed character will be capitalized. Don't call key_down+up for this, because modifiers may be configured not to latch. """ lfsh_keys = self.find_items_from_ids(["LFSH"]) rtsh_keys = self.find_items_from_ids(["RTSH"]) # unlatch all shift keys for key in rtsh_keys + lfsh_keys: if key.active: key.active = False key.locked = False if key in self._latched_sticky_keys: self._latched_sticky_keys.remove(key) if key in self._locked_sticky_keys: self._locked_sticky_keys.remove(key) self.redraw([key]) # Latch right shift for capitalization, # if there is no right shift latch left shift instead. shift_keys = rtsh_keys if rtsh_keys else lfsh_keys for key in shift_keys: if not key.active: key.active = True if key not in self._latched_sticky_keys: self._latched_sticky_keys.append(key) self.redraw([key]) self.mods[Modifiers.SHIFT] = 1 self.get_text_changer().lock_mod(1) self.redraw_labels(False) def maybe_switch_to_first_layer(self, key): """ Activate the first layer if key allows it. """ if self.active_layer_index != 0 and \ not self._layer_locked: unlatch = key.can_unlatch_layer() if unlatch is None: # for backwards compatibility with Onboard <0.99 unlatch = (not key.is_layer_button() and key.id not in ["move", "showclick"]) if unlatch: self.active_layer_index = 0 self.invalidate_visible_layers() self.invalidate_canvas() self.invalidate_context_ui() # update layer button state return unlatch def update_modifiers(self): """ Synchronize our keys with externally activated modifiers, e.g. by physical keyboards or tools like xte. """ keymap = Gdk.Keymap.get_default() if keymap: mod_mask = keymap.get_modifier_state() self.set_modifiers(mod_mask) def set_modifiers(self, mod_mask): """ Sync Onboard with modifiers of the given modifier mask. Used to sync changes of system modifier state with Onboard. """ _logger.debug("set_modifiers({}) {} {} {}" .format(mod_mask, self._alt_locked, self._temporary_modifiers, self.is_typing())) # The special handling of ALT in Onboard confuses the detection of # modifier presses from the outside. # Test case: press ALT, then LSHIFT # Expected: LSHIFT latched # Result: LSHIFT locked and RSHIFT latched # -> stop all modifier synchronization while the ALT key is active. if self._alt_locked: return if self._temporary_modifiers: return # SHIFT doesn't unlatch in Firefox, launchpad question entry, typing # "?" after inserting "collection" with Small layout, Xenial, if self.is_typing(): return for mod_bit in (1 << bit for bit in range(8)): # Directly redraw locking modifiers only. All other modifiers # redraw after a short delay. This is meant to prevent # Onboard from busily flashing keys and using CPU while # typing with a hardware keyboard. if (mod_bit & (Modifiers.CAPS | Modifiers.NUMLK)): delay = 0 else: delay = config.keyboard.modifier_update_delay # -1.0 restores the onboard 1.0.0 behavior, no updates if delay >= 0: self.set_modifier(mod_bit, bool(mod_mask & mod_bit), delay) def set_modifier(self, mod_bit, active, draw_delay=0.0): """ Update Onboard to reflect the state of the given modifier in the ui. """ # find all keys assigned to the modifier bit keys = [] for key in self.layout.iter_keys(): if key.modifier == mod_bit: keys.append(key) active_before = bool(self._mods[mod_bit]) # Was modifier turned on? if not active_before and active: self._mods[mod_bit] += 1 self._external_mod_changes[mod_bit] = 1 for key in keys: if key.sticky: self.step_sticky_key(key, 1, EventType.CLICK) # Was modifier turned off? elif active_before and not active: self._mods[mod_bit] = 0 self._external_mod_changes[mod_bit] = 0 for key in keys: if key in self._latched_sticky_keys: self._latched_sticky_keys.remove(key) if key in self._locked_sticky_keys: self._locked_sticky_keys.remove(key) key.active = False key.locked = False # Was it a change from the outside, i.e. not us? # For this to work, we always have to update self._mods _before_ # lock_mod calls when our modifier keys are clicked. if active != active_before: # re-draw delayed? if active and \ draw_delay > 0.0: self._queue_pending_modifier_redraw(mod_bit, active, keys, draw_delay) else: self._redraw_modifier_keys(keys) def _queue_pending_modifier_redraw(self, mod_bit, active, keys, delay): item = self._pending_modifier_redraws.get(mod_bit) if item is None: # draw affected keys delayed self._pending_modifier_redraws[mod_bit] = (active, keys) else: pending_active, keys = item # discard redraw if the modifier change didn't have # any lasting effects if active != pending_active: del self._pending_modifier_redraws[mod_bit] # start/restart/stop timer if self._pending_modifier_redraws: self._pending_modifier_redraws_timer.start( delay, self._redraw_pending_modifier_keys) else: self._pending_modifier_redraws_timer.stop() def _redraw_pending_modifier_keys(self): for pending_active, keys in self._pending_modifier_redraws.values(): self._redraw_modifier_keys(keys) self._pending_modifier_redraws = {} def _redraw_modifier_keys(self, keys): # redraw modifier keys self.redraw(keys) # redraw keys where labels are affected by the modifier change self.redraw_labels(False) def step_sticky_key(self, key, button, event_type): """ One cycle step when pressing a sticky (latchabe/lockable) modifier key (all sticky keys except layer buttons). """ active, locked = self.step_sticky_key_state(key, key.active, key.locked, button, event_type) # apply the new states was_active = key.active deactivated = False key.active = active key.locked = locked if active: if locked: if key in self._latched_sticky_keys: self._latched_sticky_keys.remove(key) if key not in self._locked_sticky_keys: self._locked_sticky_keys.append(key) else: if key not in self._latched_sticky_keys: self._latched_sticky_keys.append(key) if key in self._locked_sticky_keys: self._locked_sticky_keys.remove(key) else: if key in self._latched_sticky_keys: self._latched_sticky_keys.remove(key) if key in self._locked_sticky_keys: self._locked_sticky_keys.remove(key) deactivated = (was_active or not self.can_activate_key(key)) # push-button return deactivated def can_activate_key(self, key): """ Can key be latched or locked? """ behavior = self._get_sticky_key_behavior(key) return (StickyBehavior.can_latch(behavior) or StickyBehavior.can_lock(behavior)) def step_sticky_key_state(self, key, active, locked, button, event_type): """ One cycle step when pressing a sticky (latchabe/lockable) key """ behavior = self._get_sticky_key_behavior(key) double_click = event_type == EventType.DOUBLE_CLICK # double click usable? if double_click and \ StickyBehavior.can_lock_on_double_click(behavior): # any state -> locked active = True locked = True # single click or unused double click else: # off -> latched or locked if not active: if StickyBehavior.can_latch(behavior): active = True elif StickyBehavior.can_lock_on_single_click(behavior): active = True locked = True # latched -> locked elif (not key.locked and StickyBehavior.can_lock_on_single_click(behavior)): locked = True # latched or locked -> off elif StickyBehavior.can_cycle(behavior): active = False locked = False return active, locked def _get_sticky_key_behavior(self, key): """ Return sticky behavior for the given key """ # try the individual key id behavior = self._get_sticky_behavior_for(key.id) # default to the layout's behavior # CAPS was hard-coded here to LOCK_ONLY until v0.98. if (behavior is None and key.sticky_behavior is not None): behavior = key.sticky_behavior # try the key group if behavior is None: if key.is_modifier(): behavior = self._get_sticky_behavior_for("modifiers") if key.is_layer_button(): behavior = self._get_sticky_behavior_for("layers") # try the 'all' group if behavior is None: behavior = self._get_sticky_behavior_for("all") # else fall back to hard coded default if not StickyBehavior.is_valid(behavior): behavior = StickyBehavior.CYCLE return behavior def _get_sticky_behavior_for(self, group): behavior = None value = config.keyboard.sticky_key_behavior.get(group) if value: try: behavior = StickyBehavior.from_string(value) except KeyError: _logger.warning("Invalid sticky behavior '{}' for group '{}'" .format(value, group)) return behavior def on_snippets_dialog_closed(self): self.editing_snippet = False def is_key_disabled(self, key): """ Check for blacklisted key combinations """ if self._disabled_keys is None: self._disabled_keys = self.create_disabled_keys_set() _logger.debug("disabled keys: {}" .format(repr(self._disabled_keys))) set_key = (key.id, self.get_mod_mask()) return set_key in self._disabled_keys def create_disabled_keys_set(self): """ Precompute a set of (modmask, key_id) tuples for fast testing against the key blacklist. """ disabled_keys = set() available_key_ids = [key.id for key in self.layout.iter_keys()] for combo in config.lockdown.disable_keys: results = parse_key_combination(combo, available_key_ids) if results is not None: disabled_keys.update(results) else: _logger.warning("ignoring unrecognized key combination '{}' " "in lockdown.disable-keys" .format(combo)) return disabled_keys def get_key_action(self, key): action = key.action if action is None: if key.type == KeyCommon.BUTTON_TYPE: action = KeyCommon.DELAYED_STROKE_ACTION controller = self.button_controllers.get(key) if controller and \ controller.is_activated_on_press(): action = KeyCommon.SINGLE_STROKE_ACTION elif (key.type != KeyCommon.WORD_TYPE and key.type != KeyCommon.CORRECTION_TYPE): label = key.get_label() alternatives = self.find_canonical_equivalents(label) if (len(label) == 1 and label.isalnum()) or \ key.id == "SPCE" or \ bool(alternatives): action = config.keyboard.default_key_action else: action = KeyCommon.SINGLE_STROKE_ACTION # Is there a popup defined for this key? if action != KeyCommon.DELAYED_STROKE_ACTION and \ key.get_popup_layout(): action = KeyCommon.DELAYED_STROKE_ACTION return action def has_latched_sticky_keys(self, except_keys=None): """ any sticky keys latched? """ return len(self._latched_sticky_keys) > 0 def release_latched_sticky_keys(self, except_keys=None, only_unpressed=False, skip_externally_set_modifiers=True): """ release latched sticky (modifier) keys """ if len(self._latched_sticky_keys) > 0: for key in self._latched_sticky_keys[:]: if not except_keys or key not in except_keys: # Don't release still pressed modifiers, they may be # part of a multi-touch key combination. if not only_unpressed or not key.pressed: # Don't release modifiers that where latched by # set_modifiers due to external (physical keyboard) # action. # Else the latched modifiers go out of sync in # on_outside_click() while an external tool like # xte holds them down (LP: #1331549). if not skip_externally_set_modifiers or \ not key.is_modifier() or \ not self._external_mod_changes[key.modifier]: # Keep shift pressed if we're going to continue # upper-case anyway. Else multiple locks and # unlocks of SHIFT may happen when the punctuator # is active. We can change the modifier state at # most once per key release, else we can't # distinguish our changes from physical # keyboard actions in set_modifiers. if not key.modifier == Modifiers.SHIFT or \ not self._capitalization_requested: self.send_key_up(key) self._latched_sticky_keys.remove(key) key.active = False self.redraw([key]) # modifiers may change many key labels -> redraw everything self.redraw_labels(False) def release_locked_sticky_keys(self, release_all=False): """ release locked sticky (modifier) keys """ if len(self._locked_sticky_keys) > 0: for key in self._locked_sticky_keys[:]: # NumLock is special, keep its state on exit # if not told otherwise. if release_all or \ not key.modifier == Modifiers.NUMLK: self.send_key_up(key) self._locked_sticky_keys.remove(key) key.active = False key.locked = False key.pressed = False self.redraw([key]) # modifiers may change many key labels -> redraw everything self.redraw_labels(False) def _can_cycle_modifiers(self): """ Modifier cycling enabled? Not enabled for multi-touch with at least one pressed non-modifier key. """ # Any non-modifier currently held down? for key in self._pressed_keys: if not key.is_modifier(): return False # Any non-modifier released before? if self._non_modifier_released: return False return True def find_canonical_equivalents(self, char): return canonical_equivalents["all"].get(char) def invalidate_ui(self): """ Update everything. Quite expensive, don't call this while typing. """ self._invalidated_ui |= UIMask.ALL def invalidate_ui_no_resize(self): """ Update everything assuming key sizes don't change. Doesn't invalidate cached surfaces. """ self._invalidated_ui |= UIMask.ALL & ~UIMask.SIZE def invalidate_context_ui(self): """ Update text-context dependent ui """ self._invalidated_ui |= (UIMask.CONTROLLERS | UIMask.SUGGESTIONS | UIMask.LAYOUT) def invalidate_layout(self): """ Recalculate item rectangles. """ self._invalidated_ui |= UIMask.LAYOUT def invalidate_visible_layers(self): """ Update visibility of layers in the layout tree, e.g. when the active layer changed. """ self._invalidated_ui |= UIMask.LAYERS def invalidate_canvas(self): """ Just redraw everything """ self._invalidated_ui |= UIMask.REDRAW def commit_ui_updates(self): keys = set() mask = self._invalidated_ui if mask & UIMask.CONTROLLERS: # update buttons for controller in self.button_controllers.values(): controller.update() mask = self._invalidated_ui # may have been changed by controllers if mask & UIMask.SUGGESTIONS: keys.update(WordSuggestions.update_suggestions_ui(self)) # update buttons that depend on suggestions for controller in self.button_controllers.values(): controller.update_late() if mask & UIMask.LAYERS: self.update_visible_layers() if mask & UIMask.LAYOUT: self.update_layout() # after suggestions! if mask & (UIMask.SUGGESTIONS | UIMask.LAYERS): self.update_scanner() for view in self._layout_views: view.apply_ui_updates(mask) if mask & UIMask.REDRAW: self.redraw() elif keys: self.redraw(list(keys)) self._invalidated_ui = 0 def update_layout(self): """ Update layout, key sizes are probably changing. """ for view in self._layout_views: view.update_layout() def update_visible_layers(self): """ show/hide layers """ layout = self.layout if layout: layers = layout.get_layer_ids() if layers: layout.set_visible_layers([layers[0], self.active_layer]) def update_scanner(self): """ tell scanner to update on layout changes """ # notify the scanner about layer changes if self.scanner: layout = self.layout if layout: self.scanner.update_layer(layout, self.active_layer, True) else: _logger.warning("Failed to update scanner. No layout.") def hide_touch_feedback(self): self._touch_feedback.hide() def on_key_pressed(self, key, view, sequence, action): """ pressed state of a key instance was set """ if sequence: # Not a simulated key press, scanner? feedback = self.can_give_keypress_feedback() # audio feedback if action and \ config.keyboard.audio_feedback_enabled: pt = sequence.root_point \ if feedback else (-1, -1) # keep passwords privat pts = pt \ if config.keyboard.audio_feedback_place_in_space \ else (-1, -1) Sound().play(Sound.key_feedback, pt[0], pt[1], pts[0], pts[1]) # key label popup if not config.xid_mode and \ config.keyboard.touch_feedback_enabled and \ sequence.event_type != EventType.DWELL and \ key.can_show_label_popup() and \ feedback: self._touch_feedback.show(key, view) def on_key_unpressed(self, key): """ pressed state of a key instance was cleard """ self._set_temporary_modifiers(0) self._update_temporary_key_label(key, 0) self.redraw([key]) self._touch_feedback.hide(key) def on_outside_click(self, button): """ Called by outside click polling. Keep this as Francesco likes to have modifiers reset when clicking outside of onboard. """ self.release_latched_sticky_keys() self._click_sim.end_mapped_click() WordSuggestions.on_outside_click(self, button) def on_cancel_outside_click(self): """ Called when outside click polling times out. """ WordSuggestions.on_cancel_outside_click(self) def get_click_simulator(self): if config.mousetweaks and \ config.mousetweaks.is_active(): return config.mousetweaks return self._click_sim def ignore_capslock(self): """ Keep capslock from causing another send_key_up call on exit """ for key in self.iter_keys(): if key.modifier == Modifiers.CAPS: key.pressed = False key.active = False key.locked = False if key in self._latched_sticky_keys: self._latched_sticky_keys.remove(key) if key in self._locked_sticky_keys: self._locked_sticky_keys.remove(key) def release_pressed_keys(self, redraw=False): """ Release pressed keys on exit, or when recreating the main window. """ self.hide_touch_feedback() # Clear key.pressed for all keys that have already been released # but are still waiting for redrawing the unpressed state. self._unpress_timers.cancel_all() # Release keys that haven't been released yet for key in self.iter_keys(): if key.pressed and key.type in \ [KeyCommon.CHAR_TYPE, KeyCommon.KEYSYM_TYPE, KeyCommon.KEYPRESS_NAME_TYPE, KeyCommon.KEYCODE_TYPE]: # Release still pressed enter key when onboard gets killed # on enter key press. _logger.warning("Releasing still pressed key '{}'" .format(key.id)) self.send_key_up(key) key.pressed = False if redraw: self.redraw([key]) def update_auto_show(self): """ Turn on/off auto-show in response to user action (preferences) and show/hide the views accordingly. """ enable = config.is_auto_show_enabled() self._auto_show.enable(enable) self._auto_show.show_keyboard(not enable) self.update_auto_hide() def update_tablet_mode_detection(self): enable = config.is_tablet_mode_detection_enabled() self._auto_show.enable_tablet_mode_detection(enable) def update_keyboard_device_detection(self): enable = config.is_keyboard_device_detection_enabled() self._auto_show.enable_tablet_mode_detection(enable) def update_auto_hide(self): enabled_before = self._auto_hide.is_enabled() enabled_after = config.is_auto_hide_enabled() self._auto_hide.enable(enabled_after) if enabled_before and not enabled_after: self._auto_hide.auto_show_unlock() def update_auto_show_on_visibility_change(self, visible): if config.is_auto_show_enabled(): # showing keyboard while auto-hide is pausing auto-show? if visible and self._auto_hide.is_auto_show_locked(): self.auto_show_lock_visible(False) self._auto_hide.auto_show_unlock() else: self.auto_show_lock_visible(visible) # Make sure to drop the 'key-pressed' lock in case it still # exists due to e.g. stuck keys. if not visible: self.auto_show_unlock(self.LOCK_REASON_KEY_PRESSED) def auto_show_lock(self, reason, duration=None, lock_show=True, lock_hide=True): """ Reenable both, hiding and showing. """ if config.is_auto_show_enabled(): if duration is not None: if duration == 0.0: return # do nothing if duration < 0.0: # negative means auto-hide is off duration = None self._auto_show.lock(reason, duration, lock_show, lock_hide) def auto_show_unlock(self, reason): """ Remove a specific lock named by "reason". """ if config.is_auto_show_enabled(): self._auto_show.unlock(reason) def auto_show_unlock_and_apply_visibility(self, reason): """ Remove lock and apply the last requested auto-show state while the lock was applied. """ if config.is_auto_show_enabled(): visibility = self._auto_show.unlock(reason) if visibility is not None: self._auto_show.request_keyboard_visible(visibility, delay=0) def auto_show_lock_and_hide(self, reason, duration=None): """ Helper for locking auto-show from AutoHide (hide-on-key-press) and D-Bus property. """ if config.is_auto_show_enabled(): _logger.debug("auto_show_lock_and_hide({}, {})" .format(repr(reason), duration)) # Attempt to hide the keyboard. # If it doesn't hide immediately, e.g. due to currently # pressed keys, we get a second chance the next time # apply_pending_state() is called, i.e. on key-release. if not self._auto_show.is_locked(reason): self._auto_show.request_keyboard_visible(False, delay=0) # Block showing the keyboard. self._auto_show.lock(reason, duration, True, False) def is_auto_show_locked(self, reason): return self._auto_show.is_locked(reason) def auto_show_lock_visible(self, visible): """ If the user unhides onboard, don't auto-hide it until he manually hides it again. """ if config.is_auto_show_enabled(): self._auto_show.lock_visible(visible) def auto_position(self): self._broadcast_to_views("auto_position") def stop_auto_positioning(self): self._broadcast_to_views("stop_auto_positioning") def get_auto_show_repositioned_window_rect(self, view, home, limit_rects, test_clearance, move_clearance, horizontal=True, vertical=True): if not self._auto_show: # may happen on exit, rarely return None return self._auto_show.get_repositioned_window_rect( view, home, limit_rects, test_clearance, move_clearance, horizontal, vertical) def transition_visible_to(self, show): return self._broadcast_to_views("transition_visible_to", show) def commit_transition(self): return self._broadcast_to_views("commit_transition") def raise_ui_delayed(self): """ Attempt to raise keyboard over popups like the one from the firefox URL bar. Give it a moment for the popup to appear after a keypress. """ self._raise_timer.growth = 2.0 self._raise_timer.max_duration = 2.0 self._raise_timer.start(0.1, self._on_raise_timer) def _on_raise_timer(self): _logger.warning("raising window - current delay {}s" .format(self._raise_timer._current_delay)) self._broadcast_to_views("raise_to_top") self._touch_feedback.raise_all() return True def stop_raise_attempts(self): self._raise_timer.stop() def _broadcast_to_views(self, func_name, *params): for view in self._layout_views: if hasattr(view, func_name): getattr(view, func_name)(*params) def find_items_from_ids(self, ids): if self.layout is None: return [] return list(self.layout.find_ids(ids)) def find_items_from_classes(self, item_classes): if self.layout is None: return [] return list(self.layout.find_classes(item_classes)) def find_key_from_id(self, id): """ Find the first key matching the given id. Id may be a complete theme_id (key.theme_id) or just the regular item id (key.id). """ for key in self.iter_keys(): if key.theme_id: if key.theme_id == id: return key elif key.id == id: return key return None class ButtonController(object): """ MVC inspired controller that handles events and the resulting state changes of buttons. """ def __init__(self, keyboard, key): self.keyboard = keyboard self.key = key def press(self, view, button, event_type): """ button pressed """ pass def long_press(self, view, button): """ button pressed long """ pass def release(self, view, button, event_type): """ button released """ pass def update(self): """ asynchronous ui update """ pass def update_late(self): """ after suggestions have been updated """ pass def can_dwell(self): """ can start dwelling? """ return False def is_activated_on_press(self): """ Cannot cancel already called press() without consequences? """ return False def set_visible(self, visible): if self.key.visible != visible: _logger.debug("ButtonController: {}.visible = {}" .format(self.key, visible)) layout = self.keyboard.layout layout.set_item_visible(self.key, visible) self.keyboard.redraw([self.key]) def set_sensitive(self, sensitive): if self.key.sensitive != sensitive: _logger.debug("ButtonController: {}.sensitive = {}" .format(self.key, sensitive)) self.key.sensitive = sensitive self.keyboard.redraw([self.key]) def set_active(self, active=None): if active is not None and self.key.active != active: _logger.debug("ButtonController: {}.active = {}" .format(self.key, active)) self.key.active = active self.keyboard.redraw([self.key]) def set_locked(self, locked=None): if locked is not None and self.key.locked != locked: _logger.debug("ButtonController: {}.locked = {}" .format(self.key, locked)) self.key.active = locked self.key.locked = locked self.keyboard.redraw([self.key]) class BCClick(ButtonController): """ Controller for click buttons """ def release(self, view, button, event_type): cs = self.keyboard.get_click_simulator() if not cs: return if self.is_active(): # stop click mapping, reset to primary button and single click cs.map_primary_click(view, ClickSimulator.PRIMARY_BUTTON, ClickSimulator.CLICK_TYPE_SINGLE) else: # Exclude click type buttons from the click mapping # to be able to reliably cancel the click. # -> They will receive only single left clicks. rects = view.get_click_type_button_rects() self.keyboard._click_sim.set_exclusion_rects(rects) # start click mapping cs.map_primary_click(view, self.button, self.click_type) # Mark current event handled to stop ClickMapper from receiving it. view.set_xi_event_handled(True) def update(self): cs = self.keyboard.get_click_simulator() if cs: # gone on exit self.set_active(self.is_active()) self.set_sensitive( cs.supports_click_params(self.button, self.click_type)) def is_active(self): cs = self.keyboard.get_click_simulator() return (cs and cs.get_click_button() == self.button and cs.get_click_type() == self.click_type) class BCSingleClick(BCClick): id = "singleclick" button = ClickSimulator.PRIMARY_BUTTON click_type = ClickSimulator.CLICK_TYPE_SINGLE class BCMiddleClick(BCClick): id = "middleclick" button = ClickSimulator.MIDDLE_BUTTON click_type = ClickSimulator.CLICK_TYPE_SINGLE class BCSecondaryClick(BCClick): id = "secondaryclick" button = ClickSimulator.SECONDARY_BUTTON click_type = ClickSimulator.CLICK_TYPE_SINGLE class BCDoubleClick(BCClick): id = "doubleclick" button = ClickSimulator.PRIMARY_BUTTON click_type = ClickSimulator.CLICK_TYPE_DOUBLE class BCDragClick(BCClick): id = "dragclick" button = ClickSimulator.PRIMARY_BUTTON click_type = ClickSimulator.CLICK_TYPE_DRAG def release(self, view, button, event_type): BCClick.release(self, view, button, event_type) self.keyboard.show_touch_handles(show=self._can_show_handles(), auto_hide=False) def update(self): active_before = self.key.active BCClick.update(self) active_now = self.key.active if active_before and not active_now: # hide the touch handles self.keyboard.show_touch_handles(self._can_show_handles()) def _can_show_handles(self): return (self.is_active() and config.is_mousetweaks_active() and not config.xid_mode) class BCHoverClick(ButtonController): id = "hoverclick" def release(self, view, button, event_type): config.enable_hover_click(not config.mousetweaks.is_active()) def update(self): available = bool(config.mousetweaks) active = config.mousetweaks.is_active() \ if available else False # noqa: flake8 self.set_sensitive(available and not config.lockdown.disable_hover_click) # force locked color for better visibility self.set_locked(active) def can_dwell(self): return not (config.mousetweaks and config.mousetweaks.is_active()) class BCHide(ButtonController): id = "hide" def release(self, view, button, event_type): if config.unity_greeter: config.unity_greeter.onscreen_keyboard = False else: # No request_keyboard_visible() here, so hide button can # unlock_visibility in case of stuck keys. self.keyboard.set_visible(False) def update(self): # insensitive in XEmbed mode except in unity-greeter self.set_sensitive(not config.xid_mode or config.unity_greeter) class BCShowClick(ButtonController): id = "showclick" def release(self, view, button, event_type): config.keyboard.show_click_buttons = \ not config.keyboard.show_click_buttons # enable hover click when the key was dwell-activated # disabled for now, seems too confusing if False: if event_type == EventType.DWELL and \ config.keyboard.show_click_buttons and \ not config.mousetweaks.is_active(): config.enable_hover_click(True) def update(self): allowed = not config.lockdown.disable_click_buttons self.set_visible(allowed) # Don't show active state. Toggling the click column # should be enough feedback. # self.set_active(config.keyboard.show_click_buttons) # show/hide click buttons show_click = config.keyboard.show_click_buttons and allowed layout = self.keyboard.layout if layout: for item in layout.iter_items(): if item.group == 'click': layout.set_item_visible(item, show_click) elif item.group == 'noclick': layout.set_item_visible(item, not show_click) def can_dwell(self): return not config.mousetweaks or not config.mousetweaks.is_active() class BCMove(ButtonController): id = "move" def press(self, view, button, event_type): if not config.xid_mode: # not called from popup? if hasattr(view, "start_move_window"): view.start_move_window() def long_press(self, view, button): if not config.xid_mode: self.keyboard.show_touch_handles(True) def release(self, view, button, event_type): if not config.xid_mode: if hasattr(view, "start_move_window"): view.stop_move_window() else: # pressed in a popup just show touch handles self.keyboard.show_touch_handles(True) def update(self): self.set_visible(not config.has_window_decoration() and not config.xid_mode and Handle.MOVE in config.window.window_handles) def is_activated_on_press(self): return True # cannot undo on press, dragging is already in progress class BCLayer(ButtonController): """ layer switch button, switches to layer when released """ layer_index = None def _get_id(self): return "layer" + str(self.layer_index) id = property(_get_id) def release(self, view, button, event_type): keyboard = self.keyboard active_before = keyboard.active_layer_index == self.layer_index locked_before = active_before and keyboard._layer_locked active, locked = \ keyboard.step_sticky_key_state(self.key, active_before, locked_before, button, event_type) # push buttons switch layers even though they don't activate the key if not keyboard.can_activate_key(self.key): active = True keyboard.active_layer_index = (self.layer_index if active else 0) keyboard._layer_locked = (locked if self.layer_index else False) if active_before != active: keyboard.invalidate_visible_layers() keyboard.invalidate_canvas() def update(self): # don't show active state for layer 0, it'd be visible all the time active = self.key.show_active and \ self.key.get_layer_index() == self.keyboard.active_layer_index if active: active = self.keyboard.can_activate_key(self.key) self.set_active(active) self.set_locked(active and self.keyboard._layer_locked) class BCTest(ButtonController): id = "decline" def release(self, view, button, event_type): run_script("quitScript") def update(self): self.set_visible(not config.xid_mode and not config.running_under_gdm and not config.lockdown.disable_preferences) class BCBluetooth(ButtonController): id = "bt" def release(self, view, button, event_type): run_script("connect") def update(self): self.set_visible(not config.xid_mode and not config.running_under_gdm and not config.lockdown.disable_preferences) class BCPreferences(ButtonController): id = "settings" def release(self, view, button, event_type): run_script("sokSettings") def update(self): self.set_visible(not config.xid_mode and not config.running_under_gdm and not config.lockdown.disable_preferences) class BCQuit(ButtonController): id = "quit" def release(self, view, button, event_type): app = self.keyboard.get_application() if app: # finish current key processing then quit GLib.idle_add(app.do_quit_onboard) def update(self): self.set_visible(not config.xid_mode and not config.lockdown.disable_quit) class BCExpandCorrections(ButtonController): id = "expand-corrections" def release(self, view, button, event_type): wordlist = self.key.get_parent() wordlist.expand_corrections(not wordlist.are_corrections_expanded()) class BCPreviousPredictions(ButtonController): id = "previous-predictions" def release(self, view, button, event_type): wordlist = self.key.get_parent() wordlist.goto_previous_predictions() self.keyboard.invalidate_context_ui() def update_late(self): wordlist = self.key.get_parent() self.set_sensitive(wordlist.can_goto_previous_predictions()) class BCNextPredictions(ButtonController): id = "next-predictions" def release(self, view, button, event_type): wordlist = self.key.get_parent() wordlist.goto_next_predictions() self.keyboard.invalidate_context_ui() def update_late(self): key = self.key wordlist = key.get_parent() self.set_sensitive(wordlist.can_goto_next_predictions()) class BCPauseLearning(ButtonController): id = "pause-learning" def release(self, view, button, event_type): keyboard = self.keyboard key = self.key active, locked = keyboard.step_sticky_key_state(key, key.active, key.locked, button, event_type) key.active = active key.locked = locked value = 0 if active: value += 1 if locked: value += 1 pause_started = (config.word_suggestions.get_pause_learning() == 0 and value > 0) config.word_suggestions.set_pause_learning(value) # immediately forget changes if pause_started: keyboard.discard_changes() def update(self): co = config.word_suggestions self.set_active(co.get_pause_learning() >= 1) self.set_locked(co.get_pause_learning() == 2) class BCLanguage(ButtonController): id = "language" def __init__(self, keyboard, key): ButtonController.__init__(self, keyboard, key) self._menu_close_time = 0 def release(self, view, button, event_type): if time.time() - self._menu_close_time > 0.5: self.set_active(not self.key.active) if self.key.active: self._show_menu(view, self.key, button) self._menu_close_time = 0 def _show_menu(self, view, key, button): self.keyboard.hide_touch_feedback() view.show_language_menu(key, button, self._on_menu_closed) def _on_menu_closed(self): self.set_active(False) self._menu_close_time = time.time() def update(self): if config.are_word_suggestions_enabled(): key = self.key keyboard = self.keyboard langdb = keyboard._languagedb lang_id = keyboard.get_lang_id() label = langdb.get_language_code(lang_id).capitalize() if label != key.get_label() or \ not key.tooltip: key.set_labels({0: label}) key.tooltip = langdb.get_language_full_name(lang_id) keyboard.invalidate_ui() # deprecated buttons class BCInputline(ButtonController): id = "inputline" def release(self, view, button, event_type): # hide the input line display when it is clicked self.keyboard.hide_input_line() class BCAutoLearn(ButtonController): id = "learnmode" def release(self, view, button, event_type): config.wp.auto_learn = not config.wp.auto_learn # don't learn when turning auto_learn off if not config.wp.auto_learn: self.keyboard.discard_changes() # turning on auto_learn disables stealth_mode if config.wp.auto_learn and config.wp.stealth_mode: config.wp.stealth_mode = False def update(self): self.set_active(config.wp.auto_learn) class BCAutoPunctuation(ButtonController): id = "punctuation" def release(self, view, button, event_type): config.wp.auto_punctuation = not config.wp.auto_punctuation self.keyboard.punctuator.reset() def update(self): self.set_active(config.wp.auto_punctuation) class BCStealthMode(ButtonController): id = "stealthmode" def release(self, view, button, event_type): config.wp.stealth_mode = not config.wp.stealth_mode # don't learn, forget words when stealth mode is enabled if config.wp.stealth_mode: self.keyboard.discard_changes() def update(self): self.set_active(config.wp.stealth_mode)