3050 lines
104 KiB
Python
Executable File
3050 lines
104 KiB
Python
Executable File
# -*- coding: utf-8 -*-
|
|
|
|
# Copyright © 2007 Martin Böhme <martin.bohm@kubuntu.org>
|
|
# Copyright © 2007-2009 Chris Jones <tortoise@tortuga>
|
|
# Copyright © 2010 Francesco Fumanti <francesco.fumanti@gmx.net>
|
|
# Copyright © 2012 Gerd Kohlberger <lowfi@chello.at>
|
|
# Copyright © 2009, 2011-2017 marmuta <marmvta@gmail.com>
|
|
#
|
|
# 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 <http://www.gnu.org/licenses/>.
|
|
|
|
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 <layer_index> 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)
|
|
|