Skip to content

Commit

Permalink
Add option to force the use of an IBus keymap
Browse files Browse the repository at this point in the history
Resolves: #581

ibus-typing-booster >= 2.27.0 also offers simple engines emulating the
behaviour of the ibus-m17n engines, see:

#570

ibus-m17n has an option `☑️ Use US keyboard layout` which some users of
ibus-m17n might miss if they try to use the Typing Booster engines
emulating ibus-m17n.

To make the switch to Typing Booster easier for such users, I am adding
a similar option to Typing Booster here. Similar but better because it also
solves some problems with that option in ibus-m17n.
  • Loading branch information
mike-fabian committed Dec 30, 2024
1 parent 4643e9d commit f22c8ba
Show file tree
Hide file tree
Showing 4 changed files with 326 additions and 1 deletion.
177 changes: 177 additions & 0 deletions engine/hunspell_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,12 @@ def __init__(
'ibuseventsleepseconds']['user']
LOGGER.info('self._ibus_event_sleep_seconds=%s', self._ibus_event_sleep_seconds)

self._ibus_keymap: str = self._settings_dict['ibuskeymap']['user']
self._ibus_keymap_object: Optional[IBus.Keymap] = (
self._new_ibus_keymap(self._ibus_keymap))
self._use_ibus_keymap: bool = self._settings_dict[
'useibuskeymap']['user']

self._emoji_predictions: bool = self._settings_dict[
'emojipredictions']['user']

Expand Down Expand Up @@ -446,6 +452,7 @@ def __init__(
self._surrounding_text_event_happened_after_focus_in = False

self._prev_key: Optional[itb_util.KeyEvent] = None
self._translated_key_state = 0
self._typed_compose_sequence: List[int] = [] # A list of key values
self._typed_string: List[str] = [] # A list of msymbols
self._typed_string_cursor = 0
Expand Down Expand Up @@ -748,6 +755,12 @@ def _init_settings_dict(self) -> Dict[str, Any]:
'ibuseventsleepseconds': {
'set': self.set_ibus_event_sleep_seconds,
'get': self.get_ibus_event_sleep_seconds},
'useibuskeymap': {
'set': self.set_use_ibus_keymap,
'get': self.get_use_ibus_keymap},
'ibuskeymap': {
'set': self.set_ibus_keymap,
'get': self.get_ibus_keymap},
'errorsound': {
'set': self.set_error_sound,
'get': self.get_error_sound},
Expand Down Expand Up @@ -965,6 +978,7 @@ def _clear_input(self) -> None:
self._current_case_mode = 'orig'
self._typed_compose_sequence = []
self._prev_key = None
self._translated_key_state = 0
self._typed_string = []
self._typed_string_cursor = 0
for ime in self._current_imes:
Expand Down Expand Up @@ -5251,6 +5265,81 @@ def get_ibus_event_sleep_seconds(self) -> float:
'''Returns the current value ibus event sleep seconds '''
return self._ibus_event_sleep_seconds

def set_use_ibus_keymap(
self, mode: bool, update_gsettings: bool = True) -> None:
'''Sets whether the use of an IBus keymap is forced
:param mode: True if the use of an IBus keymap is forced, False if not
:param update_gsettings: Whether to write the change to Gsettings.
Set this to False if this method is
called because the Gsettings key changed
to avoid endless loops when the Gsettings
key is changed twice in a short time.
'''
if self._debug_level > 1:
LOGGER.debug(
'(%s, update_gsettings = %s)', mode, update_gsettings)
if mode == self._use_ibus_keymap:
return
self._use_ibus_keymap = mode
if update_gsettings:
self._gsettings.set_value(
'useibuskeymap',
GLib.Variant.new_boolean(mode))

@classmethod
def _new_ibus_keymap(cls, keymap: str = 'in') -> Optional[IBus.Keymap]:
'''Construct a new IBus.Keymap object and store it in
self._ibus_keymap_object
'''
if keymap not in itb_util.AVAILABLE_IBUS_KEYMAPS:
LOGGER.warning(
'keymap %s not in itb_util.AVAILABLE_IBUS_KEYMAPS=%s',
keymap, repr(itb_util.AVAILABLE_IBUS_KEYMAPS))
try:
return IBus.Keymap(keymap)
except TypeError as error:
LOGGER.exception(
'Exception in IBus.Keymap("%s"): %s: %s',
keymap, error.__class__.__name__, error)
LOGGER.error('Returning None')
return None

def set_ibus_keymap(
self,
keymap: Union[str, Any],
update_gsettings: bool = True) -> None:
'''Sets the IBus keymap to use if the use of an IBus keymap is forced
:param keymap: The IBus keymap to use
:param update_gsettings: Whether to write the change to Gsettings.
Set this to False if this method is
called because the Gsettings key changed
to avoid endless loops when the Gsettings
key is changed twice in a short time.
'''
if self._debug_level > 1:
LOGGER.debug(
'(%s, update_gsettings = %s)', keymap, update_gsettings)
if keymap == self._ibus_keymap:
return
self._ibus_keymap = keymap
self._ibus_keymap_object = self._new_ibus_keymap(keymap)
if update_gsettings:
self._gsettings.set_value(
'ibuskeymap',
GLib.Variant.new_string(keymap))

def get_use_ibus_keymap(self) -> bool:
'''Returns whether the use of an IBus keymap is forced'''
return self._use_ibus_keymap

def get_ibus_keymap(self) -> str:
'''Returns the name of the IBus keymap to use if the use of an
IBus keymap is forced
'''
return self._ibus_keymap

def set_error_sound(
self, error_sound: bool, update_gsettings: bool = True) -> None:
'''Sets whether a sound is played on error or not
Expand Down Expand Up @@ -6971,6 +7060,91 @@ def _handle_compose(self, key: itb_util.KeyEvent, add_to_preedit: bool = True) -
self._current_preedit_text = ''
return True

def _translate_to_ibus_keymap(
self, key: itb_util.KeyEvent) -> itb_util.KeyEvent:
'''Translate key to the selected IBus keymap'''
if self._ibus_keymap_object is None:
return key
state = self._translated_key_state
if key.release:
state |= IBus.ModifierType.RELEASE_MASK
new_key = itb_util.KeyEvent(
IBus.Keymap.lookup_keysym(
self._ibus_keymap_object, key.code, state),
key.code, state)
new_key.translated = True
if (key.name in ('ISO_Level3_Shift', 'Multi_key')
or
new_key.val == IBus.KEY_VoidSymbol
or
(new_key.val == key.val
and
new_key.state & itb_util.KEYBINDING_STATE_MASK
== key.state & itb_util.KEYBINDING_STATE_MASK)):
# Do not translate 'ISO_Level3_Shift' and 'Multi_key' if
# these are already available in the original keyboard
# layout, but not in the IBus keymap translated to.
# Translating them just would take something useful away.
# Also, on some desktops, 'ISO_Level3_Shift' can be set
# independently of the currently selected keyboard layout
# in the control centre of the desktop. For example in
# Gnome one can set 'Alt_L', 'Alt_R', 'Super_L', 'Super_R'
# 'Menu', or 'Control_R' to produce 'ISO_Level3_Shift'.
# If 'ISO_Level3_Shift' is on any of these keys, one should
# just leave it alone and never translate it away.
# Similar for the 'Multi_key': None of the IBus keymaps
# contains 'Multi_key', so translating 'Multi_key' would just
# always take it away and destroy the Compose support.
#
# Do not translate keys either when the new value is
# IBus.KEY_VoidSymbol, i.e. when they cannot be found in
# the IBus keymap to translate to. Skipping the
# translation for such keys keeps them around to
# potentially do something useful.
new_key = key
# Peter Hutterer explained to me that the state masks are
# updated immediately **after** pressing a modifier key. For
# example when the press event of Control is checked with
# `xev` or here in the state supplied by ibus, the state does
# not have the bit IBus.ModifierType.CONTROL_MASK set (it is
# still 0x0 if no other modifiers are pressed). But that bit
# is relevant for the next key press. Therefore I keep track
# of the modifiers bits in `self._translated_key_state` and
# set them when modifiers keys are pressed and remove them
# when modifier keys are released. Doing that I can apply the
# correct state bits when translating the next key to an IBus
# keymap.
if new_key.name in ('Shift_L', 'Shift_R'):
if new_key.release:
self._translated_key_state &= ~IBus.ModifierType.SHIFT_MASK
else:
self._translated_key_state |= IBus.ModifierType.SHIFT_MASK
if new_key.name in ('Control_L', 'Control_R'):
if new_key.release:
self._translated_key_state &= ~IBus.ModifierType.CONTROL_MASK
else:
self._translated_key_state |= IBus.ModifierType.CONTROL_MASK
if new_key.name in ('Alt_L', 'Alt_R'):
if new_key.release:
self._translated_key_state &= ~IBus.ModifierType.MOD1_MASK
else:
self._translated_key_state |= IBus.ModifierType.MOD1_MASK
if new_key.name in ('Super_L', 'Super_R'):
if new_key.release:
self._translated_key_state &= ~IBus.ModifierType.SUPER_MASK
else:
self._translated_key_state |= IBus.ModifierType.SUPER_MASK
if new_key.name in ('ISO_Level3_Shift'):
if new_key.release:
self._translated_key_state &= ~IBus.ModifierType.MOD5_MASK
else:
self._translated_key_state |= IBus.ModifierType.MOD5_MASK
self._translated_key_state &= itb_util.KEYBINDING_STATE_MASK
if self._debug_level > 1:
LOGGER.debug('new_key: %s state for next key=%x',
new_key, self._translated_key_state)
return new_key

def do_process_key_event( # pylint: disable=arguments-differ
self, keyval: int, keycode: int, state: int) -> bool:
'''Process Key Events
Expand All @@ -6980,6 +7154,8 @@ def do_process_key_event( # pylint: disable=arguments-differ
key = itb_util.KeyEvent(keyval, keycode, state)
if self._debug_level > 1:
LOGGER.debug('KeyEvent object: %s', key)
if self._use_ibus_keymap:
key = self._translate_to_ibus_keymap(key)

disabled = False
if not self._input_mode:
Expand Down Expand Up @@ -7619,6 +7795,7 @@ def do_focus_in_id( # pylint: disable=arguments-differ
client is also shown after the “:”, for example
like 'gtk3-im:firefox', 'gtk4-im:gnome-text-editor', …
'''
LOGGER.debug('FIXME self._translated_key_state=%s', self._translated_key_state)
if self._debug_level > 1:
LOGGER.debug(
'object_path=%s client=%s self.client_capabilities=%s\n',
Expand Down
9 changes: 8 additions & 1 deletion engine/itb_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@
M17N_ENGINE_NAME_PATTERN = re.compile(
r'^tb:(?P<lang>[a-z]{1,3}):(?P<name>[^\s:]+)$')

AVAILABLE_IBUS_KEYMAPS = ('in', 'jp', 'kr', 'us')

# When matching keybindings, only the bits in the following mask are
# considered for key.state:
KEYBINDING_STATE_MASK = (
Expand Down Expand Up @@ -5642,6 +5644,10 @@ def __init__(self, keyval: int, keycode: int, state: int) -> None:
# a release key if the corresponding press key has been
# handled.
self.handled: bool = False
# Whether the key was translated to an IBus keymap. This might
# be useful to prefer forward_key_event() over `return False`
# when the key was translated to an IBus keymap.
self.translated: bool = False

def __eq__(self, other: object) -> bool:
if not isinstance(other, KeyEvent):
Expand Down Expand Up @@ -5688,7 +5694,8 @@ def __str__(self) -> str:
f'release={self.release} '
f'modifier={self.modifier} '
f'time={self.time} '
f'handled={self.handled}')
f'handled={self.handled} '
f'translated={self.translated}')

def keyevent_to_keybinding(keyevent: KeyEvent) -> str:
# pylint: disable=line-too-long
Expand Down
18 changes: 18 additions & 0 deletions org.freedesktop.ibus.engine.typing-booster.gschema.xml
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,24 @@
have been typed.
</description>
</key>
<key name="useibuskeymap" type="b">
<default>false</default>
<summary>Use an IBus keymap</summary>
<description>
Whether the use of an IBus keymap is forced while Typing
Booster is active. If this option is not used, the keyboard
layout which was active before switching to typing booster is
used.
</description>
</key>
<key name="ibuskeymap" type="s">
<default>'in'</default>
<summary>The ibus keymap to use</summary>
<description>
The ibus keymap to use when forcing an IBus keymap while
Typing Booster is active is enabled.
</description>
</key>
<key name="errorsound" type="b">
<default>false</default>
</key>
Expand Down
Loading

0 comments on commit f22c8ba

Please sign in to comment.