From 0fd67fc421fb4efd4f25c0df51a3b5818d690aa8 Mon Sep 17 00:00:00 2001 From: Mike FABIAN Date: Tue, 3 Dec 2024 18:33:53 +0100 Subject: [PATCH] Offer additional engines which do simple ibus-m17n emulation by default Resolves: https://github.com/mike-fabian/ibus-typing-booster/issues/570 ([ENHANCEMENT] Offer additional engines which do simple ibus-m17n emulation by default) Resolves: https://github.com/mike-fabian/ibus-typing-booster/issues/569 ([BUG] setup tool may crash when changing the sound file) --- engine/factory.py | 26 +- engine/hunspell_table.py | 406 ++++++---- engine/itb_util.py | 5 + engine/main.py | 94 ++- engine/tabsqlitedb.py | 25 +- ...top.ibus.engine.typing-booster.gschema.xml | 3 +- setup/main.py | 756 +++++++++--------- tests/Makefile.am | 1 + tests/test_0_gtk.py | 10 +- tests/test_itb.py | 3 +- tests/test_itb_m17n_emulation.py | 386 +++++++++ 11 files changed, 1150 insertions(+), 565 deletions(-) create mode 100755 tests/test_itb_m17n_emulation.py diff --git a/engine/factory.py b/engine/factory.py index 662f7517..06c84765 100644 --- a/engine/factory.py +++ b/engine/factory.py @@ -36,6 +36,7 @@ # pylint: enable=wrong-import-position import hunspell_table import tabsqlitedb +import itb_util LOGGER = logging.getLogger('ibus-typing-booster') @@ -67,24 +68,37 @@ def do_create_engine( # pylint: disable=arguments-differ LOGGER.debug( 'EngineFactory.do_create_engine(engine_name=%s)\n', engine_name) - engine_base_path = "/com/redhat/IBus/engines/table/%s/engine/" - engine_path = engine_base_path % re.sub( - r'[^a-zA-Z0-9_/]', '_', engine_name) + engine_path = ('/com/redhat/IBus/engines/typing_booster/' + f'{re.sub(r'[^a-zA-Z0-9_/]', '_', engine_name)}' + '/engine/') try: if engine_name in self.database_dict: self.database = self.database_dict[engine_name] else: - self.database = tabsqlitedb.TabSqliteDb() + user_db_file = 'user.db' + if engine_name != 'typing-booster': + match = itb_util.M17N_ENGINE_NAME_PATTERN.search( + engine_name) + if not match: + raise ValueError('Invalid engine name.') + m17n_ime_lang = match.group('lang') + m17n_ime_name = match.group('name') + user_db_file = f'user-{m17n_ime_lang}-{m17n_ime_name}.db' + self.database = tabsqlitedb.TabSqliteDb( + user_db_file=user_db_file) self.database_dict[engine_name] = self.database if engine_name in self.enginedict: engine = self.enginedict[engine_name] else: engine = hunspell_table.TypingBoosterEngine( - self.bus, engine_path + str(self.engine_id), self.database) + self.bus, + engine_path + str(self.engine_id), + self.database, + engine_name=engine_name) self.enginedict[engine_name] = engine self.engine_id += 1 return engine - except Exception as error: + except Exception as error: # pylint: disable=broad-except LOGGER.exception( 'Failed to create engine %s: %s: %s', engine_name, error.__class__.__name__, error) diff --git a/engine/hunspell_table.py b/engine/hunspell_table.py index cdc692a6..715f6bb5 100644 --- a/engine/hunspell_table.py +++ b/engine/hunspell_table.py @@ -36,6 +36,7 @@ import fnmatch import ast import time +import copy import logging import threading from gettext import dgettext @@ -105,9 +106,6 @@ # 🕵 U+1F575 SLEUTH OR SPY OFF_THE_RECORD_MODE_SYMBOL = '🕵' -INPUT_MODE_TRUE_SYMBOL = '🚀' -INPUT_MODE_FALSE_SYMBOL = '🐌' - IBUS_VERSION = (IBus.MAJOR_VERSION, IBus.MINOR_VERSION, IBus.MICRO_VERSION) class TypingBoosterEngine(IBus.Engine): # type: ignore @@ -118,6 +116,7 @@ def __init__( bus: IBus.Bus, obj_path: str, database: Any, # tabsqlitedb.TabSqliteDb + engine_name: str = 'typing-booster', unit_test: bool = False) -> None: global DEBUG_LEVEL # pylint: disable=global-statement try: @@ -128,8 +127,9 @@ def __init__( if DEBUG_LEVEL > 1: LOGGER.debug( 'TypingBoosterEngine.__init__' - '(bus=%s, obj_path=%s, database=%s)', - bus, obj_path, database) + '(bus=%s, obj_path=%s, database=%s, ' + 'engine_name=%s, unit_test=%s)', + bus, obj_path, database, engine_name, unit_test) LOGGER.info('ibus version = %s', '.'.join(map(str, IBUS_VERSION))) if hasattr(IBus.Engine.props, 'has_focus_id'): super().__init__( @@ -143,6 +143,44 @@ def __init__( object_path=obj_path) LOGGER.info('This ibus version does *not* have focus id.') + self._engine_name = engine_name + self._m17n_ime_lang = '' + self._m17n_ime_name = '' + self._input_mode_true_symbol = '🚀' + self._input_mode_false_symbol = '🐌' + schema_path = '/org/freedesktop/ibus/engine/typing-booster/' + if self._engine_name != 'typing-booster': + try: + match = itb_util.M17N_ENGINE_NAME_PATTERN.search( + self._engine_name) + if not match: + raise ValueError('Invalid engine name.') + self._m17n_ime_lang = match.group('lang') + self._m17n_ime_name = match.group('name') + self._input_mode_true_symbol = self._m17n_ime_lang + # Not more then 2 characters allowed, '_A' works, + # '_hi' does not work. Invisible characters like + # combining characters or emoji variation selectors + # are counted towards this limit unfortunately. using + # upper case to indicate direct input mode is kind of + # weird but keeps the information for which language + # the currently selected engine is. Languages with 3 + # letter codes like “mai” do not work, not even if + # only self._input_mode_true_symbol is 3 letters long + # and self._input_mode_false_symbol is only 1 or 2 + # letters long. + self._input_mode_false_symbol = self._m17n_ime_lang.upper() + if self._m17n_ime_lang == 't': + self._input_mode_true_symbol = '⌨️' + self._input_mode_false_symbol = '🎯' + schema_path = ('/org/freedesktop/ibus/engine/tb/' + f'{self._m17n_ime_lang}/{self._m17n_ime_name}/') + except ValueError as error: + LOGGER.exception( + 'Failed to match engine_name %s: %s: %s', + engine_name, error.__class__.__name__, error) + raise # Re-raise the original exception + self._keyvals_to_keycodes = itb_util.KeyvalsToKeycodes() self._compose_sequences = itb_util.ComposeSequences() self._unit_test = unit_test @@ -158,7 +196,9 @@ def __init__( self.emoji_matcher: Optional[itb_emoji.EmojiMatcher] = None self._setup_pid = 0 self._gsettings = Gio.Settings( - schema='org.freedesktop.ibus.engine.typing-booster') + schema='org.freedesktop.ibus.engine.typing-booster', + path=schema_path) + self._settings_dict = self._init_settings_dict() self._prop_dict: Dict[str, IBus.Property] = {} self._sub_props_dict: Dict[str, IBus.PropList] = {} @@ -175,14 +215,14 @@ def __init__( self.preedit_ime_menu: Dict[str, Any] = {} self.preedit_ime_properties: Dict[str, Any] = {} self.preedit_ime_sub_properties_prop_list: IBus.PropList = [] - self._setup_property: Optional[IBus.Property] = None + self._setup_property: Optional[IBus.Property] = None # FIXME member variable never used? self._im_client: str = '' self._current_imes: List[str] = [] self._timeout_source_id: int = 0 - self._candidates_delay_milliseconds: int = itb_util.variant_to_value( - self._gsettings.get_value('candidatesdelaymilliseconds')) + self._candidates_delay_milliseconds: int = self._settings_dict[ + 'candidatesdelaymilliseconds']['user'] self._candidates_delay_milliseconds = max( self._candidates_delay_milliseconds, 0) self._candidates_delay_milliseconds = min( @@ -193,218 +233,201 @@ def __init__( # Between some events sent to ibus like forward_key_event(), # delete_surrounding_text(), commit_text(), a sleep is necessary. # Without the sleep, these events may be processed out of order. - self._ibus_event_sleep_seconds: float = itb_util.variant_to_value( - self._gsettings.get_value('ibuseventsleepseconds')) + self._ibus_event_sleep_seconds: float = self._settings_dict[ + 'ibuseventsleepseconds']['user'] LOGGER.info('self._ibus_event_sleep_seconds=%s', self._ibus_event_sleep_seconds) - self._emoji_predictions: bool = itb_util.variant_to_value( - self._gsettings.get_value('emojipredictions')) + self._emoji_predictions: bool = self._settings_dict[ + 'emojipredictions']['user'] self.is_lookup_table_enabled_by_min_char_complete = False - self._min_char_complete: int = itb_util.variant_to_value( - self._gsettings.get_value('mincharcomplete')) + self._min_char_complete: int = self._settings_dict[ + 'mincharcomplete']['user'] self._min_char_complete = max(self._min_char_complete, 0) self._min_char_complete = min(self._min_char_complete, 9) - self._debug_level: int = itb_util.variant_to_value( - self._gsettings.get_value('debuglevel')) + self._debug_level: int = self._settings_dict['debuglevel']['user'] self._debug_level = max(self._debug_level, 0) self._debug_level = min(self._debug_level, 255) DEBUG_LEVEL = self._debug_level - self._page_size: int = itb_util.variant_to_value( - self._gsettings.get_value('pagesize')) + self._page_size: int = self._settings_dict['pagesize']['user'] self._page_size = max(self._page_size, 1) self._page_size = min(self._page_size, 9) - self._lookup_table_orientation: int = itb_util.variant_to_value( - self._gsettings.get_value('lookuptableorientation')) + self._lookup_table_orientation: int = self._settings_dict[ + 'lookuptableorientation']['user'] - self._preedit_underline: int = itb_util.variant_to_value( - self._gsettings.get_value('preeditunderline')) + self._preedit_underline: int = self._settings_dict[ + 'preeditunderline']['user'] - self._preedit_style_only_when_lookup: bool = itb_util.variant_to_value( - self._gsettings.get_value('preeditstyleonlywhenlookup')) + self._preedit_style_only_when_lookup: bool = self._settings_dict[ + 'preeditstyleonlywhenlookup']['user'] - self._show_number_of_candidates: bool = itb_util.variant_to_value( - self._gsettings.get_value('shownumberofcandidates')) + self._show_number_of_candidates: bool = self._settings_dict[ + 'shownumberofcandidates']['user'] - self._show_status_info_in_auxiliary_text: bool = ( - itb_util.variant_to_value( - self._gsettings.get_value('showstatusinfoinaux'))) + self._show_status_info_in_auxiliary_text: bool = self._settings_dict[ + 'showstatusinfoinaux']['user'] self._is_candidate_auto_selected = False - self._auto_select_candidate: int = itb_util.variant_to_value( - self._gsettings.get_value('autoselectcandidate')) + self._auto_select_candidate: int = self._settings_dict[ + 'autoselectcandidate']['user'] self.is_lookup_table_enabled_by_tab = False - self._tab_enable: bool = itb_util.variant_to_value( - self._gsettings.get_value('tabenable')) + self._tab_enable: bool = self._settings_dict[ + 'tabenable']['user'] - self._disable_in_terminals: bool = itb_util.variant_to_value( - self._gsettings.get_value('disableinterminals')) + self._disable_in_terminals: bool = self._settings_dict[ + 'disableinterminals']['user'] - self._ascii_digits: bool = itb_util.variant_to_value( - self._gsettings.get_value('asciidigits')) + self._ascii_digits: bool = self._settings_dict['asciidigits']['user'] - self._off_the_record: bool = itb_util.variant_to_value( - self._gsettings.get_value('offtherecord')) + self._off_the_record: bool = self._settings_dict[ + 'offtherecord']['user'] self._hide_input = False self._input_mode = True - self._avoid_forward_key_event: bool = itb_util.variant_to_value( - self._gsettings.get_value('avoidforwardkeyevent')) + self._avoid_forward_key_event: bool = self._settings_dict[ + 'avoidforwardkeyevent']['user'] - self._arrow_keys_reopen_preedit: bool = itb_util.variant_to_value( - self._gsettings.get_value('arrowkeysreopenpreedit')) + self._arrow_keys_reopen_preedit: bool = self._settings_dict[ + 'arrowkeysreopenpreedit']['user'] - self._emoji_trigger_characters: str = itb_util.variant_to_value( - self._gsettings.get_value('emojitriggercharacters')) + self._emoji_trigger_characters: str = self._settings_dict[ + 'emojitriggercharacters']['user'] - self._auto_commit_characters: str = itb_util.variant_to_value( - self._gsettings.get_value('autocommitcharacters')) + self._auto_commit_characters: str = self._settings_dict[ + 'autocommitcharacters']['user'] - self._remember_last_used_preedit_ime: bool = itb_util.variant_to_value( - self._gsettings.get_value('rememberlastusedpreeditime')) + self._remember_last_used_preedit_ime: bool = self._settings_dict[ + 'rememberlastusedpreeditime']['user'] - self._add_space_on_commit: bool = itb_util.variant_to_value( - self._gsettings.get_value('addspaceoncommit')) + self._add_space_on_commit: bool = self._settings_dict[ + 'addspaceoncommit']['user'] - self._inline_completion: int = itb_util.variant_to_value( - self._gsettings.get_value('inlinecompletion')) + self._inline_completion: int = self._settings_dict[ + 'inlinecompletion']['user'] - self._record_mode: int = itb_util.variant_to_value( - self._gsettings.get_value('recordmode')) + self._record_mode: int = self._settings_dict['recordmode']['user'] - self._auto_capitalize: bool = itb_util.variant_to_value( - self._gsettings.get_value('autocapitalize')) + self._auto_capitalize: bool = self._settings_dict[ + 'autocapitalize']['user'] - self._color_preedit_spellcheck: bool = itb_util.variant_to_value( - self._gsettings.get_value('colorpreeditspellcheck')) + self._color_preedit_spellcheck: bool = self._settings_dict[ + 'colorpreeditspellcheck']['user'] - self._color_preedit_spellcheck_string: str = itb_util.variant_to_value( - self._gsettings.get_value('colorpreeditspellcheckstring')) + self._color_preedit_spellcheck_string: str = self._settings_dict[ + 'colorpreeditspellcheckstring']['user'] self._color_preedit_spellcheck_argb = itb_util.color_string_to_argb( self._color_preedit_spellcheck_string) - self._color_inline_completion: bool = itb_util.variant_to_value( - self._gsettings.get_value('colorinlinecompletion')) + self._color_inline_completion: bool = self._settings_dict[ + 'colorinlinecompletion']['user'] - self._color_inline_completion_string: str = itb_util.variant_to_value( - self._gsettings.get_value('colorinlinecompletionstring')) + self._color_inline_completion_string: str = self._settings_dict[ + 'colorinlinecompletionstring']['user'] self._color_inline_completion_argb = itb_util.color_string_to_argb( self._color_inline_completion_string) - self._color_compose_preview: bool = itb_util.variant_to_value( - self._gsettings.get_value('colorcomposepreview')) + self._color_compose_preview: bool = self._settings_dict[ + 'colorcomposepreview']['user'] - self._color_compose_preview_string: str = itb_util.variant_to_value( - self._gsettings.get_value('colorcomposepreviewstring')) + self._color_compose_preview_string: str = self._settings_dict[ + 'colorcomposepreviewstring']['user'] self._color_compose_preview_argb = itb_util.color_string_to_argb( self._color_compose_preview_string) - self._color_userdb: bool = itb_util.variant_to_value( - self._gsettings.get_value('coloruserdb')) + self._color_userdb: bool = self._settings_dict['coloruserdb']['user'] - self._color_userdb_string: str = itb_util.variant_to_value( - self._gsettings.get_value('coloruserdbstring')) + self._color_userdb_string: str = self._settings_dict[ + 'coloruserdbstring']['user'] self._color_userdb_argb = itb_util.color_string_to_argb( self._color_userdb_string) - self._color_spellcheck: bool = itb_util.variant_to_value( - self._gsettings.get_value('colorspellcheck')) + self._color_spellcheck: bool = self._settings_dict[ + 'colorspellcheck']['user'] - self._color_spellcheck_string: str = itb_util.variant_to_value( - self._gsettings.get_value('colorspellcheckstring')) + self._color_spellcheck_string: str = self._settings_dict[ + 'colorspellcheckstring']['user'] self._color_spellcheck_argb = itb_util.color_string_to_argb( self._color_spellcheck_string) - self._color_dictionary: bool = itb_util.variant_to_value( - self._gsettings.get_value('colordictionary')) + self._color_dictionary: bool = self._settings_dict[ + 'colordictionary']['user'] - self._color_dictionary_string: str = itb_util.variant_to_value( - self._gsettings.get_value('colordictionarystring')) + self._color_dictionary_string: str = self._settings_dict[ + 'colordictionarystring']['user'] self._color_dictionary_argb = itb_util.color_string_to_argb( self._color_dictionary_string) - self._label_userdb: bool = itb_util.variant_to_value( - self._gsettings.get_value('labeluserdb')) + self._label_userdb: bool = self._settings_dict['labeluserdb']['user'] - self._label_userdb_string: str = itb_util.variant_to_value( - self._gsettings.get_value('labeluserdbstring')) + self._label_userdb_string: str = self._settings_dict[ + 'labeluserdbstring']['user'] - self._label_spellcheck: bool = itb_util.variant_to_value( - self._gsettings.get_value('labelspellcheck')) + self._label_spellcheck: bool = self._settings_dict[ + 'labelspellcheck']['user'] - self._label_spellcheck_string: str = itb_util.variant_to_value( - self._gsettings.get_value('labelspellcheckstring')) + self._label_spellcheck_string: str = self._settings_dict[ + 'labelspellcheckstring']['user'] - self._label_dictionary: bool = itb_util.variant_to_value( - self._gsettings.get_value('labeldictionary')) + self._label_dictionary: bool = self._settings_dict[ + 'labeldictionary']['user'] self._label_dictionary_string: str = '' self._label_dictionary_dict: Dict[str, str] = {} self.set_label_dictionary_string( - itb_util.variant_to_value( - self._gsettings.get_value('labeldictionarystring')), + self._settings_dict['labeldictionarystring']['user'], update_gsettings=False) - self._flag_dictionary: bool = itb_util.variant_to_value( - self._gsettings.get_value('flagdictionary')) + self._flag_dictionary: bool = self._settings_dict[ + 'flagdictionary']['user'] - self._label_busy: bool = itb_util.variant_to_value( - self._gsettings.get_value('labelbusy')) + self._label_busy: bool = self._settings_dict['labelbusy']['user'] - self._label_busy_string: str = itb_util.variant_to_value( - self._gsettings.get_value('labelbusystring')) + self._label_busy_string: str = self._settings_dict[ + 'labelbusystring']['user'] self._label_speech_recognition: bool = True self._label_speech_recognition_string: str = '🎙️' - self._google_application_credentials: str = itb_util.variant_to_value( - self._gsettings.get_value('googleapplicationcredentials')) + self._google_application_credentials: str = self._settings_dict[ + 'googleapplicationcredentials']['user'] self._keybindings: Dict[str, List[str]] = {} self._hotkeys: Optional[itb_util.HotKeys] = None self._normal_digits_used_in_keybindings = False self._keypad_digits_used_in_keybindings = False self.set_keybindings( - itb_util.variant_to_value( - self._gsettings.get_value('keybindings')), - update_gsettings=False) + self._settings_dict['keybindings']['user'], update_gsettings=False) self._autosettings: List[Tuple[str, str, str]] = [] self.set_autosettings( - itb_util.variant_to_value( - self._gsettings.get_value('autosettings')), + self._settings_dict['autosettings']['user'], update_gsettings=False) self._autosettings_revert: Dict[str, Any] = {} - self._remember_input_mode: bool = itb_util.variant_to_value( - self._gsettings.get_value('rememberinputmode')) + self._remember_input_mode: bool = self._settings_dict[ + 'rememberinputmode']['user'] if (self._keybindings['toggle_input_mode_on_off'] and self._remember_input_mode): - self._input_mode = itb_util.variant_to_value( - self._gsettings.get_value('inputmode')) + self._input_mode = self._settings_dict['inputmode']['user'] else: self.set_input_mode(True, update_gsettings=True) - self._sound_backend: str = itb_util.variant_to_value( - self._gsettings.get_value('soundbackend')) + self._sound_backend: str = self._settings_dict['soundbackend']['user'] self._error_sound_object: Optional[itb_sound.SoundObject] = None self._error_sound_file = '' - self._error_sound: bool = itb_util.variant_to_value( - self._gsettings.get_value('errorsound')) + self._error_sound: bool = self._settings_dict['errorsound']['user'] self.set_error_sound_file( - itb_util.variant_to_value( - self._gsettings.get_value('errorsoundfile')), + self._settings_dict['errorsoundfile']['user'], update_gsettings=False) self._dictionary_names: List[str] = [] - dictionary = itb_util.variant_to_value( - self._gsettings.get_value('dictionary')) + dictionary = self._settings_dict['dictionary']['user'] self._dictionary_names = itb_util.dictionaries_str_to_list(dictionary) if ','.join(self._dictionary_names) != dictionary: # Value changed due to normalization or getting the locale @@ -429,8 +452,7 @@ def __init__( self.emoji_matcher = None # Try to get the selected input methods from Gsettings: - inputmethod = itb_util.variant_to_value( - self._gsettings.get_value('inputmethod')) + inputmethod = self._settings_dict['inputmethod']['user'] self._current_imes = itb_util.input_methods_str_to_list(inputmethod) if ','.join(self._current_imes) != inputmethod: # Value changed due to normalization or getting the locale @@ -534,12 +556,12 @@ def __init__( self.input_mode_properties = { 'InputMode.Off': { 'number': 0, - 'symbol': INPUT_MODE_FALSE_SYMBOL, + 'symbol': self._input_mode_false_symbol, 'label': _('Off'), }, 'InputMode.On': { 'number': 1, - 'symbol': INPUT_MODE_TRUE_SYMBOL, + 'symbol': self._input_mode_true_symbol, 'label': _('On'), } } @@ -633,7 +655,36 @@ def __init__( self._surrounding_text_old: Optional[Tuple[IBus.Text, int, int]] = None self._is_context_from_surrounding_text = False - self._set_get_functions: Dict[str, Dict[str, Any]] = { + self._gsettings.connect('changed', self.on_gsettings_value_changed) + + self._clear_input_and_update_ui() + + LOGGER.info( + '*** ibus-typing-booster %s initialized, ready for input: ***', + itb_version.get_version()) + + cleanup_database_thread = threading.Thread( + target=self.database.cleanup_database) + cleanup_database_thread.start() + + def _init_settings_dict(self) -> Dict[str, Any]: + '''Initialize a dictionary with the default and user settings for all + settings keys. + + The default settings start with the defaults from the + gsettings schema. Some of these generic default values may be + overridden by more specific default settings for the specific engine. + After this possible modification for a specific engine we have the final default + settings for this specific typing booster input method. + + The user settings start with a copy of these final default settings, + then they are possibly modified by user gsettings. + + Keeping a copy of the default settings in the settings dictionary + makes it easy to revert some or all settings to the defaults. + ''' + settings_dict: Dict[str, Any] = {} + set_get_functions: Dict[str, Dict[str, Any]] = { 'inputmethod': { 'set': self.set_current_imes, 'get': self.get_current_imes}, @@ -807,17 +858,55 @@ def __init__( 'set': self.set_google_application_credentials, 'get': self.get_google_application_credentials}, } - self._gsettings.connect('changed', self.on_gsettings_value_changed) - - self._clear_input_and_update_ui() - - LOGGER.info( - '*** ibus-typing-booster %s initialized, ready for input: ***', - itb_version.get_version()) - - cleanup_database_thread = threading.Thread( - target=self.database.cleanup_database) - cleanup_database_thread.start() + schema_source: Gio.SettingsSchemaSource = ( + Gio.SettingsSchemaSource.get_default()) + schema: Gio.SettingsSchema = schema_source.lookup( + 'org.freedesktop.ibus.engine.typing-booster', True) + special_defaults = { + 'dictionary': 'None', # special dummy dictionary + 'inputmethod': f'{self._m17n_ime_lang}-{self._m17n_ime_name}', + 'tabenable': True, + 'offtherecord': True, + 'preeditunderline': 0, + } + for key in schema.list_keys(): + if key == 'keybindings': # keybindings are special! + default_value = itb_util.variant_to_value( + self._gsettings.get_default_value('keybindings')) + if self._engine_name != 'typing-booster': + default_value['toggle_input_mode_on_off'] = [] + default_value['enable_lookup'] = [] + default_value['commit_and_forward_key'] = ['Left'] + # copy the updated default keybindings, i.e. the + # default keybindings for this specific engine, into + # the user keybindings: + user_value = copy.deepcopy(default_value) + user_gsettings = itb_util.variant_to_value( + self._gsettings.get_user_value(key)) + if not user_gsettings: + user_gsettings = {} + itb_util.dict_update_existing_keys(user_value, user_gsettings) + else: + default_value = itb_util.variant_to_value( + self._gsettings.get_default_value(key)) + if self._engine_name != 'typing-booster': + if key in special_defaults: + default_value = special_defaults[key] + user_value = itb_util.variant_to_value( + self._gsettings.get_user_value(key)) + if user_value is None: + user_value = default_value + settings_dict[key] = {'default': default_value, 'user': user_value} + if key in set_get_functions: + if 'set' in set_get_functions[key]: + settings_dict[ + key]['set_function'] = set_get_functions[key]['set'] + if 'get' in set_get_functions[key]: + settings_dict[ + key]['get_function'] = set_get_functions[key]['get'] + else: + LOGGER.warning('key %s missing in set_get_functions', key) + return settings_dict def _get_new_lookup_table(self) -> IBus.LookupTable: '''Get a new lookup table''' @@ -2161,6 +2250,28 @@ def _init_properties(self) -> None: self.dictionary_sub_properties_prop_list = [] self.main_prop_list = IBus.PropList() + if self._engine_name != 'typing-booster': + m17n_db_info = itb_util.M17nDbInfo() + m17n_icon_property = IBus.Property( + key='m17n_icon', + label=IBus.Text.new_from_string(self._engine_name), + icon= m17n_db_info.get_icon( + f'{self._m17n_ime_lang}-{self._m17n_ime_name}'), + tooltip=IBus.Text.new_from_string(self._engine_name), + # sensitive=True is necessary to make it clearly + # visible (not gray) in the floating toolbar. It is + # also necessary to enable the tooltip which is useful + # to show the full engine name if the icon is not clear. + sensitive=True, + # Even with visible=False it is still visible in the + # floating toolbar. It hides it only in the panel menu. + # So visible=False does exactly what I want here, in the + # panel it is useless but in the floating toolbar it shows + # which engine is selected. + visible=False + ) + self.main_prop_list.append(m17n_icon_property) + if self._keybindings['toggle_input_mode_on_off']: self._init_or_update_property_menu( self.input_mode_menu, @@ -2267,7 +2378,9 @@ def _start_setup(self) -> None: self._setup_pid = os.spawnl( os.P_NOWAIT, setup_cmd, - 'ibus-setup-typing-booster') + 'ibus-setup-typing-booster', + '--engine-name', + self._engine_name) def _clear_input_and_update_ui(self) -> None: '''Clear the preëdit and close the lookup table @@ -5098,7 +5211,7 @@ def set_candidates_delay_milliseconds( 'candidatesdelaymilliseconds', GLib.Variant.new_uint32(self._candidates_delay_milliseconds)) - def get_candidates_delay_milliseconds(self) -> float: + def get_candidates_delay_milliseconds(self) -> int: '''Returns the current value of the candidates delay in milliseconds''' return self._candidates_delay_milliseconds @@ -7505,14 +7618,14 @@ def _apply_autosettings(self) -> None: autosettings_apply: Dict[str, Any] = {} for (setting, value, regexp) in self._autosettings: if (not regexp - or setting not in self._set_get_functions - or 'set' not in self._set_get_functions[setting] - or 'get' not in self._set_get_functions[setting]): + or setting not in self._settings_dict + or 'set_function' not in self._settings_dict[setting] + or 'get_function' not in self._settings_dict[setting]): continue pattern = re.compile(regexp) if not pattern.search(self._im_client): continue - current_value = self._set_get_functions[setting]['get']() + current_value = self._settings_dict[setting]['get_function']() if setting in ('inputmethod', 'dictionary'): current_value = ','.join(current_value) if type(current_value) not in [str, int, bool]: @@ -7544,7 +7657,7 @@ def _apply_autosettings(self) -> None: for setting, value in autosettings_apply.items(): LOGGER.info('Apply autosetting: %s: %s -> %s', setting, self._autosettings_revert[setting], value) - self._set_get_functions[setting]['set']( + self._settings_dict[setting]['set_function']( value, update_gsettings=False) def _record_in_database_and_push_context( @@ -7675,7 +7788,7 @@ def _revert_autosettings(self) -> None: '''Revert automatic setting changes which were done on focus in''' for setting, value in self._autosettings_revert.items(): LOGGER.info('Revert autosetting: %s: -> %s', setting, value) - self._set_get_functions[setting]['set']( + self._settings_dict[setting]['set_function']( value, update_gsettings=False) self._autosettings_revert = {} @@ -7927,9 +8040,10 @@ def on_gsettings_value_changed( ''' value = itb_util.variant_to_value(self._gsettings.get_value(key)) LOGGER.debug('Settings changed: key=%s value=%s\n', key, value) - if (key in self._set_get_functions - and 'set' in self._set_get_functions[key]): - self._set_get_functions[key]['set'](value, update_gsettings=False) + if (key in self._settings_dict + and 'set_function' in self._settings_dict[key]): + self._settings_dict[key]['set_function']( + value, update_gsettings=False) return LOGGER.warning('Unknown key\n') return diff --git a/engine/itb_util.py b/engine/itb_util.py index eb85bf89..832ae02d 100644 --- a/engine/itb_util.py +++ b/engine/itb_util.py @@ -112,6 +112,9 @@ _: Callable[[str], str] = lambda a: gettext.dgettext(DOMAINNAME, a) N_: Callable[[str], str] = lambda a: a +M17N_ENGINE_NAME_PATTERN = re.compile( + r'^tb:(?P[a-z]{1,3}):(?P[^\s:]+)$') + # When matching keybindings, only the bits in the following mask are # considered for key.state: KEYBINDING_STATE_MASK = ( @@ -3351,6 +3354,8 @@ def variant_to_value(variant: GLib.Variant) -> Any: ''' Convert a GLib variant to a value ''' + if variant is None: + return None if not isinstance(variant, GLib.Variant): LOGGER.info('not a GLib.Variant') return variant diff --git a/engine/main.py b/engine/main.py index 5049b16e..4327483d 100644 --- a/engine/main.py +++ b/engine/main.py @@ -37,6 +37,7 @@ from gi.repository import GLib # pylint: enable=wrong-import-position +import itb_util import itb_version LOGGER = logging.getLogger('ibus-typing-booster') @@ -250,46 +251,93 @@ def write_xml() -> None: ''' Writes the XML to describe the engine(s) to standard output. ''' + m17n_db_info = itb_util.M17nDbInfo() + m17n_input_methods = set(m17n_db_info.get_imes()) + m17n_input_methods_skip = set([ + 'NoIME', + ]) + # Input methods offering multiple candidates are not yet supported + # by typing-booster: + m17n_input_methods_multiple_candidates = set([ + 'ja-anthy', + 't-lsymbol', + 'ml-swanalekha', + 'vi-han', + 'vi-nomtelex', + 'vi-nomvni', + 'vi-tcvn', + 'vi-telex', + 'vi-viqr', + 'vi-vni', + 'zh-cangjie', + 'zh-pinyin-vi', + 'zh-pinyin', + 'zh-py', + 'zh-py-b5', + 'zh-py-gb', + 'zh-quick', + 'zh-tonepy', + 'zh-tonepy-gb', + 'zh-tonepy-b5', + 'zh-zhuyin', + ]) + supported_input_methods = sorted( + (m17n_input_methods - m17n_input_methods_skip) + - m17n_input_methods_multiple_candidates + ) + ['typing-booster'] + # supported_input_methods = ['typing-booster'] egs = Element('engines') # pylint: disable=possibly-used-before-assignment - for language in ('t',): + for ime in supported_input_methods: _engine = SubElement( # pylint: disable=possibly-used-before-assignment egs, 'engine') - _name = SubElement(_engine, 'name') - _name.text = 'typing-booster' + name = 'typing-booster' + longname = 'Typing Booster' + language = 't' + license = 'GPL' + author = ('Mike FABIAN ' + ', Anish Patil ') + layout = 'default' + icon = os.path.join(ICON_DIR, 'ibus-typing-booster.svg') + description = 'A completion input method to speedup typing.' + symbol = '🚀' + setup = SETUP_TOOL + icon_prop_key = 'InputMode' + if ime != 'typing-booster': + m17n_lang, m17n_name = ime.split('-', maxsplit=1) + name = f'tb:{m17n_lang}:{m17n_name}' + longname = f'{ime} (Typing Booster)' + language = m17n_lang + icon = m17n_db_info.get_icon(ime) + description = m17n_db_info.get_description(ime) + symbol = m17n_lang + if symbol == 't': + symbol = '⌨️' + setup = SETUP_TOOL + f' --engine-name {name}' + _name = SubElement(_engine, 'name') + _name.text = name _longname = SubElement(_engine, 'longname') - _longname.text = 'Typing Booster' - + _longname.text = longname _language = SubElement(_engine, 'language') _language.text = language - _license = SubElement(_engine, 'license') - _license.text = 'GPL' - + _license.text = license _author = SubElement(_engine, 'author') - _author.text = ( - 'Mike FABIAN ' - + ', Anish Patil ') - + _author.text = author _icon = SubElement(_engine, 'icon') - _icon.text = os.path.join(ICON_DIR, 'ibus-typing-booster.svg') - + _icon.text = icon _layout = SubElement(_engine, 'layout') - _layout.text = 'default' - + _layout.text = layout _desc = SubElement(_engine, 'description') - _desc.text = 'A completion input method to speedup typing.' - + _desc.text = description _symbol = SubElement(_engine, 'symbol') - _symbol.text = '🚀' - + _symbol.text = symbol _setup = SubElement(_engine, 'setup') - _setup.text = SETUP_TOOL - + _setup.text = setup _icon_prop_key = SubElement(_engine, 'icon_prop_key') - _icon_prop_key.text = 'InputMode' + _icon_prop_key.text = icon_prop_key # now format the xmlout pretty indent(egs) diff --git a/engine/tabsqlitedb.py b/engine/tabsqlitedb.py index 0c7a9039..6aff3801 100644 --- a/engine/tabsqlitedb.py +++ b/engine/tabsqlitedb.py @@ -49,23 +49,11 @@ class TabSqliteDb: “id”, “input_phrase”, “phrase”, “p_phrase”, “pp_phrase”, “user_freq”, “timestamp” - There are 2 databases, sysdb, userdb. - - sysdb: “Database” with the suggestions from the hunspell dictionaries - user_freq = 0 always. - - Actually there is no Sqlite3 database called “sysdb”, these - are the suggestions coming from hunspell_suggest, i.e. from - grepping the hunspell dictionaries and from pyhunspell. - (Historic note: ibus-typing-booster started as a fork of - ibus-table, in ibus-table “sysdb” is a Sqlite3 database - which is installed systemwide and readonly for the user) - - user_db: Database on disk where the phrases learned from the user are stored - user_freq >= 1: The number of times the user has used this phrase + It is a database where the phrases learned from the user are stored. + user_freq >= 1: The number of times the user has used this phrase ''' # pylint: enable=line-too-long - def __init__(self, user_db_file: str = '') -> None: + def __init__(self, user_db_file: str = 'user.db') -> None: global DEBUG_LEVEL # pylint: disable=global-statement try: DEBUG_LEVEL = int(str(os.getenv('IBUS_TYPING_BOOSTER_DEBUG_LEVEL'))) @@ -75,9 +63,10 @@ def __init__(self, user_db_file: str = '') -> None: LOGGER.debug( 'TabSqliteDb.__init__(user_db_file = %s)', user_db_file) self.user_db_file = user_db_file - if not self.user_db_file and os.getenv('HOME'): - self.user_db_file = os.path.join( - str(os.getenv('HOME')), '.local/share/ibus-typing-booster/user.db') + if (self.user_db_file != ':memory:' + and os.path.basename(self.user_db_file) == self.user_db_file): + self.user_db_file = os.path.join(os.path.expanduser( + '~/.local/share/ibus-typing-booster'), self.user_db_file) if not self.user_db_file: LOGGER.debug('Falling back to ":memory:" for user.db') self.user_db_file = ':memory:' diff --git a/org.freedesktop.ibus.engine.typing-booster.gschema.xml b/org.freedesktop.ibus.engine.typing-booster.gschema.xml index bfb2b5f5..dd6fd308 100644 --- a/org.freedesktop.ibus.engine.typing-booster.gschema.xml +++ b/org.freedesktop.ibus.engine.typing-booster.gschema.xml @@ -1,7 +1,6 @@ - + true Use color for compose preview diff --git a/setup/main.py b/setup/main.py index 07f600c8..fa22e891 100644 --- a/setup/main.py +++ b/setup/main.py @@ -29,10 +29,12 @@ from typing import Any import sys import os +import re import html import signal import argparse import locale +import copy import logging import logging.handlers from time import strftime @@ -85,6 +87,7 @@ import m17n_translit import tabsqlitedb import itb_util +import itb_sound import itb_emoji import itb_version # pylint: enable=import-error @@ -106,6 +109,18 @@ def parse_args() -> Any: ''' parser = argparse.ArgumentParser( description='ibus-typing-booster setup tool') + parser.add_argument( + '-n', '--engine-name', + action='store', + type=str, + dest='engine_name', + default='typing-booster', + help=('Set the name of the engine, for example “typing-booster” ' + 'for the regular multilingual typing booster or ' + '“tb::” for special typing booster ' + 'engines defaulting to emulating ibus-m17n a specific ' + 'm17n input method. For example “tb:hi:itrans” would be ' + 'a special typing-booster engine emulating “m17n:hi:itrans”.')) parser.add_argument( '-q', '--no-debug', action='store_true', @@ -113,6 +128,12 @@ def parse_args() -> Any: help=('Do not write log file ' '~/.local/share/ibus-typing-booster/setup-debug.log, ' 'default: %(default)s')) + engine_name = parser.parse_args().engine_name + if (engine_name != 'typing-booster' + and not itb_util.M17N_ENGINE_NAME_PATTERN.search(engine_name)): + print('Invalid engine name.\n') + parser.print_help() + sys.exit(1) return parser.parse_args() _ARGS = parse_args() @@ -121,8 +142,30 @@ class SetupUI(Gtk.Window): # type: ignore ''' User interface of the setup tool ''' - def __init__(self) -> None: # pylint: disable=too-many-statements - Gtk.Window.__init__(self, title='🚀 ' + _('Preferences')) + def __init__( # pylint: disable=too-many-statements + self, engine_name: str = 'typing-booster') -> None: + self._engine_name = engine_name + if not self._engine_name: + self._engine_name = 'typing-booster' + title = '🚀 ' + _('Preferences for ibus-typing-booster') + user_db_file = 'user.db' + schema_path = '/org/freedesktop/ibus/engine/typing-booster/' + self._m17n_ime_lang = '' + self._m17n_ime_name = '' + if self._engine_name != 'typing-booster': + title = '🚀 ' + self._engine_name + ' ' + _('Preferences') + match = itb_util.M17N_ENGINE_NAME_PATTERN.search(self._engine_name) + if not match: + raise ValueError('Invalid engine name.') + self._m17n_ime_lang = match.group('lang') + self._m17n_ime_name = match.group('name') + user_db_file = ( + f'user-{self._m17n_ime_lang}-{self._m17n_ime_name}.db') + schema_path = ('/org/freedesktop/ibus/engine/tb/' + f'{self._m17n_ime_lang}/{self._m17n_ime_name}/') + + Gtk.Window.__init__(self, title=title) + self.set_title(title) self.set_name('TypingBoosterPreferences') self.set_modal(True) style_provider = Gtk.CssProvider() @@ -140,11 +183,13 @@ def __init__(self) -> None: # pylint: disable=too-many-statements style_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) - self.tabsqlitedb = tabsqlitedb.TabSqliteDb() + self.tabsqlitedb = tabsqlitedb.TabSqliteDb(user_db_file=user_db_file) self._gsettings = Gio.Settings( - schema='org.freedesktop.ibus.engine.typing-booster') - self._gsettings.connect('changed', self._on_gsettings_value_changed) + schema='org.freedesktop.ibus.engine.typing-booster', + path=schema_path) + self._settings_dict = self._init_settings_dict() + self._allowed_autosettings: Dict[str, Dict[str, str]] = {} schema_source: Gio.SettingsSchemaSource = ( Gio.SettingsSchemaSource.get_default()) @@ -169,8 +214,6 @@ def __init__(self) -> None: # pylint: disable=too-many-statements self._allowed_autosettings[key] = { 'value_type': value_type, 'value_hint': value_hint} - self.set_title('🚀 ' + _('Preferences for ibus-typing-booster')) - self.connect('destroy-event', self._on_destroy_event) self.connect('delete-event', self._on_delete_event) @@ -363,18 +406,7 @@ def __init__(self) -> None: # pylint: disable=too-many-statements self._autosettings_vbox, self._autosettings_label) - self._keybindings = {} - # Don’t just use get_value(), if the user has changed the - # settings, get_value() will get the user settings and new - # keybindings might have been added by an update to the default - # settings. Therefore, get the default settings first and - # update them with the user settings: - self._keybindings = itb_util.variant_to_value( - self._gsettings.get_default_value('keybindings')) - itb_util.dict_update_existing_keys( - self._keybindings, - itb_util.variant_to_value( - self._gsettings.get_value('keybindings'))) + self._keybindings = self._settings_dict['keybindings']['user'] _options_grid_row = -1 @@ -397,12 +429,8 @@ def __init__(self) -> None: # pylint: disable=too-many-statements 'this command is typed.')) self._tab_enable_checkbutton.connect( 'clicked', self._on_tab_enable_checkbutton) - self._tab_enable = itb_util.variant_to_value( - self._gsettings.get_value('tabenable')) - if self._tab_enable is None: - self._tab_enable = False - if self._tab_enable is True: - self._tab_enable_checkbutton.set_active(True) + self._tab_enable = self._settings_dict['tabenable']['user'] + self._tab_enable_checkbutton.set_active(self._tab_enable) _options_grid_row += 1 self._options_grid.attach( self._tab_enable_checkbutton, 0, _options_grid_row, 2, 1) @@ -440,10 +468,8 @@ def __init__(self) -> None: # pylint: disable=too-many-statements renderer_text, True) self._inline_completion_combobox.add_attribute( renderer_text, "text", 0) - self._inline_completion = itb_util.variant_to_value( - self._gsettings.get_value('inlinecompletion')) - if self._inline_completion is None: - self._inline_completion = 0 + self._inline_completion = self._settings_dict[ + 'inlinecompletion']['user'] for i, item in enumerate(self._inline_completion_store): if self._inline_completion == item[1]: self._inline_completion_combobox.set_active(i) @@ -463,12 +489,8 @@ def __init__(self) -> None: # pylint: disable=too-many-statements _('Automatically capitalize after punctuation.')) self._auto_capitalize_checkbutton.connect( 'clicked', self._on_auto_capitalize_checkbutton) - self._auto_capitalize = itb_util.variant_to_value( - self._gsettings.get_value('autocapitalize')) - if self._auto_capitalize is None: - self._auto_capitalize = False - if self._auto_capitalize is True: - self._auto_capitalize_checkbutton.set_active(True) + self._auto_capitalize = self._settings_dict['autocapitalize']['user'] + self._auto_capitalize_checkbutton.set_active(self._auto_capitalize) _options_grid_row += 1 self._options_grid.attach( self._auto_capitalize_checkbutton, 0, _options_grid_row, 2, 1) @@ -509,10 +531,8 @@ def __init__(self) -> None: # pylint: disable=too-many-statements renderer_text, True) self._auto_select_candidate_combobox.add_attribute( renderer_text, "text", 0) - self._auto_select_candidate = itb_util.variant_to_value( - self._gsettings.get_value('autoselectcandidate')) - if self._auto_select_candidate is None: - self._auto_select_candidate = 0 + self._auto_select_candidate = self._settings_dict[ + 'autoselectcandidate']['user'] for i, item in enumerate(self._auto_select_candidate_store): if self._auto_select_candidate == item[1]: self._auto_select_candidate_combobox.set_active(i) @@ -535,12 +555,9 @@ def __init__(self) -> None: # pylint: disable=too-many-statements 'with the mouse.')) self._add_space_on_commit_checkbutton.connect( 'clicked', self._on_add_space_on_commit_checkbutton) - self._add_space_on_commit = itb_util.variant_to_value( - self._gsettings.get_value('addspaceoncommit')) - if self._add_space_on_commit is None: - self._add_space_on_commit = True - if self._add_space_on_commit is True: - self._add_space_on_commit_checkbutton.set_active(True) + self._add_space_on_commit = self._settings_dict[ + 'addspaceoncommit']['user'] + self._add_space_on_commit_checkbutton.set_active(self._add_space_on_commit) _options_grid_row += 1 self._options_grid.attach( self._add_space_on_commit_checkbutton, 0, _options_grid_row, 2, 1) @@ -562,12 +579,10 @@ def __init__(self) -> None: # pylint: disable=too-many-statements 'remembered even if the session is restarted.')) self._remember_last_used_preedit_ime_checkbutton.connect( 'clicked', self._on_remember_last_used_preedit_ime_checkbutton) - self._remember_last_used_preedit_ime = itb_util.variant_to_value( - self._gsettings.get_value('rememberlastusedpreeditime')) - if self._remember_last_used_preedit_ime is None: - self._remember_last_used_preedit_ime = False - if self._remember_last_used_preedit_ime is True: - self._remember_last_used_preedit_ime_checkbutton.set_active(True) + self._remember_last_used_preedit_ime = self._settings_dict[ + 'rememberlastusedpreeditime']['user'] + self._remember_last_used_preedit_ime_checkbutton.set_active( + self._remember_last_used_preedit_ime_checkbutton) _options_grid_row += 1 self._options_grid.attach( self._remember_last_used_preedit_ime_checkbutton, @@ -586,12 +601,9 @@ def __init__(self) -> None: # pylint: disable=too-many-statements 'is remembered even if the session is restarted.')) self._remember_input_mode_checkbutton.connect( 'clicked', self._on_remember_input_mode_checkbutton) - self._remember_input_mode = itb_util.variant_to_value( - self._gsettings.get_value('rememberinputmode')) - if self._remember_input_mode is None: - self._remember_input_mode = False - if self._remember_input_mode is True: - self._remember_input_mode_checkbutton.set_active(True) + self._remember_input_mode = self._settings_dict[ + 'rememberinputmode']['user'] + self._remember_input_mode_checkbutton.set_active(self._remember_input_mode) _options_grid_row += 1 self._options_grid.attach( self._remember_input_mode_checkbutton, @@ -616,12 +628,9 @@ def __init__(self) -> None: # pylint: disable=too-many-statements 'nevertheless useful symbols.')) self._emoji_predictions_checkbutton.connect( 'clicked', self._on_emoji_predictions_checkbutton) - self._emoji_predictions = itb_util.variant_to_value( - self._gsettings.get_value('emojipredictions')) - if self._emoji_predictions is None: - self._emoji_predictions = False - if self._emoji_predictions is True: - self._emoji_predictions_checkbutton.set_active(True) + self._emoji_predictions = self._settings_dict[ + 'emojipredictions']['user'] + self._emoji_predictions_checkbutton.set_active(self._emoji_predictions) _options_grid_row += 1 self._options_grid.attach( self._emoji_predictions_checkbutton, 0, _options_grid_row, 2, 1) @@ -646,12 +655,9 @@ def __init__(self) -> None: # pylint: disable=too-many-statements 'option temporarily.')) self._off_the_record_checkbutton.connect( 'clicked', self._on_off_the_record_checkbutton) - self._off_the_record = itb_util.variant_to_value( - self._gsettings.get_value('offtherecord')) - if self._off_the_record is None: - self._off_the_record = False - if self._off_the_record is True: - self._off_the_record_checkbutton.set_active(True) + self._off_the_record = self._settings_dict[ + 'offtherecord']['user'] + self._off_the_record_checkbutton.set_active(self._off_the_record) _options_grid_row += 1 self._options_grid.attach( self._off_the_record_checkbutton, 0, _options_grid_row, 2, 1) @@ -691,10 +697,7 @@ def __init__(self) -> None: # pylint: disable=too-many-statements renderer_text, True) self._record_mode_combobox.add_attribute( renderer_text, "text", 0) - self._record_mode = itb_util.variant_to_value( - self._gsettings.get_value('recordmode')) - if self._record_mode is None: - self._record_mode = 0 + self._record_mode = self._settings_dict['recordmode']['user'] for i, item in enumerate(self._record_mode_store): if self._record_mode == item[1]: self._record_mode_combobox.set_active(i) @@ -745,12 +748,10 @@ def __init__(self) -> None: # pylint: disable=too-many-statements % 'forward_key_event()') # pylint: disable=consider-using-f-string self._avoid_forward_key_event_checkbutton.connect( 'clicked', self._on_avoid_forward_key_event_checkbutton) - self._avoid_forward_key_event = itb_util.variant_to_value( - self._gsettings.get_value('avoidforwardkeyevent')) - if self._avoid_forward_key_event is None: - self._avoid_forward_key_event = False - if self._avoid_forward_key_event is True: - self._avoid_forward_key_event_checkbutton.set_active(True) + self._avoid_forward_key_event = self._settings_dict[ + 'avoidforwardkeyevent']['user'] + self._avoid_forward_key_event_checkbutton.set_active( + self._avoid_forward_key_event) _options_grid_row += 1 self._options_grid.attach( self._avoid_forward_key_event_checkbutton, @@ -769,12 +770,10 @@ def __init__(self) -> None: # pylint: disable=too-many-statements 'committed words.')) self._arrow_keys_reopen_preedit_checkbutton.connect( 'clicked', self._on_arrow_keys_reopen_preedit_checkbutton) - self._arrow_keys_reopen_preedit = itb_util.variant_to_value( - self._gsettings.get_value('arrowkeysreopenpreedit')) - if self._arrow_keys_reopen_preedit is None: - self._arrow_keys_reopen_preedit = False - if self._arrow_keys_reopen_preedit is True: - self._arrow_keys_reopen_preedit_checkbutton.set_active(True) + self._arrow_keys_reopen_preedit = self._settings_dict[ + 'arrowkeysreopenpreedit']['user'] + self._arrow_keys_reopen_preedit_checkbutton.set_active( + self._arrow_keys_reopen_preedit) _options_grid_row += 1 self._options_grid.attach( self._arrow_keys_reopen_preedit_checkbutton, @@ -788,12 +787,10 @@ def __init__(self) -> None: # pylint: disable=too-many-statements _('Whether ibus-typing-booster should be disabled in terminals.')) self._disable_in_terminals_checkbutton.connect( 'clicked', self._on_disable_in_terminals_checkbutton) - self._disable_in_terminals = itb_util.variant_to_value( - self._gsettings.get_value('disableinterminals')) - if self._disable_in_terminals is None: - self._disable_in_terminals = False - if self._disable_in_terminals is True: - self._disable_in_terminals_checkbutton.set_active(True) + self._disable_in_terminals = self._settings_dict[ + 'disableinterminals']['user'] + self._disable_in_terminals_checkbutton.set_active( + self._disable_in_terminals) _options_grid_row += 1 self._options_grid.attach( self._disable_in_terminals_checkbutton, @@ -811,12 +808,9 @@ def __init__(self) -> None: # pylint: disable=too-many-statements 'by input methods always to ASCII digits.')) self._ascii_digits_checkbutton.connect( 'clicked', self._on_ascii_digits_checkbutton) - self._ascii_digits = itb_util.variant_to_value( - self._gsettings.get_value('asciidigits')) - if self._ascii_digits is None: - self._ascii_digits = False - if self._ascii_digits is True: - self._ascii_digits_checkbutton.set_active(True) + self._ascii_digits = self._settings_dict[ + 'asciidigits']['user'] + self._ascii_digits_checkbutton.set_active(self._ascii_digits) _options_grid_row += 1 self._options_grid.attach( self._ascii_digits_checkbutton, @@ -834,10 +828,8 @@ def __init__(self) -> None: # pylint: disable=too-many-statements '“Unicode symbols and emoji predictions” is off.')) self._emoji_trigger_characters_label.set_xalign(0) self._emoji_trigger_characters_entry = Gtk.Entry() - self._emoji_trigger_characters = itb_util.variant_to_value( - self._gsettings.get_value('emojitriggercharacters')) - if not self._emoji_trigger_characters: - self._emoji_trigger_characters = '' + self._emoji_trigger_characters = self._settings_dict[ + 'emojitriggercharacters']['user'] self._emoji_trigger_characters_entry.set_text( self._emoji_trigger_characters) self._emoji_trigger_characters_entry.connect( @@ -880,10 +872,8 @@ def __init__(self) -> None: # pylint: disable=too-many-statements 'list empty (which is the default).')) self._auto_commit_characters_label.set_xalign(0) self._auto_commit_characters_entry = Gtk.Entry() - self._auto_commit_characters = itb_util.variant_to_value( - self._gsettings.get_value('autocommitcharacters')) - if not self._auto_commit_characters: - self._auto_commit_characters = '' + self._auto_commit_characters = self._settings_dict[ + 'autocommitcharacters']['user'] self._auto_commit_characters_entry.set_text( self._auto_commit_characters) self._auto_commit_characters_entry.connect( @@ -908,8 +898,8 @@ def __init__(self) -> None: # pylint: disable=too-many-statements self._min_char_complete_adjustment.set_can_focus(True) self._min_char_complete_adjustment.set_increments(1.0, 1.0) self._min_char_complete_adjustment.set_range(0.0, 9.0) - self._min_char_complete = itb_util.variant_to_value( - self._gsettings.get_value('mincharcomplete')) + self._min_char_complete = self._settings_dict[ + 'mincharcomplete']['user'] if self._min_char_complete is not None: self._min_char_complete_adjustment.set_value( int(self._min_char_complete)) @@ -930,21 +920,17 @@ def __init__(self) -> None: # pylint: disable=too-many-statements label=_('Play sound file on error')) self._error_sound_checkbutton.set_tooltip_text( _('Here you can choose whether a sound file is played ' - 'if an error occurs. ' - 'If the simpleaudio module for Python3 is not installed, ' - 'this option does nothing.')) + 'if an error occurs.')) self._error_sound_checkbutton.set_hexpand(False) self._error_sound_checkbutton.set_vexpand(False) - self._error_sound = itb_util.variant_to_value( - self._gsettings.get_value('errorsound')) + self._error_sound = self._settings_dict['errorsound']['user'] self._error_sound_checkbutton.set_active(self._error_sound) self._error_sound_checkbutton.connect( 'clicked', self._on_error_sound_checkbutton) self._error_sound_file_button = Gtk.Button() self._error_sound_file_button_box = Gtk.Box() self._error_sound_file_button_label = Gtk.Label() - self._error_sound_file = itb_util.variant_to_value( - self._gsettings.get_value('errorsoundfile')) + self._error_sound_file = self._settings_dict['errorsoundfile']['user'] self._error_sound_file_button_label.set_text( self._error_sound_file) self._error_sound_file_button_label.set_use_markup(True) @@ -963,6 +949,10 @@ def __init__(self) -> None: # pylint: disable=too-many-statements self._error_sound_checkbutton, 0, _options_grid_row, 1, 1) self._options_grid.attach( self._error_sound_file_button, 1, _options_grid_row, 1, 1) + self._sound_backend = self._settings_dict['soundbackend']['user'] + self._error_sound_object = itb_sound.SoundObject( + os.path.expanduser(self._error_sound_file), + audio_backend=self._sound_backend) self._debug_level_label = Gtk.Label() self._debug_level_label.set_text( @@ -980,13 +970,8 @@ def __init__(self) -> None: # pylint: disable=too-many-statements self._debug_level_adjustment.set_can_focus(True) self._debug_level_adjustment.set_increments(1.0, 1.0) self._debug_level_adjustment.set_range(0.0, 255.0) - self._debug_level = itb_util.variant_to_value( - self._gsettings.get_value('debuglevel')) - if self._debug_level: - self._debug_level_adjustment.set_value( - int(self._debug_level)) - else: - self._debug_level_adjustment.set_value(0) + self._debug_level = self._settings_dict['debuglevel']['user'] + self._debug_level_adjustment.set_value(int(self._debug_level)) self._debug_level_adjustment.connect( 'value-changed', self._on_debug_level_adjustment_value_changed) @@ -1112,11 +1097,16 @@ def __init__(self) -> None: # pylint: disable=too-many-statements _('Set to default')) self._dictionaries_default_button.add( self._dictionaries_default_button_label) - self._dictionaries_default_button.set_tooltip_text( + dictionaries_default_button_tooltip_text = ( # Translators: Tooltip for a button used to set the list of # dictionaries to the default for the current locale. _('Set dictionaries to the default for the current locale.') - + f' LC_CTYPE={itb_util.get_effective_lc_ctype()}') + + f' (LC_CTYPE={itb_util.get_effective_lc_ctype()} ' + f'➡ {itb_util.dictionaries_str_to_list("")})') + if self._engine_name != 'typing-booster': + dictionaries_default_button_tooltip_text = '➡ None' + self._dictionaries_default_button.set_tooltip_text( + dictionaries_default_button_tooltip_text) self._dictionaries_default_button.connect( 'clicked', self._on_dictionaries_default_button_clicked) self._dictionaries_default_button.set_sensitive(True) @@ -1131,6 +1121,14 @@ def __init__(self) -> None: # pylint: disable=too-many-statements self._dictionaries_listbox_selected_dictionary_name = '' self._dictionaries_listbox_selected_dictionary_index = -1 self._dictionary_names: List[str] = [] + dictionary = self._settings_dict['dictionary']['user'] + self._dictionary_names = itb_util.dictionaries_str_to_list(dictionary) + if ','.join(self._dictionary_names) != dictionary: + # Value changed due to normalization or getting the locale + # defaults, save it back to settings: + self._gsettings.set_value( + 'dictionary', + GLib.Variant.new_string(','.join(self._dictionary_names))) self._dictionaries_listbox = None self._dictionaries_add_listbox = None self._dictionaries_add_listbox_dictionary_names: List[str] = [] @@ -1244,11 +1242,17 @@ def __init__(self) -> None: # pylint: disable=too-many-statements _('Set to default')) self._input_methods_default_button.add( self._input_methods_default_button_label) - self._input_methods_default_button.set_tooltip_text( + input_methods_default_button_tooltip_text = ( # Translators: Tooltip for a button used to set the list of # input methods to the default for the current locale. _('Set input methods to the default for the current locale.') - + f' LC_CTYPE={itb_util.get_effective_lc_ctype()}') + + f' (LC_CTYPE={itb_util.get_effective_lc_ctype()} ' + f'➡ {itb_util.input_methods_str_to_list("")}') + if self._engine_name != 'typing-booster': + input_methods_default_button_tooltip_text = ( + f'➡ {self._m17n_ime_lang}-{self._m17n_ime_name}') + self._input_methods_default_button.set_tooltip_text( + input_methods_default_button_tooltip_text) self._input_methods_default_button.connect( 'clicked', self._on_input_methods_default_button_clicked) self._input_methods_default_button.set_sensitive(True) @@ -1262,6 +1266,14 @@ def __init__(self) -> None: # pylint: disable=too-many-statements self._input_methods_listbox_selected_ime_name = '' self._input_methods_listbox_selected_ime_index = -1 self._current_imes: List[str] = [] + inputmethod = self._settings_dict['inputmethod']['user'] + self._current_imes = itb_util.input_methods_str_to_list(inputmethod) + if ','.join(self._current_imes) != inputmethod: + # Value changed due to normalization or getting the locale + # defaults, save it back to settings: + self._gsettings.set_value( + 'inputmethod', + GLib.Variant.new_string(','.join(self._current_imes))) self._input_methods_listbox = None self._input_methods_add_listbox = None self._input_methods_add_listbox_imes: List[str] = [] @@ -1511,12 +1523,10 @@ def __init__(self) -> None: # pylint: disable=too-many-statements 'one is selected on top of the list of candidates.')) self._show_number_of_candidates_checkbutton.connect( 'clicked', self._on_show_number_of_candidates_checkbutton) - self._show_number_of_candidates = itb_util.variant_to_value( - self._gsettings.get_value('shownumberofcandidates')) - if self._show_number_of_candidates is None: - self._show_number_of_candidates = False - if self._show_number_of_candidates is True: - self._show_number_of_candidates_checkbutton.set_active(True) + self._show_number_of_candidates = self._settings_dict[ + 'shownumberofcandidates']['user'] + self._show_number_of_candidates_checkbutton.set_active( + self._show_number_of_candidates) self._show_status_info_in_auxiliary_text_checkbutton = Gtk.CheckButton( # Translators: Checkbox to choose whether to show above @@ -1535,13 +1545,10 @@ def __init__(self) -> None: # pylint: disable=too-many-statements 'candidate list.')) self._show_status_info_in_auxiliary_text_checkbutton.connect( 'clicked', self._on_show_status_info_in_auxiliary_text_checkbutton) - self._show_status_info_in_auxiliary_text = itb_util.variant_to_value( - self._gsettings.get_value('showstatusinfoinaux')) - if self._show_status_info_in_auxiliary_text is None: - self._show_status_info_in_auxiliary_text = False - if self._show_status_info_in_auxiliary_text is True: - self._show_status_info_in_auxiliary_text_checkbutton.set_active( - True) + self._show_status_info_in_auxiliary_text = self._settings_dict[ + 'showstatusinfoinaux']['user'] + self._show_status_info_in_auxiliary_text_checkbutton.set_active( + self._show_status_info_in_auxiliary_text) _appearance_grid_row += 1 self._appearance_grid.attach( @@ -1564,12 +1571,8 @@ def __init__(self) -> None: # pylint: disable=too-many-statements self._page_size_adjustment.set_can_focus(True) self._page_size_adjustment.set_increments(1.0, 1.0) self._page_size_adjustment.set_range(1.0, 9.0) - self._page_size = itb_util.variant_to_value( - self._gsettings.get_value('pagesize')) - if self._page_size: - self._page_size_adjustment.set_value(int(self._page_size)) - else: - self._page_size_adjustment.set_value(6) + self._page_size = self._settings_dict['pagesize']['user'] + self._page_size_adjustment.set_value(int(self._page_size)) self._page_size_adjustment.connect( 'value-changed', self._on_page_size_adjustment_value_changed) _appearance_grid_row += 1 @@ -1602,10 +1605,8 @@ def __init__(self) -> None: # pylint: disable=too-many-statements renderer_text, True) self._lookup_table_orientation_combobox.add_attribute( renderer_text, "text", 0) - self._lookup_table_orientation = itb_util.variant_to_value( - self._gsettings.get_value('lookuptableorientation')) - if self._lookup_table_orientation is None: - self._lookup_table_orientation = IBus.Orientation.VERTICAL + self._lookup_table_orientation = self._settings_dict[ + 'lookuptableorientation']['user'] for i, item in enumerate(self._lookup_table_orientation_store): if self._lookup_table_orientation == item[1]: self._lookup_table_orientation_combobox.set_active(i) @@ -1639,13 +1640,10 @@ def __init__(self) -> None: # pylint: disable=too-many-statements 10.0, 100.0) self._candidates_delay_milliseconds_adjustment.set_range( 0.0, float(itb_util.UINT32_MAX)) - self._candidates_delay_milliseconds = itb_util.variant_to_value( - self._gsettings.get_value('candidatesdelaymilliseconds')) - if self._candidates_delay_milliseconds: - self._candidates_delay_milliseconds_adjustment.set_value( - int(self._candidates_delay_milliseconds)) - else: - self._candidates_delay_milliseconds_adjustment.set_value(200.0) + self._candidates_delay_milliseconds = self._settings_dict[ + 'candidatesdelaymilliseconds']['user'] + self._candidates_delay_milliseconds_adjustment.set_value( + int(self._candidates_delay_milliseconds)) self._candidates_delay_milliseconds_adjustment.connect( 'value-changed', self._on_candidates_delay_milliseconds_adjustment_value_changed) @@ -1690,10 +1688,8 @@ def __init__(self) -> None: # pylint: disable=too-many-statements renderer_text, True) self._preedit_underline_combobox.add_attribute( renderer_text, "text", 0) - self._preedit_underline = itb_util.variant_to_value( - self._gsettings.get_value('preeditunderline')) - if self._preedit_underline is None: - self._preedit_underline = IBus.AttrUnderline.SINGLE + self._preedit_underline = self._settings_dict[ + 'preeditunderline']['user'] for i, item in enumerate(self._preedit_underline_store): if self._preedit_underline == item[1]: self._preedit_underline_combobox.set_active(i) @@ -1723,12 +1719,10 @@ def __init__(self) -> None: # pylint: disable=too-many-statements 'number of characters before a lookup is done.')) self._preedit_style_only_when_lookup_checkbutton.connect( 'clicked', self._on_preedit_style_only_when_lookup_checkbutton) - self._preedit_style_only_when_lookup = itb_util.variant_to_value( - self._gsettings.get_value('preeditstyleonlywhenlookup')) - if self._preedit_style_only_when_lookup is None: - self._preedit_style_only_when_lookup = False - if self._preedit_style_only_when_lookup is True: - self._preedit_style_only_when_lookup_checkbutton.set_active(True) + self._preedit_style_only_when_lookup = self._settings_dict[ + 'preeditstyleonlywhenlookup']['user'] + self._preedit_style_only_when_lookup_checkbutton.set_active( + self._preedit_style_only_when_lookup) _appearance_grid_row += 1 self._appearance_grid.attach( self._preedit_style_only_when_lookup_checkbutton, @@ -1747,10 +1741,8 @@ def __init__(self) -> None: # pylint: disable=too-many-statements 'a spelling error.')) self._color_preedit_spellcheck_checkbutton.set_hexpand(False) self._color_preedit_spellcheck_checkbutton.set_vexpand(False) - self._color_preedit_spellcheck = itb_util.variant_to_value( - self._gsettings.get_value('colorpreeditspellcheck')) - if self._color_preedit_spellcheck is None: - self._color_preedit_spellcheck = False + self._color_preedit_spellcheck = self._settings_dict[ + 'colorpreeditspellcheck']['user'] self._color_preedit_spellcheck_checkbutton.set_active( self._color_preedit_spellcheck) self._color_preedit_spellcheck_checkbutton.connect( @@ -1777,8 +1769,8 @@ def __init__(self) -> None: # pylint: disable=too-many-statements 'the preedit when the preedit might contain a ' 'spelling error. This setting only has an ' 'effect if the preedit spellchecking is enabled.')) - self._color_preedit_spellcheck_string = itb_util.variant_to_value( - self._gsettings.get_value('colorpreeditspellcheckstring')) + self._color_preedit_spellcheck_string = self._settings_dict[ + 'colorpreeditspellcheckstring']['user'] gdk_rgba = Gdk.RGBA() gdk_rgba.parse(self._color_preedit_spellcheck_string) self._color_preedit_spellcheck_rgba_colorbutton.set_rgba(gdk_rgba) @@ -1803,10 +1795,8 @@ def __init__(self) -> None: # pylint: disable=too-many-statements 'is used for a suggestion shown inline.')) self._color_inline_completion_checkbutton.set_hexpand(False) self._color_inline_completion_checkbutton.set_vexpand(False) - self._color_inline_completion = itb_util.variant_to_value( - self._gsettings.get_value('colorinlinecompletion')) - if self._color_inline_completion is None: - self._color_inline_completion = True + self._color_inline_completion = self._settings_dict[ + 'colorinlinecompletion']['user'] self._color_inline_completion_checkbutton.set_active( self._color_inline_completion) self._color_inline_completion_checkbutton.connect( @@ -1833,8 +1823,8 @@ def __init__(self) -> None: # pylint: disable=too-many-statements 'inline completion. This setting only has an ' 'effect if the use of color for inline completion ' 'is enabled and inline completion is enabled.')) - self._color_inline_completion_string = itb_util.variant_to_value( - self._gsettings.get_value('colorinlinecompletionstring')) + self._color_inline_completion_string = self._settings_dict[ + 'colorinlinecompletionstring']['user'] gdk_rgba = Gdk.RGBA() gdk_rgba.parse(self._color_inline_completion_string) self._color_inline_completion_rgba_colorbutton.set_rgba(gdk_rgba) @@ -1859,10 +1849,8 @@ def __init__(self) -> None: # pylint: disable=too-many-statements 'is used for the compose preview.')) self._color_compose_preview_checkbutton.set_hexpand(False) self._color_compose_preview_checkbutton.set_vexpand(False) - self._color_compose_preview = itb_util.variant_to_value( - self._gsettings.get_value('colorcomposepreview')) - if self._color_compose_preview is None: - self._color_compose_preview = True + self._color_compose_preview = self._settings_dict[ + 'colorcomposepreview']['user'] self._color_compose_preview_checkbutton.set_active( self._color_compose_preview) self._color_compose_preview_checkbutton.connect( @@ -1887,8 +1875,8 @@ def __init__(self) -> None: # pylint: disable=too-many-statements self._color_compose_preview_rgba_colorbutton.set_tooltip_text( _('Here you can specify which color to use for ' 'the compose preview.')) - self._color_compose_preview_string = itb_util.variant_to_value( - self._gsettings.get_value('colorcomposepreviewstring')) + self._color_compose_preview_string = self._settings_dict[ + 'colorcomposepreviewstring']['user'] gdk_rgba = Gdk.RGBA() gdk_rgba.parse(self._color_compose_preview_string) self._color_compose_preview_rgba_colorbutton.set_rgba(gdk_rgba) @@ -1915,10 +1903,7 @@ def __init__(self) -> None: # pylint: disable=too-many-statements 'from the user database.')) self._color_userdb_checkbutton.set_hexpand(False) self._color_userdb_checkbutton.set_vexpand(False) - self._color_userdb = itb_util.variant_to_value( - self._gsettings.get_value('coloruserdb')) - if self._color_userdb is None: - self._color_userdb = False + self._color_userdb = self._settings_dict['coloruserdb']['user'] self._color_userdb_checkbutton.set_active(self._color_userdb) self._color_userdb_checkbutton.connect( 'clicked', self._on_color_userdb_checkbutton) @@ -1941,8 +1926,8 @@ def __init__(self) -> None: # pylint: disable=too-many-statements 'from the user database. This setting only ' 'has an effect if the use of color for ' 'candidates from the user database is enabled.')) - self._color_userdb_string = itb_util.variant_to_value( - self._gsettings.get_value('coloruserdbstring')) + self._color_userdb_string = self._settings_dict[ + 'coloruserdbstring']['user'] gdk_rgba = Gdk.RGBA() gdk_rgba.parse(self._color_userdb_string) self._color_userdb_rgba_colorbutton.set_rgba(gdk_rgba) @@ -1966,10 +1951,7 @@ def __init__(self) -> None: # pylint: disable=too-many-statements 'which come from spellchecking.')) self._color_spellcheck_checkbutton.set_hexpand(False) self._color_spellcheck_checkbutton.set_vexpand(False) - self._color_spellcheck = itb_util.variant_to_value( - self._gsettings.get_value('colorspellcheck')) - if self._color_spellcheck is None: - self._color_spellcheck = False + self._color_spellcheck = self._settings_dict['colorspellcheck']['user'] self._color_spellcheck_checkbutton.set_active(self._color_spellcheck) self._color_spellcheck_checkbutton.connect( 'clicked', self._on_color_spellcheck_checkbutton) @@ -1992,8 +1974,8 @@ def __init__(self) -> None: # pylint: disable=too-many-statements 'from spellchecking. This setting only has ' 'an effect if the use of color for candidates ' 'from spellchecking is enabled.')) - self._color_spellcheck_string = itb_util.variant_to_value( - self._gsettings.get_value('colorspellcheckstring')) + self._color_spellcheck_string = self._settings_dict[ + 'colorspellcheckstring']['user'] gdk_rgba = Gdk.RGBA() gdk_rgba.parse(self._color_spellcheck_string) self._color_spellcheck_rgba_colorbutton.set_rgba(gdk_rgba) @@ -2020,10 +2002,7 @@ def __init__(self) -> None: # pylint: disable=too-many-statements 'which come from a dictionary.')) self._color_dictionary_checkbutton.set_hexpand(False) self._color_dictionary_checkbutton.set_vexpand(False) - self._color_dictionary = itb_util.variant_to_value( - self._gsettings.get_value('colordictionary')) - if self._color_dictionary is None: - self._color_dictionary = False + self._color_dictionary = self._settings_dict['colordictionary']['user'] self._color_dictionary_checkbutton.set_active(self._color_dictionary) self._color_dictionary_checkbutton.connect( 'clicked', self._on_color_dictionary_checkbutton) @@ -2046,8 +2025,8 @@ def __init__(self) -> None: # pylint: disable=too-many-statements 'from a dictionary. This setting only has ' 'an effect if the use of color for candidates ' 'from a dictionary is enabled.')) - self._color_dictionary_string = itb_util.variant_to_value( - self._gsettings.get_value('colordictionarystring')) + self._color_dictionary_string = self._settings_dict[ + 'colordictionarystring']['user'] gdk_rgba = Gdk.RGBA() gdk_rgba.parse(self._color_dictionary_string) self._color_dictionary_rgba_colorbutton.set_rgba(gdk_rgba) @@ -2074,10 +2053,7 @@ def __init__(self) -> None: # pylint: disable=too-many-statements 'come from the user database.')) self._label_userdb_checkbutton.set_hexpand(False) self._label_userdb_checkbutton.set_vexpand(False) - self._label_userdb = itb_util.variant_to_value( - self._gsettings.get_value('labeluserdb')) - if self._label_userdb is None: - self._label_userdb = False + self._label_userdb = self._settings_dict['labeluserdb']['user'] self._label_userdb_checkbutton.set_active(self._label_userdb) self._label_userdb_checkbutton.connect( 'clicked', self._on_label_userdb_checkbutton) @@ -2086,12 +2062,9 @@ def __init__(self) -> None: # pylint: disable=too-many-statements self._label_userdb_entry.set_can_focus(True) self._label_userdb_entry.set_hexpand(False) self._label_userdb_entry.set_vexpand(False) - self._label_userdb_string = itb_util.variant_to_value( - self._gsettings.get_value('labeluserdbstring')) - if not self._label_userdb_string: - self._label_userdb_string = '' - self._label_userdb_entry.set_text( - self._label_userdb_string) + self._label_userdb_string = self._settings_dict[ + 'labeluserdbstring']['user'] + self._label_userdb_entry.set_text(self._label_userdb_string) self._label_userdb_entry.connect( 'notify::text', self._on_label_userdb_entry) _appearance_grid_row += 1 @@ -2111,10 +2084,7 @@ def __init__(self) -> None: # pylint: disable=too-many-statements 'come from spellchecking.')) self._label_spellcheck_checkbutton.set_hexpand(False) self._label_spellcheck_checkbutton.set_vexpand(False) - self._label_spellcheck = itb_util.variant_to_value( - self._gsettings.get_value('labelspellcheck')) - if self._label_spellcheck is None: - self._label_spellcheck = False + self._label_spellcheck = self._settings_dict['labelspellcheck']['user'] self._label_spellcheck_checkbutton.set_active(self._label_spellcheck) self._label_spellcheck_checkbutton.connect( 'clicked', self._on_label_spellcheck_checkbutton) @@ -2123,12 +2093,9 @@ def __init__(self) -> None: # pylint: disable=too-many-statements self._label_spellcheck_entry.set_can_focus(True) self._label_spellcheck_entry.set_hexpand(False) self._label_spellcheck_entry.set_vexpand(False) - self._label_spellcheck_string = itb_util.variant_to_value( - self._gsettings.get_value('labelspellcheckstring')) - if not self._label_spellcheck_string: - self._label_spellcheck_string = '' - self._label_spellcheck_entry.set_text( - self._label_spellcheck_string) + self._label_spellcheck_string = self._settings_dict[ + 'labelspellcheckstring']['user'] + self._label_spellcheck_entry.set_text(self._label_spellcheck_string) self._label_spellcheck_entry.connect( 'notify::text', self._on_label_spellcheck_entry) _appearance_grid_row += 1 @@ -2148,10 +2115,7 @@ def __init__(self) -> None: # pylint: disable=too-many-statements 'come from a dictionary.')) self._label_dictionary_checkbutton.set_hexpand(False) self._label_dictionary_checkbutton.set_vexpand(False) - self._label_dictionary = itb_util.variant_to_value( - self._gsettings.get_value('labeldictionary')) - if self._label_dictionary is None: - self._label_dictionary = False + self._label_dictionary = self._settings_dict['labeldictionary']['user'] self._label_dictionary_checkbutton.set_active(self._label_dictionary) self._label_dictionary_checkbutton.connect( 'clicked', self._on_label_dictionary_checkbutton) @@ -2160,12 +2124,9 @@ def __init__(self) -> None: # pylint: disable=too-many-statements self._label_dictionary_entry.set_can_focus(True) self._label_dictionary_entry.set_hexpand(False) self._label_dictionary_entry.set_vexpand(False) - self._label_dictionary_string = itb_util.variant_to_value( - self._gsettings.get_value('labeldictionarystring')) - if not self._label_dictionary_string: - self._label_dictionary_string = '' - self._label_dictionary_entry.set_text( - self._label_dictionary_string) + self._label_dictionary_string = self._settings_dict[ + 'labeldictionarystring']['user'] + self._label_dictionary_entry.set_text(self._label_dictionary_string) self._label_dictionary_entry.connect( 'notify::text', self._on_label_dictionary_entry) _appearance_grid_row += 1 @@ -2184,10 +2145,7 @@ def __init__(self) -> None: # pylint: disable=too-many-statements 'the lookup table which come from a dictionary.')) self._flag_dictionary_checkbutton.set_hexpand(False) self._flag_dictionary_checkbutton.set_vexpand(False) - self._flag_dictionary = itb_util.variant_to_value( - self._gsettings.get_value('flagdictionary')) - if self._flag_dictionary is None: - self._flag_dictionary = False + self._flag_dictionary = self._settings_dict['flagdictionary']['user'] self._flag_dictionary_checkbutton.set_active(self._flag_dictionary) self._flag_dictionary_checkbutton.connect( 'clicked', self._on_flag_dictionary_checkbutton) @@ -2204,10 +2162,7 @@ def __init__(self) -> None: # pylint: disable=too-many-statements 'to indicate when ibus-typing-booster is busy.')) self._label_busy_checkbutton.set_hexpand(False) self._label_busy_checkbutton.set_vexpand(False) - self._label_busy = itb_util.variant_to_value( - self._gsettings.get_value('labelbusy')) - if self._label_busy is None: - self._label_busy = True + self._label_busy = self._settings_dict['labelbusy']['user'] self._label_busy_checkbutton.set_active(self._label_busy) self._label_busy_checkbutton.connect( 'clicked', self._on_label_busy_checkbutton) @@ -2216,12 +2171,9 @@ def __init__(self) -> None: # pylint: disable=too-many-statements self._label_busy_entry.set_can_focus(True) self._label_busy_entry.set_hexpand(False) self._label_busy_entry.set_vexpand(False) - self._label_busy_string = itb_util.variant_to_value( - self._gsettings.get_value('labelbusystring')) - if not self._label_busy_string: - self._label_busy_string = '' - self._label_busy_entry.set_text( - self._label_busy_string) + self._label_busy_string = self._settings_dict[ + 'labelbusystring']['user'] + self._label_busy_entry.set_text(self._label_busy_string) self._label_busy_entry.connect( 'notify::text', self._on_label_busy_entry) _appearance_grid_row += 1 @@ -2240,8 +2192,8 @@ def __init__(self) -> None: # pylint: disable=too-many-statements self._speech_recognition_grid.attach( self._google_application_credentials_label, 0, 0, 1, 1) - self._google_application_credentials = itb_util.variant_to_value( - self._gsettings.get_value('googleapplicationcredentials')) + self._google_application_credentials = self._settings_dict[ + 'googleapplicationcredentials']['user'] if not self._google_application_credentials: self._google_application_credentials = _('File not yet set.') self._google_application_credentials_button = Gtk.Button() @@ -2376,6 +2328,135 @@ def __init__(self) -> None: # pylint: disable=too-many-statements if not self._keybindings['toggle_input_mode_on_off']: self._remember_input_mode_checkbutton.hide() + self._gsettings.connect('changed', self._on_gsettings_value_changed) + + def _init_settings_dict(self) -> Dict[str, Any]: + '''Initialize a dictionary with the default and user settings for all + settings keys. + + The default settings start with the defaults from the + gsettings schema. Some of these generic default values may be + overridden by more specific default settings for the specific engine. + After this possible modification for a specific engine we have the final default + settings for this specific typing booster input method. + + The user settings start with a copy of these final default settings, + then they are possibly modified by user gsettings. + + Keeping a copy of the default settings in the settings dictionary + makes it easy to revert some or all settings to the defaults. + ''' + settings_dict: Dict[str, Any] = {} + set_functions = { + 'disableinterminals': self.set_disable_in_terminals, + 'asciidigits': self.set_ascii_digits, + 'avoidforwardkeyevent': self.set_avoid_forward_key_event, + 'addspaceoncommit': self.set_add_space_on_commit, + 'arrowkeysreopenpreedit': self.set_arrow_keys_reopen_preedit, + 'emojipredictions': self.set_emoji_prediction_mode, + 'offtherecord': self.set_off_the_record_mode, + 'recordmode': self.set_record_mode, + 'emojitriggercharacters': self.set_emoji_trigger_characters, + 'autocommitcharacters': self.set_auto_commit_characters, + 'googleapplicationcredentials': + self.set_google_application_credentials, + 'tabenable': self.set_tab_enable, + 'inlinecompletion': self.set_inline_completion, + 'autocapitalize': self.set_auto_capitalize, + 'rememberlastusedpreeditime': + self.set_remember_last_used_preedit_ime, + 'rememberinputmode': self.set_remember_input_mode, + 'pagesize': self.set_page_size, + 'candidatesdelaymilliseconds': + self.set_candidates_delay_milliseconds, + 'lookuptableorientation': self.set_lookup_table_orientation, + 'preeditunderline': self.set_preedit_underline, + 'preeditstyleonlywhenlookup': + self.set_preedit_style_only_when_lookup, + 'mincharcomplete': self.set_min_char_complete, + 'errorsound': self.set_error_sound, + 'errorsoundfile': self.set_error_sound_file, + 'soundbackend': self.set_sound_backend, + 'debuglevel': self.set_debug_level, + 'shownumberofcandidates': self.set_show_number_of_candidates, + 'showstatusinfoinaux': self.set_show_status_info_in_auxiliary_text, + 'autoselectcandidate': self.set_auto_select_candidate, + 'colorpreeditspellcheck': self.set_color_preedit_spellcheck, + 'colorpreeditspellcheckstring': + self.set_color_preedit_spellcheck_string, + 'colorinlinecompletion': self.set_color_inline_completion, + 'colorinlinecompletionstring': + self.set_color_inline_completion_string, + 'colorcomposepreview': self.set_color_compose_preview, + 'colorcomposepreviewstring': self.set_color_compose_preview_string, + 'coloruserdb': self.set_color_userdb, + 'coloruserdbstring': self.set_color_userdb_string, + 'colorspellcheck': self.set_color_spellcheck, + 'colorspellcheckstring': self.set_color_spellcheck_string, + 'colordictionary': self.set_color_dictionary, + 'colordictionarystring': self.set_color_dictionary_string, + 'labeluserdb': self.set_label_userdb, + 'labeluserdbstring': self.set_label_userdb_string, + 'labelspellcheck': self.set_label_spellcheck, + 'labelspellcheckstring': self.set_label_spellcheck_string, + 'labeldictionary': self.set_label_dictionary, + 'labeldictionarystring': self.set_label_dictionary_string, + 'flagdictionary': self.set_flag_dictionary, + 'labelbusy': self.set_label_busy, + 'labelbusystring': self.set_label_busy_string, + 'inputmethod': self.set_current_imes, + 'dictionary': self.set_dictionary_names, + 'keybindings': self.set_keybindings, + 'dictionaryinstalltimestamp': self._reload_dictionaries, + 'inputmethodchangetimestamp': self._reload_input_methods, + 'autosettings': self.set_autosettings, + } + + schema_source: Gio.SettingsSchemaSource = ( + Gio.SettingsSchemaSource.get_default()) + schema: Gio.SettingsSchema = schema_source.lookup( + 'org.freedesktop.ibus.engine.typing-booster', True) + special_defaults = { + 'dictionary': 'None', # special dummy dictionary + 'inputmethod': f'{self._m17n_ime_lang}-{self._m17n_ime_name}', + 'tabenable': True, + 'offtherecord': True, + 'preeditunderline': 0, + } + for key in schema.list_keys(): + if key == 'keybindings': # keybindings are special! + default_value = itb_util.variant_to_value( + self._gsettings.get_default_value('keybindings')) + if self._engine_name != 'typing-booster': + default_value['toggle_input_mode_on_off'] = [] + default_value['enable_lookup'] = [] + default_value['commit_and_forward_key'] = ['Left'] + # copy the updated default keybindings, i.e. the + # default keybindings for this specific engine, into + # the user keybindings: + user_value = copy.deepcopy(default_value) + user_gsettings = itb_util.variant_to_value( + self._gsettings.get_user_value(key)) + if not user_gsettings: + user_gsettings = {} + itb_util.dict_update_existing_keys(user_value, user_gsettings) + else: + default_value = itb_util.variant_to_value( + self._gsettings.get_default_value(key)) + if self._engine_name != 'typing-booster': + if key in special_defaults: + default_value = special_defaults[key] + user_value = itb_util.variant_to_value( + self._gsettings.get_user_value(key)) + if user_value is None: + user_value = default_value + if key in set_functions: + settings_dict[key] = { + 'default': default_value, + 'user': user_value, + 'set_function': set_functions[key]} + return settings_dict + def _fill_dictionaries_listbox_row(self, name: str) -> Tuple[str, bool]: ''' Formats the text of a line in the listbox of configured dictionaries @@ -2447,16 +2528,6 @@ def _fill_dictionaries_listbox(self) -> None: self._dictionaries_listbox.set_activate_on_single_click(True) self._dictionaries_listbox.connect( 'row-selected', self._on_dictionary_selected) - self._dictionary_names = [] - dictionary = itb_util.variant_to_value( - self._gsettings.get_value('dictionary')) - self._dictionary_names = itb_util.dictionaries_str_to_list(dictionary) - if ','.join(self._dictionary_names) != dictionary: - # Value changed due to normalization or getting the locale - # defaults, save it back to settings: - self._gsettings.set_value( - 'dictionary', - GLib.Variant.new_string(','.join(self._dictionary_names))) missing_dictionaries = False if list(self._dictionary_names) != ['None']: for name in self._dictionary_names: @@ -2530,16 +2601,6 @@ def _fill_input_methods_listbox(self) -> None: self._input_methods_listbox.set_activate_on_single_click(True) self._input_methods_listbox.connect( 'row-selected', self._on_input_method_selected) - self._current_imes = [] - inputmethod = itb_util.variant_to_value( - self._gsettings.get_value('inputmethod')) - self._current_imes = itb_util.input_methods_str_to_list(inputmethod) - if ','.join(self._current_imes) != inputmethod: - # Value changed due to normalization or getting the locale - # defaults, save it back to settings: - self._gsettings.set_value( - 'inputmethod', - GLib.Variant.new_string(','.join(self._current_imes))) for ime in self._current_imes: label = Gtk.Label() label.set_text(html.escape( @@ -2796,71 +2857,10 @@ def _on_gsettings_value_changed( ''' value = itb_util.variant_to_value(self._gsettings.get_value(key)) LOGGER.info('Settings changed: key=%s value=%s\n', key, value) - set_functions = { - 'disableinterminals': self.set_disable_in_terminals, - 'asciidigits': self.set_ascii_digits, - 'avoidforwardkeyevent': self.set_avoid_forward_key_event, - 'addspaceoncommit': self.set_add_space_on_commit, - 'arrowkeysreopenpreedit': self.set_arrow_keys_reopen_preedit, - 'emojipredictions': self.set_emoji_prediction_mode, - 'offtherecord': self.set_off_the_record_mode, - 'recordmode': self.set_record_mode, - 'emojitriggercharacters': self.set_emoji_trigger_characters, - 'autocommitcharacters': self.set_auto_commit_characters, - 'googleapplicationcredentials': - self.set_google_application_credentials, - 'tabenable': self.set_tab_enable, - 'inlinecompletion': self.set_inline_completion, - 'autocapitalize': self.set_auto_capitalize, - 'rememberlastusedpreeditime': - self.set_remember_last_used_preedit_ime, - 'rememberinputmode': self.set_remember_input_mode, - 'pagesize': self.set_page_size, - 'candidatesdelaymilliseconds': - self.set_candidates_delay_milliseconds, - 'lookuptableorientation': self.set_lookup_table_orientation, - 'preeditunderline': self.set_preedit_underline, - 'preeditstyleonlywhenlookup': - self.set_preedit_style_only_when_lookup, - 'mincharcomplete': self.set_min_char_complete, - 'errorsound': self.set_error_sound, - 'errorsoundfile': self.set_error_sound_file, - 'debuglevel': self.set_debug_level, - 'shownumberofcandidates': self.set_show_number_of_candidates, - 'showstatusinfoinaux': self.set_show_status_info_in_auxiliary_text, - 'autoselectcandidate': self.set_auto_select_candidate, - 'colorpreeditspellcheck': self.set_color_preedit_spellcheck, - 'colorpreeditspellcheckstring': - self.set_color_preedit_spellcheck_string, - 'colorinlinecompletion': self.set_color_inline_completion, - 'colorinlinecompletionstring': - self.set_color_inline_completion_string, - 'colorcomposepreview': self.set_color_compose_preview, - 'colorcomposepreviewstring': self.set_color_compose_preview_string, - 'coloruserdb': self.set_color_userdb, - 'coloruserdbstring': self.set_color_userdb_string, - 'colorspellcheck': self.set_color_spellcheck, - 'colorspellcheckstring': self.set_color_spellcheck_string, - 'colordictionary': self.set_color_dictionary, - 'colordictionarystring': self.set_color_dictionary_string, - 'labeluserdb': self.set_label_userdb, - 'labeluserdbstring': self.set_label_userdb_string, - 'labelspellcheck': self.set_label_spellcheck, - 'labelspellcheckstring': self.set_label_spellcheck_string, - 'labeldictionary': self.set_label_dictionary, - 'labeldictionarystring': self.set_label_dictionary_string, - 'flagdictionary': self.set_flag_dictionary, - 'labelbusy': self.set_label_busy, - 'labelbusystring': self.set_label_busy_string, - 'inputmethod': self.set_current_imes, - 'dictionary': self.set_dictionary_names, - 'keybindings': self.set_keybindings, - 'dictionaryinstalltimestamp': self._reload_dictionaries, - 'inputmethodchangetimestamp': self._reload_input_methods, - 'autosettings': self.set_autosettings, - } - if key in set_functions: - set_functions[key](value, update_gsettings=False) + if key in self._settings_dict: + set_function = self._settings_dict[key]['set_function'] + if set_function: + set_function(value, update_gsettings=False) return LOGGER.error('Unknown key=%s', key) return @@ -2887,16 +2887,22 @@ def _on_restore_all_defaults_button_clicked( _('Do you really want to restore all default settings?')) if response == Gtk.ResponseType.OK: LOGGER.info('Restoring all defaults.') - gsettings = Gio.Settings( - schema='org.freedesktop.ibus.engine.typing-booster') - schema = gsettings.get_property('settings-schema') - for key in schema.list_keys(): + for key in self._settings_dict: if key in ('googleapplicationcredentials', + 'inputmethodchangetimestamp', 'dictionaryinstalltimestamp'): LOGGER.info('Skipping reset of gsettings key=%s', key) continue LOGGER.info('Resetting gsettings key=%s', key) - gsettings.reset(key) + self._settings_dict[key]['set_function']( + self._settings_dict[key]['default'], + update_gsettings=True) + # Call it again with update_gsettings=False to make + # sure the active state of checkbuttons etc. is + # updated immediately: + self._settings_dict[key]['set_function']( + self._settings_dict[key]['default'], + update_gsettings=False) else: LOGGER.info('Restore all defaults cancelled.') self._restore_all_defaults_button.set_sensitive(True) @@ -3698,7 +3704,10 @@ def _on_dictionaries_default_button_clicked(self, *_args: Any) -> None: Sets the dictionaries to the default for the current locale. ''' - self.set_dictionary_names(itb_util.dictionaries_str_to_list('')) + if self._engine_name == 'typing-booster': + self.set_dictionary_names(itb_util.dictionaries_str_to_list('')) + return + self.set_dictionary_names(self._settings_dict['dictionary']['default']) def _on_dictionary_selected( self, _listbox: Gtk.ListBox, listbox_row: Gtk.ListBoxRow) -> None: @@ -4204,7 +4213,10 @@ def _on_input_methods_default_button_clicked(self, *_args: Any) -> None: Sets the input methods to the default for the current locale. ''' - self.set_current_imes(itb_util.input_methods_str_to_list('')) + if self._engine_name == 'typing-booster': + self.set_current_imes(itb_util.input_methods_str_to_list('')) + return + self.set_current_imes(self._settings_dict['inputmethod']['default']) def _on_input_method_selected( self, _listbox: Gtk.ListBox, listbox_row: Gtk.ListBoxRow) -> None: @@ -6230,31 +6242,43 @@ def set_error_sound_file( GLib.Variant.new_string(path)) else: self._error_sound_file_button_label.set_text(path) - path = os.path.expanduser(path) - if not IMPORT_SIMPLEAUDIO_SUCCESSFUL: - LOGGER.info( - 'No error sound because python3-simpleaudio is not available.') - else: - if not os.path.isfile(path): - LOGGER.info('Error sound file %s does not exist.', path) - elif not os.access(path, os.R_OK): - LOGGER.info('Error sound file %s not readable.', path) - else: - try: - LOGGER.info( - 'Trying to initialize and play error sound from %s', - path) - dummy = ( - simpleaudio.WaveObject.from_wave_file(path).play()) - LOGGER.info('Error sound could be initialized.') - except (FileNotFoundError, PermissionError): - LOGGER.exception( - 'Initializing error sound object failed.' - 'File not found or no read permissions.') - except Exception: # pylint: disable=broad-except - LOGGER.exception( - 'Initializing error sound object failed ' - 'for unknown reasons.') + self._error_sound_object = itb_sound.SoundObject( + os.path.expanduser(path), + audio_backend=self._sound_backend) + if self._error_sound_object: + try: + self._error_sound_object.play() + except Exception as error: # pylint: disable=broad-except + LOGGER.exception('Playing error sound failed: %s: %s', + error.__class__.__name__, error) + + def set_sound_backend( + self, + sound_backend: Union[str, Any], + update_gsettings: bool = True) -> None: + '''Sets the sound backend to use + + :param sound_backend: The name of sound backend to use + :param update_gsettings: Whether to write the change to Gsettings. + Set this to False if this method is + called because the dconf key changed + to avoid endless loops when the dconf + key is changed twice in a short time. + ''' + LOGGER.info( + '(%s, update_gsettings = %s)', sound_backend, update_gsettings) + if not isinstance(sound_backend, str): + return + if sound_backend == self._sound_backend: + return + self._sound_backend = sound_backend + if update_gsettings: + self._gsettings.set_value( + 'soundbackend', + GLib.Variant.new_string(sound_backend)) + self._error_sound_object = itb_sound.SoundObject( + os.path.expanduser(self._error_sound_file), + audio_backend=self._sound_backend) def set_debug_level( self, @@ -6687,5 +6711,7 @@ def _on_close_button_clicked(self, _button: Gtk.Button) -> None: DIALOG.destroy() sys.exit(1) M17N_DB_INFO = itb_util.M17nDbInfo() - SETUP_UI = SetupUI() + ENGINE_NAME = _ARGS.engine_name + LOGGER.info('engine name “%s”', ENGINE_NAME) + SETUP_UI = SetupUI(engine_name=_ARGS.engine_name) Gtk.main() diff --git a/tests/Makefile.am b/tests/Makefile.am index beebb829..d2a76294 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -25,6 +25,7 @@ TESTS = \ test_emoji_unicode_version.py \ test_hunspell_suggest.py \ test_itb.py \ + test_itb_m17n_emulation.py \ test_itb_pango.py \ test_itb_util.py \ test_keyvals_to_keycodes.py \ diff --git a/tests/test_0_gtk.py b/tests/test_0_gtk.py index 2e2cc866..f8f78156 100755 --- a/tests/test_0_gtk.py +++ b/tests/test_0_gtk.py @@ -104,14 +104,15 @@ def setUpClass(cls) -> None: cls._flag = False IBus.init() cls._gsettings = Gio.Settings( - schema='org.freedesktop.ibus.engine.typing-booster') + schema='org.freedesktop.ibus.engine.typing-booster', + path='/org/freedesktop/ibus/engine/typing-booster/') cls._orig_dictionary = cls._gsettings.get_string('dictionary') cls._orig_tabenable = cls._gsettings.get_boolean('tabenable') cls._orig_inputmode = cls._gsettings.get_boolean('inputmode') cls._orig_inline_completion = cls._gsettings.get_int('inlinecompletion') cls._orig_auto_select_candidate = cls._gsettings.get_int( 'autoselectcandidate') - cls._orig_candidates_delay_milliseconds = cls._gsettings.get_int( + cls._orig_candidates_delay_milliseconds = cls._gsettings.get_uint( 'candidatesdelaymilliseconds') signums: List[Optional[signal.Signals]] = [ getattr(signal, s, None) for s in 'SIGINT SIGTERM SIGHUP'.split()] @@ -130,7 +131,7 @@ def tearDownClass(cls) -> None: cls._gsettings.set_int('inlinecompletion', cls._orig_inline_completion) cls._gsettings.set_int('autoselectcandidate', cls._orig_auto_select_candidate) - cls._gsettings.set_int('candidatesdelaymilliseconds', + cls._gsettings.set_uint('candidatesdelaymilliseconds', cls._orig_candidates_delay_milliseconds) @classmethod @@ -233,7 +234,8 @@ def __create_engine_cb( self.__engine = hunspell_table.TypingBoosterEngine( self.__bus, object_path, - database) + database, + engine_name='typing-booster') self.__engine.connect('focus-in', self.__engine_focus_in) self.__engine.connect('focus-out', self.__engine_focus_out) self.__engine.connect_after('reset', self.__engine_after_reset) diff --git a/tests/test_itb.py b/tests/test_itb.py index ae86c93e..3015113e 100755 --- a/tests/test_itb.py +++ b/tests/test_itb.py @@ -136,8 +136,9 @@ def setUp(self) -> None: self.database = tabsqlitedb.TabSqliteDb(user_db_file=':memory:') self.engine = hunspell_table.TypingBoosterEngine( self.bus, - '/com/redhat/IBus/engines/table/typing_booster/engine/0', + '/com/redhat/IBus/engines/typing_booster/typing_booster/engine/0', self.database, + engine_name='typing-booster', unit_test=True) self.backup_original_settings() self.set_default_settings() diff --git a/tests/test_itb_m17n_emulation.py b/tests/test_itb_m17n_emulation.py new file mode 100755 index 00000000..610d92f8 --- /dev/null +++ b/tests/test_itb_m17n_emulation.py @@ -0,0 +1,386 @@ +#!/usr/bin/python3 +# +# ibus-typing-booster - A completion input method for IBus +# +# Copyright (c) 2016 Mike FABIAN +# +# This program 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. +# +# This program 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 + +''' +This file implements the test cases for the unit tests of ibus-typing-booster emulating ibus-m17n +''' + + +from typing import Any +from typing import Optional +import os +import sys +import re +import logging +import unicodedata +import unittest +import importlib +from unittest import mock + +# pylint: disable=wrong-import-position +from gi import require_version # type: ignore +require_version('IBus', '1.0') +from gi.repository import IBus # type: ignore +require_version('Gdk', '3.0') +from gi.repository import Gdk +# pylint: enable=wrong-import-position + +LOGGER = logging.getLogger('ibus-typing-booster') + +# Get more verbose output in the test log: +os.environ['IBUS_TYPING_BOOSTER_DEBUG_LEVEL'] = '255' + +# pylint: disable=import-error +from mock_engine import MockEngine +from mock_engine import MockLookupTable +from mock_engine import MockProperty +from mock_engine import MockPropList +# pylint: enable=import-error + +import testutils # pylint: disable=import-error + +# pylint: disable=wrong-import-order +sys.path.insert(0, "../engine") +# pylint: disable=import-error +import hunspell_table +import tabsqlitedb +import itb_util +import m17n_translit +# pylint: enable=import-error +sys.path.pop(0) +# pylint: enable=wrong-import-order + +# pylint: disable=missing-function-docstring +# pylint: disable=protected-access +# pylint: disable=line-too-long +# pylint: disable=invalid-name + +@unittest.skipIf(Gdk.Display.open('') is None, 'Display cannot be opened.') +class ItbM17nEmuTestCase(unittest.TestCase): + ''' + Test cases for ibus-typing-booster emulating ibus-m17n + ''' + engine_patcher = mock.patch.object( + IBus, 'Engine', new=MockEngine) + lookup_table_patcher = mock.patch.object( + IBus, 'LookupTable', new=MockLookupTable) + property_patcher = mock.patch.object( + IBus, 'Property', new=MockProperty) + prop_list_patcher = mock.patch.object( + IBus, 'PropList', new=MockPropList) + ibus_engine = IBus.Engine + ibus_lookup_table = IBus.LookupTable + ibus_property = IBus.Property + ibus_prop_list = IBus.PropList + + def setUp(self) -> None: + # Patch the IBus stuff with the mock classes: + self.engine_patcher.start() + self.lookup_table_patcher.start() + self.property_patcher.start() + self.prop_list_patcher.start() + assert IBus.Engine is not self.ibus_engine + assert IBus.Engine is MockEngine + assert IBus.LookupTable is not self.ibus_lookup_table + assert IBus.LookupTable is MockLookupTable + assert IBus.Property is not self.ibus_property + assert IBus.Property is MockProperty + assert IBus.PropList is not self.ibus_prop_list + assert IBus.PropList is MockPropList + # Reload the hunspell_table module so that the patches + # are applied to TypingBoosterEngine: + sys.path.insert(0, "../engine") + importlib.reload(hunspell_table) + sys.path.pop(0) + self.bus = IBus.Bus() + self.database: Optional[tabsqlitedb.TabSqliteDb] = None + self.engine: Optional[hunspell_table.TypingBoosterEngine] = None + #self._compose_sequences = itb_util.ComposeSequences() + + def tearDown(self) -> None: + if self.engine is not None: + self.restore_original_settings() + self.engine = None + if self.database is not None: + self.database.database.close() + self.database = None + # Remove the patches from the IBus stuff: + self.engine_patcher.stop() + self.lookup_table_patcher.stop() + self.property_patcher.stop() + self.prop_list_patcher.stop() + assert IBus.Engine is self.ibus_engine + assert IBus.Engine is not MockEngine + assert IBus.LookupTable is self.ibus_lookup_table + assert IBus.LookupTable is not MockLookupTable + assert IBus.Property is self.ibus_property + assert IBus.Property is not MockProperty + assert IBus.PropList is self.ibus_prop_list + assert IBus.PropList is not MockPropList + + def backup_original_settings(self) -> None: + if self.engine is None: + return + self.orig_disable_in_terminals = ( + self.engine.get_disable_in_terminals()) + self.orig_ascii_digits = ( + self.engine.get_ascii_digits()) + self.orig_emoji_prediction_mode = ( + self.engine.get_emoji_prediction_mode()) + self.orig_off_the_record_mode = ( + self.engine.get_off_the_record_mode()) + self.orig_record_mode = ( + self.engine.get_record_mode()) + self.orig_emoji_trigger_characters = ( + self.engine.get_emoji_trigger_characters()) + self.orig_auto_commit_characters = ( + self.engine.get_auto_commit_characters()) + self.orig_tab_enable = ( + self.engine.get_tab_enable()) + self.orig_inline_completion = ( + self.engine.get_inline_completion()) + self.orig_auto_capitalize = ( + self.engine.get_auto_capitalize()) + self.orig_auto_select_candidate = ( + self.engine.get_auto_select_candidate()) + self.orig_remember_last_used_preedit_ime = ( + self.engine.get_remember_last_used_preedit_ime()) + self.orig_page_size = ( + self.engine.get_page_size()) + self.orig_lookup_table_orientation = ( + self.engine.get_lookup_table_orientation()) + self.orig_min_char_complete = ( + self.engine.get_min_char_complete()) + self.orig_show_number_of_candidates = ( + self.engine.get_show_number_of_candidates()) + self.orig_show_status_info_in_auxiliary_text = ( + self.engine.get_show_status_info_in_auxiliary_text()) + self.orig_add_space_on_commit = ( + self.engine.get_add_space_on_commit()) + self.orig_current_imes = ( + self.engine.get_current_imes()) + self.orig_dictionary_names = ( + self.engine.get_dictionary_names()) + self.orig_avoid_forward_key_event = ( + self.engine.get_avoid_forward_key_event()) + self.orig_keybindings = ( + self.engine.get_keybindings()) + + def restore_original_settings(self) -> None: + if self.engine is None: + return + self.engine.set_disable_in_terminals( + self.orig_disable_in_terminals, + update_gsettings=False) + self.engine.set_ascii_digits( + self.orig_ascii_digits, + update_gsettings=False) + self.engine.set_emoji_prediction_mode( + self.orig_emoji_prediction_mode, + update_gsettings=False) + self.engine.set_off_the_record_mode( + self.orig_off_the_record_mode, + update_gsettings=False) + self.engine.set_record_mode( + self.orig_record_mode, + update_gsettings=False) + self.engine.set_emoji_trigger_characters( + self.orig_emoji_trigger_characters, + update_gsettings=False) + self.engine.set_auto_commit_characters( + self.orig_auto_commit_characters, + update_gsettings=False) + self.engine.set_tab_enable( + self.orig_tab_enable, + update_gsettings=False) + self.engine.set_inline_completion( + self.orig_inline_completion, + update_gsettings=False) + self.engine.set_auto_capitalize( + self.orig_auto_capitalize, + update_gsettings=False) + self.engine.set_auto_select_candidate( + self.orig_auto_select_candidate, + update_gsettings=False) + self.engine.set_remember_last_used_preedit_ime( + self.orig_remember_last_used_preedit_ime, + update_gsettings=False) + self.engine.set_page_size( + self.orig_page_size, + update_gsettings=False) + self.engine.set_lookup_table_orientation( + self.orig_lookup_table_orientation, + update_gsettings=False) + self.engine.set_min_char_complete( + self.orig_min_char_complete, + update_gsettings=False) + self.engine.set_show_number_of_candidates( + self.orig_show_number_of_candidates, + update_gsettings=False) + self.engine.set_show_status_info_in_auxiliary_text( + self.orig_show_status_info_in_auxiliary_text, + update_gsettings=False) + self.engine.set_add_space_on_commit( + self.orig_add_space_on_commit, + update_gsettings=False) + self.engine.set_current_imes( + self.orig_current_imes, + update_gsettings=False) + self.engine.set_dictionary_names( + self.orig_dictionary_names, + update_gsettings=False) + self.engine.set_avoid_forward_key_event( + self.orig_avoid_forward_key_event, + update_gsettings=False) + self.engine.set_keybindings( + self.orig_keybindings, + update_gsettings=False) + + def set_default_settings(self) -> None: + if self.engine is None: + return + for key in self.engine._settings_dict: + if 'set_function' in self.engine._settings_dict[key]: + self.engine._settings_dict[key]['set_function']( + self.engine._settings_dict[key]['default'], + update_gsettings=False) + return + + def init_engine(self, engine_name: str = 'typing-booster') -> None: + self.database = tabsqlitedb.TabSqliteDb(user_db_file=':memory:') + engine_path = ('/com/redhat/IBus/engines/typing_booster/' + f'{re.sub(r'[^a-zA-Z0-9_/]', '_', engine_name)}' + '/engine/') + if engine_name != 'typing-booster': + match = itb_util.M17N_ENGINE_NAME_PATTERN.search(engine_name) + if not match: + raise ValueError('Invalid engine name.') + m17n_ime_lang = match.group('lang') + m17n_ime_name = match.group('name') + self.get_transliterator_or_skip(f'{m17n_ime_lang}-{m17n_ime_name}') + engine_id = 0 + self.engine = hunspell_table.TypingBoosterEngine( + self.bus, + engine_path + str(engine_id), + self.database, + engine_name=engine_name, + unit_test=True) + if self.engine is None: + self.skipTest('Failed to init engine.') + self.backup_original_settings() + self.set_default_settings() + + def get_transliterator_or_skip(self, ime: str) -> Optional[Any]: + try: + sys.stderr.write(f'ime "{ime}" ... ') + trans = m17n_translit.Transliterator(ime) + except ValueError as error: + trans = None + self.skipTest(error) + except Exception as error: # pylint: disable=broad-except + sys.stderr.write('Unexpected exception!') + trans = None + self.skipTest(error) + return trans + + def test_dummy(self) -> None: + self.init_engine() + self.assertEqual(True, True) + + @unittest.expectedFailure + def test_expected_failure(self) -> None: + self.init_engine() + self.assertEqual(False, True) + + def test_typing_booster_normal(self) -> None: + self.init_engine(engine_name='typing-booster') + if self.engine is None: + self.skipTest('Failed to init engine.') + self.engine.set_current_imes( + ['t-latn-post', 'NoIME'], update_gsettings=False) + self.engine.set_dictionary_names( + ['en_US'], update_gsettings=False) + self.engine.do_process_key_event(IBus.KEY_a, 0, 0) + # Normal typing booster should have some candidates now: + self.assertNotEqual([], self.engine._candidates) + self.engine.do_process_key_event(IBus.KEY_quotedbl, 0, 0) + self.engine.do_process_key_event(IBus.KEY_space, 0, 0) + self.assertEqual(self.engine.mock_committed_text, 'ä ') + + def test_tb_t_latn_post(self) -> None: + self.init_engine(engine_name='tb:t:latn-post') + if self.engine is None: + self.skipTest('Failed to init engine.') + self.assertEqual(self.engine._engine_name, 'tb:t:latn-post') + self.assertEqual(self.engine._dictionary_names, ['None']) + self.assertEqual(self.engine._current_imes, ['t-latn-post']) + self.assertEqual(self.engine._tab_enable, True) + self.assertEqual(self.engine._off_the_record, True) + self.assertEqual(self.engine._preedit_underline, 0) + self.assertEqual(self.engine._keybindings['toggle_input_mode_on_off'], []) + self.assertEqual(self.engine._keybindings['enable_lookup'], []) + self.assertEqual(self.engine._keybindings['commit_and_forward_key'], ['Left']) + self.engine.do_process_key_event(IBus.KEY_a, 0, 0) + self.assertEqual(self.engine.mock_preedit_text, 'a') + self.assertEqual(self.engine.mock_committed_text, '') + self.assertEqual(self.engine.mock_committed_text_cursor_pos, 0) + # Restricted typing booster should have *no* candidates now: + self.assertEqual([], self.engine._candidates) + self.engine.do_process_key_event(IBus.KEY_quotedbl, 0, 0) + self.assertEqual(self.engine.mock_preedit_text, 'ä') + self.assertEqual(self.engine.mock_committed_text, '') + self.assertEqual(self.engine.mock_committed_text_cursor_pos, 0) + self.engine.do_process_key_event(IBus.KEY_space, 0, 0) + self.assertEqual(self.engine.mock_preedit_text, '') + self.assertEqual(self.engine.mock_committed_text, 'ä ') + self.assertEqual(self.engine.mock_committed_text_cursor_pos, 2) + # Type more and commit with 'Left': + self.engine.do_process_key_event(IBus.KEY_n, 0, 0) + self.assertEqual(self.engine.mock_preedit_text, 'n') + self.assertEqual(self.engine.mock_committed_text, 'ä ') + self.assertEqual(self.engine.mock_committed_text_cursor_pos, 2) + self.engine.do_process_key_event(IBus.KEY_asciitilde, 0, 0) + self.assertEqual(self.engine.mock_preedit_text, 'ñ') + self.assertEqual(self.engine.mock_committed_text, 'ä ') + self.assertEqual(self.engine.mock_committed_text_cursor_pos, 2) + self.engine.do_process_key_event(IBus.KEY_Left, 0, 0) + self.assertEqual(self.engine.mock_preedit_text, '') + self.assertEqual(self.engine.mock_committed_text, 'ä ñ') + self.assertEqual(self.engine.mock_committed_text_cursor_pos, 2) + # Type more and commit with space again: + self.engine.do_process_key_event(IBus.KEY_a, 0, 0) + self.assertEqual(self.engine.mock_preedit_text, 'a') + self.assertEqual(self.engine.mock_committed_text, 'ä ñ') + self.assertEqual(self.engine.mock_committed_text_cursor_pos, 2) + self.engine.do_process_key_event(IBus.KEY_slash, 0, 0) + self.assertEqual(self.engine.mock_preedit_text, 'å') + self.assertEqual(self.engine.mock_committed_text, 'ä ñ') + self.assertEqual(self.engine.mock_committed_text_cursor_pos, 2) + self.engine.do_process_key_event(IBus.KEY_space, 0, 0) + self.assertEqual(self.engine.mock_preedit_text, '') + self.assertEqual(self.engine.mock_committed_text, 'ä å ñ') + self.assertEqual(self.engine.mock_committed_text_cursor_pos, 4) + +if __name__ == '__main__': + LOG_HANDLER = logging.StreamHandler(stream=sys.stderr) + LOGGER.setLevel(logging.DEBUG) + # Activate this to see a lot of logging when running the tests + # manually: + # LOGGER.addHandler(LOG_HANDLER) + unittest.main()