%PDF- %PDF-
Direktori : /usr/share/ibus-table/engine/ |
Current File : //usr/share/ibus-table/engine/it_util.py |
# -*- coding: utf-8 -*- # vim:et sts=4 sw=4 # # ibus-table - The Tables engine for IBus # # Copyright (c) 2008-2009 Yu Yuwei <acevery@gmail.com> # Copyright (c) 2009-2014 Caius "kaio" CHANCE <me@kaio.net> # Copyright (c) 2012-2022 Mike FABIAN <mfabian@redhat.com> # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this library. If not, see <http://www.gnu.org/licenses/> ''' Utility functions used in ibus-table ''' from typing import Any from typing import List from typing import Tuple from typing import Dict from typing import Callable from enum import Enum, Flag import sys import os import logging import gettext from gi import require_version # type: ignore require_version('Gio', '2.0') from gi.repository import Gio # type: ignore require_version('GLib', '2.0') from gi.repository import GLib require_version('Gdk', '3.0') from gi.repository import Gdk require_version('Gtk', '3.0') from gi.repository import Gtk require_version('IBus', '1.0') from gi.repository import IBus import version import tabsqlitedb LOGGER = logging.getLogger('ibus-table') DOMAINNAME = 'ibus-table' _: Callable[[str], str] = lambda a: gettext.dgettext(DOMAINNAME, a) N_: Callable[[str], str] = lambda a: a # When matching keybindings, only the bits in the following mask are # considered for key.state: KEYBINDING_STATE_MASK = ( IBus.ModifierType.MODIFIER_MASK & ~IBus.ModifierType.LOCK_MASK # Caps Lock & ~IBus.ModifierType.MOD2_MASK # Num Lock & ~IBus.ModifierType.MOD3_MASK # Scroll Lock ) def variant_to_value(variant: GLib.Variant) -> Any: ''' Convert a GLib variant to a value ''' # pylint: disable=unidiomatic-typecheck if type(variant) != GLib.Variant: LOGGER.info('not a GLib.Variant') return variant type_string = variant.get_type_string() if type_string == 's': return variant.get_string() if type_string == 'i': return variant.get_int32() if type_string == 'b': return variant.get_boolean() if type_string == 'v': return variant.unpack() if type_string and type_string[0] == 'a': return variant.unpack() LOGGER.error('unknown variant type: %s', type_string) return variant def color_string_to_argb(color_string: str) -> int: ''' Converts a color string to a 32bit ARGB value :param color_string: The color to convert to 32bit ARGB Can be expressed in the following ways: - Standard name from the X11 rgb.txt - Hex value: “#rgb”, “#rrggbb”, “#rrrgggbbb” or ”#rrrrggggbbbb” - RGB color: “rgb(r,g,b)” - RGBA color: “rgba(r,g,b,a)” Examples: >>> print('%x' %color_string_to_argb('rgb(0xff, 0x10, 0x25)')) ffff1025 >>> print('%x' %color_string_to_argb('#108040')) ff108040 >>> print('%x' %color_string_to_argb('#fff000888')) ffff0088 >>> print('%x' %color_string_to_argb('#ffff00008888')) ffff0088 >>> print('%x' %color_string_to_argb('rgba(0xff, 0x10, 0x25, 0.5)')) 7fff1025 ''' gdk_rgba = Gdk.RGBA() gdk_rgba.parse(color_string) return (((int(gdk_rgba.alpha * 0xff) & 0xff) << 24) + ((int(gdk_rgba.red * 0xff) & 0xff) << 16) + ((int(gdk_rgba.green * 0xff) & 0xff) << 8) + ((int(gdk_rgba.blue * 0xff) & 0xff))) def get_default_chinese_mode(database: tabsqlitedb.TabSqliteDb) -> int: ''' Use database value or LC_CTYPE in your box to determine the Chinese mode 0 means to show simplified Chinese only 1 means to show traditional Chinese only 2 means to show all characters but show simplified Chinese first 3 means to show all characters but show traditional Chinese first 4 means to show all characters If nothing can be found return 4 to avoid any special Chinese filtering or sorting. ''' # use db value, if applicable database_chinese_mode = database.get_chinese_mode() if database_chinese_mode >= 0: LOGGER.info( 'get_default_chinese_mode(): ' 'default Chinese mode found in database, mode=%s', database_chinese_mode) return database_chinese_mode # otherwise try: if 'LC_ALL' in os.environ: __lc = os.environ['LC_ALL'].split('.')[0].lower() LOGGER.info( 'get_default_chinese_mode(): ' '__lc=%s found in LC_ALL', __lc) elif 'LC_CTYPE' in os.environ: __lc = os.environ['LC_CTYPE'].split('.')[0].lower() LOGGER.info( 'get_default_chinese_mode(): ' '__lc=%s found in LC_CTYPE', __lc) else: __lc = os.environ['LANG'].split('.')[0].lower() LOGGER.info( 'get_default_chinese_mode(): ' '__lc=%s found in LANG', __lc) if '_cn' in __lc or '_sg' in __lc: # CN and SG should prefer traditional Chinese by default return 2 # show simplified Chinese first if '_hk' in __lc or '_tw' in __lc or '_mo' in __lc: # HK, TW, and MO should prefer traditional Chinese by default return 3 # show traditional Chinese first if database._is_chinese: # This table is used for Chinese, but we don’t # know for which variant. Therefore, better show # all Chinese characters and don’t prefer any # variant: LOGGER.info( 'get_default_chinese_mode(): last fallback, ' 'database is Chinese but we don’t know ' 'which variant, returning 4.') else: LOGGER.info( 'get_default_chinese_mode(): last fallback, ' 'database is not Chinese, returning 4.') return 4 # show all Chinese characters except: LOGGER.exception('Exception in get_default_chinese_mode(), ' 'returning 4.') return 4 def get_default_keybindings( gsettings: Gio.Settings, database: tabsqlitedb.TabSqliteDb) -> Dict[str, List[str]]: default_keybindings: Dict[str, List[str]] = {} default_keybindings = variant_to_value( gsettings.get_default_value('keybindings')) # Now update the default keybindings from gsettings with # keybindings found in the database: valid_input_chars = database.ime_properties.get('valid_input_chars') select_keys_csv = database.get_select_keys() if select_keys_csv is None: select_keys_csv = '1,2,3,4,5,6,7,8,9,0' select_keybindings = [ name.strip() for name in select_keys_csv.split(',')][:10] if len(select_keybindings) < 10: select_keybindings += [ 'VoidSymbol'] * (10 - len(select_keybindings)) commit_keybindings = default_keybindings['commit'] commit_keys_csv = database.ime_properties.get('commit_keys') if commit_keys_csv: commit_keybindings = [ name.strip() for name in commit_keys_csv.split(',')] default_keybindings['commit'] = commit_keybindings page_down_keybindings = default_keybindings['lookup_table_page_down'] page_down_keys_csv = database.ime_properties.get('page_down_keys') if page_down_keys_csv: page_down_keybindings = [ name.strip() for name in page_down_keys_csv.split(',')] page_up_keybindings = default_keybindings['lookup_table_page_up'] page_up_keys_csv = database.ime_properties.get('page_up_keys') if page_up_keys_csv: page_up_keybindings = [ name.strip() for name in page_up_keys_csv.split(',')] # If commit keys conflict with page up/down keys, remove them # from the page up/down keys (They cannot really be used for # both at the same time. Theoretically, keys from the page # up/down keys could still be used to commit when the number # of candidates is 0 because then there is nothing to # page. But that would be only confusing): for name in commit_keybindings: if name in page_down_keybindings: page_down_keybindings.remove(name) if name in page_up_keybindings: page_up_keybindings.remove(name) # Several tables have = and/or - in the list of valid input chars. # In that case they should be removed from the 'lookup_table_page_down' # and 'lookup_table_page_up' keybindings: if '-' in valid_input_chars: if 'minus' in page_up_keybindings: page_up_keybindings.remove('minus') if 'minus' in page_down_keybindings: page_down_keybindings.remove('minus') if '=' in valid_input_chars: if 'equal' in page_up_keybindings: page_up_keybindings.remove('equal') if 'equal' in page_down_keybindings: page_down_keybindings.remove('equal') default_keybindings['lookup_table_page_down'] = page_down_keybindings default_keybindings['lookup_table_page_up'] = page_up_keybindings for index, name in enumerate(select_keybindings): # Currently the cns11643 table has: # # SELECT_KEYS = 1,2,3,4,5,6,7,8,9,0 # # and # # VALID_INPUT_CHARS = 0123456789abcdef # # # Then the digit “1” could be interpreted either as an # input character or as a select key but of course not # both. If the meaning as a select key were preferred, # this would make some input impossible which probably # makes the whole input method useless. If the meaning as # an input character is preferred, this makes selection # using that key impossible. Making selection by key # impossible is not nice either, but it is not a complete # show stopper as there are still other possibilities to # select, for example using the arrow-up/arrow-down keys # or click with the mouse. # # And we don’t have to make selection by key completely # impossible, we can use F1, ..., F10 instead of the digits # if the digits are valid input chars. # # Of course one should maybe consider fixing the conflict # between the keys by using different SELECT_KEYS in that # table. if len(name) == 1 and name in list(valid_input_chars): if name in '123456789': name = 'F' + name elif name == '0': name = 'F10' default_keybindings[ 'commit_candidate_%s' % (index + 1) ] = [name] default_keybindings[ 'commit_candidate_to_preedit_%s' % (index + 1) ] = ['Control+' + name] default_keybindings[ 'remove_candidate_%s' % (index + 1) ] = ['Mod1+' + name] if name in ('1', '2', '3', '4', '5', '6', '7', '8', '9', '0'): default_keybindings[ 'commit_candidate_%s' % (index + 1) ].append('KP_' + name) default_keybindings[ 'commit_candidate_to_preedit_%s' % (index + 1) ].append('Control+KP_' + name) default_keybindings[ 'remove_candidate_%s' % (index + 1) ].append('Mod1+KP_' + name) for command in default_keybindings: for keybinding in default_keybindings[command]: if 'VoidSymbol' in keybinding: default_keybindings[command].remove(keybinding) return default_keybindings def dict_update_existing_keys( pdict: Dict[Any, Any], other_pdict: Dict[Any, Any]) -> None: '''Update values of existing keys in a Python dict from another Python dict Using pdict.update(other_pdict) would add keys and values from other_pdict to pdict even for keys which do not exist in pdict. Sometimes I want to update only existing keys and ignore new keys. :param pdict: The Python dict to update :type pdict: Python dict :param other_pdict: The Python dict to get the updates from :type other_pdict: Python dict Examples: >>> old_pdict = {'a': 1, 'b': 2} >>> new_pdict = {'b': 3, 'c': 4} >>> dict_update_existing_keys(old_pdict, new_pdict) >>> old_pdict {'a': 1, 'b': 3} >>> old_pdict.update(new_pdict) >>> old_pdict {'a': 1, 'b': 3, 'c': 4} ''' for key in other_pdict: if key in pdict: pdict[key] = other_pdict[key] class Capabilite(Flag): '''Compatibility class to handle IBus.Capabilite the same way no matter what version of ibus is used. For example, older versions of ibus might not have IBus.Capabilite.SYNC_PROCESS_KEY (or maybe even do not have IBus.Capabilite at all). Then capabilities & IBus.Capabilite.SYNC_PROCESS_KEY will produce an exception. But when using this compatibility class capabilities & IBus.Capabilite.SYNC_PROCESS_KEY will just be False but not cause an exception. >>> int(Capabilite.PREEDIT_TEXT) 1 >>> Capabilite.PREEDIT_TEXT == 1 True >>> Capabilite.PREEDIT_TEXT | 2 3 >>> 2 | Capabilite.PREEDIT_TEXT 3 >>> int(Capabilite.PREEDIT_TEXT | Capabilite.AUXILIARY_TEXT) 3 >>> 3 == Capabilite.AUXILIARY_TEXT | Capabilite.PREEDIT_TEXT True >>> 3 == Capabilite.AUXILIARY_TEXT | IBus.Capabilite.PREEDIT_TEXT True >>> Capabilite.PREEDIT_TEXT == IBus.Capabilite.PREEDIT_TEXT True ''' def __new__(cls, attr: str) -> Any: obj = object.__new__(cls) if hasattr(IBus, 'Capabilite') and hasattr(IBus.Capabilite, attr): obj._value_ = int(getattr(IBus.Capabilite, attr)) else: obj._value_ = 0 return obj def __int__(self) -> int: return int(self._value_) def __eq__(self, other: Any) -> bool: if (self.__class__ is other.__class__ or other.__class__ is IBus.Capabilite): return bool(int(self) == int(other)) if other.__class__ is int or other.__class__ is float: return bool(int(self) == other) return NotImplemented def __or__(self, other: Any) -> Any: if self.__class__ is other.__class__: return self.value | other.value if (other.__class__ is IBus.Capabilite): return int(self) | int(other) if other.__class__ is int: return int(self) | other return NotImplemented def __ror__(self, other: Any) -> Any: # type: ignore[override] return self.__or__(other) def __and__(self, other: Any) -> Any: if self.__class__ is other.__class__: return self.value & other.value if (other.__class__ is IBus.Capabilite): return int(self) & int(other) if other.__class__ is int: return int(self) & other return NotImplemented def __rand__(self, other: Any) -> Any: # type: ignore[override] return self.__and__(other) PREEDIT_TEXT = ('PREEDIT_TEXT') AUXILIARY_TEXT = ('AUXILIARY_TEXT') LOOKUP_TABLE = ('LOOKUP_TABLE') FOCUS = ('FOCUS') PROPERTY = ('PROPERTY') SURROUNDING_TEXT = ('SURROUNDING_TEXT') OSK = ('OSK') SYNC_PROCESS_KEY = ('SYNC_PROCESS_KEY') class InputPurpose(Enum): '''Compatibility class to handle InputPurpose the same way no matter what version of ibus is used. For example, older versions of ibus might not have IBus.InputPurpose.TERMINAL and then input_purpose == IBus.InputPurpose.TERMINAL will produce an exception. But when using this compatibility class input_purpose == InputPurpose.TERMINAL will just be False but not cause an exception. See also: https://docs.gtk.org/gtk3/enum.InputPurpose.html https://docs.gtk.org/gtk4/enum.InputPurpose.html Examples: >>> int(InputPurpose.PASSWORD) 8 >>> 8 == InputPurpose.PASSWORD True >>> int(InputPurpose.PIN) 9 >>> InputPurpose.PASSWORD <= InputPurpose.PIN True >>> InputPurpose.PASSWORD == Gtk.InputPurpose.PASSWORD True >>> InputPurpose.PASSWORD == IBus.InputPurpose.PASSWORD True ''' def __new__(cls, attr: str) -> Any: obj = object.__new__(cls) if hasattr(Gtk, 'InputPurpose') and hasattr(Gtk.InputPurpose, attr): obj._value_ = int(getattr(Gtk.InputPurpose, attr)) else: obj._value_ = -1 return obj def __int__(self) -> int: return int(self._value_) def __eq__(self, other: Any) -> bool: if (self.__class__ is other.__class__ or other.__class__ is Gtk.InputPurpose or other.__class__ is IBus.InputPurpose): return int(self) == int(other) if other.__class__ is int or other.__class__ is float: return bool(int(self) == other) return NotImplemented def __gt__(self, other: Any) -> bool: if (self.__class__ is other.__class__ or other.__class__ is Gtk.InputPurpose or other.__class__ is IBus.InputPurpose): return int(self) > int(other) if other.__class__ is int or other.__class__ is float: return bool(int(self) > other) return NotImplemented def __lt__(self, other: Any) -> bool: if (self.__class__ is other.__class__ or other.__class__ is Gtk.InputPurpose or other.__class__ is IBus.InputPurpose): return bool(int(self) < int(other)) if other.__class__ is int or other.__class__ is float: return bool(int(self) < other) return NotImplemented def __ge__(self, other: Any) -> bool: if (self.__class__ is other.__class__ or other.__class__ is Gtk.InputPurpose or other.__class__ is IBus.InputPurpose): return bool(int(self) >= int(other)) if other.__class__ is int or other.__class__ is float: return bool(int(self) >= other) return NotImplemented def __le__(self, other: Any) -> bool: if (self.__class__ is other.__class__ or other.__class__ is Gtk.InputPurpose or other.__class__ is IBus.InputPurpose): return bool(int(self) <= int(other)) if other.__class__ is int or other.__class__ is float: return bool(int(self) <= other) return NotImplemented FREE_FORM = ('FREE_FORM') ALPHA = ('ALPHA') DIGITS = ('DIGITS') NUMBER = ('NUMBER') PHONE = ('PHONE') URL = ('URL') EMAIL = ('EMAIL') NAME = ('NAME') PASSWORD = ('PASSWORD') PIN = ('PIN') TERMINAL = ('TERMINAL') class InputHints(Flag): '''Compatibility class to handle InputHints the same way no matter what version of ibus is used. For example, older versions of ibus might not have IBus.InputHints.PRIVATE (or maybe even do not have IBus.InputHints at all). Then input_hints & IBus.InputHints.PRIVATE will produce an exception. But when using this compatibility class input_hints & InputHints.PRIVATE will just be False but not cause an exception. See also: https://docs.gtk.org/gtk3/flags.InputHints.html https://docs.gtk.org/gtk4/flags.InputHints.html Examples: >>> int(InputHints.SPELLCHECK) 1 >>> InputHints.SPELLCHECK == 1 True >>> InputHints.SPELLCHECK | 2 3 >>> 2 | InputHints.SPELLCHECK 3 >>> int(InputHints.NO_SPELLCHECK | InputHints.SPELLCHECK) 3 >>> 3 == InputHints.NO_SPELLCHECK | InputHints.SPELLCHECK True >>> 3 == InputHints.NO_SPELLCHECK | Gtk.InputHints.SPELLCHECK True >>> 3 == InputHints.NO_SPELLCHECK | IBus.InputHints.SPELLCHECK True >>> InputHints.SPELLCHECK == IBus.InputHints.SPELLCHECK True >>> InputHints.SPELLCHECK == Gtk.InputHints.SPELLCHECK True ''' def __new__(cls, attr: str) -> Any: obj = object.__new__(cls) if hasattr(Gtk, 'InputHints') and hasattr(Gtk.InputHints, attr): obj._value_ = int(getattr(Gtk.InputHints, attr)) else: obj._value_ = 0 return obj def __int__(self) -> int: return int(self._value_) def __eq__(self, other: Any) -> bool: if (self.__class__ is other.__class__ or other.__class__ is Gtk.InputHints or other.__class__ is IBus.InputHints): return bool(int(self) == int(other)) if other.__class__ is int or other.__class__ is float: return bool(int(self) == other) return NotImplemented def __or__(self, other: Any) -> Any: if self.__class__ is other.__class__: return self.value | other.value if (other.__class__ is Gtk.InputHints or other.__class__ is IBus.InputHints): return int(self) | int(other) if other.__class__ is int: return int(self) | other return NotImplemented def __ror__(self, other: Any) -> Any: # type: ignore[override] return self.__or__(other) def __and__(self, other: Any) -> Any: if self.__class__ is other.__class__: return self.value & other.value if (other.__class__ is Gtk.InputHints or other.__class__ is IBus.InputHints): return int(self) & int(other) if other.__class__ is int: return int(self) & other return NotImplemented def __rand__(self, other: Any) -> Any: # type: ignore[override] return self.__and__(other) NONE = ('NONE') SPELLCHECK = ('SPELLCHECK') NO_SPELLCHECK = ('NO_SPELLCHECK') WORD_COMPLETION = ('WORD_COMPLETION') LOWERCASE = ('LOWERCASE') UPPERCASE_CHARS = ('UPPERCASE_CHARS') UPPERCASE_WORDS = ('UPPERCASE_WORDS') UPPERCASE_SENTENCES = ('UPPERCASE_SENTENCES') INHIBIT_OSK = ('INHIBIT_OSK') VERTICAL_WRITING = ('VERTICAL_WRITING') EMOJI = ('EMOJI') NO_EMOJI = ('NO_EMOJI') PRIVATE = ('PRIVATE') class KeyEvent: '''Key event class used to make the checking of details of the key event easy ''' def __init__(self, keyval: int, keycode: int, state: int) -> None: self.val = keyval self.code = keycode self.state = state self.name = IBus.keyval_name(self.val) self.unicode = IBus.keyval_to_unicode(self.val) self.shift = self.state & IBus.ModifierType.SHIFT_MASK != 0 self.lock = self.state & IBus.ModifierType.LOCK_MASK != 0 self.control = self.state & IBus.ModifierType.CONTROL_MASK != 0 self.super = self.state & IBus.ModifierType.SUPER_MASK != 0 self.hyper = self.state & IBus.ModifierType.HYPER_MASK != 0 self.meta = self.state & IBus.ModifierType.META_MASK != 0 # mod1: Usually Alt_L (0x40), Alt_R (0x6c), Meta_L (0xcd) self.mod1 = self.state & IBus.ModifierType.MOD1_MASK != 0 # mod2: Usually Num_Lock (0x4d) self.mod2 = self.state & IBus.ModifierType.MOD2_MASK != 0 # mod3: Usually Scroll_Lock self.mod3 = self.state & IBus.ModifierType.MOD3_MASK != 0 # mod4: Usually Super_L (0xce), Hyper_L (0xcf) self.mod4 = self.state & IBus.ModifierType.MOD4_MASK != 0 # mod5: ISO_Level3_Shift (0x5c), Mode_switch (0xcb) self.mod5 = self.state & IBus.ModifierType.MOD5_MASK != 0 self.button1 = self.state & IBus.ModifierType.BUTTON1_MASK != 0 self.button2 = self.state & IBus.ModifierType.BUTTON2_MASK != 0 self.button3 = self.state & IBus.ModifierType.BUTTON3_MASK != 0 self.button4 = self.state & IBus.ModifierType.BUTTON4_MASK != 0 self.button5 = self.state & IBus.ModifierType.BUTTON5_MASK != 0 self.release = self.state & IBus.ModifierType.RELEASE_MASK != 0 # MODIFIER_MASK: Modifier mask for the all the masks above self.modifier = self.state & IBus.ModifierType.MODIFIER_MASK != 0 def __eq__(self, other: object) -> bool: if not isinstance(other, KeyEvent): return NotImplemented if (self.val == other.val and self.code == other.code and self.state == other.state): return True return False def __ne__(self, other: object) -> bool: if not isinstance(other, KeyEvent): return NotImplemented if (self.val != other.val or self.code != other.code or self.state != other.state): return True return False def __str__(self) -> str: return repr( f'val={self.val} ' f'code={self.code} ' f'state=0x{self.state:08x} ' f'name=“{self.name}” ' f'unicode=“{self.unicode}” ' f'shift={self.shift} ' f'lock={self.lock} ' f'control={self.control} ' f'super={self.super} ' f'hyper={self.hyper} ' f'meta={self.meta} ' f'mod1={self.mod1} ' f'mod2={self.mod2} ' f'mod3={self.mod3} ' f'mod4={self.mod4} ' f'mod5={self.mod5} ' f'button1={self.button1} ' f'button2={self.button2} ' f'button3={self.button3} ' f'button4={self.button4} ' f'button5={self.button5} ' f'release={self.release} ' f'modifier={self.modifier}') def keyevent_to_keybinding(keyevent: KeyEvent) -> str: keybinding = '' if keyevent.shift: keybinding += 'Shift+' if keyevent.lock: keybinding += 'Lock+' if keyevent.control: keybinding += 'Control+' if keyevent.super: keybinding += 'Super+' if keyevent.hyper: keybinding += 'Hyper+' if keyevent.meta: keybinding += 'Meta+' if keyevent.mod1: keybinding += 'Mod1+' if keyevent.mod2: keybinding += 'Mod2+' if keyevent.mod3: keybinding += 'Mod3+' if keyevent.mod4: keybinding += 'Mod4+' if keyevent.mod5: keybinding += 'Mod5+' keybinding += keyevent.name return keybinding def keybinding_to_keyevent(keybinding: str) -> KeyEvent: name = keybinding.split('+')[-1] keyval = IBus.keyval_from_name(name) state = 0 if 'Shift+' in keybinding: state |= IBus.ModifierType.SHIFT_MASK if 'Lock+' in keybinding: state |= IBus.ModifierType.LOCK_MASK if 'Control+' in keybinding: state |= IBus.ModifierType.CONTROL_MASK if 'Super+' in keybinding: state |= IBus.ModifierType.SUPER_MASK if 'Hyper+' in keybinding: state |= IBus.ModifierType.HYPER_MASK if 'Meta+' in keybinding: state |= IBus.ModifierType.META_MASK if 'Mod1+' in keybinding: state |= IBus.ModifierType.MOD1_MASK if 'Mod2+' in keybinding: state |= IBus.ModifierType.MOD2_MASK if 'Mod3+' in keybinding: state |= IBus.ModifierType.MOD3_MASK if 'Mod4+' in keybinding: state |= IBus.ModifierType.MOD4_MASK if 'Mod5+' in keybinding: state |= IBus.ModifierType.MOD5_MASK return KeyEvent(keyval, 0, state) class HotKeys: '''Class to make checking whether a key matches a hotkey for a certain command easy ''' def __init__(self, keybindings: Dict[str, List[str]]) -> None: self._hotkeys: Dict[str, List[Tuple[int, int]]] = {} for command in keybindings: for keybinding in keybindings[command]: key = keybinding_to_keyevent(keybinding) val = key.val state = key.state & KEYBINDING_STATE_MASK if command in self._hotkeys: self._hotkeys[command].append((val, state)) else: self._hotkeys[command] = [(val, state)] def __contains__( self, command_key_tuple: Tuple[KeyEvent, KeyEvent, str]) -> bool: if not isinstance(command_key_tuple, tuple): return False command = command_key_tuple[2] key = command_key_tuple[1] prev_key = command_key_tuple[0] if prev_key is None: # When ibus-table has just started and the very first key # is pressed prev_key is not yet set. In that case, assume # that it is the same as the current key: prev_key = key val = key.val state = key.state # Do not change key.state, only the copy! if key.name in ('Shift_L', 'Shift_R', 'Control_L', 'Control_R', 'Alt_L', 'Alt_R', 'Meta_L', 'Meta_R', 'Super_L', 'Super_R', 'ISO_Level3_Shift'): # For these modifier keys, match on the release event # *and* make sure that the previous key pressed was # exactly the same key. Then we know that for example only # Shift_L was pressed and then released with nothing in # between. For example it could not have been something # like “Shift_L” then “a” followed by releasing the “a” # and the “Shift_L”. if (prev_key.val != val or not state & IBus.ModifierType.RELEASE_MASK): return False state &= ~IBus.ModifierType.RELEASE_MASK if key.name in ('Shift_L', 'Shift_R'): state &= ~IBus.ModifierType.SHIFT_MASK elif key.name in ('Control_L', 'Control_R'): state &= ~IBus.ModifierType.CONTROL_MASK elif key.name in ('Alt_L', 'Alt_R'): state &= ~IBus.ModifierType.MOD1_MASK elif key.name in ('Super_L', 'Super_R'): state &= ~IBus.ModifierType.SUPER_MASK state &= ~IBus.ModifierType.MOD4_MASK elif key.name in ('Meta_L', 'Meta_R'): state &= ~IBus.ModifierType.META_MASK state &= ~IBus.ModifierType.MOD1_MASK elif key.name in ('ISO_Level3_Shift',): state &= ~IBus.ModifierType.MOD5_MASK state = state & KEYBINDING_STATE_MASK if command in self._hotkeys: if (val, state) in self._hotkeys[command]: return True return False def __str__(self) -> str: return repr(self._hotkeys) class ItKeyInputDialog(Gtk.MessageDialog): # type: ignore def __init__( self, # Translators: This is used in the title bar of a dialog window # requesting that the user types a key to be used as a new # key binding for a command. title: str = _('Key input'), parent: Gtk.Window = None) -> None: Gtk.Dialog.__init__( self, title=title, parent=parent) self.add_button(_('Cancel'), Gtk.ResponseType.CANCEL) self.set_modal(True) self.set_markup( '<big><b>%s</b></big>' # Translators: This is from the dialog to enter a key or a # key combination to be used as a key binding for a # command. % _('Please press a key (or a key combination)')) self.format_secondary_text( # Translators: This is from the dialog to enter a key or a # key combination to be used as a key binding for a # command. _('The dialog will be closed when the key is released')) self.connect('key_press_event', self.on_key_press_event) self.connect('key_release_event', self.on_key_release_event) if parent: self.set_transient_for(parent.get_toplevel()) self.show() def on_key_press_event(# pylint: disable=no-self-use self, widget: Gtk.MessageDialog, event: Gdk.EventKey) -> bool: widget.e = (event.keyval, event.get_state() & KEYBINDING_STATE_MASK) return True def on_key_release_event(# pylint: disable=no-self-use self, widget: Gtk.MessageDialog, _event: Gdk.EventKey) -> bool: widget.response(Gtk.ResponseType.OK) return True class ItAboutDialog(Gtk.AboutDialog): # type: ignore def __init__(self, parent: Gtk.Window = None) -> None: Gtk.AboutDialog.__init__(self, parent=parent) self.set_modal(True) # An empty string in aboutdialog.set_logo_icon_name('') # prevents an ugly default icon to be shown. # self.set_logo_icon_name('') # But it looks nicer if we do not use this and use # Gtk.Window.set_default_icon_from_file(icon_file_name) # in the main window of the setup tool self.set_title('码 IBus Table %s' %version.get_version()) self.set_program_name('码 IBus Table') self.set_version(version.get_version()) self.set_comments( _('Table input method for IBus.')) self.set_copyright( 'Copyright © 2009-2012 Peng Huang,\n' 'Copyright © 2012-2022 Mike FABIAN') self.set_authors([ 'Yuwei YU (“acevery”)', 'Peng Huang', 'BYVoid', 'Peng Wu', 'Caius ‘kaio’ Chance', 'Mike FABIAN <maiku.fabian@gmail.com>', 'Contributors:', 'koterpilla', 'Zerng07', 'Mike FABIAN', 'Bernard Nauwelaerts', 'Xiaojun Ma', 'mozbugbox', 'Seán de Búrca', ]) self.set_translator_credits( # Translators: put your names here, one name per line. # The list of names of the translators for the current locale # will be displayed in the “About ibus-table” dialog. _('translator-credits')) # self.set_artists('') self.set_documenters([ 'Mike FABIAN <maiku.fabian@gmail.com>', ]) self.set_website( 'http://mike-fabian.github.io/ibus-table') self.set_website_label( _('Online documentation:') + ' ' + 'http://mike-fabian.github.io/ibus-table') self.set_license(''' This library is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 2.1 of the License, or (at your option) any later version. This library 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this program. If not, see <http://www.gnu.org/licenses/> ''') self.set_wrap_license(True) # overrides the above .set_license() self.set_license_type(Gtk.License.LGPL_2_1) self.connect('response', self.on_close_aboutdialog) if parent: self.set_transient_for(parent.get_toplevel()) self.show() def on_close_aboutdialog( # pylint: disable=no-self-use self, _about_dialog: Gtk.Dialog, _response: Gtk.ResponseType) -> None: ''' The “About” dialog has been closed by the user :param _about_dialog: The “About” dialog :param _response: The response when the “About” dialog was closed ''' self.destroy() if __name__ == "__main__": LOG_HANDLER = logging.StreamHandler(stream=sys.stderr) LOGGER.setLevel(logging.DEBUG) LOGGER.addHandler(LOG_HANDLER) import doctest (FAILED, ATTEMPTED) = doctest.testmod() if FAILED: sys.exit(1) else: sys.exit(0)