diff --git a/SOURCES/add-a-gui-test.patch b/SOURCES/add-a-gui-test.patch new file mode 100644 index 0000000..bab7113 --- /dev/null +++ b/SOURCES/add-a-gui-test.patch @@ -0,0 +1,1816 @@ +diff -Nru ibus-table-1.9.18/configure.ac ibus-table-1.9.18.new/configure.ac +--- ibus-table-1.9.18/configure.ac 2020-07-22 15:02:24.341755774 +0200 ++++ ibus-table-1.9.18.new/configure.ac 2020-07-22 15:03:04.097344610 +0200 +@@ -54,6 +54,15 @@ + AM_GNU_GETTEXT([external]) + AM_GNU_GETTEXT_VERSION(0.16.1) + ++AC_ARG_ENABLE([installed-tests], ++ [AS_HELP_STRING([--enable-installed-tests], ++ [Enable to install tests])], ++ [enable_installed_tests=$enableval], ++ [enable_installed_tests=no] ++) ++ ++AM_CONDITIONAL([ENABLE_INSTALLED_TESTS], [test x"$enable_installed_tests" = x"yes"]) ++ + # OUTPUT files + AC_CONFIG_FILES([po/Makefile.in + Makefile +diff -Nru ibus-table-1.9.18/tests/.gitignore ibus-table-1.9.18.new/tests/.gitignore +--- ibus-table-1.9.18/tests/.gitignore 2020-07-22 15:02:24.349755691 +0200 ++++ ibus-table-1.9.18.new/tests/.gitignore 2020-07-22 15:11:24.740159555 +0200 +@@ -1,5 +1,5 @@ + run_tests +-run_tests.log +-run_tests.trs +-test-suite.log ++*.log ++*.trs ++*.tap + __pycache__/ +diff -Nru ibus-table-1.9.18/tests/Makefile.am ibus-table-1.9.18.new/tests/Makefile.am +--- ibus-table-1.9.18/tests/Makefile.am 2020-07-22 15:02:24.349755691 +0200 ++++ ibus-table-1.9.18.new/tests/Makefile.am 2020-07-22 15:04:28.298473415 +0200 +@@ -19,20 +19,58 @@ + # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + # + +-TESTS = run_tests ++TESTS = \ ++ test_it.py \ ++ test_0_gtk.py \ ++ $(NULL) ++ ++check_SCRIPTS = run_tests ++ ++LOG_COMPILER = $(builddir)/run_tests ++ ++TESTS_ENVIRONMENT = \ ++ IBUS_TABLE_LOCATION=../ ++ ++test_meta_in = meta.test.in ++test_metas = ++ ++if ENABLE_INSTALLED_TESTS ++test_metas += $(patsubst %.py, %.test, $(TESTS)) ++test_source_DATA = $(test_metas) ++test_sourcedir = $(datadir)/installed-tests/ibus-table ++test_exec_SCRIPTS = \ ++ $(TESTS) \ ++ gtkcases.py \ ++ mock_engine.py \ ++ run_tests \ ++ $(NULL) ++test_execdir = $(libexecdir)/installed-tests/ibus-table ++ ++$(test_metas): $(test_meta_in) ++ @TEST_EXEC=`echo $@ | sed -e 's&\.test&\.py&'`; \ ++ sed -e "s&@TEST_EXECDIR@&$(test_execdir)&g" \ ++ -e "s&@TEST_EXEC@&$$TEST_EXEC&g" $< > $@.tmp; \ ++ mv $@.tmp $@; \ ++ $(NULL) ++endif + + run_tests: run_tests.in + sed -e 's&@PYTHON_BIN@&$(PYTHON)&g' \ ++ -e 's&@PKGDATADIR@&$(pkgdatadir)&g' \ + -e 's&@SRCDIR@&$(srcdir)&g' $< > $@ + chmod +x $@ + + EXTRA_DIST = \ +- run_tests.in \ +- test_it.py \ ++ $(test_meta_in) \ + __init__.py \ ++ gtkcases.py \ ++ mock_engine.py \ ++ run_tests.in \ ++ $(TESTS) \ + $(NULL) + + CLEANFILES = \ ++ $(test_metas) \ + run_tests \ + $(NULL) + +diff -Nru ibus-table-1.9.18/tests/gtkcases.py ibus-table-1.9.18.new/tests/gtkcases.py +--- ibus-table-1.9.18/tests/gtkcases.py 1970-01-01 01:00:00.000000000 +0100 ++++ ibus-table-1.9.18.new/tests/gtkcases.py 2020-07-22 15:03:04.097344610 +0200 +@@ -0,0 +1,38 @@ ++#!/usr/bin/python3 ++# -*- coding: utf-8 -*- ++ ++# 'init' has one array which is [keysym, keycode, modifier] and to be run ++# before the main tests. E.g. ++# Ctrl-space to enable Hiragana mode ++# ++# 'tests' cases are the main test cases. ++# 'preedit' case runs to create a preedit text. ++# 'lookup' case runs to update a lookup table. ++# 'commit' case runs to commit the preedit text. ++# 'result' case is the expected output. ++# 'preedit', 'lookup', 'commit' can choose the type of either 'string' or 'keys' ++# 'string' type is a string sequence which does not need modifiers ++ ++from gi import require_version as gi_require_version ++gi_require_version('IBus', '1.0') ++from gi.repository import IBus ++ ++TestCases = { ++ #'init': [IBus.KEY_j, 0, IBus.ModifierType.CONTROL_MASK], ++ 'tests': [ ++ {'preedit': {'string': 'a'}, ++ 'lookup': {'keys': [[IBus.KEY_Down, 0, 0]]}, ++ 'commit': {'keys': [[IBus.KEY_space, 0, 0]]}, ++ 'result': {'string': '区'} ++ }, ++ {'preedit': {'string': 'ijgl'}, ++ 'commit': {'keys': [[IBus.KEY_space, 0, 0]]}, ++ 'result': {'string': '漫画'} ++ }, ++ {'preedit': {'string': 'wgl'}, ++ 'lookup': {'keys': [[IBus.KEY_Down, 0, 0]]}, ++ 'commit': {'keys': [[IBus.KEY_space, 0, 0]]}, ++ 'result': {'string': '全国'} ++ }, ++ ] ++} +diff -Nru ibus-table-1.9.18/tests/meta.test.in ibus-table-1.9.18.new/tests/meta.test.in +--- ibus-table-1.9.18/tests/meta.test.in 1970-01-01 01:00:00.000000000 +0100 ++++ ibus-table-1.9.18.new/tests/meta.test.in 2020-07-22 15:03:04.097344610 +0200 +@@ -0,0 +1,4 @@ ++[Test] ++Type=session ++Exec=@TEST_EXECDIR@/run_tests @TEST_EXEC@ ++Output=TAP +diff -Nru ibus-table-1.9.18/tests/mock_engine.py ibus-table-1.9.18.new/tests/mock_engine.py +--- ibus-table-1.9.18/tests/mock_engine.py 1970-01-01 01:00:00.000000000 +0100 ++++ ibus-table-1.9.18.new/tests/mock_engine.py 2020-07-22 15:07:00.649895226 +0200 +@@ -0,0 +1,241 @@ ++# ibus-table - The Tables engine for IBus ++# ++# Copyright (c) 2018-2020 Mike FABIAN ++# ++# This library is free software; you can redistribute it and/or ++# modify it under the terms of the GNU Lesser General Public ++# License as published by the Free Software Foundation; either ++# version 2.1 of the License, or (at your option) any later version. ++# ++# This library is distributed in the hope that it will be useful, ++# but WITHOUT ANY WARRANTY; without even the implied warranty of ++# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ++# Lesser General Public License for more details. ++# ++# You should have received a copy of the GNU Lesser General Public ++# License along with this library; if not, write to the Free Software ++# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA ++# ++ ++''' ++Define some mock classes for the unittests. ++''' ++ ++from gi import require_version ++require_version('IBus', '1.0') ++from gi.repository import IBus ++ ++class MockEngine: ++ def __init__(self, engine_name = '', connection = None, object_path = ''): ++ self.mock_auxiliary_text = '' ++ self.mock_preedit_text = '' ++ self.mock_preedit_text_cursor_pos = 0 ++ self.mock_preedit_text_visible = True ++ self.mock_committed_text = '' ++ self.mock_committed_text_cursor_pos = 0 ++ self.client_capabilities = ( ++ IBus.Capabilite.PREEDIT_TEXT ++ | IBus.Capabilite.AUXILIARY_TEXT ++ | IBus.Capabilite.LOOKUP_TABLE ++ | IBus.Capabilite.FOCUS ++ | IBus.Capabilite.PROPERTY) ++ # There are lots of weird problems with surrounding text ++ # which makes this hard to test. Therefore this mock ++ # engine does not try to support surrounding text, i.e. ++ # we omit “| IBus.Capabilite.SURROUNDING_TEXT” here. ++ ++ def update_auxiliary_text(self, text, visible): ++ self.mock_auxiliary_text = text.text ++ ++ def hide_auxiliary_text(self): ++ pass ++ ++ def hide_preedit_text(self): ++ pass ++ ++ def commit_text(self, text): ++ self.mock_committed_text = ( ++ self.mock_committed_text[ ++ :self.mock_committed_text_cursor_pos] ++ + text.text ++ + self.mock_committed_text[ ++ self.mock_committed_text_cursor_pos:]) ++ self.mock_committed_text_cursor_pos += len(text.text) ++ ++ def forward_key_event(self, val, code, state): ++ if (val == IBus.KEY_Left ++ and self.mock_committed_text_cursor_pos > 0): ++ self.mock_committed_text_cursor_pos -= 1 ++ return ++ unicode = IBus.keyval_to_unicode(val) ++ if unicode: ++ self.mock_committed_text = ( ++ self.mock_committed_text[ ++ :self.mock_committed_text_cursor_pos] ++ + unicode ++ + self.mock_committed_text[ ++ self.mock_committed_text_cursor_pos:]) ++ self.mock_committed_text_cursor_pos += len(unicode) ++ ++ def update_lookup_table(self, table, visible): ++ pass ++ ++ def update_preedit_text(self, text, cursor_pos, visible): ++ self.mock_preedit_text = text.get_text() ++ self.mock_preedit_text_cursor_pos = cursor_pos ++ self.mock_preedit_text_visible = visible ++ ++ def register_properties(self, property_list): ++ pass ++ ++ def update_property(self, property): ++ pass ++ ++ def hide_lookup_table(self): ++ pass ++ ++class MockLookupTable: ++ def __init__(self, page_size = 9, cursor_pos = 0, cursor_visible = False, round = True): ++ self.clear() ++ self.mock_page_size = page_size ++ self.mock_cursor_pos = cursor_pos ++ self.mock_cursor_visible = cursor_visible ++ self.cursor_visible = cursor_visible ++ self.mock_round = round ++ self.mock_candidates = [] ++ self.mock_labels = [] ++ self.mock_page_number = 0 ++ ++ def clear(self): ++ self.mock_candidates = [] ++ self.mock_cursor_pos = 0 ++ ++ def set_page_size(self, size): ++ self.mock_page_size = size ++ ++ def get_page_size(self): ++ return self.mock_page_size ++ ++ def set_round(self, round): ++ self.mock_round = round ++ ++ def set_cursor_pos(self, pos): ++ self.mock_cursor_pos = pos ++ ++ def get_cursor_pos(self): ++ return self.mock_cursor_pos ++ ++ def get_cursor_in_page(self): ++ return (self.mock_cursor_pos ++ - self.mock_page_size * self.mock_page_number) ++ ++ def set_cursor_visible(self, visible): ++ self.mock_cursor_visible = visible ++ self.cursor_visible = visible ++ ++ def cursor_down(self): ++ if len(self.mock_candidates): ++ self.mock_cursor_pos += 1 ++ self.mock_cursor_pos %= len(self.mock_candidates) ++ ++ def cursor_up(self): ++ if len(self.mock_candidates): ++ if self.mock_cursor_pos > 0: ++ self.mock_cursor_pos -= 1 ++ else: ++ self.mock_cursor_pos = len(self.mock_candidates) - 1 ++ ++ def page_down(self): ++ if len(self.mock_candidates): ++ self.mock_page_number += 1 ++ self.mock_cursor_pos += self.mock_page_size ++ ++ def page_up(self): ++ if len(self.mock_candidates): ++ if self.mock_page_number > 0: ++ self.mock_page_number -= 1 ++ self.mock_cursor_pos -= self.mock_page_size ++ ++ def set_orientation(self, orientation): ++ self.mock_orientation = orientation ++ ++ def get_number_of_candidates(self): ++ return len(self.mock_candidates) ++ ++ def append_candidate(self, candidate): ++ self.mock_candidates.append(candidate.get_text()) ++ ++ def get_candidate(self, index): ++ return self.mock_candidates[index] ++ ++ def get_number_of_candidates(self): ++ return len(self.mock_candidates) ++ ++ def append_label(self, label): ++ self.mock_labels.append(label.get_text()) ++ ++class MockPropList: ++ def __init__(self, *args, **kwargs): ++ self._mock_proplist = [] ++ ++ def append(self, property): ++ self._mock_proplist.append(property) ++ ++ def get(self, index): ++ if index >= 0 and index < len(self._mock_proplist): ++ return self._mock_proplist[index] ++ else: ++ return None ++ ++ def update_property(self, property): ++ pass ++ ++class MockProperty: ++ def __init__(self, ++ key='', ++ prop_type=IBus.PropType.RADIO, ++ label=IBus.Text.new_from_string(''), ++ symbol=IBus.Text.new_from_string(''), ++ icon='', ++ tooltip=IBus.Text.new_from_string(''), ++ sensitive=True, ++ visible=True, ++ state=IBus.PropState.UNCHECKED, ++ sub_props=None): ++ self.mock_property_key = key ++ self.mock_property_prop_type = prop_type ++ self.mock_property_label = label.get_text() ++ self.mock_property_symbol = symbol.get_text() ++ self.mock_property_icon = icon ++ self.mock_property_tooltip = tooltip.get_text() ++ self.mock_property_sensitive = sensitive ++ self.mock_property_visible = visible ++ self.mock_property_state = state ++ self.mock_property_sub_props = sub_props ++ ++ def set_label(self, ibus_text): ++ self.mock_property_label = ibus_text.get_text() ++ ++ def set_symbol(self, ibus_text): ++ self.mock_property_symbol = ibus_text.get_text() ++ ++ def set_tooltip(self, ibus_text): ++ self.mock_property_tooltip = ibus_text.get_text() ++ ++ def set_icon(self, icon_path): ++ self.mock_property_icon = icon_path ++ ++ def set_sensitive(self, sensitive): ++ self.mock_property_sensitive = sensitive ++ ++ def set_visible(self, visible): ++ self.mock_property_visible = visible ++ ++ def set_state(self, state): ++ self.mock_property_state = state ++ ++ def set_sub_props(self, proplist): ++ self.mock_property_sub_props = proplist ++ ++ def get_key(self): ++ return self.mock_property_key +diff -Nru ibus-table-1.9.18/tests/run_tests.in ibus-table-1.9.18.new/tests/run_tests.in +--- ibus-table-1.9.18/tests/run_tests.in 2020-07-22 15:02:24.349755691 +0200 ++++ ibus-table-1.9.18.new/tests/run_tests.in 2020-07-22 15:04:42.505326247 +0200 +@@ -15,239 +15,47 @@ + # Lesser General Public License for more details. + # + # You should have received a copy of the GNU Lesser General Public +-# License along with this library; if not, write to the Free Software +-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +-# ++# License along with this library. If not, see + +-import os + import sys ++import os + import unittest + +-from gi import require_version +-require_version('IBus', '1.0') +-from gi.repository import IBus +- +-# -- Define some mock classes for the tests ---------------------------------- +-class MockEngine: +- def __init__(self, engine_name = '', connection = None, object_path = ''): +- self.mock_auxiliary_text = '' +- self.mock_preedit_text = '' +- self.mock_preedit_text_cursor_pos = 0 +- self.mock_preedit_text_visible = True +- self.mock_committed_text = '' +- self.mock_committed_text_cursor_pos = 0 +- self.client_capabilities = ( +- IBus.Capabilite.PREEDIT_TEXT +- | IBus.Capabilite.AUXILIARY_TEXT +- | IBus.Capabilite.LOOKUP_TABLE +- | IBus.Capabilite.FOCUS +- | IBus.Capabilite.PROPERTY) +- # There are lots of weird problems with surrounding text +- # which makes this hard to test. Therefore this mock +- # engine does not try to support surrounding text, i.e. +- # we omit “| IBus.Capabilite.SURROUNDING_TEXT” here. +- +- def update_auxiliary_text(self, text, visible): +- self.mock_auxiliary_text = text.text +- +- def hide_auxiliary_text(self): +- pass +- +- def commit_text(self, text): +- self.mock_committed_text = ( +- self.mock_committed_text[ +- :self.mock_committed_text_cursor_pos] +- + text.text +- + self.mock_committed_text[ +- self.mock_committed_text_cursor_pos:]) +- self.mock_committed_text_cursor_pos += len(text.text) +- +- def forward_key_event(self, val, code, state): +- if (val == IBus.KEY_Left +- and self.mock_committed_text_cursor_pos > 0): +- self.mock_committed_text_cursor_pos -= 1 +- return +- unicode = IBus.keyval_to_unicode(val) +- if unicode: +- self.mock_committed_text = ( +- self.mock_committed_text[ +- :self.mock_committed_text_cursor_pos] +- + unicode +- + self.mock_committed_text[ +- self.mock_committed_text_cursor_pos:]) +- self.mock_committed_text_cursor_pos += len(unicode) +- +- def update_lookup_table(self, table, visible): +- pass +- +- def update_preedit_text(self, text, cursor_pos, visible): +- self.mock_preedit_text = text.get_text() +- self.mock_preedit_text_cursor_pos = cursor_pos +- self.mock_preedit_text_visible = visible +- +- def register_properties(self, property_list): +- pass +- +- def update_property(self, property): +- pass +- +- def hide_lookup_table(self): +- pass +- +-class MockLookupTable: +- def __init__(self, page_size = 9, cursor_pos = 0, cursor_visible = False, round = True): +- self.clear() +- self.mock_page_size = page_size +- self.mock_cursor_pos = cursor_pos +- self.mock_cursor_visible = cursor_visible +- self.cursor_visible = cursor_visible +- self.mock_round = round +- self.mock_candidates = [] +- self.mock_labels = [] +- self.mock_page_number = 0 +- +- def clear(self): +- self.mock_candidates = [] +- self.mock_cursor_pos = 0 +- +- def set_page_size(self, size): +- self.mock_page_size = size +- +- def get_page_size(self): +- return self.mock_page_size +- +- def set_round(self, round): +- self.mock_round = round +- +- def set_cursor_pos(self, pos): +- self.mock_cursor_pos = pos +- +- def get_cursor_pos(self): +- return self.mock_cursor_pos +- +- def get_cursor_in_page(self): +- return (self.mock_cursor_pos +- - self.mock_page_size * self.mock_page_number) +- +- def set_cursor_visible(self, visible): +- self.mock_cursor_visible = visible +- self.cursor_visible = visible +- +- def cursor_down(self): +- if len(self.mock_candidates): +- self.mock_cursor_pos += 1 +- self.mock_cursor_pos %= len(self.mock_candidates) +- +- def cursor_up(self): +- if len(self.mock_candidates): +- if self.mock_cursor_pos > 0: +- self.mock_cursor_pos -= 1 +- else: +- self.mock_cursor_pos = len(self.mock_candidates) - 1 +- +- def page_down(self): +- if len(self.mock_candidates): +- self.mock_page_number += 1 +- self.mock_cursor_pos += self.mock_page_size +- +- def page_up(self): +- if len(self.mock_candidates): +- if self.mock_page_number > 0: +- self.mock_page_number -= 1 +- self.mock_cursor_pos -= self.mock_page_size +- +- def set_orientation(self, orientation): +- self.mock_orientation = orientation +- +- def get_number_of_candidates(self): +- return len(self.mock_candidates) +- +- def append_candidate(self, candidate): +- self.mock_candidates.append(candidate.get_text()) +- +- def get_candidate(self, index): +- return self.mock_candidates[index] +- +- def get_number_of_candidates(self): +- return len(self.mock_candidates) +- +- def append_label(self, label): +- self.mock_labels.append(label.get_text()) +- +-class MockPropList: +- def __init__(self, *args, **kwargs): +- self._mock_proplist = [] +- +- def append(self, property): +- self._mock_proplist.append(property) +- +- def get(self, index): +- if index >= 0 and index < len(self._mock_proplist): +- return self._mock_proplist[index] +- else: +- return None +- +- def update_property(self, property): +- pass +- +-class MockProperty: +- def __init__(self, +- key='', +- prop_type=IBus.PropType.RADIO, +- label=IBus.Text.new_from_string(''), +- symbol=IBus.Text.new_from_string(''), +- icon='', +- tooltip=IBus.Text.new_from_string(''), +- sensitive=True, +- visible=True, +- state=IBus.PropState.UNCHECKED, +- sub_props=None): +- self.mock_property_key = key +- self.mock_property_prop_type = prop_type +- self.mock_property_label = label.get_text() +- self.mock_property_symbol = symbol.get_text() +- self.mock_property_icon = icon +- self.mock_property_tooltip = tooltip.get_text() +- self.mock_property_sensitive = sensitive +- self.mock_property_visible = visible +- self.mock_property_state = state +- self.mock_property_sub_props = sub_props +- +- def set_label(self, ibus_text): +- self.mock_property_label = ibus_text.get_text() +- +- def set_symbol(self, ibus_text): +- self.mock_property_symbol = ibus_text.get_text() +- +- def set_tooltip(self, ibus_text): +- self.mock_property_tooltip = ibus_text.get_text() +- +- def set_sensitive(self, sensitive): +- self.mock_property_sensitive = sensitive +- +- def set_visible(self, visible): +- self.mock_property_visible = visible +- +- def set_state(self, state): +- self.mock_property_state = state +- +- def set_sub_props(self, proplist): +- self.mock_property_sub_props = proplist +- +- def get_key(self): +- return self.mock_property_key +- +-# -- Monkey patch the environment with the mock classes ---------------------- +-sys.modules["gi.repository.IBus"].Engine = MockEngine +-sys.modules["gi.repository.IBus"].LookupTable = MockLookupTable +-sys.modules["gi.repository.IBus"].Property = MockProperty +-sys.modules["gi.repository.IBus"].PropList = MockPropList ++# pip3 install tap.py --user ++IMPORT_TAP_SUCCESSFUL = False ++try: ++ from tap import TAPTestRunner ++ IMPORT_TAP_SUCCESSFUL = True ++except (ImportError,): ++ pass ++ ++if 'IBUS_TABLE_LOCATION' in os.environ: ++ location_path = os.environ['IBUS_TABLE_LOCATION'] ++ if location_path != None and location_path != '': ++ engine_path = os.path.join(location_path, 'engine') ++ sys.path.append(engine_path) ++sys.path.append('@PKGDATADIR@/engine') + + # -- Load and run our unit tests --------------------------------------------- +-os.environ['IBUS_TABLE_DEBUG_LEVEL'] = '255' ++pattern = 'test*.py' ++start_dir = os.path.dirname(__file__) ++if len(sys.argv) > 1: ++ pattern = sys.argv[-1] ++ dir = os.path.dirname(pattern) ++ pattern = os.path.basename(pattern) ++ if dir != '.': ++ start_dir = os.path.join(start_dir, dir) + loader = unittest.TestLoader() +-suite = loader.discover(".") +-runner = unittest.TextTestRunner(stream = sys.stderr, verbosity = 255) ++suite = loader.discover(start_dir=start_dir, pattern=pattern) ++ ++if IMPORT_TAP_SUCCESSFUL: ++ runner = TAPTestRunner(stream=sys.stderr, verbosity=255) ++ runner.set_outdir('.') ++ runner.set_format('Hi: {method_name} - {short_description}') ++ runner.set_combined(True) ++else: ++ runner = unittest.TextTestRunner(stream=sys.stderr, verbosity=255) ++ + result = runner.run(suite) + + if result.failures or result.errors: +diff -Nru ibus-table-1.9.18/tests/test_0_gtk.py ibus-table-1.9.18.new/tests/test_0_gtk.py +--- ibus-table-1.9.18/tests/test_0_gtk.py 1970-01-01 01:00:00.000000000 +0100 ++++ ibus-table-1.9.18.new/tests/test_0_gtk.py 2020-07-22 15:09:02.734630566 +0200 +@@ -0,0 +1,441 @@ ++#!/usr/bin/python3 ++# -*- coding: utf-8 -*- ++# ++# ibus-table - The Tables engine for IBus ++# ++# Copyright (c) 2020 Takao Fujiwara ++# Copyright (c) 2020 Mike FABIAN ++# ++# This library is free software; you can redistribute it and/or ++# modify it under the terms of the GNU Lesser General Public ++# License as published by the Free Software Foundation; either ++# version 2.1 of the License, or (at your option) any later version. ++# ++# This library is distributed in the hope that it will be useful, ++# but WITHOUT ANY WARRANTY; without even the implied warranty of ++# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ++# Lesser General Public License for more details. ++# ++# You should have received a copy of the GNU Lesser General Public ++# License along with this library. If not, see ++''' ++This file implements the test cases using GTK GUI ++''' ++# “Wrong continued indentation”: pylint: disable=bad-continuation ++# pylint: disable=attribute-defined-outside-init ++# pylint: disable=missing-function-docstring ++# pylint: disable=missing-class-docstring ++# pylint: disable=global-statement ++# pylint: disable=wrong-import-order ++# pylint: disable=wrong-import-position ++ ++import argparse ++import os ++import signal ++import sys ++import unittest ++ ++from gi import require_version as gi_require_version ++gi_require_version('GLib', '2.0') ++gi_require_version('Gdk', '3.0') ++gi_require_version('Gio', '2.0') ++gi_require_version('Gtk', '3.0') ++gi_require_version('IBus', '1.0') ++from gi.repository import GLib ++from gi.repository import Gdk ++from gi.repository import Gio ++from gi.repository import Gtk ++from gi.repository import IBus ++ ++# Get more verbose output in the test log: ++os.environ['IBUS_TABLE_DEBUG_LEVEL'] = '255' ++ ++sys.path.insert(0, "../engine") ++IMPORT_TABLE_SUCCESSFUL = False ++try: ++ import table ++ IMPORT_TABLE_SUCCESSFUL = True ++except (ImportError,): ++ pass ++IMPORT_TABSQLITEDB_SUCCESSFUL = False ++try: ++ import tabsqlitedb ++ IMPORT_TABSQLITEDB_SUCCESSFUL = True ++except (ImportError,): ++ pass ++# FIXME: ++#sys.path.pop(0) ++ ++DONE_EXIT = True ++ENGINE_NAME = 'wubi-jidian86' ++ ++from gtkcases import TestCases ++ ++# Need to flush the output against Gtk.main() ++def printflush(sentence): ++ try: ++ print(sentence, flush=True) ++ except IOError: ++ pass ++ ++def printerr(sentence): ++ try: ++ print(sentence, flush=True, file=sys.stderr) ++ except IOError: ++ pass ++ ++@unittest.skipUnless( ++ os.path.isfile( ++ os.path.join('/usr/share/ibus-table/tables', ENGINE_NAME + '.db')), ++ '%s.db is not installed.' % ENGINE_NAME + '.db') ++@unittest.skipUnless( ++ 'XDG_SESSION_TYPE' in os.environ ++ and os.environ['XDG_SESSION_TYPE'] in ('x11', 'wayland'), ++ 'XDG_SESSION_TYPE is neither "x11" nor "wayland".') ++@unittest.skipIf(Gdk.Display.open('') is None, 'Display cannot be opened.') ++class SimpleGtkTestCase(unittest.TestCase): ++ global DONE_EXIT ++ global ENGINE_NAME ++ ENGINE_PATH = '/com/redhat/IBus/engines/table/Test/Engine' ++ ++ @classmethod ++ def setUpClass(cls): ++ cls._flag = False ++ IBus.init() ++ cls._gsettings = Gio.Settings( ++ schema='org.freedesktop.ibus.engine.table', ++ path='/org/freedesktop/ibus/engine/table/%s/' % ENGINE_NAME) ++ cls._orig_chinesemode = cls._gsettings.get_int('chinesemode') ++ signums = [getattr(signal, s, None) for s in ++ 'SIGINT SIGTERM SIGHUP'.split()] ++ for signum in filter(None, signums): ++ original_handler = signal.getsignal(signum) ++ GLib.unix_signal_add(GLib.PRIORITY_HIGH, ++ signum, ++ cls.signal_handler, ++ (signum, original_handler)) ++ @classmethod ++ def tearDownClass(cls): ++ cls._gsettings.set_int('chinesemode', cls._orig_chinesemode) ++ ++ @classmethod ++ def signal_handler(cls, user_data): ++ (signum, original_handler) = user_data ++ cls.tearDownClass() ++ Gtk.main_quit() ++ signal.signal(signum, original_handler) ++ cls._flag = True ++ assert False, 'signal received: ' + str(signum) ++ ++ def setUp(self): ++ self.__id = 0 ++ self.__rerun = False ++ self.__test_index = 0 ++ self.__preedit_index = 0 ++ self.__lookup_index = 0 ++ self.__inserted_text = '' ++ self.__commit_done = False ++ self.__reset_coming = False ++ self._gsettings.set_int('chinesemode', 4) ++ ++ def register_ibus_engine(self): ++ self.__bus = IBus.Bus() ++ if not self.__bus.is_connected(): ++ self.fail('ibus-daemon is not running') ++ return False ++ self.__bus.get_connection().signal_subscribe( ++ 'org.freedesktop.DBus', ++ 'org.freedesktop.DBus', ++ 'NameOwnerChanged', ++ '/org/freedesktop/DBus', ++ None, ++ 0, ++ self.__bus_signal_cb, ++ self.__bus) ++ self.__factory = IBus.Factory( ++ object_path=IBus.PATH_FACTORY, ++ connection=self.__bus.get_connection()) ++ self.__factory.connect('create-engine', self.__create_engine_cb) ++ self.__component = IBus.Component( ++ name='org.freedesktop.IBus.Table.Test', ++ description='Test Table Component', ++ version='1.0', ++ license='GPL', ++ author=('Mike FABIAN , ' ++ + 'Caius "kaio" CHANCE '), ++ homepage='http://mike-fabian.github.io/ibus-table/', ++ command_line='', ++ textdomain='ibus-table') ++ desc = IBus.EngineDesc( ++ name=ENGINE_NAME, ++ longname='Test Table %s' % ENGINE_NAME, ++ description='Test Table Component', ++ language='t', ++ license='GPL', ++ author=('Mike FABIAN , ' ++ + 'Caius "kaio" CHANCE '), ++ icon='', ++ symbol='T') ++ self.__component.add_engine(desc) ++ self.__bus.register_component(self.__component) ++ self.__bus.request_name('org.freedesktop.IBus.Table.Test', 0) ++ return True ++ ++ def __bus_signal_cb(self, connection, sender_name, object_path, ++ interface_name, signal_name, parameters, ++ user_data): ++ if signal_name == 'NameOwnerChanged': ++ pass ++ if signal_name == 'UpdateLookupTable': ++ table = self.__engine._editor.get_lookup_table() ++ if table.get_number_of_candidates() == 0: ++ return ++ self.__lookup_test() ++ ++ def __create_engine_cb(self, factory, engine_name): ++ if engine_name != ENGINE_NAME: ++ return None ++ if (not IMPORT_TABLE_SUCCESSFUL ++ or not IMPORT_TABSQLITEDB_SUCCESSFUL): ++ with self.subTest(i='create-engine'): ++ self.fail('NG: ibus-table not installed?') ++ Gtk.main_quit() ++ return None ++ self.__id += 1 ++ object_path = '%s/%d' % (self.ENGINE_PATH, self.__id) ++ db_dir = '/usr/share/ibus-table/tables' ++ db_file = os.path.join(db_dir, engine_name + '.db') ++ database = tabsqlitedb.TabSqliteDb(filename=db_file, user_db=':memory:') ++ self.__engine = table.TabEngine( ++ self.__bus, ++ object_path, ++ database) ++ self.__engine.connect('focus-in', self.__engine_focus_in) ++ self.__engine.connect('focus-out', self.__engine_focus_out) ++ # FIXME: Need to connect 'reset' after TabEngine.clear_all_input_and_preedit() ++ # is called. ++ self.__engine.connect_after('reset', self.__engine_reset) ++ self.__bus.get_connection().signal_subscribe( ++ None, ++ IBus.INTERFACE_ENGINE, ++ 'UpdateLookupTable', ++ object_path, ++ None, ++ 0, ++ self.__bus_signal_cb, ++ self.__bus) ++ return self.__engine ++ ++ def __engine_focus_in(self, _engine): ++ if self.__test_index == len(TestCases['tests']): ++ if DONE_EXIT: ++ Gtk.main_quit() ++ return ++ # Workaround because focus-out resets the preedit text ++ # ibus_bus_set_global_engine() calls bus_input_context_set_engine() ++ # twice and it causes bus_engine_proxy_focus_out() ++ if self.__rerun: ++ self.__rerun = False ++ self.__main_test() ++ ++ def __engine_focus_out(self, _engine): ++ self.__rerun = True ++ self.__test_index = 0 ++ self.__entry.set_text('') ++ ++ def __engine_reset(self, _engine): ++ if self.__reset_coming: ++ self.__reset_coming = False ++ self.__main_test() ++ ++ def __entry_focus_in_event_cb(self, entry, event): ++ if self.__test_index == len(TestCases['tests']): ++ if DONE_EXIT: ++ Gtk.main_quit() ++ return False ++ self.__bus.set_global_engine_async(ENGINE_NAME, ++ -1, None, self.__set_engine_cb) ++ return False ++ ++ def __set_engine_cb(self, _object, res): ++ with self.subTest(i=self.__test_index): ++ if not self.__bus.set_global_engine_async_finish(res): ++ self.fail('set engine failed.') ++ return ++ # rerun always happen? ++ #self.__main_test() ++ ++ def __get_test_condition_length(self, tag): ++ tests = TestCases['tests'][self.__test_index] ++ try: ++ cases = tests[tag] ++ except KeyError: ++ return -1 ++ case_type = list(cases.keys())[0] ++ return len(cases[case_type]) ++ ++ def __entry_preedit_changed_cb(self, entry, preedit_str): ++ if len(preedit_str) == 0: ++ return ++ if self.__test_index == len(TestCases['tests']): ++ if DONE_EXIT: ++ Gtk.main_quit() ++ return ++ self.__preedit_index += 1 ++ if self.__preedit_index != self.__get_test_condition_length('preedit'): ++ return ++ if self.__get_test_condition_length('lookup') > 0: ++ return ++ self.__run_cases('commit') ++ ++ def __main_test(self): ++ self.__preedit_index = 0 ++ self.__lookup_index = 0 ++ self.__commit_done = False ++ self.__run_cases('preedit') ++ ++ def __lookup_test(self): ++ lookup_length = self.__get_test_condition_length('lookup') ++ # Need to return again even if all the lookup is finished ++ # until the final Engine.update_preedit() is called. ++ if self.__lookup_index > lookup_length: ++ return ++ self.__run_cases('lookup', ++ self.__lookup_index, ++ self.__lookup_index + 1) ++ if self.__lookup_index < lookup_length: ++ self.__lookup_index += 1 ++ return ++ self.__lookup_index += 1 ++ self.__run_cases('commit') ++ ++ def __run_cases(self, tag, start=-1, end=-1): ++ tests = TestCases['tests'][self.__test_index] ++ if tests is None: ++ return ++ try: ++ cases = tests[tag] ++ except KeyError: ++ return ++ case_type = list(cases.keys())[0] ++ i = 0 ++ if case_type == 'string': ++ printflush('test step: %s sequences: "%s"' ++ % (tag, str(cases['string']))) ++ for character in cases['string']: ++ if start >= 0 and i < start: ++ i += 1 ++ continue ++ if 0 <= end <= i: ++ break ++ self.__typing(ord(character), 0, 0) ++ i += 1 ++ if case_type == 'keys': ++ if start == -1 and end == -1: ++ printflush('test step: %s sequences: %s' ++ % (tag, str(cases['keys']))) ++ for key in cases['keys']: ++ if start >= 0 and i < start: ++ i += 1 ++ continue ++ if 0 <= end <= i: ++ break ++ if start != -1 or end != -1: ++ printflush('test step: %s sequences: [0x%X, 0x%X, 0x%X]' ++ % (tag, key[0], key[1], key[2])) ++ self.__typing(key[0], key[1], key[2]) ++ i += 1 ++ ++ def __typing(self, keyval, keycode, modifiers): ++ self.__engine.emit('process-key-event', keyval, keycode, modifiers) ++ modifiers |= IBus.ModifierType.RELEASE_MASK ++ self.__engine.emit('process-key-event', keyval, keycode, modifiers) ++ ++ def __buffer_inserted_text_cb(self, buffer, position, chars, nchars): ++ tests = TestCases['tests'][self.__test_index] ++ cases = tests['commit'] ++ case_type = list(cases.keys())[0] ++ if case_type == 'keys': ++ # space key is sent separatedly later ++ if cases['keys'][0] == [IBus.KEY_space, 0, 0]: ++ self.__inserted_text += chars ++ # FIXME: Return key emits 'reset' signal in GTK and it calls ++ # TableEngine.clear_all_input_and_preedit(). ++ elif cases['keys'][0] == [IBus.KEY_Return, 0, 0] or \ ++ cases['keys'][0] == [IBus.KEY_KP_Enter, 0, 0] or \ ++ cases['keys'][0] == [IBus.KEY_ISO_Enter, 0, 0] or \ ++ cases['keys'][0] == [IBus.KEY_Escape, 0, 0]: ++ self.__inserted_text = chars ++ self.__reset_coming = True ++ else: ++ self.__inserted_text = chars ++ cases = tests['result'] ++ if cases['string'] == self.__inserted_text: ++ printflush('OK: %d "%s"' ++ % (self.__test_index, self.__inserted_text)) ++ else: ++ if DONE_EXIT: ++ Gtk.main_quit() ++ with self.subTest(i=self.__test_index): ++ self.fail('NG: %d "%s" "%s"' ++ % (self.__test_index, str(cases['string']), ++ self.__inserted_text)) ++ self.__inserted_text = '' ++ self.__test_index += 1 ++ if self.__test_index == len(TestCases['tests']): ++ if DONE_EXIT: ++ Gtk.main_quit() ++ return ++ self.__commit_done = True ++ self.__entry.set_text('') ++ if not self.__reset_coming: ++ self.__main_test() ++ ++ def create_window(self): ++ window = Gtk.Window(type=Gtk.WindowType.TOPLEVEL) ++ self.__entry = entry = Gtk.Entry() ++ window.connect('destroy', Gtk.main_quit) ++ entry.connect('focus-in-event', self.__entry_focus_in_event_cb) ++ entry.connect('preedit-changed', self.__entry_preedit_changed_cb) ++ buffer = entry.get_buffer() ++ buffer.connect('inserted-text', self.__buffer_inserted_text_cb) ++ window.add(entry) ++ window.show_all() ++ ++ def main(self): # pylint: disable=no-self-use ++ # Some ATK relative warnings are called during launching GtkWindow. ++ flags = GLib.log_set_always_fatal(GLib.LogLevelFlags.LEVEL_CRITICAL) ++ Gtk.main() ++ GLib.log_set_always_fatal(flags) ++ ++ def test_typing(self): ++ if not self.register_ibus_engine(): ++ sys.exit(-1) ++ self.create_window() ++ self.main() ++ if self._flag: ++ self.fail('NG: signal failure') ++ ++def main(): ++ parser = argparse.ArgumentParser() ++ parser.add_argument('-k', '--keep', action='store_true', ++ help='keep this GtkWindow after test is done') ++ parser.add_argument('-F', '--unittest-failfast', action='store_true', ++ help='stop on first fail or error in unittest') ++ parser.add_argument('-H', '--unittest-help', action='store_true', ++ help='show unittest help message and exit') ++ args, unittest_args = parser.parse_known_args() ++ sys.argv[1:] = unittest_args ++ if args.keep: ++ global DONE_EXIT ++ DONE_EXIT = False ++ if args.unittest_failfast: ++ sys.argv.append('-f') ++ if args.unittest_help: ++ sys.argv.append('-h') ++ unittest.main() ++ ++ unittest.main() ++ ++if __name__ == '__main__': ++ main() +diff -Nru ibus-table-1.9.18/tests/test_it.py ibus-table-1.9.18.new/tests/test_it.py +--- ibus-table-1.9.18/tests/test_it.py 2020-07-22 15:02:24.350755681 +0200 ++++ ibus-table-1.9.18.new/tests/test_it.py 2020-07-22 15:05:43.128698254 +0200 +@@ -1,6 +1,5 @@ +-# -*- coding: utf-8 -*- +-# vim:et sts=4 sw=4 +-# ++#!/usr/bin/python3 ++ + # ibus-table - The Tables engine for IBus + # + # Copyright (c) 2018 Mike FABIAN +@@ -26,19 +25,41 @@ + + import sys + import os +-import unicodedata + import unittest +-import subprocess ++import importlib ++import mock + + from gi import require_version + require_version('IBus', '1.0') + from gi.repository import IBus + ++# Get more verbose output in the test log: ++os.environ['IBUS_TABLE_DEBUG_LEVEL'] = '255' ++ ++# Monkey patch the environment with the mock classes: ++from mock_engine import MockEngine ++from mock_engine import MockLookupTable ++from mock_engine import MockProperty ++from mock_engine import MockPropList ++ + sys.path.insert(0, "../engine") +-from table import * ++import table + import tabsqlitedb +-import it_util +-#sys.path.pop(0) ++import ibus_table_location ++sys.path.pop(0) ++ ++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 + + ENGINE = None + TABSQLITEDB = None +@@ -56,6 +77,8 @@ + ORIG_AUTOWILDCARD_MODE = None + ORIG_SINGLE_WILDCARD_CHAR = None + ORIG_MULTI_WILDCARD_CHAR = None ++ORIG_PINYIN_MODE = None ++ORIG_SUGGESTION_MODE = None + + def backup_original_settings(): + global ENGINE +@@ -73,6 +96,8 @@ + global ORIG_AUTOWILDCARD_MODE + global ORIG_SINGLE_WILDCARD_CHAR + global ORIG_MULTI_WILDCARD_CHAR ++ global ORIG_PINYIN_MODE ++ global ORIG_SUGGESTION_MODE + ORIG_INPUT_MODE = ENGINE.get_input_mode() + ORIG_CHINESE_MODE = ENGINE.get_chinese_mode() + ORIG_LETTER_WIDTH = ENGINE.get_letter_width() +@@ -87,6 +112,8 @@ + ORIG_AUTOWILDCARD_MODE = ENGINE.get_autowildcard_mode() + ORIG_SINGLE_WILDCARD_CHAR = ENGINE.get_single_wildcard_char() + ORIG_MULTI_WILDCARD_CHAR = ENGINE.get_multi_wildcard_char() ++ ORIG_PINYIN_MODE = ENGINE.get_pinyin_mode() ++ ORIG_SUGGESTION_MODE = ENGINE.get_suggestion_mode() + + def restore_original_settings(): + global ENGINE +@@ -104,6 +131,8 @@ + global ORIG_AUTOWILDCARD_MODE + global ORIG_SINGLE_WILDCARD_CHAR + global ORIG_MULTI_WILDCARD_CHAR ++ global ORIG_PINYIN_MODE ++ global ORIG_SUGGESTION_MODE + ENGINE.set_input_mode(ORIG_INPUT_MODE) + ENGINE.set_chinese_mode(ORIG_CHINESE_MODE) + ENGINE.set_letter_width(ORIG_LETTER_WIDTH[0], input_mode=0) +@@ -120,14 +149,16 @@ + ENGINE.set_autowildcard_mode(ORIG_AUTOWILDCARD_MODE) + ENGINE.set_single_wildcard_char(ORIG_SINGLE_WILDCARD_CHAR) + ENGINE.set_multi_wildcard_char(ORIG_MULTI_WILDCARD_CHAR) ++ ENGINE.set_pinyin_mode(ORIG_PINYIN_MODE) ++ ENGINE.set_suggestion_mode(ORIG_SUGGESTION_MODE) + + def set_default_settings(): + global ENGINE + global TABSQLITEDB + ENGINE.set_input_mode(mode=1) +- chinese_mode = 0 ++ chinese_mode = 4 + language_filter = TABSQLITEDB.ime_properties.get('language_filter') +- if language_filter in ['cm0', 'cm1', 'cm2', 'cm3', 'cm4']: ++ if language_filter in ('cm0', 'cm1', 'cm2', 'cm3', 'cm4'): + chinese_mode = int(language_filter[-1]) + ENGINE.set_chinese_mode(mode=chinese_mode) + +@@ -136,7 +167,7 @@ + 'def_full_width_letter') + if def_full_width_letter: + letter_width_mode = (def_full_width_letter.lower() == u'true') +- ENGINE.set_letter_width(mode=letter_width_mode, input_mode=0) ++ ENGINE.set_letter_width(mode=False, input_mode=0) + ENGINE.set_letter_width(mode=letter_width_mode, input_mode=1) + + punctuation_width_mode = False +@@ -144,7 +175,7 @@ + 'def_full_width_punct') + if def_full_width_punct: + punctuation_width_mode = (def_full_width_punct.lower() == u'true') +- ENGINE.set_punctuation_width(mode=punctuation_width_mode, input_mode=0) ++ ENGINE.set_punctuation_width(mode=False, input_mode=0) + ENGINE.set_punctuation_width(mode=punctuation_width_mode, input_mode=1) + + always_show_lookup_mode = True +@@ -161,7 +192,8 @@ + select_keys_csv = TABSQLITEDB.ime_properties.get('select_keys') + # select_keys_csv is something like: "1,2,3,4,5,6,7,8,9,0" + if select_keys_csv: +- ENGINE.set_page_size(len(select_keys_csv.split(","))) ++ page_size = len(select_keys_csv.split(",")) ++ ENGINE.set_page_size(page_size) + + onechar = False + ENGINE.set_onechar_mode(onechar) +@@ -223,28 +255,98 @@ + multi_wildcard_char = multi_wildcard_char[0] + ENGINE.set_multi_wildcard_char(multi_wildcard_char) + ++ ENGINE.set_pinyin_mode(False) ++ ENGINE.set_suggestion_mode(False) ++ + def set_up(engine_name): ++ ''' ++ Setup an ibus table engine ++ ++ :param engine_name: The name of the engine to setup ++ :type engine_name: String ++ :return: True if the engine could be setup successfully, False if not. ++ :rtype: Boolean ++ ''' ++ global ENGINE_PATCHER ++ global LOOKUP_TABLE_PATCHER ++ global PROPERTY_PATCHER ++ global PROP_LIST_PATCHER ++ global IBUS_ENGINE ++ global IBUS_LOOKUP_TABLE ++ global IBUS_PROPERTY ++ global IBUS_PROP_LIST + global TABSQLITEDB + global ENGINE ++ ENGINE_PATCHER.start() ++ LOOKUP_TABLE_PATCHER.start() ++ PROPERTY_PATCHER.start() ++ PROP_LIST_PATCHER.start() ++ assert IBus.Engine is not IBUS_ENGINE ++ assert IBus.Engine is MockEngine ++ assert IBus.LookupTable is not IBUS_LOOKUP_TABLE ++ assert IBus.LookupTable is MockLookupTable ++ assert IBus.Property is not IBUS_PROPERTY ++ assert IBus.Property is MockProperty ++ assert IBus.PropList is not IBUS_PROP_LIST ++ assert IBus.PropList is MockPropList ++ # Reload the table module so that the patches ++ # are applied to TabEngine: ++ sys.path.insert(0, '../engine') ++ importlib.reload(table) ++ sys.path.pop(0) + bus = IBus.Bus() + db_dir = '/usr/share/ibus-table/tables' + db_file = os.path.join(db_dir, engine_name + '.db') +- TABSQLITEDB = tabsqlitedb.tabsqlitedb( ++ if not os.path.isfile(db_file): ++ TABSQLITEDB = None ++ ENGINE = None ++ tear_down() ++ return False ++ TABSQLITEDB = tabsqlitedb.TabSqliteDb( + filename=db_file, user_db=':memory:') +- ENGINE = tabengine( ++ ENGINE = table.TabEngine( + bus, + '/com/redhat/IBus/engines/table/%s/engine/0' %engine_name, + TABSQLITEDB, +- unit_test = True) ++ unit_test=True) + backup_original_settings() + set_default_settings() ++ return True + + def tear_down(): ++ global ENGINE_PATCHER ++ global LOOKUP_TABLE_PATCHER ++ global PROPERTY_PATCHER ++ global PROP_LIST_PATCHER ++ global IBUS_ENGINE ++ global IBUS_LOOKUP_TABLE ++ global IBUS_PROPERTY ++ global IBUS_PROP_LIST ++ global TABSQLITEDB ++ global ENGINE ++ if ENGINE: + restore_original_settings() ++ TABSQLITEDB = None ++ ENGINE = None ++ # Remove the patches from the IBus stuff: ++ ENGINE_PATCHER.stop() ++ LOOKUP_TABLE_PATCHER.stop() ++ PROPERTY_PATCHER.stop() ++ PROP_LIST_PATCHER.stop() ++ assert IBus.Engine is IBUS_ENGINE ++ assert IBus.Engine is not MockEngine ++ assert IBus.LookupTable is IBUS_LOOKUP_TABLE ++ assert IBus.LookupTable is not MockLookupTable ++ assert IBus.Property is IBUS_PROPERTY ++ assert IBus.Property is not MockProperty ++ assert IBus.PropList is IBUS_PROP_LIST ++ assert IBus.PropList is not MockPropList + +-class Wubi_Jidian86TestCase(unittest.TestCase): ++class WubiJidian86TestCase(unittest.TestCase): + def setUp(self): +- set_up('wubi-jidian86') ++ engine_name = 'wubi-jidian86' ++ if not set_up(engine_name): ++ self.skipTest('Could not setup “%s”, skipping test.' % engine_name) + + def tearDown(self): + tear_down() +@@ -257,7 +359,81 @@ + ENGINE.do_process_key_event(IBus.KEY_space, 0, 0) + self.assertEqual(ENGINE.mock_committed_text, '工') + +- def test_commit_to_preedit_and_switching_to_pinyin_and_defining_a_phrase(self): ++ def test_pinyin_mode(self): ++ # Pinyin mode is False by default: ++ self.assertEqual(ENGINE.get_pinyin_mode(), False) ++ ENGINE.do_process_key_event(IBus.KEY_a, 0, 0) ++ self.assertEqual(ENGINE.mock_preedit_text, '工') ++ ENGINE.do_process_key_event(IBus.KEY_space, 0, 0) ++ self.assertEqual(ENGINE.mock_preedit_text, '') ++ self.assertEqual(ENGINE.mock_committed_text, '工') ++ ENGINE.set_pinyin_mode(True) ++ ENGINE.do_process_key_event(IBus.KEY_a, 0, 0) ++ self.assertEqual(ENGINE.mock_preedit_text, '爱') ++ ENGINE.do_process_key_event(IBus.KEY_space, 0, 0) ++ self.assertEqual(ENGINE.mock_preedit_text, '') ++ self.assertEqual(ENGINE.mock_committed_text, '工爱') ++ ENGINE.set_pinyin_mode(False) ++ ENGINE.do_process_key_event(IBus.KEY_a, 0, 0) ++ self.assertEqual(ENGINE.mock_preedit_text, '工') ++ ENGINE.do_process_key_event(IBus.KEY_space, 0, 0) ++ self.assertEqual(ENGINE.mock_preedit_text, '') ++ self.assertEqual(ENGINE.mock_committed_text, '工爱工') ++ ++ def test_suggestion_mode(self): ++ if not ENGINE._ime_sg: ++ self.skipTest("This engine does not have a suggestion mode.") ++ # Suggestion mode is False by default: ++ self.assertEqual(ENGINE.get_suggestion_mode(), False) ++ self.assertEqual(ENGINE.get_pinyin_mode(), False) ++ ENGINE.do_process_key_event(IBus.KEY_a, 0, 0) ++ self.assertEqual(ENGINE.mock_preedit_text, '工') ++ ENGINE.do_process_key_event(IBus.KEY_space, 0, 0) ++ self.assertEqual(ENGINE.mock_preedit_text, '') ++ self.assertEqual(ENGINE.mock_committed_text, '工') ++ self.assertEqual(ENGINE._editor._lookup_table.mock_candidates, []) ++ ENGINE.set_suggestion_mode(True) ++ ENGINE.do_process_key_event(IBus.KEY_a, 0, 0) ++ self.assertEqual(ENGINE.mock_preedit_text, '工') ++ ENGINE.do_process_key_event(IBus.KEY_space, 0, 0) ++ self.assertEqual(ENGINE.mock_preedit_text, '') ++ self.assertEqual(ENGINE.mock_committed_text, '工工') ++ self.assertEqual(ENGINE._editor._lookup_table.mock_candidates, ++ ['工作人员 673 0', ++ '工作会议 310 0', ++ '工作报告 267 0', ++ '工人阶级 146 0', ++ '工作重点 78 0', ++ '工作小组 73 0', ++ '工业企业 71 0', ++ '工业大学 69 0', ++ '工作单位 61 0', ++ '工业生产 58 0']) ++ ENGINE.do_process_key_event(IBus.KEY_space, 0, 0) ++ self.assertEqual(ENGINE.mock_preedit_text, '') ++ self.assertEqual(ENGINE.mock_committed_text, '工工作人员') ++ ENGINE.set_pinyin_mode(True) ++ ENGINE.do_process_key_event(IBus.KEY_a, 0, 0) ++ self.assertEqual(ENGINE.mock_preedit_text, '爱') ++ ENGINE.do_process_key_event(IBus.KEY_space, 0, 0) ++ self.assertEqual(ENGINE.mock_preedit_text, '') ++ self.assertEqual(ENGINE.mock_committed_text, '工工作人员爱') ++ self.assertEqual(ENGINE._editor._lookup_table.mock_candidates, ++ ['爱因斯坦 1109 0', ++ '爱情故事 519 0', ++ '爱国主义 191 0', ++ '爱尔兰语 91 0', ++ '爱好和平 62 0', ++ '爱情小说 58 0', ++ '爱不释手 39 0', ++ '爱国热情 35 0', ++ '爱莫能助 34 0', ++ '爱理不理 32 0']) ++ ENGINE.do_process_key_event(IBus.KEY_space, 0, 0) ++ self.assertEqual(ENGINE.mock_preedit_text, '') ++ self.assertEqual(ENGINE.mock_committed_text, '工工作人员爱因斯坦') ++ ++ def test_commit_to_preedit_switching_to_pinyin_defining_a_phrase(self): + ENGINE.do_process_key_event(IBus.KEY_a, 0, 0) + # commit to preëdit needs a press and release of either + # the left or the right shift key: +@@ -324,7 +500,8 @@ + IBus.KEY_Shift_L, 0, + IBus.ModifierType.SHIFT_MASK | IBus.ModifierType.RELEASE_MASK) + self.assertEqual(ENGINE.mock_preedit_text, '工了你好以在') +- # Move right two characters in the preëdit (triggers a commit to preëdit): ++ # Move right two characters in the preëdit ++ # (triggers a commit to preëdit): + ENGINE.do_process_key_event(IBus.KEY_Right, 0, 0) + ENGINE.do_process_key_event(IBus.KEY_Right, 0, 0) + self.assertEqual(ENGINE.mock_auxiliary_text, 'd dhf dhfd\t#: abwd') +@@ -353,9 +530,88 @@ + ENGINE.do_process_key_event(IBus.KEY_space, 0, 0) + self.assertEqual(ENGINE.mock_committed_text, '工了你好以在工了你好以在') + ++ def test_chinese_mode(self): ++ ENGINE.set_chinese_mode(mode=0) # show simplified Chinese only ++ ENGINE.do_process_key_event(IBus.KEY_c, 0, 0) ++ self.assertEqual(ENGINE._editor._lookup_table.mock_candidates, ++ ['以 418261033 0', ++ '能 ex 1820000000 0', ++ '能 exx 1820000000 0', ++ '对 fy 1200000000 0', ++ '又 cc 729000000 0', ++ '又 ccc 729000000 0', ++ '通 ep 521000000 0', ++ '通 epk 521000000 0', ++ '台 kf 486000000 0', ++ '难忘 wyn 404000000 0']) ++ ENGINE.do_process_key_event(IBus.KEY_BackSpace, 0, 0) ++ self.assertEqual(ENGINE._editor._lookup_table.mock_candidates, []) ++ ENGINE.set_chinese_mode(mode=1) # show traditional Chinese only ++ ENGINE.do_process_key_event(IBus.KEY_c, 0, 0) ++ self.assertEqual(ENGINE._editor._lookup_table.mock_candidates, ++ ['以 418261033 0', ++ '能 ex 1820000000 0', ++ '能 exx 1820000000 0', ++ '又 cc 729000000 0', ++ '又 ccc 729000000 0', ++ '通 ep 521000000 0', ++ '通 epk 521000000 0', ++ '台 kf 486000000 0', ++ '能 e 306980312 0', ++ '能力 elt 274000000 0']) ++ ENGINE.do_process_key_event(IBus.KEY_BackSpace, 0, 0) ++ self.assertEqual(ENGINE._editor._lookup_table.mock_candidates, []) ++ ENGINE.set_chinese_mode(mode=2) # show simplified Chinese first ++ ENGINE.do_process_key_event(IBus.KEY_c, 0, 0) ++ self.assertEqual(ENGINE._editor._lookup_table.mock_candidates, ++ ['以 418261033 0', ++ '能 ex 1820000000 0', ++ '能 exx 1820000000 0', ++ '对 fy 1200000000 0', ++ '又 cc 729000000 0', ++ '又 ccc 729000000 0', ++ '通 ep 521000000 0', ++ '通 epk 521000000 0', ++ '台 kf 486000000 0', ++ '难忘 wyn 404000000 0']) ++ ENGINE.do_process_key_event(IBus.KEY_BackSpace, 0, 0) ++ self.assertEqual(ENGINE._editor._lookup_table.mock_candidates, []) ++ ENGINE.set_chinese_mode(mode=3) # show traditional Chinese first ++ ENGINE.do_process_key_event(IBus.KEY_c, 0, 0) ++ self.assertEqual(ENGINE._editor._lookup_table.mock_candidates, ++ ['以 418261033 0', ++ '能 ex 1820000000 0', ++ '能 exx 1820000000 0', ++ '又 cc 729000000 0', ++ '又 ccc 729000000 0', ++ '通 ep 521000000 0', ++ '通 epk 521000000 0', ++ '台 kf 486000000 0', ++ '能 e 306980312 0', ++ '能力 elt 274000000 0']) ++ ENGINE.do_process_key_event(IBus.KEY_BackSpace, 0, 0) ++ self.assertEqual(ENGINE._editor._lookup_table.mock_candidates, []) ++ ENGINE.set_chinese_mode(mode=4) # show all characters ++ ENGINE.do_process_key_event(IBus.KEY_c, 0, 0) ++ self.assertEqual(ENGINE._editor._lookup_table.mock_candidates, ++ ['以 418261033 0', ++ '能 ex 1820000000 0', ++ '能 exx 1820000000 0', ++ '对 fy 1200000000 0', ++ '又 cc 729000000 0', ++ '又 ccc 729000000 0', ++ '通 ep 521000000 0', ++ '通 epk 521000000 0', ++ '台 kf 486000000 0', ++ '难忘 wyn 404000000 0']) ++ ENGINE.do_process_key_event(IBus.KEY_BackSpace, 0, 0) ++ self.assertEqual(ENGINE._editor._lookup_table.mock_candidates, []) ++ + class Stroke5TestCase(unittest.TestCase): + def setUp(self): +- set_up('stroke5') ++ engine_name = 'stroke5' ++ if not set_up(engine_name): ++ self.skipTest('Could not setup “%s”, skipping test.' % engine_name) + + def tearDown(self): + tear_down() +@@ -372,9 +628,56 @@ + ENGINE.do_process_key_event(IBus.KEY_space, 0, 0) + self.assertEqual(ENGINE.mock_committed_text, '的') + ++class TelexTestCase(unittest.TestCase): ++ def setUp(self): ++ engine_name = 'telex' ++ if not set_up(engine_name): ++ self.skipTest('Could not setup “%s”, skipping test.' % engine_name) ++ ++ def tearDown(self): ++ tear_down() ++ ++ def test_dummy(self): ++ self.assertEqual(True, True) ++ ++ def test_telex(self): ++ ENGINE.do_process_key_event(IBus.KEY_o, 0, 0) ++ self.assertEqual(ENGINE.mock_preedit_text, 'o') ++ self.assertEqual(ENGINE.mock_committed_text, '') ++ ENGINE.do_process_key_event(IBus.KEY_backslash, 0, 0) ++ self.assertEqual(ENGINE.mock_preedit_text, '') ++ self.assertEqual(ENGINE.mock_committed_text, 'o') ++ ENGINE.do_process_key_event(IBus.KEY_o, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_f, 0, 0) ++ self.assertEqual(ENGINE.mock_preedit_text, '') ++ self.assertEqual(ENGINE.mock_committed_text, 'oò') ++ ENGINE.do_process_key_event(IBus.KEY_o, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_o, 0, 0) ++ self.assertEqual(ENGINE.mock_preedit_text, 'ô') ++ self.assertEqual(ENGINE.mock_committed_text, 'oò') ++ ENGINE.do_process_key_event(IBus.KEY_backslash, 0, 0) ++ self.assertEqual(ENGINE.mock_preedit_text, '') ++ self.assertEqual(ENGINE.mock_committed_text, 'oòô') ++ ENGINE.do_process_key_event(IBus.KEY_o, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_o, 0, 0) ++ self.assertEqual(ENGINE.mock_preedit_text, 'ô') ++ self.assertEqual(ENGINE.mock_committed_text, 'oòô') ++ ENGINE.do_process_key_event(IBus.KEY_backslash, 0, 0) ++ self.assertEqual(ENGINE.mock_preedit_text, '') ++ self.assertEqual(ENGINE.mock_committed_text, 'oòôô') ++ ENGINE.do_process_key_event(IBus.KEY_o, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_o, 0, 0) ++ self.assertEqual(ENGINE.mock_preedit_text, 'ô') ++ self.assertEqual(ENGINE.mock_committed_text, 'oòôô') ++ ENGINE.do_process_key_event(IBus.KEY_j, 0, 0) ++ self.assertEqual(ENGINE.mock_preedit_text, '') ++ self.assertEqual(ENGINE.mock_committed_text, 'oòôôộ') ++ + class TranslitTestCase(unittest.TestCase): + def setUp(self): +- set_up('translit') ++ engine_name ='translit' ++ if not set_up(engine_name): ++ self.skipTest('Could not setup “%s”, skipping test.' % engine_name) + + def tearDown(self): + tear_down() +@@ -401,3 +704,215 @@ + self.assertEqual(ENGINE.mock_preedit_text, 'с') + ENGINE.do_process_key_event(IBus.KEY_space, 0, 0) + self.assertEqual(ENGINE.mock_committed_text, 'шщс ') ++ ++ def test_sh_multiple_match_slavic(self): ++ ENGINE.do_process_key_event(IBus.KEY_scaron, 0, 0) ++ self.assertEqual(ENGINE.mock_preedit_text, 'ш') ++ self.assertEqual(ENGINE.mock_committed_text, '') ++ ENGINE.do_process_key_event(IBus.KEY_h, 0, 0) ++ self.assertEqual(ENGINE.mock_preedit_text, '') ++ self.assertEqual(ENGINE.mock_committed_text, 'щ') ++ ENGINE.do_process_key_event(IBus.KEY_scaron, 0, 0) ++ self.assertEqual(ENGINE.mock_preedit_text, 'ш') ++ self.assertEqual(ENGINE.mock_committed_text, 'щ') ++ ENGINE.do_process_key_event(IBus.KEY_ccaron, 0, 0) ++ self.assertEqual(ENGINE.mock_preedit_text, '') ++ self.assertEqual(ENGINE.mock_committed_text, 'щщ') ++ ++class Cangjie5TestCase(unittest.TestCase): ++ def setUp(self): ++ engine_name = 'cangjie5' ++ if not set_up(engine_name): ++ self.skipTest('Could not setup “%s”, skipping test.' % engine_name) ++ ++ def tearDown(self): ++ tear_down() ++ ++ def test_dummy(self): ++ self.assertEqual(True, True) ++ ++ def test_single_char_commit_with_space(self): ++ ENGINE.do_process_key_event(IBus.KEY_a, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_space, 0, 0) ++ self.assertEqual(ENGINE.mock_committed_text, '日') ++ ++ def test_type_one_char_and_check_auxiliary(self): ++ ENGINE.do_process_key_event(IBus.KEY_d, 0, 0) ++ self.assertEqual(ENGINE.mock_preedit_text, '木') ++ self.assertEqual(ENGINE._editor._lookup_table.mock_candidates[8], ++ '林 木 1000 0') ++ ENGINE.do_process_key_event(IBus.KEY_v, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_i, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_i, 0, 0) ++ self.assertEqual(ENGINE.mock_preedit_text, '機') ++ self.assertEqual(ENGINE.mock_auxiliary_text, '木女戈戈 (1 / 1)') ++ self.assertEqual(ENGINE._editor._lookup_table.mock_candidates, ++ ['機 1000 0']) ++ ENGINE.do_process_key_event(IBus.KEY_space, 0, 0) ++ self.assertEqual(ENGINE.mock_committed_text, '機') ++ ++class IpaXSampaTestCase(unittest.TestCase): ++ def setUp(self): ++ engine_name = 'ipa-x-sampa' ++ if not set_up(engine_name): ++ self.skipTest('Could not setup “%s”, skipping test.' % engine_name) ++ ++ def tearDown(self): ++ tear_down() ++ ++ def test_dummy(self): ++ self.assertEqual(True, True) ++ ++ def test_single_char_commit_with_space(self): ++ ENGINE.do_process_key_event(IBus.KEY_at, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_space, 0, 0) ++ self.assertEqual(ENGINE.mock_committed_text, 'ə ') ++ ++ def test_single_char_commit_with_f3(self): ++ ENGINE.do_process_key_event(IBus.KEY_at, 0, 0) ++ self.assertEqual(ENGINE._editor._lookup_table.mock_candidates, ++ ['ə 0 0', 'ɘ \\ 0 0', 'ɚ ` 0 0']) ++ ENGINE.do_process_key_event(IBus.KEY_F3, 0, 0) ++ self.assertEqual(ENGINE.mock_committed_text, 'ɚ') ++ ++class LatexTestCase(unittest.TestCase): ++ def setUp(self): ++ engine_name = 'latex' ++ if not set_up(engine_name): ++ self.skipTest('Could not setup “%s”, skipping test.' % engine_name) ++ ++ def tearDown(self): ++ tear_down() ++ ++ def test_dummy(self): ++ self.assertEqual(True, True) ++ ++ def test_single_char_commit_with_space(self): ++ ENGINE.do_process_key_event(IBus.KEY_backslash, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_a, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_l, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_p, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_h, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_a, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_space, 0, 0) ++ self.assertEqual(ENGINE.mock_committed_text, 'α') ++ ++ def test_single_char_commit_with_space_fraktur(self): ++ # needs ibus-table-others-1.3.10 which adds ++ # most of Unicode 9.0 block Mathematical Alphanumeric Symbols ++ ENGINE.do_process_key_event(IBus.KEY_backslash, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_m, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_a, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_t, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_h, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_f, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_r, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_a, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_k, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_F, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_space, 0, 0) ++ self.assertEqual(ENGINE.mock_committed_text, '𝔉') ++ ++ def test_single_char_commit_with_f3(self): ++ ENGINE.do_process_key_event(IBus.KEY_backslash, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_b, 0, 0) ++ # Lookup table shows only the first page, subsequent ++ # pages are added on demand as a speed optimization: ++ self.assertEqual(ENGINE._editor._lookup_table.mock_candidates, ++ ['¯ ar 0 0', ++ '⊥ ot 0 0', ++ 'β eta 0 0', ++ 'ℶ eth 0 0', ++ '⋂ igcap 0 0', ++ '⋃ igcup 0 0', ++ '⋁ igvee 0 0', ++ '⋈ owtie 0 0', ++ '⊡ oxdot 0 0']) ++ ENGINE.do_process_key_event(IBus.KEY_F3, 0, 0) ++ self.assertEqual(ENGINE.mock_committed_text, 'β') ++ ENGINE.do_process_key_event(IBus.KEY_backslash, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_b, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_Page_Down, 0, 0) ++ self.assertEqual(ENGINE._editor._lookup_table.mock_candidates, ++ ['β eta 0 1', # user freq for β increased to 1 ++ '¯ ar 0 0', ++ '⊥ ot 0 0', ++ 'ℶ eth 0 0', ++ '⋂ igcap 0 0', ++ '⋃ igcup 0 0', ++ '⋁ igvee 0 0', ++ '⋈ owtie 0 0', ++ '⊡ oxdot 0 0', ++ '• ullet 0 0', ++ '∙ ullet 0 0', ++ '≏ umpeq 0 0', ++ '∽ acksim 0 0', ++ '∵ ecause 0 0', ++ '≬ etween 0 0', ++ '⊞ oxplus 0 0', ++ '⊼ arwedge 0 0', ++ '⋀ igwedge 0 0']) ++ self.assertEqual(ENGINE._editor._lookup_table.get_cursor_pos(), 9) ++ ENGINE.do_process_key_event(IBus.KEY_Down, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_Down, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_Down, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_Down, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_Down, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_Down, 0, 0) ++ self.assertEqual(ENGINE._editor._lookup_table.get_cursor_pos(), 15) ++ self.assertEqual(ENGINE._editor._lookup_table.mock_candidates[0:18], ++ ['β eta 0 1', # user freq for β increased to 1 ++ '¯ ar 0 0', ++ '⊥ ot 0 0', ++ 'ℶ eth 0 0', ++ '⋂ igcap 0 0', ++ '⋃ igcup 0 0', ++ '⋁ igvee 0 0', ++ '⋈ owtie 0 0', ++ '⊡ oxdot 0 0', ++ '• ullet 0 0', ++ '∙ ullet 0 0', ++ '≏ umpeq 0 0', ++ '∽ acksim 0 0', ++ '∵ ecause 0 0', ++ '≬ etween 0 0', ++ '⊞ oxplus 0 0', ++ '⊼ arwedge 0 0', ++ '⋀ igwedge 0 0']) ++ ENGINE.do_process_key_event(IBus.KEY_space, 0, 0) ++ self.assertEqual(ENGINE.mock_committed_text, 'β⊞') ++ ENGINE.do_process_key_event(IBus.KEY_backslash, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_b, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_Page_Down, 0, 0) ++ self.assertEqual(ENGINE._editor._lookup_table.mock_candidates, ++ ['β eta 0 1', # user freq for β increased to 1 ++ '⊞ oxplus 0 1', # user freq for ⊞ increased to 1 ++ '¯ ar 0 0', ++ '⊥ ot 0 0', ++ 'ℶ eth 0 0', ++ '⋂ igcap 0 0', ++ '⋃ igcup 0 0', ++ '⋁ igvee 0 0', ++ '⋈ owtie 0 0', ++ '⊡ oxdot 0 0', ++ '• ullet 0 0', ++ '∙ ullet 0 0', ++ '≏ umpeq 0 0', ++ '∽ acksim 0 0', ++ '∵ ecause 0 0', ++ '≬ etween 0 0', ++ '⊼ arwedge 0 0', ++ '⋀ igwedge 0 0']) ++ self.assertEqual(ENGINE._editor._lookup_table.get_cursor_pos(), 9) ++ ENGINE.do_process_key_event(IBus.KEY_Down, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_Down, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_Down, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_Down, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_Down, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_Down, 0, 0) ++ self.assertEqual(ENGINE._editor._lookup_table.get_cursor_pos(), 15) ++ ENGINE.do_process_key_event(IBus.KEY_space, 0, 0) ++ self.assertEqual(ENGINE.mock_committed_text, 'β⊞≬') ++ ++if __name__ == '__main__': ++ unittest.main() diff --git a/SOURCES/add-a-test-suite.patch b/SOURCES/add-a-test-suite.patch new file mode 100644 index 0000000..13bd70f --- /dev/null +++ b/SOURCES/add-a-test-suite.patch @@ -0,0 +1,6974 @@ +diff -Nru ibus-table-1.9.18.orig/Makefile.am ibus-table-1.9.18/Makefile.am +--- ibus-table-1.9.18.orig/Makefile.am 2020-07-22 11:52:11.640532230 +0200 ++++ ibus-table-1.9.18/Makefile.am 2020-07-22 14:43:51.905260956 +0200 +@@ -30,6 +30,7 @@ + data \ + po \ + setup \ ++ tests \ + $(NULL) + + ACLOCAL_AMFLAGS = -I m4 +diff -Nru ibus-table-1.9.18.orig/Makefile.in ibus-table-1.9.18/Makefile.in +--- ibus-table-1.9.18.orig/Makefile.in 2017-08-02 11:32:47.000000000 +0200 ++++ ibus-table-1.9.18/Makefile.in 2020-07-22 16:15:15.492860836 +0200 +@@ -1,7 +1,7 @@ +-# Makefile.in generated by automake 1.15 from Makefile.am. ++# Makefile.in generated by automake 1.16.1 from Makefile.am. + # @configure_input@ + +-# Copyright (C) 1994-2014 Free Software Foundation, Inc. ++# Copyright (C) 1994-2018 Free Software Foundation, Inc. + + # This Makefile.in is free software; the Free Software Foundation + # gives unlimited permission to copy and/or distribute it, +@@ -168,7 +168,7 @@ + $(RECURSIVE_CLEAN_TARGETS) \ + $(am__extra_recursive_targets) + AM_RECURSIVE_TARGETS = $(am__recursive_targets:-recursive=) TAGS CTAGS \ +- cscope distdir dist dist-all distcheck ++ cscope distdir distdir-am dist dist-all distcheck + am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP) + # Read a list of newline-separated strings from the standard input, + # and print each of them once, without duplicates. Input order is +@@ -193,7 +193,7 @@ + am__DIST_COMMON = $(srcdir)/Makefile.in $(srcdir)/ibus-table.pc.in \ + $(srcdir)/ibus-table.spec.in ABOUT-NLS AUTHORS COPYING \ + ChangeLog INSTALL NEWS README compile config.guess \ +- config.rpath config.sub install-sh missing ++ config.rpath config.sub install-sh missing py-compile + DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST) + distdir = $(PACKAGE)-$(VERSION) + top_distdir = $(distdir) +@@ -392,6 +392,7 @@ + data \ + po \ + setup \ ++ tests \ + $(NULL) + + ACLOCAL_AMFLAGS = -I m4 +@@ -457,8 +458,8 @@ + echo ' $(SHELL) ./config.status'; \ + $(SHELL) ./config.status;; \ + *) \ +- echo ' cd $(top_builddir) && $(SHELL) ./config.status $@ $(am__depfiles_maybe)'; \ +- cd $(top_builddir) && $(SHELL) ./config.status $@ $(am__depfiles_maybe);; \ ++ echo ' cd $(top_builddir) && $(SHELL) ./config.status $@ $(am__maybe_remake_depfiles)'; \ ++ cd $(top_builddir) && $(SHELL) ./config.status $@ $(am__maybe_remake_depfiles);; \ + esac; + + $(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES) +@@ -622,7 +623,10 @@ + -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags + -rm -f cscope.out cscope.in.out cscope.po.out cscope.files + +-distdir: $(DISTFILES) ++distdir: $(BUILT_SOURCES) ++ $(MAKE) $(AM_MAKEFLAGS) distdir-am ++ ++distdir-am: $(DISTFILES) + $(am__remove_distdir) + test -d "$(distdir)" || mkdir "$(distdir)" + @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \ +diff -Nru ibus-table-1.9.18.orig/configure.ac ibus-table-1.9.18/configure.ac +diff -Nru ibus-table-1.9.18.orig/tests/Makefile.in ibus-table-1.9.18/tests/Makefile.in +--- ibus-table-1.9.18.orig/tests/Makefile.in 1970-01-01 01:00:00.000000000 +0100 ++++ ibus-table-1.9.18/tests/Makefile.in 2020-07-22 16:28:37.394963243 +0200 +@@ -0,0 +1,853 @@ ++# Makefile.in generated by automake 1.16.1 from Makefile.am. ++# @configure_input@ ++ ++# Copyright (C) 1994-2018 Free Software Foundation, Inc. ++ ++# This Makefile.in is free software; the Free Software Foundation ++# gives unlimited permission to copy and/or distribute it, ++# with or without modifications, as long as this notice is preserved. ++ ++# This program is distributed in the hope that it will be useful, ++# but WITHOUT ANY WARRANTY, to the extent permitted by law; without ++# even the implied warranty of MERCHANTABILITY or FITNESS FOR A ++# PARTICULAR PURPOSE. ++ ++@SET_MAKE@ ++ ++# vim:set noet ts=4 ++# ++# ibus-table - The Tables engine for IBus ++# ++# Copyright (c) 2018 Mike FABIAN ++# ++# This library is free software; you can redistribute it and/or ++# modify it under the terms of the GNU Lesser General Public ++# License as published by the Free Software Foundation; either ++# version 2.1 of the License, or (at your option) any later version. ++# ++# This library is distributed in the hope that it will be useful, ++# but WITHOUT ANY WARRANTY; without even the implied warranty of ++# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ++# Lesser General Public License for more details. ++# ++# You should have received a copy of the GNU Lesser General Public ++# License along with this library; if not, write to the Free Software ++# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA ++# ++VPATH = @srcdir@ ++am__is_gnu_make = { \ ++ if test -z '$(MAKELEVEL)'; then \ ++ false; \ ++ elif test -n '$(MAKE_HOST)'; then \ ++ true; \ ++ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \ ++ true; \ ++ else \ ++ false; \ ++ fi; \ ++} ++am__make_running_with_option = \ ++ case $${target_option-} in \ ++ ?) ;; \ ++ *) echo "am__make_running_with_option: internal error: invalid" \ ++ "target option '$${target_option-}' specified" >&2; \ ++ exit 1;; \ ++ esac; \ ++ has_opt=no; \ ++ sane_makeflags=$$MAKEFLAGS; \ ++ if $(am__is_gnu_make); then \ ++ sane_makeflags=$$MFLAGS; \ ++ else \ ++ case $$MAKEFLAGS in \ ++ *\\[\ \ ]*) \ ++ bs=\\; \ ++ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \ ++ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \ ++ esac; \ ++ fi; \ ++ skip_next=no; \ ++ strip_trailopt () \ ++ { \ ++ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \ ++ }; \ ++ for flg in $$sane_makeflags; do \ ++ test $$skip_next = yes && { skip_next=no; continue; }; \ ++ case $$flg in \ ++ *=*|--*) continue;; \ ++ -*I) strip_trailopt 'I'; skip_next=yes;; \ ++ -*I?*) strip_trailopt 'I';; \ ++ -*O) strip_trailopt 'O'; skip_next=yes;; \ ++ -*O?*) strip_trailopt 'O';; \ ++ -*l) strip_trailopt 'l'; skip_next=yes;; \ ++ -*l?*) strip_trailopt 'l';; \ ++ -[dEDm]) skip_next=yes;; \ ++ -[JT]) skip_next=yes;; \ ++ esac; \ ++ case $$flg in \ ++ *$$target_option*) has_opt=yes; break;; \ ++ esac; \ ++ done; \ ++ test $$has_opt = yes ++am__make_dryrun = (target_option=n; $(am__make_running_with_option)) ++am__make_keepgoing = (target_option=k; $(am__make_running_with_option)) ++pkgdatadir = $(datadir)/@PACKAGE@ ++pkgincludedir = $(includedir)/@PACKAGE@ ++pkglibdir = $(libdir)/@PACKAGE@ ++pkglibexecdir = $(libexecdir)/@PACKAGE@ ++am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd ++install_sh_DATA = $(install_sh) -c -m 644 ++install_sh_PROGRAM = $(install_sh) -c ++install_sh_SCRIPT = $(install_sh) -c ++INSTALL_HEADER = $(INSTALL_DATA) ++transform = $(program_transform_name) ++NORMAL_INSTALL = : ++PRE_INSTALL = : ++POST_INSTALL = : ++NORMAL_UNINSTALL = : ++PRE_UNINSTALL = : ++POST_UNINSTALL = : ++build_triplet = @build@ ++host_triplet = @host@ ++subdir = tests ++ACLOCAL_M4 = $(top_srcdir)/aclocal.m4 ++am__aclocal_m4_deps = $(top_srcdir)/m4/gettext.m4 \ ++ $(top_srcdir)/m4/iconv.m4 $(top_srcdir)/m4/lib-ld.m4 \ ++ $(top_srcdir)/m4/lib-link.m4 $(top_srcdir)/m4/lib-prefix.m4 \ ++ $(top_srcdir)/m4/nls.m4 $(top_srcdir)/m4/po.m4 \ ++ $(top_srcdir)/m4/progtest.m4 $(top_srcdir)/configure.ac ++am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \ ++ $(ACLOCAL_M4) ++DIST_COMMON = $(srcdir)/Makefile.am $(am__DIST_COMMON) ++mkinstalldirs = $(install_sh) -d ++CONFIG_CLEAN_FILES = ++CONFIG_CLEAN_VPATH_FILES = ++AM_V_P = $(am__v_P_@AM_V@) ++am__v_P_ = $(am__v_P_@AM_DEFAULT_V@) ++am__v_P_0 = false ++am__v_P_1 = : ++AM_V_GEN = $(am__v_GEN_@AM_V@) ++am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@) ++am__v_GEN_0 = @echo " GEN " $@; ++am__v_GEN_1 = ++AM_V_at = $(am__v_at_@AM_V@) ++am__v_at_ = $(am__v_at_@AM_DEFAULT_V@) ++am__v_at_0 = @ ++am__v_at_1 = ++SOURCES = ++DIST_SOURCES = ++am__can_run_installinfo = \ ++ case $$AM_UPDATE_INFO_DIR in \ ++ n|no|NO) false;; \ ++ *) (install-info --version) >/dev/null 2>&1;; \ ++ esac ++am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP) ++am__tty_colors_dummy = \ ++ mgn= red= grn= lgn= blu= brg= std=; \ ++ am__color_tests=no ++am__tty_colors = { \ ++ $(am__tty_colors_dummy); \ ++ if test "X$(AM_COLOR_TESTS)" = Xno; then \ ++ am__color_tests=no; \ ++ elif test "X$(AM_COLOR_TESTS)" = Xalways; then \ ++ am__color_tests=yes; \ ++ elif test "X$$TERM" != Xdumb && { test -t 1; } 2>/dev/null; then \ ++ am__color_tests=yes; \ ++ fi; \ ++ if test $$am__color_tests = yes; then \ ++ red=''; \ ++ grn=''; \ ++ lgn=''; \ ++ blu=''; \ ++ mgn=''; \ ++ brg=''; \ ++ std=''; \ ++ fi; \ ++} ++am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`; ++am__vpath_adj = case $$p in \ ++ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \ ++ *) f=$$p;; \ ++ esac; ++am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`; ++am__install_max = 40 ++am__nobase_strip_setup = \ ++ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'` ++am__nobase_strip = \ ++ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||" ++am__nobase_list = $(am__nobase_strip_setup); \ ++ for p in $$list; do echo "$$p $$p"; done | \ ++ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \ ++ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \ ++ if (++n[$$2] == $(am__install_max)) \ ++ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \ ++ END { for (dir in files) print dir, files[dir] }' ++am__base_list = \ ++ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \ ++ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g' ++am__uninstall_files_from_dir = { \ ++ test -z "$$files" \ ++ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \ ++ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \ ++ $(am__cd) "$$dir" && rm -f $$files; }; \ ++ } ++am__recheck_rx = ^[ ]*:recheck:[ ]* ++am__global_test_result_rx = ^[ ]*:global-test-result:[ ]* ++am__copy_in_global_log_rx = ^[ ]*:copy-in-global-log:[ ]* ++# A command that, given a newline-separated list of test names on the ++# standard input, print the name of the tests that are to be re-run ++# upon "make recheck". ++am__list_recheck_tests = $(AWK) '{ \ ++ recheck = 1; \ ++ while ((rc = (getline line < ($$0 ".trs"))) != 0) \ ++ { \ ++ if (rc < 0) \ ++ { \ ++ if ((getline line2 < ($$0 ".log")) < 0) \ ++ recheck = 0; \ ++ break; \ ++ } \ ++ else if (line ~ /$(am__recheck_rx)[nN][Oo]/) \ ++ { \ ++ recheck = 0; \ ++ break; \ ++ } \ ++ else if (line ~ /$(am__recheck_rx)[yY][eE][sS]/) \ ++ { \ ++ break; \ ++ } \ ++ }; \ ++ if (recheck) \ ++ print $$0; \ ++ close ($$0 ".trs"); \ ++ close ($$0 ".log"); \ ++}' ++# A command that, given a newline-separated list of test names on the ++# standard input, create the global log from their .trs and .log files. ++am__create_global_log = $(AWK) ' \ ++function fatal(msg) \ ++{ \ ++ print "fatal: making $@: " msg | "cat >&2"; \ ++ exit 1; \ ++} \ ++function rst_section(header) \ ++{ \ ++ print header; \ ++ len = length(header); \ ++ for (i = 1; i <= len; i = i + 1) \ ++ printf "="; \ ++ printf "\n\n"; \ ++} \ ++{ \ ++ copy_in_global_log = 1; \ ++ global_test_result = "RUN"; \ ++ while ((rc = (getline line < ($$0 ".trs"))) != 0) \ ++ { \ ++ if (rc < 0) \ ++ fatal("failed to read from " $$0 ".trs"); \ ++ if (line ~ /$(am__global_test_result_rx)/) \ ++ { \ ++ sub("$(am__global_test_result_rx)", "", line); \ ++ sub("[ ]*$$", "", line); \ ++ global_test_result = line; \ ++ } \ ++ else if (line ~ /$(am__copy_in_global_log_rx)[nN][oO]/) \ ++ copy_in_global_log = 0; \ ++ }; \ ++ if (copy_in_global_log) \ ++ { \ ++ rst_section(global_test_result ": " $$0); \ ++ while ((rc = (getline line < ($$0 ".log"))) != 0) \ ++ { \ ++ if (rc < 0) \ ++ fatal("failed to read from " $$0 ".log"); \ ++ print line; \ ++ }; \ ++ printf "\n"; \ ++ }; \ ++ close ($$0 ".trs"); \ ++ close ($$0 ".log"); \ ++}' ++# Restructured Text title. ++am__rst_title = { sed 's/.*/ & /;h;s/./=/g;p;x;s/ *$$//;p;g' && echo; } ++# Solaris 10 'make', and several other traditional 'make' implementations, ++# pass "-e" to $(SHELL), and POSIX 2008 even requires this. Work around it ++# by disabling -e (using the XSI extension "set +e") if it's set. ++am__sh_e_setup = case $$- in *e*) set +e;; esac ++# Default flags passed to test drivers. ++am__common_driver_flags = \ ++ --color-tests "$$am__color_tests" \ ++ --enable-hard-errors "$$am__enable_hard_errors" \ ++ --expect-failure "$$am__expect_failure" ++# To be inserted before the command running the test. Creates the ++# directory for the log if needed. Stores in $dir the directory ++# containing $f, in $tst the test, in $log the log. Executes the ++# developer- defined test setup AM_TESTS_ENVIRONMENT (if any), and ++# passes TESTS_ENVIRONMENT. Set up options for the wrapper that ++# will run the test scripts (or their associated LOG_COMPILER, if ++# thy have one). ++am__check_pre = \ ++$(am__sh_e_setup); \ ++$(am__vpath_adj_setup) $(am__vpath_adj) \ ++$(am__tty_colors); \ ++srcdir=$(srcdir); export srcdir; \ ++case "$@" in \ ++ */*) am__odir=`echo "./$@" | sed 's|/[^/]*$$||'`;; \ ++ *) am__odir=.;; \ ++esac; \ ++test "x$$am__odir" = x"." || test -d "$$am__odir" \ ++ || $(MKDIR_P) "$$am__odir" || exit $$?; \ ++if test -f "./$$f"; then dir=./; \ ++elif test -f "$$f"; then dir=; \ ++else dir="$(srcdir)/"; fi; \ ++tst=$$dir$$f; log='$@'; \ ++if test -n '$(DISABLE_HARD_ERRORS)'; then \ ++ am__enable_hard_errors=no; \ ++else \ ++ am__enable_hard_errors=yes; \ ++fi; \ ++case " $(XFAIL_TESTS) " in \ ++ *[\ \ ]$$f[\ \ ]* | *[\ \ ]$$dir$$f[\ \ ]*) \ ++ am__expect_failure=yes;; \ ++ *) \ ++ am__expect_failure=no;; \ ++esac; \ ++$(AM_TESTS_ENVIRONMENT) $(TESTS_ENVIRONMENT) ++# A shell command to get the names of the tests scripts with any registered ++# extension removed (i.e., equivalently, the names of the test logs, with ++# the '.log' extension removed). The result is saved in the shell variable ++# '$bases'. This honors runtime overriding of TESTS and TEST_LOGS. Sadly, ++# we cannot use something simpler, involving e.g., "$(TEST_LOGS:.log=)", ++# since that might cause problem with VPATH rewrites for suffix-less tests. ++# See also 'test-harness-vpath-rewrite.sh' and 'test-trs-basic.sh'. ++am__set_TESTS_bases = \ ++ bases='$(TEST_LOGS)'; \ ++ bases=`for i in $$bases; do echo $$i; done | sed 's/\.log$$//'`; \ ++ bases=`echo $$bases` ++RECHECK_LOGS = $(TEST_LOGS) ++AM_RECURSIVE_TARGETS = check recheck ++TEST_SUITE_LOG = test-suite.log ++TEST_EXTENSIONS = @EXEEXT@ .test ++LOG_DRIVER = $(SHELL) $(top_srcdir)/test-driver ++LOG_COMPILE = $(LOG_COMPILER) $(AM_LOG_FLAGS) $(LOG_FLAGS) ++am__set_b = \ ++ case '$@' in \ ++ */*) \ ++ case '$*' in \ ++ */*) b='$*';; \ ++ *) b=`echo '$@' | sed 's/\.log$$//'`; \ ++ esac;; \ ++ *) \ ++ b='$*';; \ ++ esac ++am__test_logs1 = $(TESTS:=.log) ++am__test_logs2 = $(am__test_logs1:@EXEEXT@.log=.log) ++TEST_LOGS = $(am__test_logs2:.test.log=.log) ++TEST_LOG_DRIVER = $(SHELL) $(top_srcdir)/test-driver ++TEST_LOG_COMPILE = $(TEST_LOG_COMPILER) $(AM_TEST_LOG_FLAGS) \ ++ $(TEST_LOG_FLAGS) ++am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/test-driver ++DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST) ++ACLOCAL = @ACLOCAL@ ++AMTAR = @AMTAR@ ++AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@ ++AUTOCONF = @AUTOCONF@ ++AUTOHEADER = @AUTOHEADER@ ++AUTOMAKE = @AUTOMAKE@ ++AWK = @AWK@ ++CC = @CC@ ++CCDEPMODE = @CCDEPMODE@ ++CFLAGS = @CFLAGS@ ++CPPFLAGS = @CPPFLAGS@ ++CYGPATH_W = @CYGPATH_W@ ++DEFS = @DEFS@ ++DEPDIR = @DEPDIR@ ++ECHO_C = @ECHO_C@ ++ECHO_N = @ECHO_N@ ++ECHO_T = @ECHO_T@ ++EXEEXT = @EXEEXT@ ++GETTEXT_PACKAGE = @GETTEXT_PACKAGE@ ++GMSGFMT = @GMSGFMT@ ++GMSGFMT_015 = @GMSGFMT_015@ ++IBUS_CFLAGS = @IBUS_CFLAGS@ ++IBUS_LIBS = @IBUS_LIBS@ ++INSTALL = @INSTALL@ ++INSTALL_DATA = @INSTALL_DATA@ ++INSTALL_PROGRAM = @INSTALL_PROGRAM@ ++INSTALL_SCRIPT = @INSTALL_SCRIPT@ ++INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@ ++INTLLIBS = @INTLLIBS@ ++INTL_MACOSX_LIBS = @INTL_MACOSX_LIBS@ ++LDFLAGS = @LDFLAGS@ ++LIBICONV = @LIBICONV@ ++LIBINTL = @LIBINTL@ ++LIBOBJS = @LIBOBJS@ ++LIBS = @LIBS@ ++LTLIBICONV = @LTLIBICONV@ ++LTLIBINTL = @LTLIBINTL@ ++LTLIBOBJS = @LTLIBOBJS@ ++MAINT = @MAINT@ ++MAKEINFO = @MAKEINFO@ ++MKDIR_P = @MKDIR_P@ ++MSGFMT = @MSGFMT@ ++MSGFMT_015 = @MSGFMT_015@ ++MSGMERGE = @MSGMERGE@ ++OBJEXT = @OBJEXT@ ++PACKAGE = @PACKAGE@ ++PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@ ++PACKAGE_NAME = @PACKAGE_NAME@ ++PACKAGE_STRING = @PACKAGE_STRING@ ++PACKAGE_TARNAME = @PACKAGE_TARNAME@ ++PACKAGE_URL = @PACKAGE_URL@ ++PACKAGE_VERSION = @PACKAGE_VERSION@ ++PATH_SEPARATOR = @PATH_SEPARATOR@ ++PKG_CONFIG = @PKG_CONFIG@ ++PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@ ++PKG_CONFIG_PATH = @PKG_CONFIG_PATH@ ++POSUB = @POSUB@ ++PYTHON = @PYTHON@ ++PYTHON_EXEC_PREFIX = @PYTHON_EXEC_PREFIX@ ++PYTHON_PLATFORM = @PYTHON_PLATFORM@ ++PYTHON_PREFIX = @PYTHON_PREFIX@ ++PYTHON_VERSION = @PYTHON_VERSION@ ++SET_MAKE = @SET_MAKE@ ++SHELL = @SHELL@ ++STRIP = @STRIP@ ++USE_NLS = @USE_NLS@ ++VERSION = @VERSION@ ++XGETTEXT = @XGETTEXT@ ++XGETTEXT_015 = @XGETTEXT_015@ ++abs_builddir = @abs_builddir@ ++abs_srcdir = @abs_srcdir@ ++abs_top_builddir = @abs_top_builddir@ ++abs_top_srcdir = @abs_top_srcdir@ ++ac_ct_CC = @ac_ct_CC@ ++am__include = @am__include@ ++am__leading_dot = @am__leading_dot@ ++am__quote = @am__quote@ ++am__tar = @am__tar@ ++am__untar = @am__untar@ ++bindir = @bindir@ ++build = @build@ ++build_alias = @build_alias@ ++build_cpu = @build_cpu@ ++build_os = @build_os@ ++build_vendor = @build_vendor@ ++builddir = @builddir@ ++datadir = @datadir@ ++datarootdir = @datarootdir@ ++docdir = @docdir@ ++dvidir = @dvidir@ ++exec_prefix = @exec_prefix@ ++host = @host@ ++host_alias = @host_alias@ ++host_cpu = @host_cpu@ ++host_os = @host_os@ ++host_vendor = @host_vendor@ ++htmldir = @htmldir@ ++includedir = @includedir@ ++infodir = @infodir@ ++install_sh = @install_sh@ ++libdir = @libdir@ ++libexecdir = @libexecdir@ ++localedir = @localedir@ ++localstatedir = @localstatedir@ ++mandir = @mandir@ ++mkdir_p = @mkdir_p@ ++oldincludedir = @oldincludedir@ ++pdfdir = @pdfdir@ ++pkgpyexecdir = @pkgpyexecdir@ ++pkgpythondir = @pkgpythondir@ ++prefix = @prefix@ ++program_transform_name = @program_transform_name@ ++psdir = @psdir@ ++pyexecdir = @pyexecdir@ ++pythondir = @pythondir@ ++sbindir = @sbindir@ ++sharedstatedir = @sharedstatedir@ ++srcdir = @srcdir@ ++sysconfdir = @sysconfdir@ ++target_alias = @target_alias@ ++top_build_prefix = @top_build_prefix@ ++top_builddir = @top_builddir@ ++top_srcdir = @top_srcdir@ ++TESTS = run_tests ++EXTRA_DIST = \ ++ run_tests.in \ ++ test_it.py \ ++ __init__.py \ ++ $(NULL) ++ ++CLEANFILES = \ ++ run_tests \ ++ $(NULL) ++ ++MAINTAINERCLEANFILES = \ ++ Makefile.in \ ++ $(NULL) ++ ++all: all-am ++ ++.SUFFIXES: ++.SUFFIXES: .log .test .test$(EXEEXT) .trs ++$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps) ++ @for dep in $?; do \ ++ case '$(am__configure_deps)' in \ ++ *$$dep*) \ ++ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \ ++ && { if test -f $@; then exit 0; else break; fi; }; \ ++ exit 1;; \ ++ esac; \ ++ done; \ ++ echo ' cd $(top_srcdir) && $(AUTOMAKE) --gnu tests/Makefile'; \ ++ $(am__cd) $(top_srcdir) && \ ++ $(AUTOMAKE) --gnu tests/Makefile ++Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status ++ @case '$?' in \ ++ *config.status*) \ ++ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \ ++ *) \ ++ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \ ++ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \ ++ esac; ++ ++$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES) ++ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ++ ++$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps) ++ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ++$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps) ++ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ++$(am__aclocal_m4_deps): ++tags TAGS: ++ ++ctags CTAGS: ++ ++cscope cscopelist: ++ ++ ++# Recover from deleted '.trs' file; this should ensure that ++# "rm -f foo.log; make foo.trs" re-run 'foo.test', and re-create ++# both 'foo.log' and 'foo.trs'. Break the recipe in two subshells ++# to avoid problems with "make -n". ++.log.trs: ++ rm -f $< $@ ++ $(MAKE) $(AM_MAKEFLAGS) $< ++ ++# Leading 'am--fnord' is there to ensure the list of targets does not ++# expand to empty, as could happen e.g. with make check TESTS=''. ++am--fnord $(TEST_LOGS) $(TEST_LOGS:.log=.trs): $(am__force_recheck) ++am--force-recheck: ++ @: ++ ++$(TEST_SUITE_LOG): $(TEST_LOGS) ++ @$(am__set_TESTS_bases); \ ++ am__f_ok () { test -f "$$1" && test -r "$$1"; }; \ ++ redo_bases=`for i in $$bases; do \ ++ am__f_ok $$i.trs && am__f_ok $$i.log || echo $$i; \ ++ done`; \ ++ if test -n "$$redo_bases"; then \ ++ redo_logs=`for i in $$redo_bases; do echo $$i.log; done`; \ ++ redo_results=`for i in $$redo_bases; do echo $$i.trs; done`; \ ++ if $(am__make_dryrun); then :; else \ ++ rm -f $$redo_logs && rm -f $$redo_results || exit 1; \ ++ fi; \ ++ fi; \ ++ if test -n "$$am__remaking_logs"; then \ ++ echo "fatal: making $(TEST_SUITE_LOG): possible infinite" \ ++ "recursion detected" >&2; \ ++ elif test -n "$$redo_logs"; then \ ++ am__remaking_logs=yes $(MAKE) $(AM_MAKEFLAGS) $$redo_logs; \ ++ fi; \ ++ if $(am__make_dryrun); then :; else \ ++ st=0; \ ++ errmsg="fatal: making $(TEST_SUITE_LOG): failed to create"; \ ++ for i in $$redo_bases; do \ ++ test -f $$i.trs && test -r $$i.trs \ ++ || { echo "$$errmsg $$i.trs" >&2; st=1; }; \ ++ test -f $$i.log && test -r $$i.log \ ++ || { echo "$$errmsg $$i.log" >&2; st=1; }; \ ++ done; \ ++ test $$st -eq 0 || exit 1; \ ++ fi ++ @$(am__sh_e_setup); $(am__tty_colors); $(am__set_TESTS_bases); \ ++ ws='[ ]'; \ ++ results=`for b in $$bases; do echo $$b.trs; done`; \ ++ test -n "$$results" || results=/dev/null; \ ++ all=` grep "^$$ws*:test-result:" $$results | wc -l`; \ ++ pass=` grep "^$$ws*:test-result:$$ws*PASS" $$results | wc -l`; \ ++ fail=` grep "^$$ws*:test-result:$$ws*FAIL" $$results | wc -l`; \ ++ skip=` grep "^$$ws*:test-result:$$ws*SKIP" $$results | wc -l`; \ ++ xfail=`grep "^$$ws*:test-result:$$ws*XFAIL" $$results | wc -l`; \ ++ xpass=`grep "^$$ws*:test-result:$$ws*XPASS" $$results | wc -l`; \ ++ error=`grep "^$$ws*:test-result:$$ws*ERROR" $$results | wc -l`; \ ++ if test `expr $$fail + $$xpass + $$error` -eq 0; then \ ++ success=true; \ ++ else \ ++ success=false; \ ++ fi; \ ++ br='==================='; br=$$br$$br$$br$$br; \ ++ result_count () \ ++ { \ ++ if test x"$$1" = x"--maybe-color"; then \ ++ maybe_colorize=yes; \ ++ elif test x"$$1" = x"--no-color"; then \ ++ maybe_colorize=no; \ ++ else \ ++ echo "$@: invalid 'result_count' usage" >&2; exit 4; \ ++ fi; \ ++ shift; \ ++ desc=$$1 count=$$2; \ ++ if test $$maybe_colorize = yes && test $$count -gt 0; then \ ++ color_start=$$3 color_end=$$std; \ ++ else \ ++ color_start= color_end=; \ ++ fi; \ ++ echo "$${color_start}# $$desc $$count$${color_end}"; \ ++ }; \ ++ create_testsuite_report () \ ++ { \ ++ result_count $$1 "TOTAL:" $$all "$$brg"; \ ++ result_count $$1 "PASS: " $$pass "$$grn"; \ ++ result_count $$1 "SKIP: " $$skip "$$blu"; \ ++ result_count $$1 "XFAIL:" $$xfail "$$lgn"; \ ++ result_count $$1 "FAIL: " $$fail "$$red"; \ ++ result_count $$1 "XPASS:" $$xpass "$$red"; \ ++ result_count $$1 "ERROR:" $$error "$$mgn"; \ ++ }; \ ++ { \ ++ echo "$(PACKAGE_STRING): $(subdir)/$(TEST_SUITE_LOG)" | \ ++ $(am__rst_title); \ ++ create_testsuite_report --no-color; \ ++ echo; \ ++ echo ".. contents:: :depth: 2"; \ ++ echo; \ ++ for b in $$bases; do echo $$b; done \ ++ | $(am__create_global_log); \ ++ } >$(TEST_SUITE_LOG).tmp || exit 1; \ ++ mv $(TEST_SUITE_LOG).tmp $(TEST_SUITE_LOG); \ ++ if $$success; then \ ++ col="$$grn"; \ ++ else \ ++ col="$$red"; \ ++ test x"$$VERBOSE" = x || cat $(TEST_SUITE_LOG); \ ++ fi; \ ++ echo "$${col}$$br$${std}"; \ ++ echo "$${col}Testsuite summary for $(PACKAGE_STRING)$${std}"; \ ++ echo "$${col}$$br$${std}"; \ ++ create_testsuite_report --maybe-color; \ ++ echo "$$col$$br$$std"; \ ++ if $$success; then :; else \ ++ echo "$${col}See $(subdir)/$(TEST_SUITE_LOG)$${std}"; \ ++ if test -n "$(PACKAGE_BUGREPORT)"; then \ ++ echo "$${col}Please report to $(PACKAGE_BUGREPORT)$${std}"; \ ++ fi; \ ++ echo "$$col$$br$$std"; \ ++ fi; \ ++ $$success || exit 1 ++ ++check-TESTS: ++ @list='$(RECHECK_LOGS)'; test -z "$$list" || rm -f $$list ++ @list='$(RECHECK_LOGS:.log=.trs)'; test -z "$$list" || rm -f $$list ++ @test -z "$(TEST_SUITE_LOG)" || rm -f $(TEST_SUITE_LOG) ++ @set +e; $(am__set_TESTS_bases); \ ++ log_list=`for i in $$bases; do echo $$i.log; done`; \ ++ trs_list=`for i in $$bases; do echo $$i.trs; done`; \ ++ log_list=`echo $$log_list`; trs_list=`echo $$trs_list`; \ ++ $(MAKE) $(AM_MAKEFLAGS) $(TEST_SUITE_LOG) TEST_LOGS="$$log_list"; \ ++ exit $$?; ++recheck: all ++ @test -z "$(TEST_SUITE_LOG)" || rm -f $(TEST_SUITE_LOG) ++ @set +e; $(am__set_TESTS_bases); \ ++ bases=`for i in $$bases; do echo $$i; done \ ++ | $(am__list_recheck_tests)` || exit 1; \ ++ log_list=`for i in $$bases; do echo $$i.log; done`; \ ++ log_list=`echo $$log_list`; \ ++ $(MAKE) $(AM_MAKEFLAGS) $(TEST_SUITE_LOG) \ ++ am__force_recheck=am--force-recheck \ ++ TEST_LOGS="$$log_list"; \ ++ exit $$? ++run_tests.log: run_tests ++ @p='run_tests'; \ ++ b='run_tests'; \ ++ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ ++ --log-file $$b.log --trs-file $$b.trs \ ++ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ ++ "$$tst" $(AM_TESTS_FD_REDIRECT) ++.test.log: ++ @p='$<'; \ ++ $(am__set_b); \ ++ $(am__check_pre) $(TEST_LOG_DRIVER) --test-name "$$f" \ ++ --log-file $$b.log --trs-file $$b.trs \ ++ $(am__common_driver_flags) $(AM_TEST_LOG_DRIVER_FLAGS) $(TEST_LOG_DRIVER_FLAGS) -- $(TEST_LOG_COMPILE) \ ++ "$$tst" $(AM_TESTS_FD_REDIRECT) ++@am__EXEEXT_TRUE@.test$(EXEEXT).log: ++@am__EXEEXT_TRUE@ @p='$<'; \ ++@am__EXEEXT_TRUE@ $(am__set_b); \ ++@am__EXEEXT_TRUE@ $(am__check_pre) $(TEST_LOG_DRIVER) --test-name "$$f" \ ++@am__EXEEXT_TRUE@ --log-file $$b.log --trs-file $$b.trs \ ++@am__EXEEXT_TRUE@ $(am__common_driver_flags) $(AM_TEST_LOG_DRIVER_FLAGS) $(TEST_LOG_DRIVER_FLAGS) -- $(TEST_LOG_COMPILE) \ ++@am__EXEEXT_TRUE@ "$$tst" $(AM_TESTS_FD_REDIRECT) ++ ++distdir: $(BUILT_SOURCES) ++ $(MAKE) $(AM_MAKEFLAGS) distdir-am ++ ++distdir-am: $(DISTFILES) ++ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \ ++ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \ ++ list='$(DISTFILES)'; \ ++ dist_files=`for file in $$list; do echo $$file; done | \ ++ sed -e "s|^$$srcdirstrip/||;t" \ ++ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \ ++ case $$dist_files in \ ++ */*) $(MKDIR_P) `echo "$$dist_files" | \ ++ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \ ++ sort -u` ;; \ ++ esac; \ ++ for file in $$dist_files; do \ ++ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \ ++ if test -d $$d/$$file; then \ ++ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \ ++ if test -d "$(distdir)/$$file"; then \ ++ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \ ++ fi; \ ++ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \ ++ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \ ++ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \ ++ fi; \ ++ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \ ++ else \ ++ test -f "$(distdir)/$$file" \ ++ || cp -p $$d/$$file "$(distdir)/$$file" \ ++ || exit 1; \ ++ fi; \ ++ done ++check-am: all-am ++ $(MAKE) $(AM_MAKEFLAGS) check-TESTS ++check: check-am ++all-am: Makefile ++installdirs: ++install: install-am ++install-exec: install-exec-am ++install-data: install-data-am ++uninstall: uninstall-am ++ ++install-am: all-am ++ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am ++ ++installcheck: installcheck-am ++install-strip: ++ if test -z '$(STRIP)'; then \ ++ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \ ++ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \ ++ install; \ ++ else \ ++ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \ ++ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \ ++ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \ ++ fi ++mostlyclean-generic: ++ -test -z "$(TEST_LOGS)" || rm -f $(TEST_LOGS) ++ -test -z "$(TEST_LOGS:.log=.trs)" || rm -f $(TEST_LOGS:.log=.trs) ++ -test -z "$(TEST_SUITE_LOG)" || rm -f $(TEST_SUITE_LOG) ++ ++clean-generic: ++ -test -z "$(CLEANFILES)" || rm -f $(CLEANFILES) ++ ++distclean-generic: ++ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES) ++ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES) ++ ++maintainer-clean-generic: ++ @echo "This command is intended for maintainers to use" ++ @echo "it deletes files that may require special tools to rebuild." ++ -test -z "$(MAINTAINERCLEANFILES)" || rm -f $(MAINTAINERCLEANFILES) ++clean: clean-am ++ ++clean-am: clean-generic mostlyclean-am ++ ++distclean: distclean-am ++ -rm -f Makefile ++distclean-am: clean-am distclean-generic ++ ++dvi: dvi-am ++ ++dvi-am: ++ ++html: html-am ++ ++html-am: ++ ++info: info-am ++ ++info-am: ++ ++install-data-am: ++ ++install-dvi: install-dvi-am ++ ++install-dvi-am: ++ ++install-exec-am: ++ ++install-html: install-html-am ++ ++install-html-am: ++ ++install-info: install-info-am ++ ++install-info-am: ++ ++install-man: ++ ++install-pdf: install-pdf-am ++ ++install-pdf-am: ++ ++install-ps: install-ps-am ++ ++install-ps-am: ++ ++installcheck-am: ++ ++maintainer-clean: maintainer-clean-am ++ -rm -f Makefile ++maintainer-clean-am: distclean-am maintainer-clean-generic ++ ++mostlyclean: mostlyclean-am ++ ++mostlyclean-am: mostlyclean-generic ++ ++pdf: pdf-am ++ ++pdf-am: ++ ++ps: ps-am ++ ++ps-am: ++ ++uninstall-am: ++ ++.MAKE: check-am install-am install-strip ++ ++.PHONY: all all-am check check-TESTS check-am clean clean-generic \ ++ cscopelist-am ctags-am distclean distclean-generic distdir dvi \ ++ dvi-am html html-am info info-am install install-am \ ++ install-data install-data-am install-dvi install-dvi-am \ ++ install-exec install-exec-am install-html install-html-am \ ++ install-info install-info-am install-man install-pdf \ ++ install-pdf-am install-ps install-ps-am install-strip \ ++ installcheck installcheck-am installdirs maintainer-clean \ ++ maintainer-clean-generic mostlyclean mostlyclean-generic pdf \ ++ pdf-am ps ps-am recheck tags-am uninstall uninstall-am ++ ++.PRECIOUS: Makefile ++ ++ ++run_tests: run_tests.in ++ sed -e 's&@PYTHON_BIN@&$(PYTHON)&g' \ ++ -e 's&@SRCDIR@&$(srcdir)&g' $< > $@ ++ chmod +x $@ ++ ++# Tell versions [3.59,3.63) of GNU make to not export all variables. ++# Otherwise a system limit (for SysV at least) may be exceeded. ++.NOEXPORT: +--- ibus-table-1.9.18.orig/configure.ac 2020-07-22 11:52:11.639532241 +0200 ++++ ibus-table-1.9.18/configure.ac 2020-07-22 14:43:51.905260956 +0200 +@@ -68,6 +68,7 @@ + setup/Makefile + setup/ibus-setup-table + setup/version.py ++ tests/Makefile + ibus-table.spec + ibus-table.pc] + ) +diff -Nru ibus-table-1.9.18.orig/engine/Makefile.am ibus-table-1.9.18/engine/Makefile.am +--- ibus-table-1.9.18.orig/engine/Makefile.am 2020-07-22 11:52:11.650532123 +0200 ++++ ibus-table-1.9.18/engine/Makefile.am 2020-07-22 14:43:51.906260946 +0200 +@@ -32,6 +32,7 @@ + table.py \ + tabcreatedb.py \ + tabsqlitedb.py \ ++ it_util.py \ + $(NULL) + engine_table_DATA = \ + $(NULL) +diff -Nru ibus-table-1.9.18.orig/engine/it_util.py ibus-table-1.9.18/engine/it_util.py +--- ibus-table-1.9.18.orig/engine/it_util.py 1970-01-01 01:00:00.000000000 +0100 ++++ ibus-table-1.9.18/engine/it_util.py 2020-07-22 14:43:51.906260946 +0200 +@@ -0,0 +1,63 @@ ++# -*- coding: utf-8 -*- ++# vim:et sts=4 sw=4 ++# ++# ibus-table - The Tables engine for IBus ++# ++# Copyright (c) 2008-2009 Yu Yuwei ++# Copyright (c) 2009-2014 Caius "kaio" CHANCE ++# Copyright (c) 2012-2015 Mike FABIAN ++# ++# This library is free software; you can redistribute it and/or ++# modify it under the terms of the GNU Lesser General Public ++# License as published by the Free Software Foundation; either ++# version 2.1 of the License, or (at your option) any later version. ++# ++# This library is distributed in the hope that it will be useful, ++# but WITHOUT ANY WARRANTY; without even the implied warranty of ++# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ++# Lesser General Public License for more details. ++# ++# You should have received a copy of the GNU Lesser General Public ++# License along with this library; if not, write to the Free Software ++# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA ++# ++''' ++Utility functions used in ibus-table ++''' ++ ++import sys ++import re ++import string ++ ++def config_section_normalize(section): ++ '''Replaces “_:” with “-” in the dconf section and converts to lower case ++ ++ :param section: The name of the dconf section ++ :type section: string ++ :rtype: string ++ ++ To make the comparison of the dconf sections work correctly. ++ ++ I avoid using .lower() here because it is locale dependent, when ++ using .lower() this would not achieve the desired effect of ++ comparing the dconf sections case insentively in some locales, it ++ would fail for example if Turkish locale (tr_TR.UTF-8) is set. ++ ++ Examples: ++ ++ >>> config_section_normalize('Foo_bAr:Baz') ++ 'foo-bar-baz' ++ ''' ++ return re.sub(r'[_:]', r'-', section).translate( ++ bytes.maketrans( ++ bytes(string.ascii_uppercase.encode('ascii')), ++ bytes(string.ascii_lowercase.encode('ascii')))) ++ ++ ++if __name__ == "__main__": ++ import doctest ++ (FAILED, ATTEMPTED) = doctest.testmod() ++ if FAILED: ++ sys.exit(1) ++ else: ++ sys.exit(0) +diff -Nru ibus-table-1.9.18.orig/engine/table.py ibus-table-1.9.18/engine/table.py +--- ibus-table-1.9.18.orig/engine/table.py 2020-07-22 11:52:11.650532123 +0200 ++++ ibus-table-1.9.18/engine/table.py 2020-07-22 14:43:51.907260935 +0200 +@@ -37,6 +37,7 @@ + import re + from gi.repository import GObject + import time ++import it_util + + debug_level = int(0) + +@@ -215,8 +216,10 @@ + max_key_length, database): + self.db = database + self._config = config +- engine_name = os.path.basename(self.db.filename).replace('.db', '') +- self._config_section = "engine/Table/%s" % engine_name.replace(' ','_') ++ engine_name = os.path.basename( ++ self.db.filename).replace('.db', '').replace(' ','_') ++ self._config_section = it_util.config_section_normalize( ++ "engine/Table/%s" % engine_name) + self._max_key_length = int(max_key_length) + self._max_key_length_pinyin = 7 + self._valid_input_chars = valid_input_chars +@@ -320,7 +323,7 @@ + self._config_section, + "ChineseMode")) + if self._chinese_mode == None: +- self._chinese_mode = self.get_chinese_mode() ++ self._chinese_mode = self.get_default_chinese_mode() + elif debug_level > 1: + sys.stderr.write( + "Chinese mode found in user config, mode=%s\n" +@@ -342,7 +345,7 @@ + def get_new_lookup_table( + self, page_size=10, + select_keys=[49, 50, 51, 52, 53, 54, 55, 56, 57, 48], +- orientation=True): ++ orientation=IBus.Orientation.VERTICAL): + ''' + [49, 50, 51, 52, 53, 54, 55, 56, 57, 48] are the key codes + for the characters ['1', '2', '3', '4', '5', '6', '7', '8', '0'] +@@ -351,7 +354,7 @@ + page_size = 1 + if page_size > len(select_keys): + page_size = len(select_keys) +- lookup_table = IBus.LookupTable.new( ++ lookup_table = IBus.LookupTable( + page_size=page_size, + cursor_pos=0, + cursor_visible=True, +@@ -371,7 +374,7 @@ + """ + return self._select_keys + +- def get_chinese_mode (self): ++ def get_default_chinese_mode (self): + ''' + Use db value or LC_CTYPE in your box to determine the _chinese_mode + ''' +@@ -380,7 +383,7 @@ + if __db_chinese_mode >= 0: + if debug_level > 1: + sys.stderr.write( +- "get_chinese_mode(): " ++ "get_default_chinese_mode(): " + + "default Chinese mode found in database, mode=%s\n" + %__db_chinese_mode) + return __db_chinese_mode +@@ -390,19 +393,19 @@ + __lc = os.environ['LC_ALL'].split('.')[0].lower() + if debug_level > 1: + sys.stderr.write( +- "get_chinese_mode(): __lc=%s found in LC_ALL\n" ++ "get_default_chinese_mode(): __lc=%s found in LC_ALL\n" + % __lc) + elif 'LC_CTYPE' in os.environ: + __lc = os.environ['LC_CTYPE'].split('.')[0].lower() + if debug_level > 1: + sys.stderr.write( +- "get_chinese_mode(): __lc=%s found in LC_CTYPE\n" ++ "get_default_chinese_mode(): __lc=%s found in LC_CTYPE\n" + % __lc) + else: + __lc = os.environ['LANG'].split('.')[0].lower() + if debug_level > 1: + sys.stderr.write( +- "get_chinese_mode(): __lc=%s found in LANG\n" ++ "get_default_chinese_mode(): __lc=%s found in LANG\n" + % __lc) + + if '_cn' in __lc or '_sg' in __lc: +@@ -419,14 +422,14 @@ + # variant: + if debug_level > 1: + sys.stderr.write( +- "get_chinese_mode(): last fallback, " ++ "get_default_chinese_mode(): last fallback, " + + "database is Chinese but we don’t know " + + "which variant.\n") + return 4 # show all Chinese characters + else: + if debug_level > 1: + sys.stderr.write( +- "get_chinese_mode(): last fallback, " ++ "get_default_chinese_mode(): last fallback, " + + "database is not Chinese, returning -1.\n") + return -1 + except: +@@ -1202,7 +1205,7 @@ + class tabengine (IBus.Engine): + '''The IM Engine for Tables''' + +- def __init__(self, bus, obj_path, db ): ++ def __init__(self, bus, obj_path, db, unit_test=False): + super(tabengine, self).__init__(connection=bus.get_connection(), + object_path=obj_path) + global debug_level +@@ -1210,6 +1213,7 @@ + debug_level = int(os.getenv('IBUS_TABLE_DEBUG_LEVEL')) + except (TypeError, ValueError): + debug_level = int(0) ++ self._unit_test = unit_test + self._input_purpose = 0 + self._has_input_purpose = False + if hasattr(IBus, 'InputPurpose'): +@@ -1224,12 +1228,16 @@ + os.path.sep, 'icons', os.path.sep) + # name for config section + self._engine_name = os.path.basename( +- self.db.filename).replace('.db', '') +- self._config_section = ( +- "engine/Table/%s" % self._engine_name.replace(' ','_')) ++ self.db.filename).replace('.db', '').replace(' ','_') ++ self._config_section = it_util.config_section_normalize( ++ "engine/Table/%s" % self._engine_name) ++ if debug_level > 1: ++ sys.stderr.write( ++ 'tabengine.__init__() self._config_section = %s\n' ++ % self._config_section) + + # config module +- self._config = self._bus.get_config () ++ self._config = self._bus.get_config() + self._config.connect ("value-changed", self.config_value_changed_cb) + + # self._ime_py: Indicates whether this table supports pinyin mode +@@ -1699,7 +1707,7 @@ + self._save_user_count = 0 + super(tabengine, self).destroy() + +- def set_input_mode(self, mode=0): ++ def set_input_mode(self, mode=1): + if mode == self._input_mode: + return + self._input_mode = mode +@@ -1722,6 +1730,9 @@ + self._full_width_punct[self._input_mode]) + self.reset() + ++ def get_input_mode(self): ++ return self._input_mode ++ + def set_pinyin_mode(self, mode=False): + if mode == self._editor._py_mode: + return +@@ -1741,76 +1752,302 @@ + self._input_mode) + self._update_ui() + +- def set_onechar_mode(self, mode=False): ++ def set_onechar_mode(self, mode=False, update_dconf=True): + if mode == self._editor._onechar: + return + self._editor._onechar = mode + self._init_or_update_property_menu( + self.onechar_mode_menu, mode) +- self._config.set_value( +- self._config_section, +- "OneChar", +- GLib.Variant.new_boolean(mode)) ++ self.db.reset_phrases_cache() ++ if update_dconf: ++ self._config.set_value( ++ self._config_section, ++ "OneChar", ++ GLib.Variant.new_boolean(mode)) ++ ++ def get_onechar_mode(self): ++ return self._editor._onechar + +- def set_autocommit_mode(self, mode=False): ++ def set_autocommit_mode(self, mode=False, update_dconf=True): + if mode == self._auto_commit: + return + self._auto_commit = mode + self._init_or_update_property_menu( + self.autocommit_mode_menu, mode) +- self._config.set_value( +- self._config_section, +- "AutoCommit", +- GLib.Variant.new_boolean(mode)) ++ if update_dconf: ++ self._config.set_value( ++ self._config_section, ++ "AutoCommit", ++ GLib.Variant.new_boolean(mode)) + +- def set_letter_width(self, mode=False, input_mode=0): +- if mode == self._full_width_letter[input_mode]: ++ def get_autocommit_mode(self): ++ return self._auto_commit ++ ++ def set_autoselect_mode(self, mode=False, update_dconf=True): ++ if mode == self._auto_select: + return +- self._full_width_letter[input_mode] = mode +- self._editor._full_width_letter[input_mode] = mode +- if input_mode == self._input_mode: +- self._init_or_update_property_menu( +- self.letter_width_menu, mode) +- if input_mode: ++ self._auto_select = mode ++ self._editor._auto_select = mode ++ if update_dconf: + self._config.set_value( + self._config_section, +- "TabDefFullWidthLetter", ++ "AutoSelect", + GLib.Variant.new_boolean(mode)) +- else: ++ ++ def get_autoselect_mode(self): ++ return self._auto_select ++ ++ def set_autowildcard_mode(self, mode=False, update_dconf=True): ++ if mode == self._auto_wildcard: ++ return ++ self._auto_wildcard = mode ++ self._editor._auto_wildcard = mode ++ self.db.reset_phrases_cache() ++ if update_dconf: + self._config.set_value( + self._config_section, +- "EnDefFullWidthLetter", ++ "AutoWildcard", + GLib.Variant.new_boolean(mode)) + +- def set_punctuation_width(self, mode=False, input_mode=0): +- if mode == self._full_width_punct[input_mode]: ++ def get_autowildcard_mode(self): ++ return self._auto_wildcard ++ ++ def set_single_wildcard_char(self, char=u'', update_dconf=True): ++ if char == self._single_wildcard_char: + return +- self._full_width_punct[input_mode] = mode +- self._editor._full_width_punct[input_mode] = mode +- if input_mode == self._input_mode: +- self._init_or_update_property_menu( +- self.punctuation_width_menu, mode) +- if input_mode: ++ self._single_wildcard_char = char ++ self._editor._single_wildcard_char = char ++ self.db.reset_phrases_cache() ++ if update_dconf: ++ self._config.set_value( ++ self._config_section, ++ "singlewildcardchar", ++ GLib.Variant.new_string(char)) ++ ++ def get_single_wildcard_char(self): ++ return self._single_wildcard_char ++ ++ def set_multi_wildcard_char(self, char=u'', update_dconf=True): ++ if char == self._multi_wildcard_char: ++ return ++ self._multi_wildcard_char = char ++ self._editor._multi_wildcard_char = char ++ self.db.reset_phrases_cache() ++ if update_dconf: ++ self._config.set_value( ++ self._config_section, ++ "multiwildcardchar", ++ GLib.Variant.new_string(char)) ++ ++ def get_multi_wildcard_char(self): ++ return self._multi_wildcard_char ++ ++ def set_space_key_behavior_mode(self, mode=False, update_dconf=True): ++ '''Sets the behaviour of the space key ++ ++ :param mode: How the space key should behave ++ :type mode: Boolean ++ True: space is used as a page down key ++ and not as a commit key. ++ False: space is used as a commit key ++ and not used as a page down key ++ :param update_dconf: Whether to write the change to dconf. ++ 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. ++ :type update_dconf: boolean ++ ''' ++ if debug_level > 1: ++ sys.stderr.write( ++ "set_space_key_behavior_mode(%s)\n" ++ %mode) ++ if mode == True: ++ # space is used as a page down key and not as a commit key: ++ if IBus.KEY_space not in self._page_down_keys: ++ self._page_down_keys.append(IBus.KEY_space) ++ if IBus.KEY_space in self._commit_keys: ++ self._commit_keys.remove(IBus.KEY_space) ++ if mode == False: ++ # space is used as a commit key and not used as a page down key: ++ if IBus.KEY_space in self._page_down_keys: ++ self._page_down_keys.remove(IBus.KEY_space) ++ if IBus.KEY_space not in self._commit_keys: ++ self._commit_keys.append(IBus.KEY_space) ++ if debug_level > 1: ++ sys.stderr.write( ++ 'set_space_key_behavior_mode(): self._page_down_keys=%s\n' ++ % repr(self._page_down_keys)) ++ sys.stderr.write( ++ 'set_space_key_behavior_mode(): self._commit_keys=%s\n' ++ % repr(self._commit_keys)) ++ if update_dconf: + self._config.set_value( + self._config_section, +- "TabDefFullWidthPunct", ++ "spacekeybehavior", + GLib.Variant.new_boolean(mode)) +- else: ++ ++ def get_space_key_behavior_mode(self): ++ mode = False ++ if IBus.KEY_space in self._page_down_keys: ++ mode = True ++ if IBus.KEY_space in self._commit_keys: ++ # commit key behaviour overrides the page down behaviour ++ mode = False ++ return mode ++ ++ def set_always_show_lookup(self, mode=False, update_dconf=True): ++ if mode == self._always_show_lookup: ++ return ++ self._always_show_lookup = mode ++ if update_dconf: + self._config.set_value( + self._config_section, +- "EnDefFullWidthPunct", ++ "AlwaysShowLookup", + GLib.Variant.new_boolean(mode)) + +- def set_chinese_mode(self, mode=0): ++ def get_always_show_lookup(self): ++ return self._always_show_lookup ++ ++ def set_lookup_table_orientation(self, orientation, update_dconf=True): ++ '''Sets the orientation of the lookup table ++ ++ :param orientation: The orientation of the lookup table ++ :type mode: integer >= 0 and <= 2 ++ IBUS_ORIENTATION_HORIZONTAL = 0, ++ IBUS_ORIENTATION_VERTICAL = 1, ++ IBUS_ORIENTATION_SYSTEM = 2. ++ :param update_dconf: Whether to write the change to dconf. ++ 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. ++ :type update_dconf: boolean ++ ''' ++ if debug_level > 1: ++ sys.stderr.write( ++ "set_lookup_table_orientation(%s)\n" ++ %orientation) ++ if orientation == self._editor._orientation: ++ return ++ if orientation >= 0 and orientation <= 2: ++ self._editor._orientation = orientation ++ self._editor._lookup_table.set_orientation(orientation) ++ if update_dconf: ++ self._config.set_value( ++ self._config_section, ++ 'lookuptableorientation', ++ GLib.Variant.new_int32(orientation)) ++ ++ def get_lookup_table_orientation(self): ++ '''Returns the current orientation of the lookup table ++ ++ :rtype: integer ++ ''' ++ return self._editor._orientation ++ ++ def set_page_size(self, page_size, update_dconf=True): ++ '''Sets the page size of the lookup table ++ ++ :param orientation: The orientation of the lookup table ++ :type mode: integer >= 1 and <= number of select keys ++ :param update_dconf: Whether to write the change to dconf. ++ 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. ++ :type update_dconf: boolean ++ ''' ++ if debug_level > 1: ++ sys.stderr.write( ++ "set_page_size(%s)\n" ++ %page_size) ++ if page_size == self._editor._page_size: ++ return ++ if page_size > len(self._editor._select_keys): ++ page_size = len(self._editor._select_keys) ++ if page_size < 1: ++ page_size = 1 ++ self._editor._page_size = page_size ++ self._editor._lookup_table = self._editor.get_new_lookup_table( ++ page_size = self._editor._page_size, ++ select_keys = self._editor._select_keys, ++ orientation = self._editor._orientation) ++ self.reset() ++ if update_dconf: ++ self._config.set_value( ++ self._config_section, ++ 'lookuptablepagesize', ++ GLib.Variant.new_int32(page_size)) ++ ++ def get_page_size(self): ++ '''Returns the current page size of the lookup table ++ ++ :rtype: integer ++ ''' ++ return self._editor._page_size ++ ++ def set_letter_width(self, mode=False, input_mode=0, update_dconf=True): ++ if mode == self._full_width_letter[input_mode]: ++ return ++ self._full_width_letter[input_mode] = mode ++ self._editor._full_width_letter[input_mode] = mode ++ if input_mode == self._input_mode: ++ self._init_or_update_property_menu( ++ self.letter_width_menu, mode) ++ if update_dconf: ++ if input_mode: ++ self._config.set_value( ++ self._config_section, ++ "TabDefFullWidthLetter", ++ GLib.Variant.new_boolean(mode)) ++ else: ++ self._config.set_value( ++ self._config_section, ++ "EnDefFullWidthLetter", ++ GLib.Variant.new_boolean(mode)) ++ ++ def get_letter_width(self): ++ return self._full_width_letter ++ ++ def set_punctuation_width(self, mode=False, input_mode=0, update_dconf=True): ++ if mode == self._full_width_punct[input_mode]: ++ return ++ self._full_width_punct[input_mode] = mode ++ self._editor._full_width_punct[input_mode] = mode ++ if input_mode == self._input_mode: ++ self._init_or_update_property_menu( ++ self.punctuation_width_menu, mode) ++ if update_dconf: ++ if input_mode: ++ self._config.set_value( ++ self._config_section, ++ "TabDefFullWidthPunct", ++ GLib.Variant.new_boolean(mode)) ++ else: ++ self._config.set_value( ++ self._config_section, ++ "EnDefFullWidthPunct", ++ GLib.Variant.new_boolean(mode)) ++ ++ def get_punctuation_width(self): ++ return self._full_width_punct ++ ++ def set_chinese_mode(self, mode=0, update_dconf=True): + if mode == self._editor._chinese_mode: + return + self._editor._chinese_mode = mode ++ self.db.reset_phrases_cache() + self._init_or_update_property_menu( + self.chinese_mode_menu, mode) +- self._config.set_value( +- self._config_section, +- "ChineseMode", +- GLib.Variant.new_int32(mode)) ++ if update_dconf: ++ self._config.set_value( ++ self._config_section, ++ "ChineseMode", ++ GLib.Variant.new_int32(mode)) ++ ++ def get_chinese_mode(self): ++ return self._editor._chinese_mode + + def _init_or_update_property_menu(self, menu, current_mode=0): + key = menu['key'] +@@ -2233,6 +2470,44 @@ + return True + return False + ++ def _return_false(self, keyval, keycode, state): ++ '''A replacement for “return False” in do_process_key_event() ++ ++ do_process_key_event should return “True” if a key event has ++ been handled completely. It should return “False” if the key ++ event should be passed to the application. ++ ++ But just doing “return False” doesn’t work well when trying to ++ do the unit tests. The MockEngine class in the unit tests ++ cannot get that return value. Therefore, it cannot do the ++ necessary updates to the self._mock_committed_text etc. which ++ prevents proper testing of the effects of such keys passed to ++ the application. Instead of “return False”, one can also use ++ self.forward_key_event(keyval, keycode, keystate) to pass the ++ key to the application. And this works fine with the unit ++ tests because a forward_key_event function is implemented in ++ MockEngine as well which then gets the key and can test its ++ effects. ++ ++ Unfortunately, “forward_key_event()” does not work in Qt5 ++ applications because the ibus module in Qt5 does not implement ++ “forward_key_event()”. Therefore, always using ++ “forward_key_event()” instead of “return False” in ++ “do_process_key_event()” would break ibus-typing-booster ++ completely for all Qt5 applictions. ++ ++ To work around this problem and make unit testing possible ++ without breaking Qt5 applications, we use this helper function ++ which uses “forward_key_event()” when unit testing and “return ++ False” during normal usage. ++ ++ ''' ++ if self._unit_test: ++ self.forward_key_event(keyval, keycode, state) ++ return True ++ else: ++ return False ++ + def do_process_key_event(self, keyval, keycode, state): + '''Process Key Events + Key Events include Key Press and Key Release, +@@ -2243,9 +2518,13 @@ + if (self._has_input_purpose + and self._input_purpose + in [IBus.InputPurpose.PASSWORD, IBus.InputPurpose.PIN]): +- return False ++ return self._return_false(keyval, keycode, state) + + key = KeyEvent(keyval, keycode, state) ++ if debug_level > 1: ++ sys.stderr.write( ++ "process_key_event() " ++ "KeyEvent object: %s" % key) + + result = self._process_key_event (key) + self._prev_key = key +@@ -2308,13 +2587,13 @@ + def _english_mode_process_key_event(self, key): + # Ignore key release events + if key.state & IBus.ModifierType.RELEASE_MASK: +- return False ++ return self._return_false(key.val, key.code, key.state) + if key.val >= 128: +- return False ++ return self._return_false(key.val, key.code, key.state) + # we ignore all hotkeys here + if (key.state + & (IBus.ModifierType.CONTROL_MASK|IBus.ModifierType.MOD1_MASK)): +- return False ++ return self._return_false(key.val, key.code, key.state) + keychar = IBus.keyval_to_unicode(key.val) + if type(keychar) != type(u''): + keychar = keychar.decode('UTF-8') +@@ -2323,7 +2602,7 @@ + else: + trans_char = self.cond_letter_translate(keychar) + if trans_char == keychar: +- return False ++ return self._return_false(key.val, key.code, key.state) + self.commit_string(trans_char) + return True + +@@ -2387,7 +2666,7 @@ + # (Must be below all self._match_hotkey() callse + # because these match on a release event). + if key.state & IBus.ModifierType.RELEASE_MASK: +- return False ++ return self._return_false(key.val, key.code, key.state) + + keychar = IBus.keyval_to_unicode(key.val) + if type(keychar) != type(u''): +@@ -2419,7 +2698,7 @@ + trans_char = self.cond_letter_translate(keychar) + if trans_char == keychar: + self._prev_char = trans_char +- return False ++ return self._return_false(key.val, key.code, key.state) + else: + self.commit_string(trans_char) + return True +@@ -2441,12 +2720,12 @@ + # input but it ends up here. If it is leading input + # (i.e. the preëdit is empty) we should always pass + # IBus.KEY_KP_Enter to the application: +- return False ++ return self._return_false(key.val, key.code, key.state) + if self._auto_select: + self._editor.commit_to_preedit() + commit_string = self._editor.get_preedit_string_complete() + self.commit_string(commit_string) +- return False ++ return self._return_false(key.val, key.code, key.state) + else: + commit_string = self._editor.get_preedit_tabkeys_complete() + self.commit_string(commit_string) +@@ -2474,18 +2753,18 @@ + # to “шшш”. + self._editor.commit_to_preedit() + self.commit_string(self._editor.get_preedit_string_complete()) +- return False ++ return self._return_false(key.val, key.code, key.state) + + if key.val in (IBus.KEY_Down, IBus.KEY_KP_Down) : + if not self._editor.get_preedit_string_complete(): +- return False ++ return self._return_false(key.val, key.code, key.state) + res = self._editor.cursor_down() + self._update_ui() + return res + + if key.val in (IBus.KEY_Up, IBus.KEY_KP_Up): + if not self._editor.get_preedit_string_complete(): +- return False ++ return self._return_false(key.val, key.code, key.state) + res = self._editor.cursor_up() + self._update_ui() + return res +@@ -2493,7 +2772,7 @@ + if (key.val in (IBus.KEY_Left, IBus.KEY_KP_Left) + and key.state & IBus.ModifierType.CONTROL_MASK): + if not self._editor.get_preedit_string_complete(): +- return False ++ return self._return_false(key.val, key.code, key.state) + self._editor.control_arrow_left() + self._update_ui() + return True +@@ -2501,21 +2780,21 @@ + if (key.val in (IBus.KEY_Right, IBus.KEY_KP_Right) + and key.state & IBus.ModifierType.CONTROL_MASK): + if not self._editor.get_preedit_string_complete(): +- return False ++ return self._return_false(key.val, key.code, key.state) + self._editor.control_arrow_right() + self._update_ui() + return True + + if key.val in (IBus.KEY_Left, IBus.KEY_KP_Left): + if not self._editor.get_preedit_string_complete(): +- return False ++ return self._return_false(key.val, key.code, key.state) + self._editor.arrow_left() + self._update_ui() + return True + + if key.val in (IBus.KEY_Right, IBus.KEY_KP_Right): + if not self._editor.get_preedit_string_complete(): +- return False ++ return self._return_false(key.val, key.code, key.state) + self._editor.arrow_right() + self._update_ui() + return True +@@ -2523,14 +2802,14 @@ + if (key.val == IBus.KEY_BackSpace + and key.state & IBus.ModifierType.CONTROL_MASK): + if not self._editor.get_preedit_string_complete(): +- return False ++ return self._return_false(key.val, key.code, key.state) + self._editor.remove_preedit_before_cursor() + self._update_ui() + return True + + if key.val == IBus.KEY_BackSpace: + if not self._editor.get_preedit_string_complete(): +- return False ++ return self._return_false(key.val, key.code, key.state) + self._editor.remove_char() + self._update_ui() + return True +@@ -2538,14 +2817,14 @@ + if (key.val == IBus.KEY_Delete + and key.state & IBus.ModifierType.CONTROL_MASK): + if not self._editor.get_preedit_string_complete(): +- return False ++ return self._return_false(key.val, key.code, key.state) + self._editor.remove_preedit_after_cursor() + self._update_ui() + return True + + if key.val == IBus.KEY_Delete: + if not self._editor.get_preedit_string_complete(): +- return False ++ return self._return_false(key.val, key.code, key.state) + self._editor.delete() + self._update_ui() + return True +@@ -2567,10 +2846,10 @@ + # now we ignore all other hotkeys + if (key.state + & (IBus.ModifierType.CONTROL_MASK|IBus.ModifierType.MOD1_MASK)): +- return False ++ return self._return_false(key.val, key.code, key.state) + + if key.state & IBus.ModifierType.MOD1_MASK: +- return False ++ return self._return_false(key.val, key.code, key.state) + + # Section to handle valid input characters: + # +@@ -2731,7 +3010,7 @@ + # + # returned no result. So whatever this was, we cannot handle it, + # just pass it through to the application by returning “False”. +- return False ++ return self._return_false(key.val, key.code, key.state) + + def do_focus_in (self): + if debug_level > 1: +@@ -2802,92 +3081,47 @@ + self.set_input_mode(value) + return + if name == u'autoselect': +- self._editor._auto_select = value +- self._auto_select = value ++ self.set_autoselect_mode(value, update_dconf=False) + return + if name == u'autocommit': +- self.set_autocommit_mode(value) ++ self.set_autocommit_mode(value, update_dconf=False) + return + if name == u'chinesemode': +- self.set_chinese_mode(value) +- self.db.reset_phrases_cache() ++ self.set_chinese_mode(value, update_dconf=False) + return + if name == u'endeffullwidthletter': +- self.set_letter_width(value, input_mode=0) ++ self.set_letter_width(value, input_mode=0, update_dconf=False) + return + if name == u'endeffullwidthpunct': +- self.set_punctuation_width(value, input_mode=0) ++ self.set_punctuation_width(value, input_mode=0, update_dconf=False) + return + if name == u'lookuptableorientation': +- self._editor._orientation = value +- self._editor._lookup_table.set_orientation(value) ++ self.set_lookup_table_orientation(value, update_dconf=False) + return + if name == u'lookuptablepagesize': +- if value > len(self._editor._select_keys): +- value = len(self._editor._select_keys) +- self._config.set_value( +- self._config_section, +- 'lookuptablepagesize', +- GLib.Variant.new_int32(value)) +- if value < 1: +- value = 1 +- self._config.set_value( +- self._config_section, +- 'lookuptablepagesize', +- GLib.Variant.new_int32(value)) +- self._editor._page_size = value +- self._editor._lookup_table = self._editor.get_new_lookup_table( +- page_size = self._editor._page_size, +- select_keys = self._editor._select_keys, +- orientation = self._editor._orientation) +- self.reset() +- return +- if name == u'lookuptableselectkeys': +- self._editor.set_select_keys(value) ++ self.set_page_size(value, update_dconf=False) + return + if name == u'onechar': +- self.set_onechar_mode(value) +- self.db.reset_phrases_cache() ++ self.set_onechar_mode(value, update_dconf=False) + return + if name == u'tabdeffullwidthletter': +- self.set_letter_width(value, input_mode=1) ++ self.set_letter_width(value, input_mode=1, update_dconf=False) + return + if name == u'tabdeffullwidthpunct': +- self.set_punctuation_width(value, input_mode=1) ++ self.set_punctuation_width(value, input_mode=1, update_dconf=False) + return + if name == u'alwaysshowlookup': +- self._always_show_lookup = value ++ self.set_always_show_lookup(value, update_dconf=False) + return + if name == u'spacekeybehavior': +- if value == True: +- # space is used as a page down key and not as a commit key: +- if IBus.KEY_space not in self._page_down_keys: +- self._page_down_keys.append(IBus.KEY_space) +- if IBus.KEY_space in self._commit_keys: +- self._commit_keys.remove(IBus.KEY_space) +- if value == False: +- # space is used as a commit key and not used as a page down key: +- if IBus.KEY_space in self._page_down_keys: +- self._page_down_keys.remove(IBus.KEY_space) +- if IBus.KEY_space not in self._commit_keys: +- self._commit_keys.append(IBus.KEY_space) +- if debug_level > 1: +- sys.stderr.write( +- "self._page_down_keys=%s\n" +- % repr(self._page_down_keys)) ++ self.set_space_key_behavior_mode(value, update_dconf=False) + return + if name == u'singlewildcardchar': +- self._single_wildcard_char = value +- self._editor._single_wildcard_char = value +- self.db.reset_phrases_cache() ++ self.set_single_wildcard_char(value, update_dconf=False) + return + if name == u'multiwildcardchar': +- self._multi_wildcard_char = value +- self._editor._multi_wildcard_char = value +- self.db.reset_phrases_cache() ++ self.set_multi_wildcard_char(value, update_dconf=False) + return + if name == u'autowildcard': +- self._auto_wildcard = value +- self._editor._auto_wildcard = value +- self.db.reset_phrases_cache() ++ self.set_autowildcard_mode(value, update_dconf=False) + return +diff -Nru ibus-table-1.9.18.orig/engine/table.py.orig ibus-table-1.9.18/engine/table.py.orig +--- ibus-table-1.9.18.orig/engine/table.py.orig 1970-01-01 01:00:00.000000000 +0100 ++++ ibus-table-1.9.18/engine/table.py.orig 2020-07-22 14:43:13.607657038 +0200 +@@ -0,0 +1,2890 @@ ++# -*- coding: utf-8 -*- ++# vim:et sts=4 sw=4 ++# ++# ibus-table - The Tables engine for IBus ++# ++# Copyright (c) 2008-2009 Yu Yuwei ++# Copyright (c) 2009-2014 Caius "kaio" CHANCE ++# Copyright (c) 2012-2015 Mike FABIAN ++# ++# This library is free software; you can redistribute it and/or ++# modify it under the terms of the GNU Lesser General Public ++# License as published by the Free Software Foundation; either ++# version 2.1 of the License, or (at your option) any later version. ++# ++# This library is distributed in the hope that it will be useful, ++# but WITHOUT ANY WARRANTY; without even the implied warranty of ++# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ++# Lesser General Public License for more details. ++# ++# You should have received a copy of the GNU Lesser General Public ++# License along with this library; if not, write to the Free Software ++# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA ++# ++ ++__all__ = ( ++ "tabengine", ++) ++ ++import sys ++import os ++import string ++from gi import require_version ++require_version('IBus', '1.0') ++from gi.repository import IBus ++from gi.repository import GLib ++#import tabsqlitedb ++import re ++from gi.repository import GObject ++import time ++ ++debug_level = int(0) ++ ++from gettext import dgettext ++_ = lambda a : dgettext ("ibus-table", a) ++N_ = lambda a : a ++ ++ ++def ascii_ispunct(character): ++ ''' ++ Use our own function instead of ascii.ispunct() ++ from “from curses import ascii” because the behaviour ++ of the latter is kind of weird. In Python 3.3.2 it does ++ for example: ++ ++ >>> from curses import ascii ++ >>> ascii.ispunct('.') ++ True ++ >>> ascii.ispunct(u'.') ++ True ++ >>> ascii.ispunct('a') ++ False ++ >>> ascii.ispunct(u'a') ++ False ++ >>> ++ >>> ascii.ispunct(u'あ') ++ True ++ >>> ascii.ispunct('あ') ++ True ++ >>> ++ ++ あ isn’t punctuation. ascii.ispunct() only really works ++ in the ascii range, it returns weird results when used ++ over the whole unicode range. Maybe we should better use ++ unicodedata.category(), which works fine to figure out ++ what is punctuation for all of unicode. But at the moment ++ I am only porting from Python2 to Python3 and just want to ++ preserve the original behaviour for the moment. ++ ''' ++ if character in '''!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~''': ++ return True ++ else: ++ return False ++ ++def variant_to_value(variant): ++ if type(variant) != GLib.Variant: ++ return variant ++ type_string = variant.get_type_string() ++ if type_string == 's': ++ return variant.get_string() ++ elif type_string == 'i': ++ return variant.get_int32() ++ elif type_string == 'b': ++ return variant.get_boolean() ++ elif type_string == 'as': ++ # In the latest pygobject3 3.3.4 or later, g_variant_dup_strv ++ # returns the allocated strv but in the previous release, ++ # it returned the tuple of (strv, length) ++ if type(GLib.Variant.new_strv([]).dup_strv()) == tuple: ++ return variant.dup_strv()[0] ++ else: ++ return variant.dup_strv() ++ else: ++ print('error: unknown variant type: %s' %type_string) ++ return variant ++ ++def argb(a, r, g, b): ++ return (((a & 0xff)<<24) ++ + ((r & 0xff) << 16) ++ + ((g & 0xff) << 8) ++ + (b & 0xff)) ++ ++def rgb(r, g, b): ++ return argb(255, r, g, b) ++ ++__half_full_table = [ ++ (0x0020, 0x3000, 1), ++ (0x0021, 0xFF01, 0x5E), ++ (0x00A2, 0xFFE0, 2), ++ (0x00A5, 0xFFE5, 1), ++ (0x00A6, 0xFFE4, 1), ++ (0x00AC, 0xFFE2, 1), ++ (0x00AF, 0xFFE3, 1), ++ (0x20A9, 0xFFE6, 1), ++ (0xFF61, 0x3002, 1), ++ (0xFF62, 0x300C, 2), ++ (0xFF64, 0x3001, 1), ++ (0xFF65, 0x30FB, 1), ++ (0xFF66, 0x30F2, 1), ++ (0xFF67, 0x30A1, 1), ++ (0xFF68, 0x30A3, 1), ++ (0xFF69, 0x30A5, 1), ++ (0xFF6A, 0x30A7, 1), ++ (0xFF6B, 0x30A9, 1), ++ (0xFF6C, 0x30E3, 1), ++ (0xFF6D, 0x30E5, 1), ++ (0xFF6E, 0x30E7, 1), ++ (0xFF6F, 0x30C3, 1), ++ (0xFF70, 0x30FC, 1), ++ (0xFF71, 0x30A2, 1), ++ (0xFF72, 0x30A4, 1), ++ (0xFF73, 0x30A6, 1), ++ (0xFF74, 0x30A8, 1), ++ (0xFF75, 0x30AA, 2), ++ (0xFF77, 0x30AD, 1), ++ (0xFF78, 0x30AF, 1), ++ (0xFF79, 0x30B1, 1), ++ (0xFF7A, 0x30B3, 1), ++ (0xFF7B, 0x30B5, 1), ++ (0xFF7C, 0x30B7, 1), ++ (0xFF7D, 0x30B9, 1), ++ (0xFF7E, 0x30BB, 1), ++ (0xFF7F, 0x30BD, 1), ++ (0xFF80, 0x30BF, 1), ++ (0xFF81, 0x30C1, 1), ++ (0xFF82, 0x30C4, 1), ++ (0xFF83, 0x30C6, 1), ++ (0xFF84, 0x30C8, 1), ++ (0xFF85, 0x30CA, 6), ++ (0xFF8B, 0x30D2, 1), ++ (0xFF8C, 0x30D5, 1), ++ (0xFF8D, 0x30D8, 1), ++ (0xFF8E, 0x30DB, 1), ++ (0xFF8F, 0x30DE, 5), ++ (0xFF94, 0x30E4, 1), ++ (0xFF95, 0x30E6, 1), ++ (0xFF96, 0x30E8, 6), ++ (0xFF9C, 0x30EF, 1), ++ (0xFF9D, 0x30F3, 1), ++ (0xFFA0, 0x3164, 1), ++ (0xFFA1, 0x3131, 30), ++ (0xFFC2, 0x314F, 6), ++ (0xFFCA, 0x3155, 6), ++ (0xFFD2, 0x315B, 9), ++ (0xFFE9, 0x2190, 4), ++ (0xFFED, 0x25A0, 1), ++ (0xFFEE, 0x25CB, 1)] ++ ++def unichar_half_to_full(c): ++ code = ord(c) ++ for half, full, size in __half_full_table: ++ if code >= half and code < half + size: ++ if sys.version_info >= (3, 0, 0): ++ return chr(full + code - half) ++ else: ++ return unichr(full + code - half) ++ return c ++ ++def unichar_full_to_half(c): ++ code = ord(c) ++ for half, full, size in __half_full_table: ++ if code >= full and code < full + size: ++ if sys.version_info >= (3, 0, 0): ++ return chr(half + code - full) ++ else: ++ return unichr(half + code - full) ++ return c ++ ++SAVE_USER_COUNT_MAX = 16 ++SAVE_USER_TIMEOUT = 30 # in seconds ++ ++class KeyEvent: ++ def __init__(self, keyval, keycode, state): ++ self.val = keyval ++ self.code = keycode ++ self.state = state ++ def __str__(self): ++ return "%s 0x%08x" % (IBus.keyval_name(self.val), self.state) ++ ++ ++class editor(object): ++ '''Hold user inputs chars and preedit string''' ++ def __init__ (self, config, valid_input_chars, pinyin_valid_input_chars, ++ single_wildcard_char, multi_wildcard_char, ++ auto_wildcard, full_width_letter, full_width_punct, ++ max_key_length, database): ++ self.db = database ++ self._config = config ++ engine_name = os.path.basename(self.db.filename).replace('.db', '') ++ self._config_section = "engine/Table/%s" % engine_name.replace(' ','_') ++ self._max_key_length = int(max_key_length) ++ self._max_key_length_pinyin = 7 ++ self._valid_input_chars = valid_input_chars ++ self._pinyin_valid_input_chars = pinyin_valid_input_chars ++ self._single_wildcard_char = single_wildcard_char ++ self._multi_wildcard_char = multi_wildcard_char ++ self._auto_wildcard = auto_wildcard ++ self._full_width_letter = full_width_letter ++ self._full_width_punct = full_width_punct ++ # ++ # The values below will be reset in ++ # self.clear_input_not_committed_to_preedit() ++ self._chars_valid = u'' # valid user input in table mode ++ self._chars_invalid = u'' # invalid user input in table mode ++ self._chars_valid_update_candidates_last = u'' ++ self._chars_invalid_update_candidates_last = u'' ++ # self._candidates holds the “best” candidates matching the user input ++ # [(tabkeys, phrase, freq, user_freq), ...] ++ self._candidates = [] ++ self._candidates_previous = [] ++ ++ # self._u_chars: holds the user input of the phrases which ++ # have been automatically committed to preedit (but not yet ++ # “really” committed). ++ self._u_chars = [] ++ # self._strings: holds the phrases which have been ++ # automatically committed to preedit (but not yet “really” ++ # committed). ++ # ++ # self._u_chars and self._strings should always have the same ++ # length, if I understand it correctly. ++ # ++ # Example when using the wubi-jidian86 table: ++ # ++ # self._u_chars = ['gaaa', 'gggg', 'ihty'] ++ # self._strings = ['形式', '王', '小'] ++ # ++ # I.e. after typing 'gaaa', '形式' is in the preedit and ++ # both self._u_chars and self._strings are empty. When typing ++ # another 'g', the maximum key length of the wubi table (which is 4) ++ # is exceeded and '形式' is automatically committed to the preedit ++ # (but not yet “really” committed, i.e. not yet committed into ++ # the application). The key 'gaaa' and the matching phrase '形式' ++ # are stored in self._u_chars and self._strings respectively ++ # and 'gaaa' is removed from self._chars_valid. Now self._chars_valid ++ # contains only the 'g' which starts a new search for candidates ... ++ # When removing the 'g' with backspace, the 'gaaa' is moved ++ # back from self._u_chars into self._chars_valid again and ++ # the same candidate list is shown as before the last 'g' had ++ # been entered. ++ self._strings = [] ++ # self._cursor_precommit: The cursor ++ # position inthe array of strings which have already been ++ # committed to preëdit but not yet “really” committed. ++ self._cursor_precommit = 0 ++ ++ self._prompt_characters = eval( ++ self.db.ime_properties.get('char_prompts')) ++ ++ select_keys_csv = variant_to_value(self._config.get_value( ++ self._config_section, ++ "LookupTableSelectKeys")) ++ if select_keys_csv == None: ++ select_keys_csv = self.db.get_select_keys() ++ if select_keys_csv == None: ++ select_keys_csv = '1,2,3,4,5,6,7,8,9' ++ self._select_keys = [ ++ IBus.keyval_from_name(y) ++ for y in [x.strip() for x in select_keys_csv.split(",")]] ++ self._page_size = variant_to_value(self._config.get_value( ++ self._config_section, ++ "lookuptablepagesize")) ++ if self._page_size == None or self._page_size > len(self._select_keys): ++ self._page_size = len(self._select_keys) ++ self._orientation = variant_to_value(self._config.get_value( ++ self._config_section, ++ "LookupTableOrientation")) ++ if self._orientation == None: ++ self._orientation = self.db.get_orientation() ++ self._lookup_table = self.get_new_lookup_table( ++ page_size = self._page_size, ++ select_keys = self._select_keys, ++ orientation = self._orientation) ++ # self._py_mode: whether in pinyin mode ++ self._py_mode = False ++ # self._onechar: whether we only select single character ++ self._onechar = variant_to_value(self._config.get_value( ++ self._config_section, ++ "OneChar")) ++ if self._onechar == None: ++ self._onechar = False ++ # self._chinese_mode: the candidate filter mode, ++ # 0 means to show simplified Chinese only ++ # 1 means to show traditional Chinese only ++ # 2 means to show all characters but show simplified Chinese first ++ # 3 means to show all characters but show traditional Chinese first ++ # 4 means to show all characters ++ # we use LC_CTYPE or LANG to determine which one to use if ++ # no default comes from the config. ++ self._chinese_mode = variant_to_value(self._config.get_value( ++ self._config_section, ++ "ChineseMode")) ++ if self._chinese_mode == None: ++ self._chinese_mode = self.get_chinese_mode() ++ elif debug_level > 1: ++ sys.stderr.write( ++ "Chinese mode found in user config, mode=%s\n" ++ % self._chinese_mode) ++ ++ # If auto select is true, then the first candidate phrase will ++ # be selected automatically during typing. Auto select is true ++ # by default for the stroke5 table for example. ++ self._auto_select = variant_to_value(self._config.get_value( ++ self._config_section, ++ "AutoSelect")) ++ if self._auto_select == None: ++ if self.db.ime_properties.get('auto_select') != None: ++ self._auto_select = self.db.ime_properties.get( ++ 'auto_select').lower() == u'true' ++ else: ++ self._auto_select = False ++ ++ def get_new_lookup_table( ++ self, page_size=10, ++ select_keys=[49, 50, 51, 52, 53, 54, 55, 56, 57, 48], ++ orientation=True): ++ ''' ++ [49, 50, 51, 52, 53, 54, 55, 56, 57, 48] are the key codes ++ for the characters ['1', '2', '3', '4', '5', '6', '7', '8', '0'] ++ ''' ++ if page_size < 1: ++ page_size = 1 ++ if page_size > len(select_keys): ++ page_size = len(select_keys) ++ lookup_table = IBus.LookupTable.new( ++ page_size=page_size, ++ cursor_pos=0, ++ cursor_visible=True, ++ round=True) ++ for keycode in select_keys: ++ lookup_table.append_label( ++ IBus.Text.new_from_string("%s." %IBus.keyval_name(keycode))) ++ lookup_table.set_orientation(orientation) ++ return lookup_table ++ ++ def get_select_keys(self): ++ """ ++ Returns the list of key codes for the select keys. ++ For example, if the select keys are ["1", "2", ...] the ++ key codes are [49, 50, ...]. If the select keys are ++ ["F1", "F2", ...] the key codes are [65470, 65471, ...] ++ """ ++ return self._select_keys ++ ++ def get_chinese_mode (self): ++ ''' ++ Use db value or LC_CTYPE in your box to determine the _chinese_mode ++ ''' ++ # use db value, if applicable ++ __db_chinese_mode = self.db.get_chinese_mode() ++ if __db_chinese_mode >= 0: ++ if debug_level > 1: ++ sys.stderr.write( ++ "get_chinese_mode(): " ++ + "default Chinese mode found in database, mode=%s\n" ++ %__db_chinese_mode) ++ return __db_chinese_mode ++ # otherwise ++ try: ++ if 'LC_ALL' in os.environ: ++ __lc = os.environ['LC_ALL'].split('.')[0].lower() ++ if debug_level > 1: ++ sys.stderr.write( ++ "get_chinese_mode(): __lc=%s found in LC_ALL\n" ++ % __lc) ++ elif 'LC_CTYPE' in os.environ: ++ __lc = os.environ['LC_CTYPE'].split('.')[0].lower() ++ if debug_level > 1: ++ sys.stderr.write( ++ "get_chinese_mode(): __lc=%s found in LC_CTYPE\n" ++ % __lc) ++ else: ++ __lc = os.environ['LANG'].split('.')[0].lower() ++ if debug_level > 1: ++ sys.stderr.write( ++ "get_chinese_mode(): __lc=%s found in LANG\n" ++ % __lc) ++ ++ if '_cn' in __lc or '_sg' in __lc: ++ # CN and SG should prefer traditional Chinese by default ++ return 2 # show simplified Chinese first ++ elif '_hk' in __lc or '_tw' in __lc or '_mo' in __lc: ++ # HK, TW, and MO should prefer traditional Chinese by default ++ return 3 # show traditional Chinese first ++ else: ++ if self.db._is_chinese: ++ # This table is used for Chinese, but we don’t ++ # know for which variant. Therefore, better show ++ # all Chinese characters and don’t prefer any ++ # variant: ++ if debug_level > 1: ++ sys.stderr.write( ++ "get_chinese_mode(): last fallback, " ++ + "database is Chinese but we don’t know " ++ + "which variant.\n") ++ return 4 # show all Chinese characters ++ else: ++ if debug_level > 1: ++ sys.stderr.write( ++ "get_chinese_mode(): last fallback, " ++ + "database is not Chinese, returning -1.\n") ++ return -1 ++ except: ++ import traceback ++ traceback.print_exc() ++ return -1 ++ ++ def clear_all_input_and_preedit(self): ++ ''' ++ Clear all input, whether committed to preëdit or not. ++ ''' ++ if debug_level > 1: ++ sys.stderr.write("clear_all_input_and_preedit()\n") ++ self.clear_input_not_committed_to_preedit() ++ self._u_chars = [] ++ self._strings = [] ++ self._cursor_precommit = 0 ++ self.update_candidates() ++ ++ def is_empty(self): ++ return u'' == self._chars_valid + self._chars_invalid ++ ++ def clear_input_not_committed_to_preedit(self): ++ ''' ++ Clear the input which has not yet been committed to preëdit. ++ ''' ++ if debug_level > 1: ++ sys.stderr.write("clear_input_not_committed_to_preedit()\n") ++ self._chars_valid = u'' ++ self._chars_invalid = u'' ++ self._chars_valid_update_candidates_last = u'' ++ self._chars_invalid_update_candidates_last = u'' ++ self._lookup_table.clear() ++ self._lookup_table.set_cursor_visible(True) ++ self._candidates = [] ++ self._candidates_previous = [] ++ ++ def add_input(self, c): ++ ''' ++ Add input character and update candidates. ++ ++ Returns “True” if candidates were found, “False” if not. ++ ''' ++ if (self._chars_invalid ++ or (not self._py_mode ++ and (c not in ++ self._valid_input_chars ++ + self._single_wildcard_char ++ + self._multi_wildcard_char)) ++ or (self._py_mode ++ and (c not in ++ self._pinyin_valid_input_chars ++ + self._single_wildcard_char ++ + self._multi_wildcard_char))): ++ self._chars_invalid += c ++ else: ++ self._chars_valid += c ++ res = self.update_candidates() ++ return res ++ ++ def pop_input(self): ++ '''remove and display last input char held''' ++ _c = '' ++ if self._chars_invalid: ++ _c = self._chars_invalid[-1] ++ self._chars_invalid = self._chars_invalid[:-1] ++ elif self._chars_valid: ++ _c = self._chars_valid[-1] ++ self._chars_valid = self._chars_valid[:-1] ++ if (not self._chars_valid) and self._u_chars: ++ self._chars_valid = self._u_chars.pop( ++ self._cursor_precommit - 1) ++ self._strings.pop(self._cursor_precommit - 1) ++ self._cursor_precommit -= 1 ++ self.update_candidates () ++ return _c ++ ++ def get_input_chars (self): ++ '''get characters held, valid and invalid''' ++ return self._chars_valid + self._chars_invalid ++ ++ def split_strings_committed_to_preedit(self, index, index_in_phrase): ++ head = self._strings[index][:index_in_phrase] ++ tail = self._strings[index][index_in_phrase:] ++ self._u_chars.pop(index) ++ self._strings.pop(index) ++ self._u_chars.insert(index, self.db.parse_phrase(head)) ++ self._strings.insert(index, head) ++ self._u_chars.insert(index+1, self.db.parse_phrase(tail)) ++ self._strings.insert(index+1, tail) ++ ++ def remove_preedit_before_cursor(self): ++ '''Remove preëdit left of cursor''' ++ if self._chars_invalid: ++ return ++ if self.get_input_chars(): ++ self.commit_to_preedit() ++ if not self._strings: ++ return ++ if self._cursor_precommit <= 0: ++ return ++ self._u_chars = self._u_chars[self._cursor_precommit:] ++ self._strings = self._strings[self._cursor_precommit:] ++ self._cursor_precommit = 0 ++ ++ def remove_preedit_after_cursor(self): ++ '''Remove preëdit right of cursor''' ++ if self._chars_invalid: ++ return ++ if self.get_input_chars(): ++ self.commit_to_preedit() ++ if not self._strings: ++ return ++ if self._cursor_precommit >= len(self._strings): ++ return ++ self._u_chars = self._u_chars[:self._cursor_precommit] ++ self._strings = self._strings[:self._cursor_precommit] ++ self._cursor_precommit = len(self._strings) ++ ++ def remove_preedit_character_before_cursor(self): ++ '''Remove character before cursor in strings comitted to preëdit''' ++ if self._chars_invalid: ++ return ++ if self.get_input_chars(): ++ self.commit_to_preedit() ++ if not self._strings: ++ return ++ if self._cursor_precommit < 1: ++ return ++ self._cursor_precommit -= 1 ++ self._chars_valid = self._u_chars.pop(self._cursor_precommit) ++ self._strings.pop(self._cursor_precommit) ++ self.update_candidates() ++ ++ def remove_preedit_character_after_cursor (self): ++ '''Remove character after cursor in strings committed to preëdit''' ++ if self._chars_invalid: ++ return ++ if self.get_input_chars(): ++ self.commit_to_preedit() ++ if not self._strings: ++ return ++ if self._cursor_precommit > len(self._strings) - 1: ++ return ++ self._u_chars.pop(self._cursor_precommit) ++ self._strings.pop(self._cursor_precommit) ++ ++ def get_preedit_tabkeys_parts(self): ++ '''Returns the tabkeys which were used to type the parts ++ of the preëdit string. ++ ++ Such as “(left_of_current_edit, current_edit, right_of_current_edit)” ++ ++ “left_of_current_edit” and “right_of_current_edit” are ++ strings of tabkeys which have been typed to get the phrases ++ which have already been committed to preëdit, but not ++ “really” committed yet. “current_edit” is the string of ++ tabkeys of the part of the preëdit string which is not ++ committed at all. ++ ++ For example, the return value could look like: ++ ++ (('gggg', 'aahw'), 'adwu', ('ijgl', 'jbus')) ++ ++ See also get_preedit_string_parts() which might return something ++ like ++ ++ (('王', '工具'), '其', ('漫画', '最新')) ++ ++ when the wubi-jidian86 table is used. ++ ''' ++ left_of_current_edit = () ++ current_edit = u'' ++ right_of_current_edit = () ++ if self.get_input_chars(): ++ current_edit = self.get_input_chars() ++ if self._u_chars: ++ left_of_current_edit = tuple( ++ self._u_chars[:self._cursor_precommit]) ++ right_of_current_edit = tuple( ++ self._u_chars[self._cursor_precommit:]) ++ return (left_of_current_edit, current_edit, right_of_current_edit) ++ ++ def get_preedit_tabkeys_complete(self): ++ '''Returns the tabkeys which belong to the parts of the preëdit ++ string as a single string ++ ''' ++ (left_tabkeys, ++ current_tabkeys, ++ right_tabkeys) = self.get_preedit_tabkeys_parts() ++ return (u''.join(left_tabkeys) ++ + current_tabkeys ++ + u''.join(right_tabkeys)) ++ ++ def get_preedit_string_parts(self): ++ '''Returns the phrases which are parts of the preëdit string. ++ ++ Such as “(left_of_current_edit, current_edit, right_of_current_edit)” ++ ++ “left_of_current_edit” and “right_of_current_edit” are ++ tuples of strings which have already been committed to preëdit, but not ++ “really” committed yet. “current_edit” is the phrase in the part of the ++ preëdit string which is not yet committed at all. ++ ++ For example, the return value could look like: ++ ++ (('王', '工具'), '其', ('漫画', '最新')) ++ ++ See also get_preedit_tabkeys_parts() which might return something ++ like ++ ++ (('gggg', 'aahw'), 'adwu', ('ijgl', 'jbus')) ++ ++ when the wubi-jidian86 table is used. ++ ''' ++ left_of_current_edit = () ++ current_edit = u'' ++ right_of_current_edit = () ++ if self._candidates: ++ current_edit = self._candidates[ ++ int(self._lookup_table.get_cursor_pos())][1] ++ elif self.get_input_chars(): ++ current_edit = self.get_input_chars() ++ if self._strings: ++ left_of_current_edit = tuple( ++ self._strings[:self._cursor_precommit]) ++ right_of_current_edit = tuple( ++ self._strings[self._cursor_precommit:]) ++ return (left_of_current_edit, current_edit, right_of_current_edit) ++ ++ def get_preedit_string_complete(self): ++ '''Returns the phrases which are parts of the preëdit string as a ++ single string ++ ++ ''' ++ (left_strings, ++ current_string, ++ right_strings) = self.get_preedit_string_parts() ++ return u''.join(left_strings) + current_string + u''.join(right_strings) ++ ++ def get_caret (self): ++ '''Get caret position in preëdit string''' ++ caret = 0 ++ if self._cursor_precommit and self._strings: ++ for x in self._strings[:self._cursor_precommit]: ++ caret += len(x) ++ if self._candidates: ++ caret += len( ++ self._candidates[int(self._lookup_table.get_cursor_pos())][1]) ++ else: ++ caret += len(self.get_input_chars()) ++ return caret ++ ++ def arrow_left(self): ++ '''Move cursor left in the preëdit string.''' ++ if self._chars_invalid: ++ return ++ if self.get_input_chars(): ++ self.commit_to_preedit() ++ if not self._strings: ++ return ++ if self._cursor_precommit <= 0: ++ return ++ if len(self._strings[self._cursor_precommit-1]) <= 1: ++ self._cursor_precommit -= 1 ++ else: ++ self.split_strings_committed_to_preedit( ++ self._cursor_precommit-1, -1) ++ self.update_candidates() ++ ++ def arrow_right(self): ++ '''Move cursor right in the preëdit string.''' ++ if self._chars_invalid: ++ return ++ if self.get_input_chars(): ++ self.commit_to_preedit() ++ if not self._strings: ++ return ++ if self._cursor_precommit >= len(self._strings): ++ return ++ self._cursor_precommit += 1 ++ if len(self._strings[self._cursor_precommit-1]) > 1: ++ self.split_strings_committed_to_preedit(self._cursor_precommit-1, 1) ++ self.update_candidates() ++ ++ def control_arrow_left(self): ++ '''Move cursor to the beginning of the preëdit string.''' ++ if self._chars_invalid: ++ return ++ if self.get_input_chars(): ++ self.commit_to_preedit() ++ if not self._strings: ++ return ++ self._cursor_precommit = 0 ++ self.update_candidates () ++ ++ def control_arrow_right(self): ++ '''Move cursor to the end of the preëdit string''' ++ if self._chars_invalid: ++ return ++ if self.get_input_chars(): ++ self.commit_to_preedit() ++ if not self._strings: ++ return ++ self._cursor_precommit = len(self._strings) ++ self.update_candidates () ++ ++ def append_candidate_to_lookup_table( ++ self, tabkeys=u'', phrase=u'', freq=0, user_freq=0): ++ '''append candidate to lookup_table''' ++ if debug_level > 1: ++ sys.stderr.write( ++ "append_candidate() " ++ + "tabkeys=%(t)s phrase=%(p)s freq=%(f)s user_freq=%(u)s\n" ++ % {'t': tabkeys, 'p': phrase, 'f': freq, 'u': user_freq}) ++ if not tabkeys or not phrase: ++ return ++ regexp = self._chars_valid ++ if self._multi_wildcard_char: ++ regexp = regexp.replace( ++ self._multi_wildcard_char, '_multi_wildchard_char_') ++ if self._single_wildcard_char: ++ regexp = regexp.replace( ++ self._single_wildcard_char, '_single_wildchard_char_') ++ regexp = re.escape(regexp) ++ regexp = regexp.replace('_multi_wildchard_char_', '.*') ++ regexp = regexp.replace('_single_wildchard_char_', '.?') ++ match = re.match(r'^'+regexp, tabkeys) ++ if match: ++ remaining_tabkeys = tabkeys[match.end():] ++ else: ++ # This should never happen! For the candidates ++ # added to the lookup table here, a match has ++ # been found for self._chars_valid in the database. ++ # In that case, the above regular expression should ++ # match as well. ++ remaining_tabkeys = tabkeys ++ if debug_level > 1: ++ sys.stderr.write( ++ "append_candidate() " ++ + "remaining_tabkeys=%(remaining_tabkeys)s " ++ + "self._chars_valid=%(chars_valid)s phrase=%(phrase)s\n" ++ % {'remaining_tabkeys': remaining_tabkeys, ++ 'chars_valid': self._chars_valid, ++ 'phrase': phrase}) ++ table_code = u'' ++ if self.db._is_chinese and self._py_mode: ++ # restore tune symbol ++ remaining_tabkeys = remaining_tabkeys.replace( ++ '!','↑1').replace( ++ '@','↑2').replace( ++ '#','↑3').replace( ++ '$','↑4').replace( ++ '%','↑5') ++ # If in pinyin mode, phrase can only be one character. ++ # When using pinyin mode for a table like Wubi or Cangjie, ++ # the reason is probably because one does not know the ++ # Wubi or Cangjie code. So get that code from the table ++ # and display it as well to help the user learn that code. ++ # The Wubi tables contain several codes for the same ++ # character, therefore self.db.find_zi_code(phrase) may ++ # return a list. The last code in that list is the full ++ # table code for that characters, other entries in that ++ # list are shorter substrings of the full table code which ++ # are not interesting to display. Therefore, we use only ++ # the last element of the list of table codes. ++ possible_table_codes = self.db.find_zi_code(phrase) ++ if possible_table_codes: ++ table_code = possible_table_codes[-1] ++ table_code_new = u'' ++ for char in table_code: ++ if char in self._prompt_characters: ++ table_code_new += self._prompt_characters[char] ++ else: ++ table_code_new += char ++ table_code = table_code_new ++ if not self._py_mode: ++ remaining_tabkeys_new = u'' ++ for char in remaining_tabkeys: ++ if char in self._prompt_characters: ++ remaining_tabkeys_new += self._prompt_characters[char] ++ else: ++ remaining_tabkeys_new += char ++ remaining_tabkeys = remaining_tabkeys_new ++ candidate_text = phrase + u' ' + remaining_tabkeys ++ if table_code: ++ candidate_text = candidate_text + u' ' + table_code ++ attrs = IBus.AttrList () ++ attrs.append(IBus.attr_foreground_new( ++ rgb(0x19,0x73,0xa2), 0, len(candidate_text))) ++ if not self._py_mode and freq < 0: ++ # this is a user defined phrase: ++ attrs.append( ++ IBus.attr_foreground_new(rgb(0x77,0x00,0xc3), 0, len(phrase))) ++ elif not self._py_mode and user_freq > 0: ++ # this is a system phrase which has already been used by the user: ++ attrs.append(IBus.attr_foreground_new( ++ rgb(0x00,0x00,0x00), 0, len(phrase))) ++ else: ++ # this is a system phrase that has not been used yet: ++ attrs.append(IBus.attr_foreground_new( ++ rgb(0x00,0x00,0x00), 0, len(phrase))) ++ if debug_level > 0: ++ debug_text = u' ' + str(freq) + u' ' + str(user_freq) ++ candidate_text += debug_text ++ attrs.append(IBus.attr_foreground_new( ++ rgb(0x00,0xff,0x00), ++ len(candidate_text) - len(debug_text), ++ len(candidate_text))) ++ text = IBus.Text.new_from_string(candidate_text) ++ i = 0 ++ while attrs.get(i) != None: ++ attr = attrs.get(i) ++ text.append_attribute(attr.get_attr_type(), ++ attr.get_value(), ++ attr.get_start_index(), ++ attr.get_end_index()) ++ i += 1 ++ self._lookup_table.append_candidate (text) ++ self._lookup_table.set_cursor_visible(True) ++ ++ def update_candidates (self): ++ ''' ++ Searches for candidates and updates the lookuptable. ++ ++ Returns “True” if candidates were found and “False” if not. ++ ''' ++ if debug_level > 1: ++ sys.stderr.write( ++ "update_candidates() " ++ + "self._chars_valid=%(chars_valid)s " ++ + "self._chars_invalid=%(chars_invalid)s " ++ + "self._chars_valid_update_candidates_last=%(chars_last)s " ++ + "self._candidates=%(candidates)s " ++ + "self.db.startchars=%(start)s " ++ + "self._strings=%(strings)s\n" ++ % {'chars_valid': self._chars_valid, ++ 'chars_invalid': self._chars_invalid, ++ 'chars_last': self._chars_valid_update_candidates_last, ++ 'candidates': self._candidates, ++ 'start': self.db.startchars, ++ 'strings': self._strings}) ++ if (self._chars_valid == self._chars_valid_update_candidates_last ++ and ++ self._chars_invalid == self._chars_invalid_update_candidates_last): ++ # The input did not change since we came here last, do ++ # nothing and leave candidates and lookup table unchanged: ++ if self._candidates: ++ return True ++ else: ++ return False ++ self._chars_valid_update_candidates_last = self._chars_valid ++ self._chars_invalid_update_candidates_last = self._chars_invalid ++ self._lookup_table.clear() ++ self._lookup_table.set_cursor_visible(True) ++ if self._chars_invalid or not self._chars_valid: ++ self._candidates = [] ++ self._candidates_previous = self._candidates ++ return False ++ if self._py_mode and self.db._is_chinese: ++ self._candidates = self.db.select_chinese_characters_by_pinyin( ++ tabkeys=self._chars_valid, ++ chinese_mode=self._chinese_mode, ++ single_wildcard_char=self._single_wildcard_char, ++ multi_wildcard_char=self._multi_wildcard_char) ++ else: ++ self._candidates = self.db.select_words( ++ tabkeys=self._chars_valid, ++ onechar=self._onechar, ++ chinese_mode=self._chinese_mode, ++ single_wildcard_char=self._single_wildcard_char, ++ multi_wildcard_char=self._multi_wildcard_char, ++ auto_wildcard=self._auto_wildcard) ++ # If only a wildcard character has been typed, insert a ++ # special candidate at the first position for the wildcard ++ # character itself. For example, if “?” is used as a ++ # wildcard character and this is the only character typed, add ++ # a candidate ('?', '?', 0, 1000000000) in halfwidth mode or a ++ # candidate ('?', '?', 0, 1000000000) in fullwidth mode. ++ # This is needed to make it possible to input the wildcard ++ # characters themselves, if “?” acted only as a wildcard ++ # it would be impossible to input a fullwidth question mark. ++ if (self._chars_valid ++ in [self._single_wildcard_char, self._multi_wildcard_char]): ++ wildcard_key = self._chars_valid ++ wildcard_phrase = self._chars_valid ++ if ascii_ispunct(wildcard_key): ++ if self._full_width_punct[1]: ++ wildcard_phrase = unichar_half_to_full(wildcard_phrase) ++ else: ++ wildcard_phrase = unichar_full_to_half(wildcard_phrase) ++ else: ++ if self._full_width_letter[1]: ++ wildcard_phrase = unichar_half_to_full(wildcard_phrase) ++ else: ++ wildcard_phrase = unichar_full_to_half(wildcard_phrase) ++ self._candidates.insert( ++ 0, (wildcard_key, wildcard_phrase, 0, 1000000000)) ++ if self._candidates: ++ self.fill_lookup_table() ++ self._candidates_previous = self._candidates ++ return True ++ # There are only valid and no invalid input characters but no ++ # matching candidates could be found from the databases. The ++ # last of self._chars_valid must have caused this. That ++ # character is valid in the sense that it is listed in ++ # self._valid_input_chars, it is only invalid in the sense ++ # that after adding this character, no candidates could be ++ # found anymore. Add this character to self._chars_invalid ++ # and remove it from self._chars_valid. ++ self._chars_invalid += self._chars_valid[-1] ++ self._chars_valid = self._chars_valid[:-1] ++ self._chars_valid_update_candidates_last = self._chars_valid ++ self._chars_invalid_update_candidates_last = self._chars_invalid ++ return False ++ ++ def commit_to_preedit(self): ++ '''Add selected phrase in lookup table to preëdit string''' ++ if not self._chars_valid: ++ return False ++ if self._candidates: ++ self._u_chars.insert(self._cursor_precommit, ++ self._candidates[self.get_cursor_pos()][0]) ++ self._strings.insert(self._cursor_precommit, ++ self._candidates[self.get_cursor_pos()][1]) ++ self._cursor_precommit += 1 ++ self.clear_input_not_committed_to_preedit() ++ self.update_candidates() ++ return True ++ ++ def commit_to_preedit_current_page(self, index): ++ ''' ++ Commits the candidate at position “index” in the current ++ page of the lookup table to the preëdit. Does not yet “really” ++ commit the candidate, only to the preëdit. ++ ''' ++ cursor_pos = self._lookup_table.get_cursor_pos() ++ cursor_in_page = self._lookup_table.get_cursor_in_page() ++ current_page_start = cursor_pos - cursor_in_page ++ real_index = current_page_start + index ++ if real_index >= len(self._candidates): ++ # the index given is out of range we do not commit anything ++ return False ++ self._lookup_table.set_cursor_pos(real_index) ++ return self.commit_to_preedit() ++ ++ def get_aux_strings (self): ++ '''Get aux strings''' ++ input_chars = self.get_input_chars () ++ if input_chars: ++ aux_string = input_chars ++ if debug_level > 0 and self._u_chars: ++ (tabkeys_left, ++ tabkeys_current, ++ tabkeys_right) = self.get_preedit_tabkeys_parts() ++ (strings_left, ++ string_current, ++ strings_right) = self.get_preedit_string_parts() ++ aux_string = u'' ++ for i in range(0, len(strings_left)): ++ aux_string += ( ++ u'(' ++ + tabkeys_left[i] + u' '+ strings_left[i] ++ + u') ') ++ aux_string += input_chars ++ for i in range(0, len(strings_right)): ++ aux_string += ( ++ u' (' ++ + tabkeys_right[i]+u' '+strings_right[i] ++ + u')') ++ if self._py_mode: ++ aux_string = aux_string.replace( ++ '!','1').replace( ++ '@','2').replace( ++ '#','3').replace( ++ '$','4').replace( ++ '%','5') ++ else: ++ aux_string_new = u'' ++ for char in aux_string: ++ if char in self._prompt_characters: ++ aux_string_new += self._prompt_characters[char] ++ else: ++ aux_string_new += char ++ aux_string = aux_string_new ++ return aux_string ++ ++ # There are no input strings at the moment. But there could ++ # be stuff committed to the preëdit. If there is something ++ # committed to the preëdit, show some information in the ++ # auxiliary text. ++ # ++ # For the character at the position of the cursor in the ++ # preëdit, show a list of possible input key sequences which ++ # could be used to type that character at the left side of the ++ # auxiliary text. ++ # ++ # If the preëdit is longer than one character, show the input ++ # key sequence which will be defined for the complete current ++ # contents of the preëdit, if the preëdit is committed. ++ aux_string = u'' ++ if self._strings: ++ if self._cursor_precommit >= len(self._strings): ++ char = self._strings[-1][0] ++ else: ++ char = self._strings[self._cursor_precommit][0] ++ aux_string = u' '.join(self.db.find_zi_code(char)) ++ cstr = u''.join(self._strings) ++ if self.db.user_can_define_phrase: ++ if len(cstr) > 1: ++ aux_string += (u'\t#: ' + self.db.parse_phrase(cstr)) ++ aux_string_new = u'' ++ for char in aux_string: ++ if char in self._prompt_characters: ++ aux_string_new += self._prompt_characters[char] ++ else: ++ aux_string_new += char ++ return aux_string_new ++ ++ def fill_lookup_table(self): ++ '''Fill more entries to self._lookup_table if needed. ++ ++ If the cursor in _lookup_table moved beyond current length, ++ add more entries from _candidiate[0] to _lookup_table.''' ++ ++ looklen = self._lookup_table.get_number_of_candidates() ++ psize = self._lookup_table.get_page_size() ++ if (self._lookup_table.get_cursor_pos() + psize >= looklen and ++ looklen < len(self._candidates)): ++ endpos = looklen + psize ++ batch = self._candidates[looklen:endpos] ++ for x in batch: ++ self.append_candidate_to_lookup_table( ++ tabkeys=x[0], phrase=x[1], freq=x[2], user_freq=x[3]) ++ ++ def cursor_down(self): ++ '''Process Arrow Down Key Event ++ Move Lookup Table cursor down''' ++ self.fill_lookup_table() ++ ++ res = self._lookup_table.cursor_down() ++ self.update_candidates () ++ if not res and self._candidates: ++ return True ++ return res ++ ++ def cursor_up(self): ++ '''Process Arrow Up Key Event ++ Move Lookup Table cursor up''' ++ res = self._lookup_table.cursor_up() ++ self.update_candidates () ++ if not res and self._candidates: ++ return True ++ return res ++ ++ def page_down(self): ++ '''Process Page Down Key Event ++ Move Lookup Table page down''' ++ self.fill_lookup_table() ++ res = self._lookup_table.page_down() ++ self.update_candidates () ++ if not res and self._candidates: ++ return True ++ return res ++ ++ def page_up(self): ++ '''Process Page Up Key Event ++ move Lookup Table page up''' ++ res = self._lookup_table.page_up() ++ self.update_candidates () ++ if not res and self._candidates: ++ return True ++ return res ++ ++ def select_key(self, keycode): ++ ''' ++ Commit a candidate which was selected by typing a selection key ++ from the lookup table to the preedit. Does not yet “really” ++ commit the candidate, only to the preedit. ++ ''' ++ if keycode not in self._select_keys: ++ return False ++ return self.commit_to_preedit_current_page( ++ self._select_keys.index(keycode)) ++ ++ def remove_candidate_from_user_database(self, keycode): ++ '''Remove a candidate displayed in the lookup table from the user ++ database. ++ ++ The candidate indicated by the selection key with the key code ++ “keycode” is removed, if possible. If it is not in the user ++ database at all, nothing happens. ++ ++ If this is a candidate which is also in the system database, ++ removing it from the user database only means that its user ++ frequency data is reset. It might still appear in subsequent ++ matches but with much lower priority. ++ ++ If this is a candidate which is user defined and not in the system ++ database, it will not match at all anymore after removing it. ++ ++ ''' ++ if keycode not in self._select_keys: ++ return False ++ index = self._select_keys.index(keycode) ++ cursor_pos = self._lookup_table.get_cursor_pos() ++ cursor_in_page = self._lookup_table.get_cursor_in_page() ++ current_page_start = cursor_pos - cursor_in_page ++ real_index = current_page_start + index ++ if len(self._candidates) > real_index: # this index is valid ++ candidate = self._candidates[real_index] ++ self.db.remove_phrase( ++ tabkeys=candidate[0], phrase=candidate[1], commit=True) ++ # call update_candidates() to get a new SQL query. The ++ # input has not really changed, therefore we must clear ++ # the remembered list of characters to ++ # force update_candidates() to really do something and not ++ # return immediately: ++ self._chars_valid_update_candidates_last = u'' ++ self._chars_invalid_update_candidates_last = u'' ++ self.update_candidates() ++ return True ++ else: ++ return False ++ ++ def get_cursor_pos (self): ++ '''get lookup table cursor position''' ++ return self._lookup_table.get_cursor_pos() ++ ++ def get_lookup_table (self): ++ '''Get lookup table''' ++ return self._lookup_table ++ ++ def remove_char(self): ++ '''Process remove_char Key Event''' ++ if debug_level > 1: ++ sys.stderr.write("remove_char()\n") ++ if self.get_input_chars(): ++ self.pop_input () ++ return ++ self.remove_preedit_character_before_cursor() ++ ++ def delete(self): ++ '''Process delete Key Event''' ++ if self.get_input_chars(): ++ return ++ self.remove_preedit_character_after_cursor() ++ ++ def cycle_next_cand(self): ++ '''Cycle cursor to next candidate in the page.''' ++ total = len(self._candidates) ++ ++ if total > 0: ++ page_size = self._lookup_table.get_page_size() ++ pos = self._lookup_table.get_cursor_pos() ++ page = int(pos/page_size) ++ pos += 1 ++ if pos >= (page+1)*page_size or pos >= total: ++ pos = page*page_size ++ res = self._lookup_table.set_cursor_pos(pos) ++ return True ++ else: ++ return False ++ ++ def one_candidate (self): ++ '''Return true if there is only one candidate''' ++ return len(self._candidates) == 1 ++ ++ ++######################## ++### Engine Class ##### ++#################### ++class tabengine (IBus.Engine): ++ '''The IM Engine for Tables''' ++ ++ def __init__(self, bus, obj_path, db ): ++ super(tabengine, self).__init__(connection=bus.get_connection(), ++ object_path=obj_path) ++ global debug_level ++ try: ++ debug_level = int(os.getenv('IBUS_TABLE_DEBUG_LEVEL')) ++ except (TypeError, ValueError): ++ debug_level = int(0) ++ self._input_purpose = 0 ++ self._has_input_purpose = False ++ if hasattr(IBus, 'InputPurpose'): ++ self._has_input_purpose = True ++ self._bus = bus ++ # this is the backend sql db we need for our IME ++ # we receive this db from IMEngineFactory ++ #self.db = tabsqlitedb.tabsqlitedb( name = dbname ) ++ self.db = db ++ self._setup_pid = 0 ++ self._icon_dir = '%s%s%s%s' % (os.getenv('IBUS_TABLE_LOCATION'), ++ os.path.sep, 'icons', os.path.sep) ++ # name for config section ++ self._engine_name = os.path.basename( ++ self.db.filename).replace('.db', '') ++ self._config_section = ( ++ "engine/Table/%s" % self._engine_name.replace(' ','_')) ++ ++ # config module ++ self._config = self._bus.get_config () ++ self._config.connect ("value-changed", self.config_value_changed_cb) ++ ++ # self._ime_py: Indicates whether this table supports pinyin mode ++ self._ime_py = self.db.ime_properties.get('pinyin_mode') ++ if self._ime_py: ++ if self._ime_py.lower() == u'true': ++ self._ime_py = True ++ else: ++ self._ime_py = False ++ else: ++ print('We could not find "pinyin_mode" entry in database, ' ++ + 'is it an outdated database?') ++ self._ime_py = False ++ ++ self._symbol = self.db.ime_properties.get('symbol') ++ if self._symbol == None or self._symbol == u'': ++ self._symbol = self.db.ime_properties.get('status_prompt') ++ if self._symbol == None: ++ self._symbol = u'' ++ # some Chinese tables have “STATUS_PROMPT = CN” replace it ++ # with the shorter and nicer “中”: ++ if self._symbol == u'CN': ++ self._symbol = u'中' ++ # workaround for the translit and translit-ua tables which ++ # have 2 character symbols. '☑' + self._symbol then is ++ # 3 characters and currently gnome-shell ignores symbols longer ++ # than 3 characters: ++ if self._symbol == u'Ya': ++ self._symbol = u'Я' ++ if self._symbol == u'Yi': ++ self._symbol = u'Ї' ++ # now we check and update the valid input characters ++ self._valid_input_chars = self.db.ime_properties.get( ++ 'valid_input_chars') ++ self._pinyin_valid_input_chars = u'abcdefghijklmnopqrstuvwxyz!@#$%' ++ ++ self._single_wildcard_char = variant_to_value(self._config.get_value( ++ self._config_section, ++ "singlewildcardchar")) ++ if self._single_wildcard_char == None: ++ self._single_wildcard_char = self.db.ime_properties.get( ++ 'single_wildcard_char') ++ if self._single_wildcard_char == None: ++ self._single_wildcard_char = u'' ++ if len(self._single_wildcard_char) > 1: ++ self._single_wildcard_char = self._single_wildcard_char[0] ++ ++ self._multi_wildcard_char = variant_to_value(self._config.get_value( ++ self._config_section, ++ "multiwildcardchar")) ++ if self._multi_wildcard_char == None: ++ self._multi_wildcard_char = self.db.ime_properties.get( ++ 'multi_wildcard_char') ++ if self._multi_wildcard_char == None: ++ self._multi_wildcard_char = u'' ++ if len(self._multi_wildcard_char) > 1: ++ self._multi_wildcard_char = self._multi_wildcard_char[0] ++ ++ self._auto_wildcard = variant_to_value(self._config.get_value( ++ self._config_section, ++ "autowildcard")) ++ if self._auto_wildcard == None: ++ self._auto_wildcard = self.db.ime_properties.get('auto_wildcard') ++ if self._auto_wildcard and self._auto_wildcard.lower() == u'false': ++ self._auto_wildcard = False ++ else: ++ self._auto_wildcard = True ++ ++ self._max_key_length = int(self.db.ime_properties.get('max_key_length')) ++ self._max_key_length_pinyin = 7 ++ ++ self._page_up_keys = [ ++ IBus.KEY_Page_Up, ++ IBus.KEY_KP_Page_Up, ++ IBus.KEY_minus ++ ] ++ self._page_down_keys = [ ++ IBus.KEY_Page_Down, ++ IBus.KEY_KP_Page_Down, ++ IBus.KEY_equal ++ ] ++ # If page up or page down keys are defined in the database, ++ # use the values from the database instead of the above ++ # hardcoded defaults: ++ page_up_keys_csv = self.db.ime_properties.get('page_up_keys') ++ page_down_keys_csv = self.db.ime_properties.get('page_down_keys') ++ if page_up_keys_csv: ++ self._page_up_keys = [ ++ IBus.keyval_from_name(x) ++ for x in page_up_keys_csv.split(',')] ++ if page_down_keys_csv: ++ self._page_down_keys = [ ++ IBus.keyval_from_name(x) ++ for x in page_down_keys_csv.split(',')] ++ # Remove keys from the page up/down keys if they are needed ++ # for input (for example, '=' or '-' could well be needed for ++ # input. Input is more important): ++ for character in ( ++ self._valid_input_chars ++ + self._single_wildcard_char ++ + self._multi_wildcard_char): ++ keyval = IBus.unicode_to_keyval(character) ++ if keyval in self._page_up_keys: ++ self._page_up_keys.remove(keyval) ++ if keyval in self._page_down_keys: ++ self._page_down_keys.remove(keyval) ++ self._commit_keys = [IBus.KEY_space] ++ # If commit keys are are defined in the database, use the ++ # value from the database instead of the above hardcoded ++ # default: ++ commit_keys_csv = self.db.ime_properties.get('commit_keys') ++ if commit_keys_csv: ++ self._commit_keys = [ ++ IBus.keyval_from_name(x) ++ for x in commit_keys_csv.split(',')] ++ # If commit keys conflict with page up/down keys, remove them ++ # from the page up/down keys (They cannot really be used for ++ # both at the same time. Theoretically, keys from the page ++ # up/down keys could still be used to commit when the number ++ # of candidates is 0 because then there is nothing to ++ # page. But that would be only confusing): ++ for keyval in self._commit_keys: ++ if keyval in self._page_up_keys: ++ self._page_up_keys.remove(keyval) ++ if keyval in self._page_down_keys: ++ self._page_down_keys.remove(keyval) ++ # Finally, check the user setting, i.e. the config value ++ # “spacekeybehavior” and let the user have the last word ++ # how to use the space key: ++ spacekeybehavior = variant_to_value(self._config.get_value( ++ self._config_section, ++ "spacekeybehavior")) ++ if spacekeybehavior == True: ++ # space is used as a page down key and not as a commit key: ++ if IBus.KEY_space not in self._page_down_keys: ++ self._page_down_keys.append(IBus.KEY_space) ++ if IBus.KEY_space in self._commit_keys: ++ self._commit_keys.remove(IBus.KEY_space) ++ if spacekeybehavior == False: ++ # space is used as a commit key and not used as a page down key: ++ if IBus.KEY_space in self._page_down_keys: ++ self._page_down_keys.remove(IBus.KEY_space) ++ if IBus.KEY_space not in self._commit_keys: ++ self._commit_keys.append(IBus.KEY_space) ++ if debug_level > 1: ++ sys.stderr.write( ++ "self._page_down_keys=%s\n" %repr(self._page_down_keys)) ++ sys.stderr.write( ++ "self._commit_keys=%s\n" %repr(self._commit_keys)) ++ ++ # 0 = Direct input, i.e. table input OFF (aka “English input mode”), ++ # most characters are just passed through to the application ++ # (but some fullwidth ↔ halfwidth conversion may be done even ++ # in this mode, depending on the settings) ++ # 1 = Table input ON (aka “Table input mode”, “Chinese mode”) ++ self._input_mode = variant_to_value(self._config.get_value( ++ self._config_section, ++ "inputmode")) ++ if self._input_mode == None: ++ self._input_mode = 1 ++ ++ # self._prev_key: hold the key event last time. ++ self._prev_key = None ++ self._prev_char = None ++ self._double_quotation_state = False ++ self._single_quotation_state = False ++ ++ self._full_width_letter = [ ++ variant_to_value(self._config.get_value( ++ self._config_section, ++ "EnDefFullWidthLetter")), ++ variant_to_value(self._config.get_value( ++ self._config_section, ++ "TabDefFullWidthLetter")) ++ ] ++ if self._full_width_letter[0] == None: ++ self._full_width_letter[0] = False ++ if self._full_width_letter[1] == None: ++ self._full_width_letter[1] = self.db.ime_properties.get( ++ 'def_full_width_letter').lower() == u'true' ++ self._full_width_punct = [ ++ variant_to_value(self._config.get_value( ++ self._config_section, ++ "EnDefFullWidthPunct")), ++ variant_to_value(self._config.get_value( ++ self._config_section, ++ "TabDefFullWidthPunct")) ++ ] ++ if self._full_width_punct[0] == None: ++ self._full_width_punct[0] = False ++ if self._full_width_punct[1] == None: ++ self._full_width_punct[1] = self.db.ime_properties.get( ++ 'def_full_width_punct').lower() == u'true' ++ ++ self._auto_commit = variant_to_value(self._config.get_value( ++ self._config_section, ++ "AutoCommit")) ++ if self._auto_commit == None: ++ self._auto_commit = self.db.ime_properties.get( ++ 'auto_commit').lower() == u'true' ++ ++ # If auto select is true, then the first candidate phrase will ++ # be selected automatically during typing. Auto select is true ++ # by default for the stroke5 table for example. ++ self._auto_select = variant_to_value(self._config.get_value( ++ self._config_section, ++ "AutoSelect")) ++ if self._auto_select == None: ++ if self.db.ime_properties.get('auto_select') != None: ++ self._auto_select = self.db.ime_properties.get( ++ 'auto_select').lower() == u'true' ++ else: ++ self._auto_select = False ++ ++ self._always_show_lookup = variant_to_value(self._config.get_value( ++ self._config_section, ++ "AlwaysShowLookup")) ++ if self._always_show_lookup == None: ++ if self.db.ime_properties.get('always_show_lookup') != None: ++ self._always_show_lookup = self.db.ime_properties.get( ++ 'always_show_lookup').lower() == u'true' ++ else: ++ self._always_show_lookup = True ++ ++ self._editor = editor(self._config, ++ self._valid_input_chars, ++ self._pinyin_valid_input_chars, ++ self._single_wildcard_char, ++ self._multi_wildcard_char, ++ self._auto_wildcard, ++ self._full_width_letter, ++ self._full_width_punct, ++ self._max_key_length, ++ self.db) ++ ++ self.chinese_mode_properties = { ++ 'ChineseMode.Simplified': { ++ # show simplified Chinese only ++ 'number': 0, ++ 'symbol': '簡', ++ 'icon': 'sc-mode.svg', ++ 'label': _('Simplified Chinese'), ++ 'tooltip': ++ _('Switch to “Simplified Chinese only”.')}, ++ 'ChineseMode.Traditional': { ++ # show traditional Chinese only ++ 'number': 1, ++ 'symbol': '繁', ++ 'icon': 'tc-mode.svg', ++ 'label': _('Traditional Chinese'), ++ 'tooltip': ++ _('Switch to “Traditional Chinese only”.')}, ++ 'ChineseMode.SimplifiedFirst': { ++ # show all but simplified first ++ 'number': 2, ++ 'symbol': '簡/大', ++ 'icon': 'scb-mode.svg', ++ 'label': _('Simplified Chinese first'), ++ 'tooltip': ++ _('Switch to “Simplified Chinese before traditional”.')}, ++ 'ChineseMode.TraditionalFirst': { ++ # show all but traditional first ++ 'number': 3, ++ 'symbol': '繁/大', ++ 'icon': 'tcb-mode.svg', ++ 'label': _('Traditional Chinese first'), ++ 'tooltip': ++ _('Switch to “Traditional Chinese before simplified”.')}, ++ 'ChineseMode.All': { ++ # show all Chinese characters, no particular order ++ 'number': 4, ++ 'symbol': '大', ++ 'icon': 'cb-mode.svg', ++ 'label': _('All Chinese characters'), ++ 'tooltip': _('Switch to “All Chinese characters”.')} ++ } ++ self.chinese_mode_menu = { ++ 'key': 'ChineseMode', ++ 'label': _('Chinese mode'), ++ 'tooltip': _('Switch Chinese mode'), ++ 'shortcut_hint': '(Ctrl-;)', ++ 'sub_properties': self.chinese_mode_properties ++ } ++ if self.db._is_chinese: ++ self.input_mode_properties = { ++ 'InputMode.Direct': { ++ 'number': 0, ++ 'symbol': '英', ++ 'icon': 'english.svg', ++ 'label': _('English'), ++ 'tooltip': _('Switch to English input')}, ++ 'InputMode.Table': { ++ 'number': 1, ++ 'symbol': '中', ++ 'symbol_table': '中', ++ 'symbol_pinyin': '拼音', ++ 'icon': 'chinese.svg', ++ 'label': _('Chinese'), ++ 'tooltip': _('Switch to Chinese input')} ++ } ++ else: ++ self.input_mode_properties = { ++ 'InputMode.Direct': { ++ 'number': 0, ++ 'symbol': '☐' + self._symbol, ++ 'icon': 'english.svg', ++ 'label': _('Direct'), ++ 'tooltip': _('Switch to direct input')}, ++ 'InputMode.Table': { ++ 'number': 1, ++ 'symbol': '☑' + self._symbol, ++ 'icon': 'ibus-table.svg', ++ 'label': _('Table'), ++ 'tooltip': _('Switch to table input')} ++ } ++ # The symbol of the property “InputMode” is displayed ++ # in the input method indicator of the Gnome3 panel. ++ # This depends on the property name “InputMode” and ++ # is case sensitive! ++ self.input_mode_menu = { ++ 'key': 'InputMode', ++ 'label': _('Input mode'), ++ 'tooltip': _('Switch Input mode'), ++ 'shortcut_hint': '(Left Shift)', ++ 'sub_properties': self.input_mode_properties ++ } ++ self.letter_width_properties = { ++ 'LetterWidth.Half': { ++ 'number': 0, ++ 'symbol': '◑', ++ 'icon': 'half-letter.svg', ++ 'label': _('Half'), ++ 'tooltip': _('Switch to halfwidth letters')}, ++ 'LetterWidth.Full': { ++ 'number': 1, ++ 'symbol': '●', ++ 'icon': 'full-letter.svg', ++ 'label': _('Full'), ++ 'tooltip': _('Switch to fullwidth letters')} ++ } ++ self.letter_width_menu = { ++ 'key': 'LetterWidth', ++ 'label': _('Letter width'), ++ 'tooltip': _('Switch letter width'), ++ 'shortcut_hint': '(Shift-Space)', ++ 'sub_properties': self.letter_width_properties ++ } ++ self.punctuation_width_properties = { ++ 'PunctuationWidth.Half': { ++ 'number': 0, ++ 'symbol': ',.', ++ 'icon': 'half-punct.svg', ++ 'label': _('Half'), ++ 'tooltip': _('Switch to halfwidth punctuation')}, ++ 'PunctuationWidth.Full': { ++ 'number': 1, ++ 'symbol': '、。', ++ 'icon': 'full-punct.svg', ++ 'label': _('Full'), ++ 'tooltip': _('Switch to fullwidth punctuation')} ++ } ++ self.punctuation_width_menu = { ++ 'key': 'PunctuationWidth', ++ 'label': _('Punctuation width'), ++ 'tooltip': _('Switch punctuation width'), ++ 'shortcut_hint': '(Ctrl-.)', ++ 'sub_properties': self.punctuation_width_properties ++ } ++ self.pinyin_mode_properties = { ++ 'PinyinMode.Table': { ++ 'number': 0, ++ 'symbol': '☐ 拼音', ++ 'icon': 'tab-mode.svg', ++ 'label': _('Table'), ++ 'tooltip': _('Switch to table mode')}, ++ 'PinyinMode.Pinyin': { ++ 'number': 1, ++ 'symbol': '☑ 拼音', ++ 'icon': 'py-mode.svg', ++ 'label': _('Pinyin'), ++ 'tooltip': _('Switch to pinyin mode')} ++ } ++ self.pinyin_mode_menu = { ++ 'key': 'PinyinMode', ++ 'label': _('Pinyin mode'), ++ 'tooltip': _('Switch pinyin mode'), ++ 'shortcut_hint': '(Right Shift)', ++ 'sub_properties': self.pinyin_mode_properties ++ } ++ self.onechar_mode_properties = { ++ 'OneCharMode.Phrase': { ++ 'number': 0, ++ 'symbol': '☐ 1', ++ 'icon': 'phrase.svg', ++ 'label': _('Multiple character match'), ++ 'tooltip': _('Switch to matching multiple characters at once')}, ++ 'OneCharMode.OneChar': { ++ 'number': 1, ++ 'symbol': '☑ 1', ++ 'icon': 'onechar.svg', ++ 'label': _('Single character match'), ++ 'tooltip': _('Switch to matching only single characters')} ++ } ++ self.onechar_mode_menu = { ++ 'key': 'OneCharMode', ++ 'label': _('Onechar mode'), ++ 'tooltip': _('Switch onechar mode'), ++ 'shortcut_hint': '(Ctrl-,)', ++ 'sub_properties': self.onechar_mode_properties ++ } ++ self.autocommit_mode_properties = { ++ 'AutoCommitMode.Direct': { ++ 'number': 0, ++ 'symbol': '☐ ↑', ++ 'icon': 'ncommit.svg', ++ 'label': _('Normal'), ++ 'tooltip': ++ _('Switch to normal commit mode ' ++ + '(automatic commits go into the preedit ' ++ + 'instead of into the application. ' ++ + 'This enables automatic definitions of new shortcuts)')}, ++ 'AutoCommitMode.Normal': { ++ 'number': 1, ++ 'symbol': '☑ ↑', ++ 'icon': 'acommit.svg', ++ 'label': _('Direct'), ++ 'tooltip': ++ _('Switch to direct commit mode ' ++ + '(automatic commits go directly into the application)')} ++ } ++ self.autocommit_mode_menu = { ++ 'key': 'AutoCommitMode', ++ 'label': _('Auto commit mode'), ++ 'tooltip': _('Switch autocommit mode'), ++ 'shortcut_hint': '(Ctrl-/)', ++ 'sub_properties': self.autocommit_mode_properties ++ } ++ self._prop_dict = {} ++ self._init_properties() ++ ++ self._on = False ++ self._save_user_count = 0 ++ self._save_user_start = time.time() ++ ++ self._save_user_count_max = SAVE_USER_COUNT_MAX ++ self._save_user_timeout = SAVE_USER_TIMEOUT ++ self.reset() ++ ++ self.sync_timeout_id = GObject.timeout_add_seconds(1, ++ self._sync_user_db) ++ ++ def reset(self): ++ self._editor.clear_all_input_and_preedit() ++ self._double_quotation_state = False ++ self._single_quotation_state = False ++ self._prev_key = None ++ self._update_ui() ++ ++ def do_destroy(self): ++ if self.sync_timeout_id > 0: ++ GObject.source_remove(self.sync_timeout_id) ++ self.sync_timeout_id = 0 ++ self.reset () ++ self.do_focus_out () ++ if self._save_user_count > 0: ++ self.db.sync_usrdb() ++ self._save_user_count = 0 ++ super(tabengine, self).destroy() ++ ++ def set_input_mode(self, mode=0): ++ if mode == self._input_mode: ++ return ++ self._input_mode = mode ++ # Not saved to config on purpose. In the setup tool one ++ # can select whether “Table input” or “Direct input” should ++ # be the default when the input method starts. But when ++ # changing this input mode using the property menu, ++ # the change is not remembered. ++ self._init_or_update_property_menu( ++ self.input_mode_menu, ++ self._input_mode) ++ # Letter width and punctuation width depend on the input mode. ++ # Therefore, the properties for letter width and punctuation ++ # width need to be updated here: ++ self._init_or_update_property_menu( ++ self.letter_width_menu, ++ self._full_width_letter[self._input_mode]) ++ self._init_or_update_property_menu( ++ self.punctuation_width_menu, ++ self._full_width_punct[self._input_mode]) ++ self.reset() ++ ++ def set_pinyin_mode(self, mode=False): ++ if mode == self._editor._py_mode: ++ return ++ # The pinyin mode is never saved to config on purpose ++ self._editor.commit_to_preedit() ++ self._editor._py_mode = mode ++ self._init_or_update_property_menu( ++ self.pinyin_mode_menu, mode) ++ if mode: ++ self.input_mode_properties['InputMode.Table']['symbol'] = ( ++ self.input_mode_properties['InputMode.Table']['symbol_pinyin']) ++ else: ++ self.input_mode_properties['InputMode.Table']['symbol'] = ( ++ self.input_mode_properties['InputMode.Table']['symbol_table']) ++ self._init_or_update_property_menu( ++ self.input_mode_menu, ++ self._input_mode) ++ self._update_ui() ++ ++ def set_onechar_mode(self, mode=False): ++ if mode == self._editor._onechar: ++ return ++ self._editor._onechar = mode ++ self._init_or_update_property_menu( ++ self.onechar_mode_menu, mode) ++ self._config.set_value( ++ self._config_section, ++ "OneChar", ++ GLib.Variant.new_boolean(mode)) ++ ++ def set_autocommit_mode(self, mode=False): ++ if mode == self._auto_commit: ++ return ++ self._auto_commit = mode ++ self._init_or_update_property_menu( ++ self.autocommit_mode_menu, mode) ++ self._config.set_value( ++ self._config_section, ++ "AutoCommit", ++ GLib.Variant.new_boolean(mode)) ++ ++ def set_letter_width(self, mode=False, input_mode=0): ++ if mode == self._full_width_letter[input_mode]: ++ return ++ self._full_width_letter[input_mode] = mode ++ self._editor._full_width_letter[input_mode] = mode ++ if input_mode == self._input_mode: ++ self._init_or_update_property_menu( ++ self.letter_width_menu, mode) ++ if input_mode: ++ self._config.set_value( ++ self._config_section, ++ "TabDefFullWidthLetter", ++ GLib.Variant.new_boolean(mode)) ++ else: ++ self._config.set_value( ++ self._config_section, ++ "EnDefFullWidthLetter", ++ GLib.Variant.new_boolean(mode)) ++ ++ def set_punctuation_width(self, mode=False, input_mode=0): ++ if mode == self._full_width_punct[input_mode]: ++ return ++ self._full_width_punct[input_mode] = mode ++ self._editor._full_width_punct[input_mode] = mode ++ if input_mode == self._input_mode: ++ self._init_or_update_property_menu( ++ self.punctuation_width_menu, mode) ++ if input_mode: ++ self._config.set_value( ++ self._config_section, ++ "TabDefFullWidthPunct", ++ GLib.Variant.new_boolean(mode)) ++ else: ++ self._config.set_value( ++ self._config_section, ++ "EnDefFullWidthPunct", ++ GLib.Variant.new_boolean(mode)) ++ ++ def set_chinese_mode(self, mode=0): ++ if mode == self._editor._chinese_mode: ++ return ++ self._editor._chinese_mode = mode ++ self._init_or_update_property_menu( ++ self.chinese_mode_menu, mode) ++ self._config.set_value( ++ self._config_section, ++ "ChineseMode", ++ GLib.Variant.new_int32(mode)) ++ ++ def _init_or_update_property_menu(self, menu, current_mode=0): ++ key = menu['key'] ++ if key in self._prop_dict: ++ update_prop = True ++ else: ++ update_prop = False ++ sub_properties = menu['sub_properties'] ++ for prop in sub_properties: ++ if sub_properties[prop]['number'] == int(current_mode): ++ symbol = sub_properties[prop]['symbol'] ++ icon = sub_properties[prop]['icon'] ++ label = '%(label)s (%(symbol)s) %(shortcut_hint)s' % { ++ 'label': menu['label'], ++ 'symbol': symbol, ++ 'shortcut_hint': menu['shortcut_hint']} ++ tooltip = '%(tooltip)s\n%(shortcut_hint)s' % { ++ 'tooltip': menu['tooltip'], ++ 'shortcut_hint': menu['shortcut_hint']} ++ self._prop_dict[key] = IBus.Property( ++ key=key, ++ prop_type=IBus.PropType.MENU, ++ label=IBus.Text.new_from_string(label), ++ symbol=IBus.Text.new_from_string(symbol), ++ icon=os.path.join(self._icon_dir, icon), ++ tooltip=IBus.Text.new_from_string(tooltip), ++ sensitive=True, ++ visible=True, ++ state=IBus.PropState.UNCHECKED, ++ sub_props=None) ++ self._prop_dict[key].set_sub_props( ++ self._init_sub_properties( ++ sub_properties, current_mode=current_mode)) ++ if update_prop: ++ self.properties.update_property(self._prop_dict[key]) ++ self.update_property(self._prop_dict[key]) ++ else: ++ self.properties.append(self._prop_dict[key]) ++ ++ def _init_sub_properties(self, modes, current_mode=0): ++ sub_props = IBus.PropList() ++ for mode in sorted(modes, key=lambda x: (modes[x]['number'])): ++ sub_props.append(IBus.Property( ++ key=mode, ++ prop_type=IBus.PropType.RADIO, ++ label=IBus.Text.new_from_string(modes[mode]['label']), ++ icon=os.path.join(modes[mode]['icon']), ++ tooltip=IBus.Text.new_from_string(modes[mode]['tooltip']), ++ sensitive=True, ++ visible=True, ++ state=IBus.PropState.UNCHECKED, ++ sub_props=None)) ++ i = 0 ++ while sub_props.get(i) != None: ++ prop = sub_props.get(i) ++ key = prop.get_key() ++ self._prop_dict[key] = prop ++ if modes[key]['number'] == int(current_mode): ++ prop.set_state(IBus.PropState.CHECKED) ++ else: ++ prop.set_state(IBus.PropState.UNCHECKED) ++ self.update_property(prop) # important! ++ i += 1 ++ return sub_props ++ ++ def _init_properties(self): ++ self._prop_dict = {} ++ self.properties = IBus.PropList() ++ ++ self._init_or_update_property_menu( ++ self.input_mode_menu, ++ self._input_mode) ++ ++ if self.db._is_chinese and self._editor._chinese_mode != -1: ++ self._init_or_update_property_menu( ++ self.chinese_mode_menu, ++ self._editor._chinese_mode) ++ ++ if self.db._is_cjk: ++ self._init_or_update_property_menu( ++ self.letter_width_menu, ++ self._full_width_letter[self._input_mode]) ++ self._init_or_update_property_menu( ++ self.punctuation_width_menu, ++ self._full_width_punct[self._input_mode]) ++ ++ if self._ime_py: ++ self._init_or_update_property_menu( ++ self.pinyin_mode_menu, ++ self._editor._py_mode) ++ ++ if self.db._is_cjk: ++ self._init_or_update_property_menu( ++ self.onechar_mode_menu, ++ self._editor._onechar) ++ ++ if self.db.user_can_define_phrase and self.db.rules: ++ self._init_or_update_property_menu( ++ self.autocommit_mode_menu, ++ self._auto_commit) ++ ++ self._setup_property = IBus.Property( ++ key = u'setup', ++ label = IBus.Text.new_from_string(_('Setup')), ++ icon = 'gtk-preferences', ++ tooltip = IBus.Text.new_from_string(_('Configure ibus-table “%(engine-name)s”') %{ ++ 'engine-name': self._engine_name}), ++ sensitive = True, ++ visible = True) ++ self.properties.append(self._setup_property) ++ ++ self.register_properties(self.properties) ++ ++ def do_property_activate( ++ self, property, prop_state = IBus.PropState.UNCHECKED): ++ ''' ++ Handle clicks on properties ++ ''' ++ if debug_level > 1: ++ sys.stderr.write( ++ "do_property_activate() property=%(p)s prop_state=%(ps)s\n" ++ % {'p': property, 'ps': prop_state}) ++ if property == "setup": ++ self._start_setup() ++ return ++ if prop_state != IBus.PropState.CHECKED: ++ # If the mouse just hovered over a menu button and ++ # no sub-menu entry was clicked, there is nothing to do: ++ return ++ if property.startswith(self.input_mode_menu['key']+'.'): ++ self.set_input_mode( ++ self.input_mode_properties[property]['number']) ++ return ++ if (property.startswith(self.pinyin_mode_menu['key']+'.') ++ and self._ime_py): ++ self.set_pinyin_mode( ++ bool(self.pinyin_mode_properties[property]['number'])) ++ return ++ if (property.startswith(self.onechar_mode_menu['key']+'.') ++ and self.db._is_cjk): ++ self.set_onechar_mode( ++ bool(self.onechar_mode_properties[property]['number'])) ++ return ++ if (property.startswith(self.autocommit_mode_menu['key']+'.') ++ and self.db.user_can_define_phrase and self.db.rules): ++ self.set_autocommit_mode( ++ bool(self.autocommit_mode_properties[property]['number'])) ++ return ++ if (property.startswith(self.letter_width_menu['key']+'.') ++ and self.db._is_cjk): ++ self.set_letter_width( ++ bool(self.letter_width_properties[property]['number']), ++ input_mode=self._input_mode) ++ return ++ if (property.startswith(self.punctuation_width_menu['key']+'.') ++ and self.db._is_cjk): ++ self.set_punctuation_width( ++ bool(self.punctuation_width_properties[property]['number']), ++ input_mode=self._input_mode) ++ return ++ if (property.startswith(self.chinese_mode_menu['key']+'.') ++ and self.db._is_chinese ++ and self._editor._chinese_mode != -1): ++ self.set_chinese_mode( ++ self.chinese_mode_properties[property]['number']) ++ return ++ ++ def _start_setup(self): ++ if self._setup_pid != 0: ++ pid, state = os.waitpid(self._setup_pid, os.P_NOWAIT) ++ if pid != self._setup_pid: ++ # If the last setup tool started from here is still ++ # running the pid returned by the above os.waitpid() ++ # is 0. In that case just return, don’t start a ++ # second setup tool. ++ return ++ self._setup_pid = 0 ++ setup_cmd = os.path.join( ++ os.getenv('IBUS_TABLE_LIB_LOCATION'), ++ 'ibus-setup-table') ++ self._setup_pid = os.spawnl( ++ os.P_NOWAIT, ++ setup_cmd, ++ 'ibus-setup-table', ++ '--engine-name table:%s' %self._engine_name) ++ ++ def _update_preedit(self): ++ '''Update Preedit String in UI''' ++ preedit_string_parts = self._editor.get_preedit_string_parts() ++ left_of_current_edit = u''.join(preedit_string_parts[0]) ++ current_edit = preedit_string_parts[1] ++ right_of_current_edit = u''.join(preedit_string_parts[2]) ++ if not self._editor._py_mode: ++ current_edit_new = u'' ++ for char in current_edit: ++ if char in self._editor._prompt_characters: ++ current_edit_new += self._editor._prompt_characters[char] ++ else: ++ current_edit_new += char ++ current_edit = current_edit_new ++ preedit_string_complete = ( ++ left_of_current_edit + current_edit + right_of_current_edit) ++ if not preedit_string_complete: ++ super(tabengine, self).update_preedit_text( ++ IBus.Text.new_from_string(u''), 0, False) ++ return ++ color_left = rgb(0xf9, 0x0f, 0x0f) # bright red ++ color_right = rgb(0x1e, 0xdc, 0x1a) # light green ++ color_invalid = rgb(0xff, 0x00, 0xff) # magenta ++ attrs = IBus.AttrList() ++ attrs.append( ++ IBus.attr_foreground_new( ++ color_left, ++ 0, ++ len(left_of_current_edit))) ++ attrs.append( ++ IBus.attr_foreground_new( ++ color_right, ++ len(left_of_current_edit) + len(current_edit), ++ len(preedit_string_complete))) ++ if self._editor._chars_invalid: ++ attrs.append( ++ IBus.attr_foreground_new( ++ color_invalid, ++ len(left_of_current_edit) + len(current_edit) ++ - len(self._editor._chars_invalid), ++ len(left_of_current_edit) + len(current_edit) ++ )) ++ attrs.append( ++ IBus.attr_underline_new( ++ IBus.AttrUnderline.SINGLE, ++ 0, ++ len(preedit_string_complete))) ++ text = IBus.Text.new_from_string(preedit_string_complete) ++ i = 0 ++ while attrs.get(i) != None: ++ attr = attrs.get(i) ++ text.append_attribute(attr.get_attr_type(), ++ attr.get_value(), ++ attr.get_start_index(), ++ attr.get_end_index()) ++ i += 1 ++ super(tabengine, self).update_preedit_text( ++ text, self._editor.get_caret(), True) ++ ++ def _update_aux (self): ++ '''Update Aux String in UI''' ++ aux_string = self._editor.get_aux_strings() ++ if len(self._editor._candidates) > 0: ++ aux_string += u' (%d / %d)' % ( ++ self._editor._lookup_table.get_cursor_pos() +1, ++ self._editor._lookup_table.get_number_of_candidates()) ++ if aux_string: ++ attrs = IBus.AttrList() ++ attrs.append(IBus.attr_foreground_new( ++ rgb(0x95,0x15,0xb5),0, len(aux_string))) ++ text = IBus.Text.new_from_string(aux_string) ++ i = 0 ++ while attrs.get(i) != None: ++ attr = attrs.get(i) ++ text.append_attribute(attr.get_attr_type(), ++ attr.get_value(), ++ attr.get_start_index(), ++ attr.get_end_index()) ++ i += 1 ++ visible = True ++ if not aux_string or not self._always_show_lookup: ++ visible = False ++ super(tabengine, self).update_auxiliary_text(text, visible) ++ else: ++ self.hide_auxiliary_text() ++ ++ def _update_lookup_table (self): ++ '''Update Lookup Table in UI''' ++ if len(self._editor._candidates) == 0: ++ # Also make sure to hide lookup table if there are ++ # no candidates to display. On f17, this makes no ++ # difference but gnome-shell in f18 will display ++ # an empty suggestion popup if the number of candidates ++ # is zero! ++ self.hide_lookup_table() ++ return ++ if self._editor.is_empty (): ++ self.hide_lookup_table() ++ return ++ if not self._always_show_lookup: ++ self.hide_lookup_table() ++ return ++ self.update_lookup_table(self._editor.get_lookup_table(), True) ++ ++ def _update_ui (self): ++ '''Update User Interface''' ++ self._update_lookup_table () ++ self._update_preedit () ++ self._update_aux () ++ ++ def _check_phrase (self, tabkeys=u'', phrase=u''): ++ """Check the given phrase and update save user db info""" ++ if not tabkeys or not phrase: ++ return ++ self.db.check_phrase(tabkeys=tabkeys, phrase=phrase) ++ ++ if self._save_user_count <= 0: ++ self._save_user_start = time.time() ++ self._save_user_count += 1 ++ ++ def _sync_user_db(self): ++ """Save user db to disk""" ++ if self._save_user_count >= 0: ++ now = time.time() ++ time_delta = now - self._save_user_start ++ if (self._save_user_count > self._save_user_count_max or ++ time_delta >= self._save_user_timeout): ++ self.db.sync_usrdb() ++ self._save_user_count = 0 ++ self._save_user_start = now ++ return True ++ ++ def commit_string (self, phrase, tabkeys=u''): ++ if debug_level > 1: ++ sys.stderr.write("commit_string() phrase=%(p)s\n" ++ %{'p': phrase}) ++ self._editor.clear_all_input_and_preedit() ++ self._update_ui() ++ super(tabengine, self).commit_text(IBus.Text.new_from_string(phrase)) ++ if len(phrase) > 0: ++ self._prev_char = phrase[-1] ++ else: ++ self._prev_char = None ++ self._check_phrase(tabkeys=tabkeys, phrase=phrase) ++ ++ def commit_everything_unless_invalid(self): ++ ''' ++ Commits the current input to the preëdit and then ++ commits the preëdit to the application unless there are ++ invalid input characters. ++ ++ Returns “True” if something was committed, “False” if not. ++ ''' ++ if debug_level > 1: ++ sys.stderr.write("commit_everything_unless_invalid()\n") ++ if self._editor._chars_invalid: ++ return False ++ if not self._editor.is_empty(): ++ self._editor.commit_to_preedit() ++ self.commit_string(self._editor.get_preedit_string_complete(), ++ tabkeys=self._editor.get_preedit_tabkeys_complete()) ++ return True ++ ++ def _convert_to_full_width(self, c): ++ '''Convert half width character to full width''' ++ ++ # This function handles punctuation that does not comply to the ++ # Unicode conversion formula in unichar_half_to_full(c). ++ # For ".", "\"", "'"; there are even variations under specific ++ # cases. This function should be more abstracted by extracting ++ # that to another handling function later on. ++ special_punct_dict = {u"<": u"《", # 《 U+300A LEFT DOUBLE ANGLE BRACKET ++ u">": u"》", # 》 U+300B RIGHT DOUBLE ANGLE BRACKET ++ u"[": u"「", # 「 U+300C LEFT CORNER BRACKET ++ u"]": u"」", # 」U+300D RIGHT CORNER BRACKET ++ u"{": u"『", # 『 U+300E LEFT WHITE CORNER BRACKET ++ u"}": u"』", # 』U+300F RIGHT WHITE CORNER BRACKET ++ u"\\": u"、", # 、 U+3001 IDEOGRAPHIC COMMA ++ u"^": u"……", # … U+2026 HORIZONTAL ELLIPSIS ++ u"_": u"——", # — U+2014 EM DASH ++ u"$": u"¥" # ¥ U+FFE5 FULLWIDTH YEN SIGN ++ } ++ ++ # special puncts w/o further conditions ++ if c in special_punct_dict.keys(): ++ if c in [u"\\", u"^", u"_", u"$"]: ++ return special_punct_dict[c] ++ elif self._input_mode: ++ return special_punct_dict[c] ++ ++ # special puncts w/ further conditions ++ if c == u".": ++ if (self._prev_char ++ and self._prev_char.isdigit() ++ and self._prev_key ++ and chr(self._prev_key.val) == self._prev_char): ++ return u"." ++ else: ++ return u"。" # 。U+3002 IDEOGRAPHIC FULL STOP ++ elif c == u"\"": ++ self._double_quotation_state = not self._double_quotation_state ++ if self._double_quotation_state: ++ return u"“" # “ U+201C LEFT DOUBLE QUOTATION MARK ++ else: ++ return u"”" # ” U+201D RIGHT DOUBLE QUOTATION MARK ++ elif c == u"'": ++ self._single_quotation_state = not self._single_quotation_state ++ if self._single_quotation_state: ++ return u"‘" # ‘ U+2018 LEFT SINGLE QUOTATION MARK ++ else: ++ return u"’" # ’ U+2019 RIGHT SINGLE QUOTATION MARK ++ ++ return unichar_half_to_full(c) ++ ++ def _match_hotkey (self, key, keyval, state): ++ ++ # Match only when keys are released ++ state = state | IBus.ModifierType.RELEASE_MASK ++ if key.val == keyval and (key.state & state) == state: ++ # If it is a key release event, the previous key ++ # must have been the same key pressed down. ++ if (self._prev_key ++ and key.val == self._prev_key.val): ++ return True ++ ++ return False ++ ++ def do_candidate_clicked(self, index, button, state): ++ if self._editor.commit_to_preedit_current_page(index): ++ # commits to preëdit ++ self.commit_string( ++ self._editor.get_preedit_string_complete(), ++ tabkeys=self._editor.get_preedit_tabkeys_complete()) ++ return True ++ return False ++ ++ def do_process_key_event(self, keyval, keycode, state): ++ '''Process Key Events ++ Key Events include Key Press and Key Release, ++ modifier means Key Pressed ++ ''' ++ if debug_level > 1: ++ sys.stderr.write("do_process_key_event()\n") ++ if (self._has_input_purpose ++ and self._input_purpose ++ in [IBus.InputPurpose.PASSWORD, IBus.InputPurpose.PIN]): ++ return False ++ ++ key = KeyEvent(keyval, keycode, state) ++ ++ result = self._process_key_event (key) ++ self._prev_key = key ++ return result ++ ++ def _process_key_event (self, key): ++ '''Internal method to process key event''' ++ # Match mode switch hotkey ++ if (self._editor.is_empty() ++ and (self._match_hotkey( ++ key, IBus.KEY_Shift_L, ++ IBus.ModifierType.SHIFT_MASK))): ++ self.set_input_mode(int(not self._input_mode)) ++ return True ++ ++ # Match fullwidth/halfwidth letter mode switch hotkey ++ if self.db._is_cjk: ++ if (key.val == IBus.KEY_space ++ and key.state & IBus.ModifierType.SHIFT_MASK ++ and not key.state & IBus.ModifierType.RELEASE_MASK): ++ # Ignore when Shift+Space was pressed, the key release ++ # event will toggle the fullwidth/halfwidth letter mode, we ++ # don’t want to insert an extra space on the key press ++ # event. ++ return True ++ if (self._match_hotkey( ++ key, IBus.KEY_space, ++ IBus.ModifierType.SHIFT_MASK)): ++ self.set_letter_width( ++ not self._full_width_letter[self._input_mode], ++ input_mode = self._input_mode) ++ return True ++ ++ # Match full half punct mode switch hotkey ++ if (self._match_hotkey( ++ key, IBus.KEY_period, ++ IBus.ModifierType.CONTROL_MASK) and self.db._is_cjk): ++ self.set_punctuation_width( ++ not self._full_width_punct[self._input_mode], ++ input_mode = self._input_mode) ++ return True ++ ++ if self._input_mode: ++ return self._table_mode_process_key_event (key) ++ else: ++ return self._english_mode_process_key_event (key) ++ ++ def cond_letter_translate(self, char): ++ if self._full_width_letter[self._input_mode] and self.db._is_cjk: ++ return self._convert_to_full_width(char) ++ else: ++ return char ++ ++ def cond_punct_translate(self, char): ++ if self._full_width_punct[self._input_mode] and self.db._is_cjk: ++ return self._convert_to_full_width(char) ++ else: ++ return char ++ ++ def _english_mode_process_key_event(self, key): ++ # Ignore key release events ++ if key.state & IBus.ModifierType.RELEASE_MASK: ++ return False ++ if key.val >= 128: ++ return False ++ # we ignore all hotkeys here ++ if (key.state ++ & (IBus.ModifierType.CONTROL_MASK|IBus.ModifierType.MOD1_MASK)): ++ return False ++ keychar = IBus.keyval_to_unicode(key.val) ++ if type(keychar) != type(u''): ++ keychar = keychar.decode('UTF-8') ++ if ascii_ispunct(keychar): ++ trans_char = self.cond_punct_translate(keychar) ++ else: ++ trans_char = self.cond_letter_translate(keychar) ++ if trans_char == keychar: ++ return False ++ self.commit_string(trans_char) ++ return True ++ ++ def _table_mode_process_key_event(self, key): ++ if debug_level > 0: ++ sys.stderr.write('_table_mode_process_key_event() ') ++ sys.stderr.write('repr(key)=%(key)s\n' %{'key': key}) ++ # Change pinyin mode ++ # (change only if the editor is empty. When the editor ++ # is not empty, the right shift key should commit to preëdit ++ # and not change the pinyin mode). ++ if (self._ime_py ++ and self._editor.is_empty() ++ and self._match_hotkey( ++ key, IBus.KEY_Shift_R, ++ IBus.ModifierType.SHIFT_MASK)): ++ self.set_pinyin_mode(not self._editor._py_mode) ++ return True ++ # process commit to preedit ++ if (self._match_hotkey( ++ key, IBus.KEY_Shift_R, ++ IBus.ModifierType.SHIFT_MASK) ++ or self._match_hotkey( ++ key, IBus.KEY_Shift_L, ++ IBus.ModifierType.SHIFT_MASK)): ++ res = self._editor.commit_to_preedit() ++ self._update_ui() ++ return res ++ ++ # Left ALT key to cycle candidates in the current page. ++ if (self._match_hotkey( ++ key, IBus.KEY_Alt_L, ++ IBus.ModifierType.MOD1_MASK)): ++ res = self._editor.cycle_next_cand() ++ self._update_ui() ++ return res ++ ++ # Match single char mode switch hotkey ++ if (self._match_hotkey( ++ key, IBus.KEY_comma, ++ IBus.ModifierType.CONTROL_MASK) and self.db._is_cjk): ++ self.set_onechar_mode(not self._editor._onechar) ++ return True ++ ++ # Match direct commit mode switch hotkey ++ if (self._match_hotkey( ++ key, IBus.KEY_slash, ++ IBus.ModifierType.CONTROL_MASK) ++ and self.db.user_can_define_phrase and self.db.rules): ++ self.set_autocommit_mode(not self._auto_commit) ++ return True ++ ++ # Match Chinese mode shift ++ if (self._match_hotkey( ++ key, IBus.KEY_semicolon, ++ IBus.ModifierType.CONTROL_MASK) and self.db._is_chinese): ++ self.set_chinese_mode((self._editor._chinese_mode+1) % 5) ++ return True ++ ++ # Ignore key release events ++ # (Must be below all self._match_hotkey() callse ++ # because these match on a release event). ++ if key.state & IBus.ModifierType.RELEASE_MASK: ++ return False ++ ++ keychar = IBus.keyval_to_unicode(key.val) ++ if type(keychar) != type(u''): ++ keychar = keychar.decode('UTF-8') ++ ++ # Section to handle leading invalid input: ++ # ++ # This is the first character typed, if it is invalid ++ # input, handle it immediately here, if it is valid, continue. ++ if (self._editor.is_empty() ++ and not self._editor.get_preedit_string_complete()): ++ if ((keychar not in ( ++ self._valid_input_chars ++ + self._single_wildcard_char ++ + self._multi_wildcard_char) ++ or (self.db.startchars and keychar not in self.db.startchars)) ++ and (not key.state & ++ (IBus.ModifierType.MOD1_MASK | ++ IBus.ModifierType.CONTROL_MASK))): ++ if debug_level > 0: ++ sys.stderr.write( ++ '_table_mode_process_key_event() ' ++ + 'leading invalid input: ' ++ + 'repr(keychar)=%(keychar)s\n' ++ % {'keychar': keychar}) ++ if ascii_ispunct(keychar): ++ trans_char = self.cond_punct_translate(keychar) ++ else: ++ trans_char = self.cond_letter_translate(keychar) ++ if trans_char == keychar: ++ self._prev_char = trans_char ++ return False ++ else: ++ self.commit_string(trans_char) ++ return True ++ ++ if key.val == IBus.KEY_Escape: ++ self.reset() ++ self._update_ui() ++ return True ++ ++ if key.val in (IBus.KEY_Return, IBus.KEY_KP_Enter): ++ if (self._editor.is_empty() ++ and not self._editor.get_preedit_string_complete()): ++ # When IBus.KEY_Return is typed, ++ # IBus.keyval_to_unicode(key.val) returns a non-empty ++ # string. But when IBus.KEY_KP_Enter is typed it ++ # returns an empty string. Therefore, when typing ++ # IBus.KEY_KP_Enter as leading input, the key is not ++ # handled by the section to handle leading invalid ++ # input but it ends up here. If it is leading input ++ # (i.e. the preëdit is empty) we should always pass ++ # IBus.KEY_KP_Enter to the application: ++ return False ++ if self._auto_select: ++ self._editor.commit_to_preedit() ++ commit_string = self._editor.get_preedit_string_complete() ++ self.commit_string(commit_string) ++ return False ++ else: ++ commit_string = self._editor.get_preedit_tabkeys_complete() ++ self.commit_string(commit_string) ++ return True ++ ++ if key.val in (IBus.KEY_Tab, IBus.KEY_KP_Tab) and self._auto_select: ++ # Used for example for the Russian transliteration method ++ # “translit”, which uses “auto select”. If for example ++ # a file with the name “шшш” exists and one types in ++ # a bash shell: ++ # ++ # “ls sh” ++ # ++ # the “sh” is converted to “ш” and one sees ++ # ++ # “ls ш” ++ # ++ # in the shell where the “ш” is still in preëdit ++ # because “shh” would be converted to “щ”, i.e. there ++ # is more than one candidate and the input method is still ++ # waiting whether one more “h” will be typed or not. But ++ # if the next character typed is a Tab, the preëdit is ++ # committed here and “False” is returned to pass the Tab ++ # character through to the bash to complete the file name ++ # to “шшш”. ++ self._editor.commit_to_preedit() ++ self.commit_string(self._editor.get_preedit_string_complete()) ++ return False ++ ++ if key.val in (IBus.KEY_Down, IBus.KEY_KP_Down) : ++ if not self._editor.get_preedit_string_complete(): ++ return False ++ res = self._editor.cursor_down() ++ self._update_ui() ++ return res ++ ++ if key.val in (IBus.KEY_Up, IBus.KEY_KP_Up): ++ if not self._editor.get_preedit_string_complete(): ++ return False ++ res = self._editor.cursor_up() ++ self._update_ui() ++ return res ++ ++ if (key.val in (IBus.KEY_Left, IBus.KEY_KP_Left) ++ and key.state & IBus.ModifierType.CONTROL_MASK): ++ if not self._editor.get_preedit_string_complete(): ++ return False ++ self._editor.control_arrow_left() ++ self._update_ui() ++ return True ++ ++ if (key.val in (IBus.KEY_Right, IBus.KEY_KP_Right) ++ and key.state & IBus.ModifierType.CONTROL_MASK): ++ if not self._editor.get_preedit_string_complete(): ++ return False ++ self._editor.control_arrow_right() ++ self._update_ui() ++ return True ++ ++ if key.val in (IBus.KEY_Left, IBus.KEY_KP_Left): ++ if not self._editor.get_preedit_string_complete(): ++ return False ++ self._editor.arrow_left() ++ self._update_ui() ++ return True ++ ++ if key.val in (IBus.KEY_Right, IBus.KEY_KP_Right): ++ if not self._editor.get_preedit_string_complete(): ++ return False ++ self._editor.arrow_right() ++ self._update_ui() ++ return True ++ ++ if (key.val == IBus.KEY_BackSpace ++ and key.state & IBus.ModifierType.CONTROL_MASK): ++ if not self._editor.get_preedit_string_complete(): ++ return False ++ self._editor.remove_preedit_before_cursor() ++ self._update_ui() ++ return True ++ ++ if key.val == IBus.KEY_BackSpace: ++ if not self._editor.get_preedit_string_complete(): ++ return False ++ self._editor.remove_char() ++ self._update_ui() ++ return True ++ ++ if (key.val == IBus.KEY_Delete ++ and key.state & IBus.ModifierType.CONTROL_MASK): ++ if not self._editor.get_preedit_string_complete(): ++ return False ++ self._editor.remove_preedit_after_cursor() ++ self._update_ui() ++ return True ++ ++ if key.val == IBus.KEY_Delete: ++ if not self._editor.get_preedit_string_complete(): ++ return False ++ self._editor.delete() ++ self._update_ui() ++ return True ++ ++ if (key.val in self._editor.get_select_keys() ++ and self._editor._candidates ++ and key.state & IBus.ModifierType.CONTROL_MASK): ++ res = self._editor.select_key(key.val) ++ self._update_ui() ++ return res ++ ++ if (key.val in self._editor.get_select_keys() ++ and self._editor._candidates ++ and key.state & IBus.ModifierType.MOD1_MASK): ++ res = self._editor.remove_candidate_from_user_database(key.val) ++ self._update_ui() ++ return res ++ ++ # now we ignore all other hotkeys ++ if (key.state ++ & (IBus.ModifierType.CONTROL_MASK|IBus.ModifierType.MOD1_MASK)): ++ return False ++ ++ if key.state & IBus.ModifierType.MOD1_MASK: ++ return False ++ ++ # Section to handle valid input characters: ++ # ++ # All keys which could possibly conflict with the valid input ++ # characters should be checked below this section. These are ++ # SELECT_KEYS, PAGE_UP_KEYS, PAGE_DOWN_KEYS, and COMMIT_KEYS. ++ # ++ # For example, consider a table has ++ # ++ # SELECT_KEYS = 1,2,3,4,5,6,7,8,9,0 ++ # ++ # and ++ # ++ # VALID_INPUT_CHARS = 0123456789abcdef ++ # ++ # (Currently the cns11643 table has this, for example) ++ # ++ # Then the digit “1” could be interpreted either as an input ++ # character or as a select key but of course not both. If the ++ # meaning as a select key or page down key were preferred, ++ # this would make some input impossible which probably makes ++ # the whole input method useless. If the meaning as an input ++ # character is preferred, this makes selection using that key ++ # impossible. Making selection by key impossible is not nice ++ # either, but it is not a complete show stopper as there are ++ # still other possibilities to select, for example using the ++ # arrow-up/arrow-down keys or click with the mouse. ++ # ++ # Of course one should maybe consider fixing the conflict ++ # between the keys by using different SELECT_KEYS and/or ++ # PAGE_UP_KEYS/PAGE_DOWN_KEYS in that table ... ++ if (keychar ++ and (keychar in (self._valid_input_chars ++ + self._single_wildcard_char ++ + self._multi_wildcard_char) ++ or (self._editor._py_mode ++ and keychar in (self._pinyin_valid_input_chars ++ + self._single_wildcard_char ++ + self._multi_wildcard_char)))): ++ if debug_level > 0: ++ sys.stderr.write( ++ '_table_mode_process_key_event() valid input: ' ++ + 'repr(keychar)=%(keychar)s\n' ++ % {'keychar': keychar}) ++ if self._editor._py_mode: ++ if ((len(self._editor._chars_valid) ++ == self._max_key_length_pinyin) ++ or (len(self._editor._chars_valid) > 1 ++ and self._editor._chars_valid[-1] in '!@#$%')): ++ if self._auto_commit: ++ self.commit_everything_unless_invalid() ++ else: ++ self._editor.commit_to_preedit() ++ else: ++ if ((len(self._editor._chars_valid) ++ == self._max_key_length) ++ or (len(self._editor._chars_valid) ++ in self.db.possible_tabkeys_lengths)): ++ if self._auto_commit: ++ self.commit_everything_unless_invalid() ++ else: ++ self._editor.commit_to_preedit() ++ res = self._editor.add_input(keychar) ++ if not res: ++ if self._auto_select and self._editor._candidates_previous: ++ # Used for example for the Russian transliteration method ++ # “translit”, which uses “auto select”. ++ # The “translit” table contains: ++ # ++ # sh ш ++ # shh щ ++ # ++ # so typing “sh” matches “ш” and “щ”. The ++ # candidate with the shortest key sequence comes ++ # first in the lookup table, therefore “sh ш” ++ # is shown in the preëdit (The other candidate, ++ # “shh щ” comes second in the lookup table and ++ # could be selected using arrow-down. But ++ # “translit” hides the lookup table by default). ++ # ++ # Now, when after typing “sh” one types “s”, ++ # the key “shs” has no match, so add_input('s') ++ # returns “False” and we end up here. We pop the ++ # last character “s” which caused the match to ++ # fail, commit first of the previous candidates, ++ # i.e. “sh ш” and feed the “s” into the ++ # key event handler again. ++ self._editor.pop_input() ++ self.commit_everything_unless_invalid() ++ return self._table_mode_process_key_event(key) ++ self.commit_everything_unless_invalid() ++ self._update_ui() ++ return True ++ else: ++ if (self._auto_commit and self._editor.one_candidate() ++ and ++ (self._editor._chars_valid ++ == self._editor._candidates[0][0])): ++ self.commit_everything_unless_invalid() ++ self._update_ui() ++ return True ++ ++ if key.val in self._commit_keys: ++ if self.commit_everything_unless_invalid(): ++ if self._editor._auto_select: ++ self.commit_string(u' ') ++ return True ++ ++ if key.val in self._page_down_keys and self._editor._candidates: ++ res = self._editor.page_down() ++ self._update_ui() ++ return res ++ ++ if key.val in self._page_up_keys and self._editor._candidates: ++ res = self._editor.page_up() ++ self._update_ui() ++ return res ++ ++ if (key.val in self._editor.get_select_keys() ++ and self._editor._candidates): ++ if self._editor.select_key(key.val): # commits to preëdit ++ self.commit_string( ++ self._editor.get_preedit_string_complete(), ++ tabkeys=self._editor.get_preedit_tabkeys_complete()) ++ return True ++ ++ # Section to handle trailing invalid input: ++ # ++ # If the key has still not been handled when this point is ++ # reached, it cannot be a valid input character. Neither can ++ # it be a select key nor a page-up/page-down key. Adding this ++ # key to the tabkeys and search for matching candidates in the ++ # table would thus be pointless. ++ # ++ # So we commit all pending input immediately and then commit ++ # this invalid input character as well, possibly converted to ++ # fullwidth or halfwidth. ++ if keychar: ++ if debug_level > 0: ++ sys.stderr.write( ++ '_table_mode_process_key_event() trailing invalid input: ' ++ + 'repr(keychar)=%(keychar)s\n' ++ % {'keychar': keychar}) ++ if not self._editor._candidates: ++ self.commit_string(self._editor.get_preedit_tabkeys_complete()) ++ else: ++ self._editor.commit_to_preedit() ++ self.commit_string(self._editor.get_preedit_string_complete()) ++ if ascii_ispunct(keychar): ++ self.commit_string(self.cond_punct_translate(keychar)) ++ else: ++ self.commit_string(self.cond_letter_translate(keychar)) ++ return True ++ ++ # What kind of key was this?? ++ # ++ # keychar = IBus.keyval_to_unicode(key.val) ++ # ++ # returned no result. So whatever this was, we cannot handle it, ++ # just pass it through to the application by returning “False”. ++ return False ++ ++ def do_focus_in (self): ++ if debug_level > 1: ++ sys.stderr.write("do_focus_in()") ++ if self._on: ++ self.register_properties(self.properties) ++ self._init_or_update_property_menu( ++ self.input_mode_menu, ++ self._input_mode) ++ self._update_ui () ++ ++ def do_focus_out (self): ++ if self._has_input_purpose: ++ self._input_purpose = 0 ++ self._editor.clear_all_input_and_preedit() ++ ++ def do_set_content_type(self, purpose, hints): ++ if self._has_input_purpose: ++ self._input_purpose = purpose ++ ++ def do_enable (self): ++ self._on = True ++ self.do_focus_in() ++ ++ def do_disable (self): ++ self._on = False ++ ++ def do_page_up (self): ++ if self._editor.page_up (): ++ self._update_ui () ++ return True ++ return False ++ ++ def do_page_down (self): ++ if self._editor.page_down (): ++ self._update_ui () ++ return True ++ return False ++ ++ def config_section_normalize(self, section): ++ # This function replaces _: with - in the dconf ++ # section and converts to lower case to make ++ # the comparison of the dconf sections work correctly. ++ # I avoid using .lower() here because it is locale dependent, ++ # when using .lower() this would not achieve the desired ++ # effect of comparing the dconf sections case insentively ++ # in some locales, it would fail for example if Turkish ++ # locale (tr_TR.UTF-8) is set. ++ if sys.version_info >= (3, 0, 0): # Python3 ++ return re.sub(r'[_:]', r'-', section).translate( ++ ''.maketrans( ++ string.ascii_uppercase, ++ string.ascii_lowercase)) ++ else: # Python2 ++ return re.sub(r'[_:]', r'-', section).translate( ++ string.maketrans( ++ string.ascii_uppercase, ++ string.ascii_lowercase).decode('ISO-8859-1')) ++ ++ def config_value_changed_cb(self, config, section, name, value): ++ if (self.config_section_normalize(self._config_section) ++ != self.config_section_normalize(section)): ++ return ++ value = variant_to_value(value) ++ print('config value %(n)s for engine %(en)s changed to %(value)s' ++ % {'n': name, 'en': self._engine_name, 'value': value}) ++ if name == u'inputmode': ++ self.set_input_mode(value) ++ return ++ if name == u'autoselect': ++ self._editor._auto_select = value ++ self._auto_select = value ++ return ++ if name == u'autocommit': ++ self.set_autocommit_mode(value) ++ return ++ if name == u'chinesemode': ++ self.set_chinese_mode(value) ++ self.db.reset_phrases_cache() ++ return ++ if name == u'endeffullwidthletter': ++ self.set_letter_width(value, input_mode=0) ++ return ++ if name == u'endeffullwidthpunct': ++ self.set_punctuation_width(value, input_mode=0) ++ return ++ if name == u'lookuptableorientation': ++ self._editor._orientation = value ++ self._editor._lookup_table.set_orientation(value) ++ return ++ if name == u'lookuptablepagesize': ++ if value > len(self._editor._select_keys): ++ value = len(self._editor._select_keys) ++ self._config.set_value( ++ self._config_section, ++ 'lookuptablepagesize', ++ GLib.Variant.new_int32(value)) ++ if value < 1: ++ value = 1 ++ self._config.set_value( ++ self._config_section, ++ 'lookuptablepagesize', ++ GLib.Variant.new_int32(value)) ++ self._editor._page_size = value ++ self._editor._lookup_table = self._editor.get_new_lookup_table( ++ page_size = self._editor._page_size, ++ select_keys = self._editor._select_keys, ++ orientation = self._editor._orientation) ++ self.reset() ++ return ++ if name == u'onechar': ++ self.set_onechar_mode(value) ++ self.db.reset_phrases_cache() ++ return ++ if name == u'tabdeffullwidthletter': ++ self.set_letter_width(value, input_mode=1) ++ return ++ if name == u'tabdeffullwidthpunct': ++ self.set_punctuation_width(value, input_mode=1) ++ return ++ if name == u'alwaysshowlookup': ++ self._always_show_lookup = value ++ return ++ if name == u'spacekeybehavior': ++ if value == True: ++ # space is used as a page down key and not as a commit key: ++ if IBus.KEY_space not in self._page_down_keys: ++ self._page_down_keys.append(IBus.KEY_space) ++ if IBus.KEY_space in self._commit_keys: ++ self._commit_keys.remove(IBus.KEY_space) ++ if value == False: ++ # space is used as a commit key and not used as a page down key: ++ if IBus.KEY_space in self._page_down_keys: ++ self._page_down_keys.remove(IBus.KEY_space) ++ if IBus.KEY_space not in self._commit_keys: ++ self._commit_keys.append(IBus.KEY_space) ++ if debug_level > 1: ++ sys.stderr.write( ++ "self._page_down_keys=%s\n" ++ % repr(self._page_down_keys)) ++ return ++ if name == u'singlewildcardchar': ++ self._single_wildcard_char = value ++ self._editor._single_wildcard_char = value ++ self.db.reset_phrases_cache() ++ return ++ if name == u'multiwildcardchar': ++ self._multi_wildcard_char = value ++ self._editor._multi_wildcard_char = value ++ self.db.reset_phrases_cache() ++ return ++ if name == u'autowildcard': ++ self._auto_wildcard = value ++ self._editor._auto_wildcard = value ++ self.db.reset_phrases_cache() ++ return +diff -Nru ibus-table-1.9.18.orig/engine/tabsqlitedb.py ibus-table-1.9.18/engine/tabsqlitedb.py +--- ibus-table-1.9.18.orig/engine/tabsqlitedb.py 2020-07-22 11:52:11.651532112 +0200 ++++ ibus-table-1.9.18/engine/tabsqlitedb.py 2020-07-22 14:43:51.907260935 +0200 +@@ -1047,6 +1047,8 @@ + traceback.print_exc () + + def init_user_db(self, db_file): ++ if db_file == ':memory:': ++ return + if not path.exists(db_file): + db = sqlite3.connect(db_file) + # 20000 pages should be enough to cache the whole database +diff -Nru ibus-table-1.9.18.orig/engine/tabsqlitedb.py.orig ibus-table-1.9.18/engine/tabsqlitedb.py.orig +--- ibus-table-1.9.18.orig/engine/tabsqlitedb.py.orig 1970-01-01 01:00:00.000000000 +0100 ++++ ibus-table-1.9.18/engine/tabsqlitedb.py.orig 2020-07-22 11:52:11.651532112 +0200 +@@ -0,0 +1,1433 @@ ++# -*- coding: utf-8 -*- ++# vim:et sts=4 sw=4 ++# ++# ibus-table - The Tables engine for IBus ++# ++# Copyright (c) 2008-2009 Yu Yuwei ++# Copyright (c) 2009-2014 Caius "kaio" CHANCE ++# Copyright (c) 2012-2015 Mike FABIAN ++# ++# This library is free software; you can redistribute it and/or ++# modify it under the terms of the GNU Lesser General Public ++# License as published by the Free Software Foundation; either ++# version 2.1 of the License, or (at your option) any later version. ++# ++# This library is distributed in the hope that it will be useful, ++# but WITHOUT ANY WARRANTY; without even the implied warranty of ++# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ++# Lesser General Public License for more details. ++# ++# You should have received a copy of the GNU Lesser General Public ++# License along with this library; if not, write to the Free Software ++# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA ++# ++ ++import sys ++if sys.version_info < (3, 0, 0): ++ reload (sys) ++ sys.setdefaultencoding('utf-8') ++import os ++import os.path as path ++import shutil ++import sqlite3 ++import uuid ++import time ++import re ++import chinese_variants ++ ++debug_level = int(0) ++ ++database_version = '1.00' ++ ++patt_r = re.compile(r'c([ea])(\d):(.*)') ++patt_p = re.compile(r'p(-{0,1}\d)(-{0,1}\d)') ++ ++chinese_nocheck_chars = u"“”‘’《》〈〉〔〕「」『』【】〖〗()[]{}"\ ++ u".。,、;:?!…—·ˉˇ¨々~‖∶"'`|"\ ++ u"⒈⒉⒊⒋⒌⒍⒎⒏⒐⒑⒒⒓⒔⒕⒖⒗⒘⒙⒚⒛"\ ++ u"АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯЁ"\ ++ u"ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅪⅫ"\ ++ u"⒈⒉⒊⒋⒌⒍⒎⒏⒐⒑⒒⒓⒔⒕⒖⒗⒘⒙⒚⒛"\ ++ u"㎎㎏㎜㎝㎞㎡㏄㏎㏑㏒㏕"\ ++ u"ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩ"\ ++ u"⑴⑵⑶⑷⑸⑹⑺⑻⑼⑽⑾⑿⒀⒁⒂⒃⒄⒅⒆⒇"\ ++ u"€$¢£¥"\ ++ u"¤→↑←↓↖↗↘↙"\ ++ u"ァアィイゥウェエォオカガキギクグケゲコゴサザシジ"\ ++ u"スズセゼソゾタダチヂッツヅテデトドナニヌネノハバパ"\ ++ u"ヒビピフブプヘベペホボポマミムメモャヤュユョヨラ"\ ++ u"リルレロヮワヰヱヲンヴヵヶーヽヾ"\ ++ u"ぁあぃいぅうぇえぉおかがきぎぱくぐけげこごさざしじ"\ ++ u"すずせぜそぞただちぢっつづてでとどなにぬねのはば"\ ++ u"ひびぴふぶぷへべぺほぼぽまみむめもゃやゅゆょよらり"\ ++ u"るれろゎわゐゑをん゛゜ゝゞ"\ ++ u"勹灬冫艹屮辶刂匚阝廾丨虍彐卩钅冂冖宀疒肀丿攵凵犭"\ ++ u"亻彡饣礻扌氵纟亠囗忄讠衤廴尢夂丶"\ ++ u"āáǎàōóǒòêēéěèīíǐìǖǘǚǜüūúǔù"\ ++ u"+-<=>±×÷∈∏∑∕√∝∞∟∠∣∥∧∨∩∪∫∮"\ ++ u"∴∵∶∷∽≈≌≒≠≡≤≥≦≧≮≯⊕⊙⊥⊿℃°‰"\ ++ u"♂♀§№☆★○●◎◇◆□■△▲※〓#&@\^_ ̄"\ ++ u"абвгдежзийклмнопрстуфхцчшщъыьэюяё"\ ++ u"ⅰⅱⅲⅳⅴⅵⅶⅷⅸⅹβγδεζηαικλμνξοπρστυφθψω"\ ++ u"①②③④⑤⑥⑦⑧⑨⑩①②③④⑤⑥⑦⑧⑨⑩"\ ++ u"㈠㈡㈢㈣㈤㈥㈦㈧㈨㈩㈠㈡㈢㈣㈤㈥㈦㈧㈨㈩"\ ++ u"ㄅㄆㄇㄈㄉㄊㄋㄌㄍㄎㄏㄐㄑㄒㄓㄔㄕㄖㄗㄘㄙㄧㄨㄩ"\ ++ u"ㄚㄛㄜㄝㄞㄟㄠㄡㄢㄣㄤㄥㄦ" ++ ++class ImeProperties: ++ def __init__(self, db=None, default_properties={}): ++ ''' ++ “db” is the handle of the sqlite3 database file obtained by ++ sqlite3.connect(). ++ ''' ++ if not db: ++ return None ++ self.ime_property_cache = default_properties ++ sqlstr = 'SELECT attr, val FROM main.ime;' ++ try: ++ results = db.execute(sqlstr).fetchall() ++ except: ++ import traceback ++ traceback.print_exc() ++ for result in results: ++ self.ime_property_cache[result[0]] = result[1] ++ ++ def get(self, key): ++ if key in self.ime_property_cache: ++ return self.ime_property_cache[key] ++ else: ++ return None ++ ++class tabsqlitedb: ++ '''Phrase database for tables ++ ++ The phrases table in the database has columns with the names: ++ ++ “id”, “tabkeys”, “phrase”, “freq”, “user_freq” ++ ++ There are 2 databases, sysdb, userdb. ++ ++ sysdb: System database for the input method, for example something ++ like /usr/share/ibus-table/tables/wubi-jidian86.db ++ “user_freq” is always 0 in a system database. “freq” ++ is some number in a system database indicating a frequency ++ of use of that phrase relative to the other phrases in that ++ database. ++ ++ user_db: Database on disk where the phrases used or defined by the ++ user are stored. “user_freq” is a counter which counts how ++ many times that combination of “tabkeys” and “phrase” has ++ been used. “freq” is equal to 0 for all combinations of ++ “tabkeys” and “phrase” where an entry for that phrase is ++ already in the system database which starts with the same ++ “tabkeys”. ++ For combinations of “tabkeys” and “phrase” which do not exist ++ at all in the system database, “freq” is equal to -1 to ++ indidated that this is a user defined phrase. ++ ''' ++ def __init__( ++ self, filename = None, user_db = None, create_database = False): ++ global debug_level ++ try: ++ debug_level = int(os.getenv('IBUS_TABLE_DEBUG_LEVEL')) ++ except (TypeError, ValueError): ++ debug_level = int(0) ++ self.old_phrases = [] ++ self.filename = filename ++ self._user_db = user_db ++ self.reset_phrases_cache() ++ ++ if create_database or os.path.isfile(self.filename): ++ self.db = sqlite3.connect(self.filename) ++ else: ++ print('Cannot open database file %s' %self.filename) ++ try: ++ self.db.execute('PRAGMA encoding = "UTF-8";') ++ self.db.execute('PRAGMA case_sensitive_like = true;') ++ self.db.execute('PRAGMA page_size = 4096;') ++ # 20000 pages should be enough to cache the whole database ++ self.db.execute('PRAGMA cache_size = 20000;') ++ self.db.execute('PRAGMA temp_store = MEMORY;') ++ self.db.execute('PRAGMA journal_size_limit = 1000000;') ++ self.db.execute('PRAGMA synchronous = NORMAL;') ++ except: ++ import traceback ++ traceback.print_exc() ++ print('Error while initializing database.') ++ # create IME property table ++ self.db.executescript( ++ 'CREATE TABLE IF NOT EXISTS main.ime (attr TEXT, val TEXT);') ++ # Initalize missing attributes in the ime table with some ++ # default values, they should be updated using the attributes ++ # found in the source when creating a system database with ++ # tabcreatedb.py ++ self._default_ime_attributes = { ++ 'name':'', ++ 'name.zh_cn':'', ++ 'name.zh_hk':'', ++ 'name.zh_tw':'', ++ 'author':'somebody', ++ 'uuid':'%s' % uuid.uuid4(), ++ 'serial_number':'%s' % time.strftime('%Y%m%d'), ++ 'icon':'ibus-table.svg', ++ 'license':'LGPL', ++ 'languages':'', ++ 'language_filter':'', ++ 'valid_input_chars':'abcdefghijklmnopqrstuvwxyz', ++ 'max_key_length':'4', ++ 'commit_keys':'space', ++ # 'forward_keys':'Return', ++ 'select_keys':'1,2,3,4,5,6,7,8,9,0', ++ 'page_up_keys':'Page_Up,minus', ++ 'page_down_keys':'Page_Down,equal', ++ 'status_prompt':'', ++ 'def_full_width_punct':'true', ++ 'def_full_width_letter':'false', ++ 'user_can_define_phrase':'false', ++ 'pinyin_mode':'false', ++ 'dynamic_adjust':'false', ++ 'auto_select':'false', ++ 'auto_commit':'false', ++ # 'no_check_chars':u'', ++ 'description':'A IME under IBus Table', ++ 'layout':'us', ++ 'symbol':'', ++ 'rules':'', ++ 'least_commit_length':'0', ++ 'start_chars':'', ++ 'orientation':'true', ++ 'always_show_lookup':'true', ++ 'char_prompts':'{}' ++ # we use this entry for those IME, which don't ++ # have rules to build up phrase, but still need ++ # auto commit to preedit ++ } ++ if create_database: ++ select_sqlstr = ''' ++ SELECT val FROM main.ime WHERE attr = :attr;''' ++ insert_sqlstr = ''' ++ INSERT INTO main.ime (attr, val) VALUES (:attr, :val);''' ++ for attr in self._default_ime_attributes: ++ sqlargs = { ++ 'attr': attr, ++ 'val': self._default_ime_attributes[attr] ++ } ++ if not self.db.execute(select_sqlstr, sqlargs).fetchall(): ++ self.db.execute(insert_sqlstr, sqlargs) ++ self.ime_properties = ImeProperties( ++ db=self.db, ++ default_properties=self._default_ime_attributes) ++ # shared variables in this class: ++ self._mlen = int(self.ime_properties.get("max_key_length")) ++ self._is_chinese = self.is_chinese() ++ self._is_cjk = self.is_cjk() ++ self.user_can_define_phrase = self.ime_properties.get( ++ 'user_can_define_phrase') ++ if self.user_can_define_phrase: ++ if self.user_can_define_phrase.lower() == u'true' : ++ self.user_can_define_phrase = True ++ else: ++ self.user_can_define_phrase = False ++ else: ++ print( ++ 'Could not find "user_can_define_phrase" entry from database, ' ++ + 'is it an outdated database?') ++ self.user_can_define_phrase = False ++ ++ self.dynamic_adjust = self.ime_properties.get('dynamic_adjust') ++ if self.dynamic_adjust: ++ if self.dynamic_adjust.lower() == u'true' : ++ self.dynamic_adjust = True ++ else: ++ self.dynamic_adjust = False ++ else: ++ print( ++ 'Could not find "dynamic_adjust" entry from database, ' ++ + 'is it an outdated database?') ++ self.dynamic_adjust = False ++ ++ self.rules = self.get_rules () ++ self.possible_tabkeys_lengths = self.get_possible_tabkeys_lengths() ++ self.startchars = self.get_start_chars () ++ ++ if not user_db or create_database: ++ # No user database requested or we are ++ # just creating the system database and ++ # we do not need a user database for that ++ return ++ ++ if user_db != ":memory:": ++ # Do not move this import to the beginning of this script! ++ # If for example the home directory is not writeable, ++ # ibus_table_location.py would fail because it cannot ++ # create some directories. ++ # ++ # But for tabcreatedb.py, no such directories are needed, ++ # tabcreatedb.py should not fail just because ++ # ibus_table_location.py cannot create some directories. ++ # ++ # “HOME=/foobar ibus-table-createdb” should not fail if ++ # “/foobar” is not writeable. ++ import ibus_table_location ++ tables_path = path.join(ibus_table_location.data_home(), "tables") ++ if not path.isdir(tables_path): ++ old_tables_path = os.path.join( ++ os.getenv('HOME'), '.ibus/tables') ++ if path.isdir(old_tables_path): ++ if os.access(os.path.join( ++ old_tables_path, 'debug.log'), os.F_OK): ++ os.unlink(os.path.join(old_tables_path, 'debug.log')) ++ if os.access(os.path.join( ++ old_tables_path, 'setup-debug.log'), os.F_OK): ++ os.unlink(os.path.join( ++ old_tables_path, 'setup-debug.log')) ++ shutil.copytree(old_tables_path, tables_path) ++ shutil.rmtree(old_tables_path) ++ os.symlink(tables_path, old_tables_path) ++ else: ++ os.makedirs(tables_path) ++ user_db = path.join(tables_path, user_db) ++ if not path.exists(user_db): ++ sys.stderr.write( ++ 'The user database %(udb)s does not exist yet.\n' ++ % {'udb': user_db}) ++ else: ++ try: ++ desc = self.get_database_desc(user_db) ++ phrase_table_column_names = [ ++ 'id', 'tabkeys', 'phrase','freq','user_freq'] ++ if (desc == None ++ or desc["version"] != database_version ++ or (self.get_number_of_columns_of_phrase_table(user_db) ++ != len(phrase_table_column_names))): ++ sys.stderr.write( ++ 'The user database %s seems to be incompatible.\n' ++ % user_db) ++ if desc == None: ++ sys.stderr.write( ++ 'There is no version information in ' ++ + 'the database.\n') ++ self.old_phrases = self.extract_user_phrases( ++ user_db, old_database_version = '0.0') ++ elif desc["version"] != database_version: ++ sys.stderr.write( ++ 'The version of the database does not match ' ++ + '(too old or too new?).\n' ++ 'ibus-table wants version=%s\n' ++ % database_version ++ + 'But the database actually has version=%s\n' ++ % desc['version']) ++ self.old_phrases = self.extract_user_phrases( ++ user_db, old_database_version = desc['version']) ++ elif (self.get_number_of_columns_of_phrase_table( ++ user_db) ++ != len(phrase_table_column_names)): ++ sys.stderr.write( ++ 'The number of columns of the database ' ++ + 'does not match.\n' ++ + 'ibus-table expects %s columns.\n' ++ % len(phrase_table_column_names) ++ + 'But the database actually has %s columns.\n' ++ % self.get_number_of_columns_of_phrase_table( ++ user_db) ++ + 'But the versions of the databases are ' ++ + 'identical.\n' ++ + 'This should never happen!\n') ++ self.old_phrases = None ++ from time import strftime ++ timestamp = strftime('-%Y-%m-%d_%H:%M:%S') ++ sys.stderr.write( ++ 'Renaming the incompatible database to "%s".\n' ++ % user_db+timestamp) ++ if os.path.exists(user_db): ++ os.rename(user_db, user_db+timestamp) ++ if os.path.exists(user_db+'-shm'): ++ os.rename(user_db+'-shm', user_db+'-shm'+timestamp) ++ if os.path.exists(user_db+'-wal'): ++ os.rename(user_db+'-wal', user_db+'-wal'+timestamp) ++ sys.stderr.write( ++ 'Creating a new, empty database "s".\n' ++ % user_db) ++ self.init_user_db(user_db) ++ sys.stderr.write( ++ 'If user phrases were successfully recovered from ' ++ + 'the old,\n' ++ + 'incompatible database, they will be used to ' ++ + 'initialize the new database.\n') ++ else: ++ sys.stderr.write( ++ 'Compatible database %s found.\n' % user_db) ++ except: ++ import traceback ++ traceback.print_exc() ++ ++ # open user phrase database ++ try: ++ sys.stderr.write( ++ 'Connect to the database %(name)s.\n' %{'name': user_db}) ++ self.db.executescript(''' ++ ATTACH DATABASE "%s" AS user_db; ++ PRAGMA user_db.encoding = "UTF-8"; ++ PRAGMA user_db.case_sensitive_like = true; ++ PRAGMA user_db.page_size = 4096; ++ PRAGMA user_db.cache_size = 20000; ++ PRAGMA user_db.temp_store = MEMORY; ++ PRAGMA user_db.journal_mode = WAL; ++ PRAGMA user_db.journal_size_limit = 1000000; ++ PRAGMA user_db.synchronous = NORMAL; ++ ''' % user_db) ++ except: ++ sys.stderr.write('Could not open the database %s.\n' % user_db) ++ from time import strftime ++ timestamp = strftime('-%Y-%m-%d_%H:%M:%S') ++ sys.stderr.write('Renaming the incompatible database to "%s".\n' ++ % user_db+timestamp) ++ if os.path.exists(user_db): ++ os.rename(user_db, user_db+timestamp) ++ if os.path.exists(user_db+'-shm'): ++ os.rename(user_db+'-shm', user_db+'-shm'+timestamp) ++ if os.path.exists(user_db+'-wal'): ++ os.rename(user_db+'-wal', user_db+'-wal'+timestamp) ++ sys.stderr.write('Creating a new, empty database "%s".\n' ++ % user_db) ++ self.init_user_db(user_db) ++ self.db.executescript(''' ++ ATTACH DATABASE "%s" AS user_db; ++ PRAGMA user_db.encoding = "UTF-8"; ++ PRAGMA user_db.case_sensitive_like = true; ++ PRAGMA user_db.page_size = 4096; ++ PRAGMA user_db.cache_size = 20000; ++ PRAGMA user_db.temp_store = MEMORY; ++ PRAGMA user_db.journal_mode = WAL; ++ PRAGMA user_db.journal_size_limit = 1000000; ++ PRAGMA user_db.synchronous = NORMAL; ++ ''' % user_db) ++ self.create_tables("user_db") ++ if self.old_phrases: ++ sqlargs = [] ++ for x in self.old_phrases: ++ sqlargs.append( ++ {'tabkeys': x[0], ++ 'phrase': x[1], ++ 'freq': x[2], ++ 'user_freq': x[3]}) ++ sqlstr = ''' ++ INSERT INTO user_db.phrases (tabkeys, phrase, freq, user_freq) ++ VALUES (:tabkeys, :phrase, :freq, :user_freq) ++ ''' ++ try: ++ self.db.executemany(sqlstr, sqlargs) ++ except: ++ import traceback ++ traceback.print_exec() ++ self.db.commit () ++ self.db.execute('PRAGMA wal_checkpoint;') ++ ++ # try create all tables in user database ++ self.create_indexes ("user_db", commit=False) ++ self.generate_userdb_desc () ++ ++ def update_phrase( ++ self, tabkeys=u'', phrase=u'', ++ user_freq=0, database='user_db', commit=True): ++ '''update phrase freqs''' ++ if debug_level > 1: ++ sys.stderr.write( ++ 'update_phrase() tabkeys=%(t)s phrase=%(p)s ' ++ % {'t': tabkeys, 'p': phrase} ++ + 'user_freq=%(u)s database=%(d)s\n' ++ % {'u': user_freq, 'd': database}) ++ if not tabkeys or not phrase: ++ return ++ sqlstr = ''' ++ UPDATE %s.phrases SET user_freq = :user_freq ++ WHERE tabkeys = :tabkeys AND phrase = :phrase ++ ;''' % database ++ sqlargs = {'user_freq': user_freq, 'tabkeys': tabkeys, 'phrase': phrase} ++ try: ++ self.db.execute(sqlstr, sqlargs) ++ if commit: ++ self.db.commit() ++ self.invalidate_phrases_cache(tabkeys) ++ except: ++ import traceback ++ traceback.print_exc() ++ ++ def sync_usrdb (self): ++ ''' ++ Trigger a checkpoint operation. ++ ''' ++ if self._user_db is None: ++ return ++ self.db.commit() ++ self.db.execute('PRAGMA wal_checkpoint;') ++ ++ def reset_phrases_cache (self): ++ self._phrases_cache = {} ++ ++ def invalidate_phrases_cache (self, tabkeys=u''): ++ for i in range(1, self._mlen + 1): ++ if self._phrases_cache.get(tabkeys[0:i]): ++ self._phrases_cache.pop(tabkeys[0:i]) ++ ++ def is_chinese (self): ++ __lang = self.ime_properties.get('languages') ++ if __lang: ++ __langs = __lang.split(',') ++ for _l in __langs: ++ if _l.lower().find('zh') != -1: ++ return True ++ return False ++ ++ def is_cjk(self): ++ languages = self.ime_properties.get('languages') ++ if languages: ++ languages = languages.split(',') ++ for language in languages: ++ for lang in ['zh', 'ja', 'ko']: ++ if language.strip().startswith(lang): ++ return True ++ return False ++ ++ def get_chinese_mode (self): ++ try: ++ __dict = {'cm0':0, 'cm1':1, 'cm2':2, 'cm3':3, 'cm4':4} ++ __filt = self.ime_properties.get('language_filter') ++ return __dict[__filt] ++ except: ++ return -1 ++ ++ def get_select_keys (self): ++ ret = self.ime_properties.get("select_keys") ++ if ret: ++ return ret ++ return "1,2,3,4,5,6,7,8,9,0" ++ ++ def get_orientation (self): ++ try: ++ return int(self.ime_properties.get('orientation')) ++ except: ++ return 1 ++ ++ def create_tables (self, database): ++ '''Create tables that contain all phrase''' ++ if database == 'main': ++ sqlstr = ''' ++ CREATE TABLE IF NOT EXISTS %s.goucima ++ (zi TEXT PRIMARY KEY, goucima TEXT); ++ ''' % database ++ self.db.execute (sqlstr) ++ sqlstr = ''' ++ CREATE TABLE IF NOT EXISTS %s.pinyin ++ (pinyin TEXT, zi TEXT, freq INTEGER); ++ ''' % database ++ self.db.execute(sqlstr) ++ ++ sqlstr = ''' ++ CREATE TABLE IF NOT EXISTS %s.phrases ++ (id INTEGER PRIMARY KEY, tabkeys TEXT, phrase TEXT, ++ freq INTEGER, user_freq INTEGER); ++ ''' % database ++ self.db.execute (sqlstr) ++ self.db.commit() ++ ++ def update_ime (self, attrs): ++ '''Update or insert attributes in ime table, attrs is a iterable object ++ Like [(attr,val), (attr,val), ...] ++ ++ This is called only by tabcreatedb.py. ++ ''' ++ select_sqlstr = 'SELECT val from main.ime WHERE attr = :attr' ++ update_sqlstr = 'UPDATE main.ime SET val = :val WHERE attr = :attr;' ++ insert_sqlstr = 'INSERT INTO main.ime (attr, val) VALUES (:attr, :val);' ++ for attr, val in attrs: ++ sqlargs = {'attr': attr, 'val': val} ++ if self.db.execute(select_sqlstr, sqlargs).fetchall(): ++ self.db.execute(update_sqlstr, sqlargs) ++ else: ++ self.db.execute(insert_sqlstr, sqlargs) ++ self.db.commit() ++ # update ime properties cache: ++ self.ime_properties = ImeProperties( ++ db=self.db, ++ default_properties=self._default_ime_attributes) ++ # The self variables used by tabcreatedb.py need to be updated now: ++ self._mlen = int(self.ime_properties.get('max_key_length')) ++ self._is_chinese = self.is_chinese() ++ self.user_can_define_phrase = self.ime_properties.get( ++ 'user_can_define_phrase') ++ if self.user_can_define_phrase: ++ if self.user_can_define_phrase.lower() == u'true' : ++ self.user_can_define_phrase = True ++ else: ++ self.user_can_define_phrase = False ++ else: ++ print( ++ 'Could not find "user_can_define_phrase" entry from database, ' ++ + 'is it a outdated database?') ++ self.user_can_define_phrase = False ++ self.rules = self.get_rules() ++ ++ def get_rules (self): ++ '''Get phrase construct rules''' ++ rules = {} ++ if self.user_can_define_phrase: ++ try: ++ _rules = self.ime_properties.get('rules') ++ if _rules: ++ _rules = _rules.strip().split(';') ++ for rule in _rules: ++ res = patt_r.match (rule) ++ if res: ++ cms = [] ++ if res.group(1) == 'a': ++ rules['above'] = int(res.group(2)) ++ _cms = res.group(3).split('+') ++ if len(_cms) > self._mlen: ++ print('rule: "%s" over max key length' %rule) ++ break ++ for _cm in _cms: ++ cm_res = patt_p.match(_cm) ++ cms.append((int(cm_res.group(1)), ++ int(cm_res.group(2)))) ++ rules[int(res.group(2))]=cms ++ else: ++ print('not a legal rule: "%s"' %rule) ++ except Exception: ++ import traceback ++ traceback.print_exc () ++ return rules ++ else: ++ return "" ++ ++ def get_possible_tabkeys_lengths(self): ++ '''Return a list of the possible lengths for tabkeys in this table. ++ ++ Example: ++ ++ If the table source has rules like: ++ ++ RULES = ce2:p11+p12+p21+p22;ce3:p11+p21+p22+p31;ca4:p11+p21+p31+p41 ++ ++ self._rules will be set to ++ ++ self._rules={2: [(1, 1), (1, 2), (2, 1), (2, 2)], 3: [(1, 1), (1, 2), (2, 1), (3, 1)], 4: [(1, 1), (2, 1), (3, 1), (-1, 1)], 'above': 4} ++ ++ and then this function returns “[4, 4, 4]” ++ ++ Or, if the table source has no RULES but LEAST_COMMIT_LENGTH=2 ++ and MAX_KEY_LENGTH = 4, then it returns “[2, 3, 4]” ++ ++ I cannot find any tables which use LEAST_COMMIT_LENGTH though. ++ ''' ++ if self.rules: ++ max_len = self.rules["above"] ++ return [len(self.rules[x]) for x in range(2, max_len+1)][:] ++ else: ++ try: ++ least_commit_len = int( ++ self.ime_properties.get('least_commit_length')) ++ except: ++ least_commit_len = 0 ++ if least_commit_len > 0: ++ return list(range(least_commit_len, self._mlen + 1)) ++ else: ++ return [] ++ ++ def get_start_chars (self): ++ '''return possible start chars of IME''' ++ return self.ime_properties.get('start_chars') ++ ++ def get_no_check_chars (self): ++ '''Get the characters which engine should not change freq''' ++ _chars = self.ime_properties.get('no_check_chars') ++ if type(_chars) != type(u''): ++ _chars = _chars.decode('utf-8') ++ return _chars ++ ++ def add_phrases (self, phrases, database = 'main'): ++ '''Add many phrases to database fast. Used by tabcreatedb.py when ++ creating the system database from scratch. ++ ++ “phrases” is a iterable object which looks like: ++ ++ [(tabkeys, phrase, freq ,user_freq), (tabkeys, phrase, freq, user_freq), ...] ++ ++ This function does not check whether phrases are already ++ there. As this function is only used while creating the ++ system database, it is not really necessary to check whether ++ phrases are already there because the database is initially ++ empty anyway. And the caller should take care that the ++ “phrases” argument does not contain duplicates. ++ ++ ''' ++ if debug_level > 1: ++ sys.stderr.write("add_phrases() len(phrases)=%s\n" ++ %len(phrases)) ++ insert_sqlstr = ''' ++ INSERT INTO %(database)s.phrases ++ (tabkeys, phrase, freq, user_freq) ++ VALUES (:tabkeys, :phrase, :freq, :user_freq); ++ ''' % {'database': database} ++ insert_sqlargs = [] ++ for (tabkeys, phrase, freq, user_freq) in phrases: ++ insert_sqlargs.append({ ++ 'tabkeys': tabkeys, ++ 'phrase': phrase, ++ 'freq': freq, ++ 'user_freq': user_freq}) ++ self.invalidate_phrases_cache(tabkeys) ++ self.db.executemany(insert_sqlstr, insert_sqlargs) ++ self.db.commit() ++ self.db.execute('PRAGMA wal_checkpoint;') ++ ++ def add_phrase( ++ self, tabkeys=u'', phrase=u'', freq=0, user_freq=0, ++ database='main',commit=True): ++ '''Add phrase to database, phrase is a object of ++ (tabkeys, phrase, freq ,user_freq) ++ ''' ++ if debug_level > 1: ++ sys.stderr.write( ++ 'add_phrase tabkeys=%(t)s phrase=%(p)s ' ++ % {'t': tabkeys, 'p': phrase} ++ + 'freq=%(f)s user_freq=%(u)s\n' ++ % {'f': freq, 'u': user_freq}) ++ if not tabkeys or not phrase: ++ return ++ select_sqlstr = ''' ++ SELECT * FROM %(database)s.phrases ++ WHERE tabkeys = :tabkeys AND phrase = :phrase; ++ ''' % {'database': database} ++ select_sqlargs = {'tabkeys': tabkeys, 'phrase': phrase} ++ results = self.db.execute(select_sqlstr, select_sqlargs).fetchall() ++ if results: ++ # there is already such a phrase, i.e. add_phrase was called ++ # in error, do nothing to avoid duplicate entries. ++ if debug_level > 1: ++ sys.stderr.write( ++ 'add_phrase() ' ++ + 'select_sqlstr=%(sql)s select_sqlargs=%(arg)s ' ++ % {'sql': select_sqlstr, 'arg': select_sqlargs} ++ + 'already there!: results=%(r)s \n' ++ % {'r': results}) ++ return ++ ++ insert_sqlstr = ''' ++ INSERT INTO %(database)s.phrases ++ (tabkeys, phrase, freq, user_freq) ++ VALUES (:tabkeys, :phrase, :freq, :user_freq); ++ ''' % {'database': database} ++ insert_sqlargs = { ++ 'tabkeys': tabkeys, ++ 'phrase': phrase, ++ 'freq': freq, ++ 'user_freq': user_freq} ++ if debug_level > 1: ++ sys.stderr.write( ++ 'add_phrase() insert_sqlstr=%(sql)s insert_sqlargs=%(arg)s\n' ++ % {'sql': insert_sqlstr, 'arg': insert_sqlargs}) ++ try: ++ self.db.execute (insert_sqlstr, insert_sqlargs) ++ if commit: ++ self.db.commit() ++ self.invalidate_phrases_cache(tabkeys) ++ except: ++ import traceback ++ traceback.print_exc() ++ ++ def add_goucima (self, goucimas): ++ '''Add goucima into database, goucimas is iterable object ++ Like goucimas = [(zi,goucima), (zi,goucima), ...] ++ ''' ++ sqlstr = ''' ++ INSERT INTO main.goucima (zi, goucima) VALUES (:zi, :goucima); ++ ''' ++ sqlargs = [] ++ for zi, goucima in goucimas: ++ sqlargs.append({'zi': zi, 'goucima': goucima}) ++ try: ++ self.db.commit() ++ self.db.executemany(sqlstr, sqlargs) ++ self.db.commit() ++ self.db.execute('PRAGMA wal_checkpoint;') ++ except: ++ import traceback ++ traceback.print_exc() ++ ++ def add_pinyin (self, pinyins, database = 'main'): ++ '''Add pinyin to database, pinyins is a iterable object ++ Like: [(zi,pinyin, freq), (zi, pinyin, freq), ...] ++ ''' ++ sqlstr = ''' ++ INSERT INTO %s.pinyin (pinyin, zi, freq) VALUES (:pinyin, :zi, :freq); ++ ''' % database ++ count = 0 ++ for pinyin, zi, freq in pinyins: ++ count += 1 ++ pinyin = pinyin.replace( ++ '1','!').replace( ++ '2','@').replace( ++ '3','#').replace( ++ '4','$').replace( ++ '5','%') ++ try: ++ self.db.execute( ++ sqlstr, {'pinyin': pinyin, 'zi': zi, 'freq': freq}) ++ except Exception: ++ sys.stderr.write( ++ 'Error when inserting into pinyin table. ' ++ + 'count=%(c)s pinyin=%(p)s zi=%(z)s freq=%(f)s\n' ++ % {'c': count, 'p': pinyin, 'z': zi, 'f': freq}) ++ import traceback ++ traceback.print_exc() ++ self.db.commit() ++ ++ def optimize_database (self, database='main'): ++ sqlstr = ''' ++ CREATE TABLE tmp AS SELECT * FROM %(database)s.phrases; ++ DELETE FROM %(database)s.phrases; ++ INSERT INTO %(database)s.phrases SELECT * FROM tmp ORDER BY ++ tabkeys ASC, phrase ASC, user_freq DESC, freq DESC, id ASC; ++ DROP TABLE tmp; ++ CREATE TABLE tmp AS SELECT * FROM %(database)s.goucima; ++ DELETE FROM %(database)s.goucima; ++ INSERT INTO %(database)s.goucima SELECT * FROM tmp ORDER BY zi, goucima; ++ DROP TABLE tmp; ++ CREATE TABLE tmp AS SELECT * FROM %(database)s.pinyin; ++ DELETE FROM %(database)s.pinyin; ++ INSERT INTO %(database)s.pinyin SELECT * FROM tmp ORDER BY pinyin ASC, freq DESC; ++ DROP TABLE tmp; ++ ''' % {'database':database} ++ self.db.executescript (sqlstr) ++ self.db.executescript ("VACUUM;") ++ self.db.commit() ++ ++ def drop_indexes(self, database): ++ '''Drop the indexes in the database to reduce its size ++ ++ We do not use any indexes at the moment, therefore this ++ function does nothing. ++ ''' ++ if debug_level > 1: ++ sys.stderr.write("drop_indexes()\n") ++ return ++ ++ def create_indexes(self, database, commit=True): ++ '''Create indexes for the database. ++ ++ We do not use any indexes at the moment, therefore ++ this function does nothing. We used indexes before, ++ but benchmarking showed that none of them was really ++ speeding anything up, therefore we deleted all of them ++ to get much smaller databases (about half the size). ++ ++ If some index turns out to be very useful in future, it could ++ be created here (and dropped in “drop_indexes()”). ++ ''' ++ if debug_level > 1: ++ sys.stderr.write("create_indexes()\n") ++ return ++ ++ def big5_code(self, phrase): ++ try: ++ big5 = phrase.encode('Big5') ++ except: ++ big5 = b'\xff\xff' # higher than any Big5 code ++ return big5 ++ ++ def best_candidates( ++ self, typed_tabkeys=u'', candidates=[], chinese_mode=-1): ++ ''' ++ “candidates” is an array containing something like: ++ [(tabkeys, phrase, freq, user_freq), ...] ++ ++ “typed_tabkeys” is key sequence the user really typed, which ++ maybe only the beginning part of the “tabkeys” in a matched ++ candidate. ++ ''' ++ maximum_number_of_candidates = 100 ++ engine_name = os.path.basename(self.filename).replace('.db', '') ++ if engine_name in [ ++ 'cangjie3', 'cangjie5', 'cangjie-big', ++ 'quick-classic', 'quick3', 'quick5']: ++ code_point_function = self.big5_code ++ else: ++ code_point_function = lambda x: (1) ++ if chinese_mode in (2, 3) and self._is_chinese: ++ if chinese_mode == 2: ++ bitmask = (1 << 0) # used in simplified Chinese ++ else: ++ bitmask = (1 << 1) # used in traditional Chinese ++ return sorted(candidates, ++ key=lambda x: ( ++ - int( ++ typed_tabkeys == x[0] ++ ), # exact matches first! ++ -1*x[3], # user_freq descending ++ # Prefer characters used in the ++ # desired Chinese variant: ++ -(bitmask ++ & chinese_variants.detect_chinese_category( ++ x[1])), ++ -1*x[2], # freq descending ++ len(x[0]), # len(tabkeys) ascending ++ x[0], # tabkeys alphabetical ++ code_point_function(x[1][0]), ++ # Unicode codepoint of first character of phrase: ++ ord(x[1][0]) ++ ))[:maximum_number_of_candidates] ++ return sorted(candidates, ++ key=lambda x: ( ++ - int( ++ typed_tabkeys == x[0] ++ ), # exact matches first! ++ -1*x[3], # user_freq descending ++ -1*x[2], # freq descending ++ len(x[0]), # len(tabkeys) ascending ++ x[0], # tabkeys alphabetical ++ code_point_function(x[1][0]), ++ # Unicode codepoint of first character of phrase: ++ ord(x[1][0]) ++ ))[:maximum_number_of_candidates] ++ ++ def select_words( ++ self, tabkeys=u'', onechar=False, chinese_mode=-1, ++ single_wildcard_char=u'', multi_wildcard_char=u'', ++ auto_wildcard=False): ++ ''' ++ Get matching phrases for tabkeys from the database. ++ ''' ++ if not tabkeys: ++ return [] ++ # query phrases cache first ++ best = self._phrases_cache.get(tabkeys) ++ if best: ++ return best ++ one_char_condition = '' ++ if onechar: ++ # for some users really like to select only single characters ++ one_char_condition = ' AND length(phrase)=1 ' ++ ++ if self.user_can_define_phrase or self.dynamic_adjust: ++ sqlstr = ''' ++ SELECT tabkeys, phrase, freq, user_freq FROM ++ ( ++ SELECT tabkeys, phrase, freq, user_freq FROM main.phrases ++ WHERE tabkeys LIKE :tabkeys ESCAPE :escapechar %(one_char_condition)s ++ UNION ALL ++ SELECT tabkeys, phrase, freq, user_freq FROM user_db.phrases ++ WHERE tabkeys LIKE :tabkeys ESCAPE :escapechar %(one_char_condition)s ++ ) ++ ''' % {'one_char_condition': one_char_condition} ++ else: ++ sqlstr = ''' ++ SELECT tabkeys, phrase, freq, user_freq FROM main.phrases ++ WHERE tabkeys LIKE :tabkeys ESCAPE :escapechar %(one_char_condition)s ++ ''' % {'one_char_condition': one_char_condition} ++ escapechar = '☺' ++ for c in '!@#': ++ if c not in [single_wildcard_char, multi_wildcard_char]: ++ escapechar = c ++ tabkeys_for_like = tabkeys ++ tabkeys_for_like = tabkeys_for_like.replace( ++ escapechar, escapechar+escapechar) ++ if '%' not in [single_wildcard_char, multi_wildcard_char]: ++ tabkeys_for_like = tabkeys_for_like.replace('%', escapechar+'%') ++ if '_' not in [single_wildcard_char, multi_wildcard_char]: ++ tabkeys_for_like = tabkeys_for_like.replace('_', escapechar+'_') ++ if single_wildcard_char: ++ tabkeys_for_like = tabkeys_for_like.replace( ++ single_wildcard_char, '_') ++ if multi_wildcard_char: ++ tabkeys_for_like = tabkeys_for_like.replace( ++ multi_wildcard_char, '%%') ++ if auto_wildcard: ++ tabkeys_for_like += '%%' ++ sqlargs = {'tabkeys': tabkeys_for_like, 'escapechar': escapechar} ++ unfiltered_results = self.db.execute(sqlstr, sqlargs).fetchall() ++ bitmask = None ++ if chinese_mode == 0: ++ bitmask = (1 << 0) # simplified only ++ elif chinese_mode == 1: ++ bitmask = (1 << 1) # traditional only ++ if not bitmask: ++ results = unfiltered_results ++ else: ++ results = [] ++ for result in unfiltered_results: ++ if (bitmask ++ & chinese_variants.detect_chinese_category(result[1])): ++ results.append(result) ++ # merge matches from the system database and from the user ++ # database to avoid duplicates in the candidate list for ++ # example, if we have the result ('aaaa', '工', 551000000, 0) ++ # from the system database and ('aaaa', '工', 0, 5) from the ++ # user database, these should be merged into one match ++ # ('aaaa', '工', 551000000, 5). ++ phrase_frequencies = {} ++ for result in results: ++ key = (result[0], result[1]) ++ if key not in phrase_frequencies: ++ phrase_frequencies[key] = result ++ else: ++ phrase_frequencies.update([( ++ key, ++ key + ++ ( ++ max(result[2], phrase_frequencies[key][2]), ++ max(result[3], phrase_frequencies[key][3])) ++ )]) ++ best = self.best_candidates( ++ typed_tabkeys=tabkeys, ++ candidates=phrase_frequencies.values(), ++ chinese_mode=chinese_mode) ++ if debug_level > 1: ++ sys.stderr.write("select_words() best=%s\n" %repr(best)) ++ self._phrases_cache[tabkeys] = best ++ return best ++ ++ def select_chinese_characters_by_pinyin( ++ self, tabkeys=u'', chinese_mode=-1, single_wildcard_char=u'', ++ multi_wildcard_char=u''): ++ ''' ++ Get Chinese characters matching the pinyin given by tabkeys ++ from the database. ++ ''' ++ if not tabkeys: ++ return [] ++ sqlstr = ''' ++ SELECT pinyin, zi, freq FROM main.pinyin WHERE pinyin LIKE :tabkeys ++ ORDER BY freq DESC, pinyin ASC ++ ;''' ++ tabkeys_for_like = tabkeys ++ if single_wildcard_char: ++ tabkeys_for_like = tabkeys_for_like.replace( ++ single_wildcard_char, '_') ++ if multi_wildcard_char: ++ tabkeys_for_like = tabkeys_for_like.replace( ++ multi_wildcard_char, '%%') ++ tabkeys_for_like += '%%' ++ sqlargs = {'tabkeys': tabkeys_for_like} ++ results = self.db.execute(sqlstr, sqlargs).fetchall() ++ # now convert the results into a list of candidates in the format ++ # which was returned before I simplified the pinyin database table. ++ bitmask = None ++ if chinese_mode == 0: ++ bitmask = (1 << 0) # simplified only ++ elif chinese_mode == 1: ++ bitmask = (1 << 1) # traditional only ++ phrase_frequencies = [] ++ for (pinyin, zi, freq) in results: ++ if not bitmask: ++ phrase_frequencies.append(tuple([pinyin, zi, freq, 0])) ++ else: ++ if bitmask & chinese_variants.detect_chinese_category(zi): ++ phrase_frequencies.append(tuple([pinyin, zi, freq, 0])) ++ return self.best_candidates( ++ typed_tabkeys=tabkeys, ++ candidates=phrase_frequencies, ++ chinese_mode=chinese_mode) ++ ++ def generate_userdb_desc (self): ++ try: ++ sqlstring = ( ++ 'CREATE TABLE IF NOT EXISTS user_db.desc ' ++ + '(name PRIMARY KEY, value);') ++ self.db.executescript (sqlstring) ++ sqlstring = 'INSERT OR IGNORE INTO user_db.desc VALUES (?, ?);' ++ self.db.execute (sqlstring, ('version', database_version)) ++ sqlstring = ( ++ 'INSERT OR IGNORE INTO user_db.desc ' ++ + 'VALUES (?, DATETIME("now", "localtime"));') ++ self.db.execute (sqlstring, ("create-time", )) ++ self.db.commit () ++ except: ++ import traceback ++ traceback.print_exc () ++ ++ def init_user_db(self, db_file): ++ if not path.exists(db_file): ++ db = sqlite3.connect(db_file) ++ # 20000 pages should be enough to cache the whole database ++ db.executescript(''' ++ PRAGMA encoding = "UTF-8"; ++ PRAGMA case_sensitive_like = true; ++ PRAGMA page_size = 4096; ++ PRAGMA cache_size = 20000; ++ PRAGMA temp_store = MEMORY; ++ PRAGMA journal_mode = WAL; ++ PRAGMA journal_size_limit = 1000000; ++ PRAGMA synchronous = NORMAL; ++ ''') ++ db.commit() ++ ++ def get_database_desc (self, db_file): ++ if not path.exists (db_file): ++ return None ++ try: ++ db = sqlite3.connect (db_file) ++ desc = {} ++ for row in db.execute ("SELECT * FROM desc;").fetchall(): ++ desc [row[0]] = row[1] ++ db.close() ++ return desc ++ except: ++ return None ++ ++ def get_number_of_columns_of_phrase_table(self, db_file): ++ ''' ++ Get the number of columns in the 'phrases' table in ++ the database in db_file. ++ ++ Determines the number of columns by parsing this: ++ ++ sqlite> select sql from sqlite_master where name='phrases'; ++ CREATE TABLE phrases ++ (id INTEGER PRIMARY KEY, tabkeys TEXT, phrase TEXT, ++ freq INTEGER, user_freq INTEGER) ++ sqlite> ++ ++ This result could be on a single line, as above, or on multiple ++ lines. ++ ''' ++ if not path.exists (db_file): ++ return 0 ++ try: ++ db = sqlite3.connect (db_file) ++ tp_res = db.execute( ++ "select sql from sqlite_master where name='phrases';" ++ ).fetchall() ++ # Remove possible line breaks from the string where we ++ # want to match: ++ string = ' '.join(tp_res[0][0].splitlines()) ++ res = re.match(r'.*\((.*)\)', string) ++ if res: ++ tp = res.group(1).split(',') ++ return len(tp) ++ else: ++ return 0 ++ except: ++ return 0 ++ ++ def get_goucima (self, zi): ++ '''Get goucima of given character''' ++ if not zi: ++ return u'' ++ sqlstr = 'SELECT goucima FROM main.goucima WHERE zi = :zi;' ++ results = self.db.execute(sqlstr, {'zi': zi}).fetchall() ++ if results: ++ goucima = results[0][0] ++ else: ++ goucima = u'' ++ if debug_level > 1: ++ sys.stderr.write("get_goucima() goucima=%s\n" %goucima) ++ return goucima ++ ++ def parse_phrase (self, phrase): ++ '''Parse phrase to get its table code ++ ++ Example: ++ ++ Let’s assume we use wubi-jidian86. The rules in the source of ++ that table are: ++ ++ RULES = ce2:p11+p12+p21+p22;ce3:p11+p21+p31+p32;ca4:p11+p21+p31+p-11 ++ ++ “ce2” is a rule for phrases of length 2, “ce3” is a rule ++ for phrases of length 3, “ca4” is a rule for phrases of ++ length 4 *and* for all phrases with a length greater then ++ 4. “pnm” in such a rule means to use the n-th character of ++ the phrase and take the m-th character of the table code of ++ that character. I.e. “p-11” is the first character of the ++ table code of the last character in the phrase. ++ ++ Let’s assume the phrase is “天下大事”. The goucima (構詞碼 ++ = “word formation keys”) for these 4 characters are: ++ ++ character goucima ++ 天 gdi ++ 下 ghi ++ 大 dddd ++ 事 gkvh ++ ++ (If no special goucima are defined by the user, the longest ++ encoding for a single character in a table is the goucima for ++ that character). ++ ++ The length of the phrase “天下大事” is 4 characters, ++ therefore the rule ca4:p11+p21+p31+p-11 applies, i.e. the ++ table code for “天下大事” is calculated by using the first, ++ second, third and last character of the phrase and taking the ++ first character of the goucima for each of these. Therefore, ++ the table code for “天下大事” is “ggdg”. ++ ++ ''' ++ if debug_level > 1: ++ sys.stderr.write( ++ 'parse_phrase() phrase=%(p)s rules%(r)s\n' ++ % {'p': phrase, 'r': self.rules}) ++ if type(phrase) != type(u''): ++ phrase = phrase.decode('UTF-8') ++ # Shouldn’t this function try first whether the system database ++ # already has an entry for this phrase and if yes return it ++ # instead of constructing a new entry according to the rules? ++ # And construct a new entry only when no entry already exists ++ # in the system database?? ++ if len(phrase) == 0: ++ return u'' ++ if len(phrase) == 1: ++ return self.get_goucima(phrase) ++ if not self.rules: ++ return u'' ++ if len(phrase) in self.rules: ++ rule = self.rules[len(phrase)] ++ elif len(phrase) > self.rules['above']: ++ rule = self.rules[self.rules['above']] ++ else: ++ sys.stderr.write( ++ 'No rule for this phrase length. phrase=%(p)s rules=%(r)s\n' ++ %{'p': phrase, 'r': self.rules}) ++ return u'' ++ if len(rule) > self._mlen: ++ sys.stderr.write( ++ 'Rule exceeds maximum key length. rule=%(r)s self._mlen=%(m)s\n' ++ %{'r': rule, 'm': self._mlen}) ++ return u'' ++ tabkeys = u'' ++ for (zi, ma) in rule: ++ if zi > 0: ++ zi -= 1 ++ if ma > 0: ++ ma -= 1 ++ tabkey = self.get_goucima(phrase[zi])[ma] ++ if not tabkey: ++ return u'' ++ tabkeys += tabkey ++ if debug_level > 1: ++ sys.stderr.write("parse_phrase() tabkeys=%s\n" %tabkeys) ++ return tabkeys ++ ++ def is_in_system_database(self, tabkeys=u'', phrase=u''): ++ ''' ++ Checks whether “phrase” can be matched in the system database ++ with a key sequence *starting* with “tabkeys”. ++ ''' ++ if debug_level > 1: ++ sys.stderr.write( ++ 'is_in_system_database() tabkeys=%(t)s phrase=%(p)s\n' ++ % {'t': tabkeys, 'p': phrase}) ++ if not tabkeys or not phrase: ++ return False ++ sqlstr = ''' ++ SELECT * FROM main.phrases ++ WHERE tabkeys LIKE :tabkeys AND phrase = :phrase; ++ ''' ++ sqlargs = {'tabkeys': tabkeys+'%%', 'phrase': phrase} ++ results = self.db.execute(sqlstr, sqlargs).fetchall() ++ if debug_level > 1: ++ sys.stderr.write( ++ 'is_in_system_database() tabkeys=%(t)s phrase=%(p)s ' ++ % {'t': tabkeys, 'p': phrase} ++ + 'results=%(r)s\n' ++ % {'r': results}) ++ if results: ++ return True ++ else: ++ return False ++ ++ def user_frequency(self, tabkeys=u'', phrase=u''): ++ if debug_level > 1: ++ sys.stderr.write( ++ 'user_frequency() tabkeys=%(t)s phrase=%(p)s\n' ++ % {'t': tabkeys, 'p': phrase}) ++ if not tabkeys or not phrase: ++ return 0 ++ sqlstr = ''' ++ SELECT sum(user_freq) FROM user_db.phrases ++ WHERE tabkeys = :tabkeys AND phrase = :phrase GROUP BY tabkeys, phrase; ++ ''' ++ sqlargs = {'tabkeys': tabkeys, 'phrase': phrase} ++ result = self.db.execute(sqlstr, sqlargs).fetchall() ++ if debug_level > 1: ++ sys.stderr.write("user_frequency() result=%s\n" %result) ++ if result: ++ return result[0][0] ++ else: ++ return 0 ++ ++ def check_phrase(self, tabkeys=u'', phrase=u''): ++ '''Adjust user_freq in user database if necessary. ++ ++ Also, if the phrase is not in the system database, and it is a ++ Chinese table, and defining user phrases is allowed, add it as ++ a user defined phrase to the user database if it is not yet ++ there. ++ ''' ++ if debug_level > 1: ++ sys.stderr.write( ++ 'check_phrase_internal() tabkey=%(t)s phrase=%(p)s\n' ++ % {'t': tabkeys, 'p': phrase}) ++ if type(phrase) != type(u''): ++ phrase = phrase.decode('utf8') ++ if type(tabkeys) != type(u''): ++ tabkeys = tabkeys.decode('utf8') ++ if not tabkeys or not phrase: ++ return ++ if self._is_chinese and phrase in chinese_nocheck_chars: ++ return ++ if not self.dynamic_adjust: ++ if not self.user_can_define_phrase or not self.is_chinese: ++ return ++ tabkeys = self.parse_phrase(phrase) ++ if not tabkeys: ++ # no tabkeys could be constructed from the rules in the table ++ return ++ if self.is_in_system_database(tabkeys=tabkeys, phrase=phrase): ++ # if it is in the system database, it does not need to ++ # be defined ++ return ++ if self.user_frequency(tabkeys=tabkeys, phrase=phrase) > 0: ++ # if it is in the user database, it has been defined before ++ return ++ # add this user defined phrase to the user database: ++ self.add_phrase( ++ tabkeys=tabkeys, phrase=phrase, freq=-1, user_freq=1, ++ database='user_db') ++ else: ++ if self.is_in_system_database(tabkeys=tabkeys, phrase=phrase): ++ user_freq = self.user_frequency(tabkeys=tabkeys, phrase=phrase) ++ if user_freq > 0: ++ self.update_phrase( ++ tabkeys=tabkeys, phrase=phrase, user_freq=user_freq+1) ++ else: ++ self.add_phrase( ++ tabkeys=tabkeys, phrase=phrase, freq=0, user_freq=1, ++ database='user_db') ++ else: ++ if not self.user_can_define_phrase or not self.is_chinese: ++ return ++ tabkeys = self.parse_phrase(phrase) ++ if not tabkeys: ++ # no tabkeys could be constructed from the rules ++ # in the table ++ return ++ user_freq = self.user_frequency(tabkeys=tabkeys, phrase=phrase) ++ if user_freq > 0: ++ self.update_phrase( ++ tabkeys=tabkeys, phrase=phrase, user_freq=user_freq+1) ++ else: ++ self.add_phrase( ++ tabkeys=tabkeys, phrase=phrase, freq=-1, user_freq=1, ++ database='user_db') ++ ++ def find_zi_code (self, phrase): ++ ''' ++ Return the list of possible tabkeys for a phrase. ++ ++ For example, if “phrase” is “你” and the table is wubi-jidian.86.txt, ++ the result will be ['wq', 'wqi', 'wqiy'] because that table ++ contains the following 3 lines matching that phrase exactly: ++ ++ wq 你 597727619 ++ wqi 你 1490000000 ++ wqiy 你 1490000000 ++ ''' ++ if type(phrase) != type(u''): ++ phrase = phrase.decode('utf8') ++ sqlstr = ''' ++ SELECT tabkeys FROM main.phrases WHERE phrase = :phrase ++ ORDER by length(tabkeys) ASC; ++ ''' ++ sqlargs = {'phrase': phrase} ++ results = self.db.execute(sqlstr, sqlargs).fetchall() ++ list_of_possible_tabkeys = [x[0] for x in results] ++ return list_of_possible_tabkeys ++ ++ def remove_phrase ( ++ self, tabkeys=u'', phrase=u'', database='user_db', commit=True): ++ '''Remove phrase from database ++ ''' ++ if not phrase: ++ return ++ if tabkeys: ++ delete_sqlstr = ''' ++ DELETE FROM %(database)s.phrases ++ WHERE tabkeys = :tabkeys AND phrase = :phrase; ++ ''' % {'database': database} ++ else: ++ delete_sqlstr = ''' ++ DELETE FROM %(database)s.phrases ++ WHERE phrase = :phrase; ++ ''' % {'database': database} ++ delete_sqlargs = {'tabkeys': tabkeys, 'phrase': phrase} ++ self.db.execute(delete_sqlstr, delete_sqlargs) ++ if commit: ++ self.db.commit() ++ self.invalidate_phrases_cache(tabkeys) ++ ++ def extract_user_phrases( ++ self, database_file='', old_database_version='0.0'): ++ '''extract user phrases from database''' ++ sys.stderr.write( ++ 'Trying to recover the phrases from the old, ' ++ + 'incompatible database.\n') ++ try: ++ db = sqlite3.connect(database_file) ++ db.execute('PRAGMA wal_checkpoint;') ++ if old_database_version >= '1.00': ++ phrases = db.execute( ++ ''' ++ SELECT tabkeys, phrase, freq, sum(user_freq) FROM phrases ++ GROUP BY tabkeys, phrase, freq; ++ ''' ++ ).fetchall() ++ db.close() ++ phrases = sorted( ++ phrases, key=lambda x: (x[0], x[1], x[2], x[3])) ++ sys.stderr.write( ++ 'Recovered phrases from the old database: phrases=%s\n' ++ % repr(phrases)) ++ return phrases[:] ++ else: ++ # database is very old, it may still use many columns ++ # of type INTEGER for the tabkeys. Therefore, ignore ++ # the tabkeys in the database and try to get them ++ # from the system database instead. ++ phrases = [] ++ results = db.execute( ++ 'SELECT phrase, sum(user_freq) ' ++ + 'FROM phrases GROUP BY phrase;' ++ ).fetchall() ++ for result in results: ++ sqlstr = ''' ++ SELECT tabkeys FROM main.phrases WHERE phrase = :phrase ++ ORDER BY length(tabkeys) DESC; ++ ''' ++ sqlargs = {'phrase': result[0]} ++ tabkeys_results = self.db.execute( ++ sqlstr, sqlargs).fetchall() ++ if tabkeys_results: ++ phrases.append( ++ (tabkeys_results[0][0], result[0], 0, result[1])) ++ else: ++ # No tabkeys for that phrase could not be ++ # found in the system database. Try to get ++ # tabkeys by calling self.parse_phrase(), that ++ # might return something if the table has ++ # rules to construct user defined phrases: ++ tabkeys = self.parse_phrase(result[0]) ++ if tabkeys: ++ # for user defined phrases, the “freq” column is -1: ++ phrases.append((tabkeys, result[0], -1, result[1])) ++ db.close() ++ phrases = sorted( ++ phrases, key=lambda x: (x[0], x[1], x[2], x[3])) ++ sys.stderr.write( ++ 'Recovered phrases from the very old database: phrases=%s\n' ++ % repr(phrases)) ++ return phrases[:] ++ except: ++ import traceback ++ traceback.print_exc() ++ return [] +diff -Nru ibus-table-1.9.18.orig/setup/main.py ibus-table-1.9.18/setup/main.py +--- ibus-table-1.9.18.orig/setup/main.py 2020-07-22 11:52:11.654532080 +0200 ++++ ibus-table-1.9.18/setup/main.py 2020-07-22 14:43:51.908260925 +0200 +@@ -62,7 +62,7 @@ + "endeffullwidthletter": False, + "endeffullwidthpunct": False, + "alwaysshowlookup": True, +- "lookuptableorientation": True, ++ "lookuptableorientation": 1, # 0 = horizontal, 1 = vertical, 2 = system + "lookuptablepagesize": 6, + "onechar": False, + "autoselect": False, +@@ -224,12 +224,8 @@ + and type(auto_commit) == type(u'') + and auto_commit.lower() in [u'true', u'false']): + OPTION_DEFAULTS['autocommit'] = auto_commit.lower() == u'true' +- orientation = self.tabsqlitedb.ime_properties.get('orientation') +- if (orientation +- and type(orientation) == type(u'') +- and orientation.lower() in [u'true', u'false']): +- OPTION_DEFAULTS['lookuptableorientation'] = ( +- orientation.lower() == u'true') ++ orientation = self.tabsqlitedb.get_orientation() ++ OPTION_DEFAULTS['lookuptableorientation'] = orientation + # if space is a page down key, set the option + # “spacekeybehavior” to “True”: + page_down_keys_csv = self.tabsqlitedb.ime_properties.get( +@@ -258,14 +254,16 @@ + single_wildcard_char = self.tabsqlitedb.ime_properties.get( + 'single_wildcard_char') + if (single_wildcard_char +- and type(single_wildcard_char) == type(u'') +- and len(single_wildcard_char) == 1): ++ and type(single_wildcard_char) == type(u'')): ++ if len(single_wildcard_char) > 1: ++ single_wildcard_char = single_wildcard_char[0] + OPTION_DEFAULTS['singlewildcardchar'] = single_wildcard_char + multi_wildcard_char = self.tabsqlitedb.ime_properties.get( + 'multi_wildcard_char') + if (multi_wildcard_char +- and type(multi_wildcard_char) == type(u'') +- and len(multi_wildcard_char) == 1): ++ and type(multi_wildcard_char) == type(u'')): ++ if len(multi_wildcard_char) > 1: ++ multi_wildcard_char = multi_wildcard_char[0] + OPTION_DEFAULTS['multiwildcardchar'] = multi_wildcard_char + + def __restore_defaults(self): +diff -Nru ibus-table-1.9.18.orig/tests/.gitignore ibus-table-1.9.18/tests/.gitignore +--- ibus-table-1.9.18.orig/tests/.gitignore 1970-01-01 01:00:00.000000000 +0100 ++++ ibus-table-1.9.18/tests/.gitignore 2020-07-22 14:43:51.908260925 +0200 +@@ -0,0 +1,5 @@ ++run_tests ++run_tests.log ++run_tests.trs ++test-suite.log ++__pycache__/ +diff -Nru ibus-table-1.9.18.orig/tests/Makefile.am ibus-table-1.9.18/tests/Makefile.am +--- ibus-table-1.9.18.orig/tests/Makefile.am 1970-01-01 01:00:00.000000000 +0100 ++++ ibus-table-1.9.18/tests/Makefile.am 2020-07-22 14:43:51.908260925 +0200 +@@ -0,0 +1,41 @@ ++# vim:set noet ts=4 ++# ++# ibus-table - The Tables engine for IBus ++# ++# Copyright (c) 2018 Mike FABIAN ++# ++# This library is free software; you can redistribute it and/or ++# modify it under the terms of the GNU Lesser General Public ++# License as published by the Free Software Foundation; either ++# version 2.1 of the License, or (at your option) any later version. ++# ++# This library is distributed in the hope that it will be useful, ++# but WITHOUT ANY WARRANTY; without even the implied warranty of ++# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ++# Lesser General Public License for more details. ++# ++# You should have received a copy of the GNU Lesser General Public ++# License along with this library; if not, write to the Free Software ++# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA ++# ++ ++TESTS = run_tests ++ ++run_tests: run_tests.in ++ sed -e 's&@PYTHON_BIN@&$(PYTHON)&g' \ ++ -e 's&@SRCDIR@&$(srcdir)&g' $< > $@ ++ chmod +x $@ ++ ++EXTRA_DIST = \ ++ run_tests.in \ ++ test_it.py \ ++ __init__.py \ ++ $(NULL) ++ ++CLEANFILES = \ ++ run_tests \ ++ $(NULL) ++ ++MAINTAINERCLEANFILES = \ ++ Makefile.in \ ++ $(NULL) +diff -Nru ibus-table-1.9.18.orig/tests/run_tests.in ibus-table-1.9.18/tests/run_tests.in +--- ibus-table-1.9.18.orig/tests/run_tests.in 1970-01-01 01:00:00.000000000 +0100 ++++ ibus-table-1.9.18/tests/run_tests.in 2020-07-22 14:43:51.908260925 +0200 +@@ -0,0 +1,254 @@ ++#!/usr/bin/python3 ++ ++# ibus-table - The Tables engine for IBus ++# ++# Copyright (c) 2018 Mike FABIAN ++# ++# This library is free software; you can redistribute it and/or ++# modify it under the terms of the GNU Lesser General Public ++# License as published by the Free Software Foundation; either ++# version 2.1 of the License, or (at your option) any later version. ++# ++# This library is distributed in the hope that it will be useful, ++# but WITHOUT ANY WARRANTY; without even the implied warranty of ++# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ++# Lesser General Public License for more details. ++# ++# You should have received a copy of the GNU Lesser General Public ++# License along with this library; if not, write to the Free Software ++# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA ++# ++ ++import os ++import sys ++import unittest ++ ++from gi import require_version ++require_version('IBus', '1.0') ++from gi.repository import IBus ++ ++# -- Define some mock classes for the tests ---------------------------------- ++class MockEngine: ++ def __init__(self, engine_name = '', connection = None, object_path = ''): ++ self.mock_auxiliary_text = '' ++ self.mock_preedit_text = '' ++ self.mock_preedit_text_cursor_pos = 0 ++ self.mock_preedit_text_visible = True ++ self.mock_committed_text = '' ++ self.mock_committed_text_cursor_pos = 0 ++ self.client_capabilities = ( ++ IBus.Capabilite.PREEDIT_TEXT ++ | IBus.Capabilite.AUXILIARY_TEXT ++ | IBus.Capabilite.LOOKUP_TABLE ++ | IBus.Capabilite.FOCUS ++ | IBus.Capabilite.PROPERTY) ++ # There are lots of weird problems with surrounding text ++ # which makes this hard to test. Therefore this mock ++ # engine does not try to support surrounding text, i.e. ++ # we omit “| IBus.Capabilite.SURROUNDING_TEXT” here. ++ ++ def update_auxiliary_text(self, text, visible): ++ self.mock_auxiliary_text = text.text ++ ++ def hide_auxiliary_text(self): ++ pass ++ ++ def commit_text(self, text): ++ self.mock_committed_text = ( ++ self.mock_committed_text[ ++ :self.mock_committed_text_cursor_pos] ++ + text.text ++ + self.mock_committed_text[ ++ self.mock_committed_text_cursor_pos:]) ++ self.mock_committed_text_cursor_pos += len(text.text) ++ ++ def forward_key_event(self, val, code, state): ++ if (val == IBus.KEY_Left ++ and self.mock_committed_text_cursor_pos > 0): ++ self.mock_committed_text_cursor_pos -= 1 ++ return ++ unicode = IBus.keyval_to_unicode(val) ++ if unicode: ++ self.mock_committed_text = ( ++ self.mock_committed_text[ ++ :self.mock_committed_text_cursor_pos] ++ + unicode ++ + self.mock_committed_text[ ++ self.mock_committed_text_cursor_pos:]) ++ self.mock_committed_text_cursor_pos += len(unicode) ++ ++ def update_lookup_table(self, table, visible): ++ pass ++ ++ def update_preedit_text(self, text, cursor_pos, visible): ++ self.mock_preedit_text = text.get_text() ++ self.mock_preedit_text_cursor_pos = cursor_pos ++ self.mock_preedit_text_visible = visible ++ ++ def register_properties(self, property_list): ++ pass ++ ++ def update_property(self, property): ++ pass ++ ++ def hide_lookup_table(self): ++ pass ++ ++class MockLookupTable: ++ def __init__(self, page_size = 9, cursor_pos = 0, cursor_visible = False, round = True): ++ self.clear() ++ self.mock_page_size = page_size ++ self.mock_cursor_pos = cursor_pos ++ self.mock_cursor_visible = cursor_visible ++ self.cursor_visible = cursor_visible ++ self.mock_round = round ++ self.mock_candidates = [] ++ self.mock_labels = [] ++ self.mock_page_number = 0 ++ ++ def clear(self): ++ self.mock_candidates = [] ++ self.mock_cursor_pos = 0 ++ ++ def set_page_size(self, size): ++ self.mock_page_size = size ++ ++ def get_page_size(self): ++ return self.mock_page_size ++ ++ def set_round(self, round): ++ self.mock_round = round ++ ++ def set_cursor_pos(self, pos): ++ self.mock_cursor_pos = pos ++ ++ def get_cursor_pos(self): ++ return self.mock_cursor_pos ++ ++ def get_cursor_in_page(self): ++ return (self.mock_cursor_pos ++ - self.mock_page_size * self.mock_page_number) ++ ++ def set_cursor_visible(self, visible): ++ self.mock_cursor_visible = visible ++ self.cursor_visible = visible ++ ++ def cursor_down(self): ++ if len(self.mock_candidates): ++ self.mock_cursor_pos += 1 ++ self.mock_cursor_pos %= len(self.mock_candidates) ++ ++ def cursor_up(self): ++ if len(self.mock_candidates): ++ if self.mock_cursor_pos > 0: ++ self.mock_cursor_pos -= 1 ++ else: ++ self.mock_cursor_pos = len(self.mock_candidates) - 1 ++ ++ def page_down(self): ++ if len(self.mock_candidates): ++ self.mock_page_number += 1 ++ self.mock_cursor_pos += self.mock_page_size ++ ++ def page_up(self): ++ if len(self.mock_candidates): ++ if self.mock_page_number > 0: ++ self.mock_page_number -= 1 ++ self.mock_cursor_pos -= self.mock_page_size ++ ++ def set_orientation(self, orientation): ++ self.mock_orientation = orientation ++ ++ def get_number_of_candidates(self): ++ return len(self.mock_candidates) ++ ++ def append_candidate(self, candidate): ++ self.mock_candidates.append(candidate.get_text()) ++ ++ def get_candidate(self, index): ++ return self.mock_candidates[index] ++ ++ def get_number_of_candidates(self): ++ return len(self.mock_candidates) ++ ++ def append_label(self, label): ++ self.mock_labels.append(label.get_text()) ++ ++class MockPropList: ++ def __init__(self, *args, **kwargs): ++ self._mock_proplist = [] ++ ++ def append(self, property): ++ self._mock_proplist.append(property) ++ ++ def get(self, index): ++ if index >= 0 and index < len(self._mock_proplist): ++ return self._mock_proplist[index] ++ else: ++ return None ++ ++ def update_property(self, property): ++ pass ++ ++class MockProperty: ++ def __init__(self, ++ key='', ++ prop_type=IBus.PropType.RADIO, ++ label=IBus.Text.new_from_string(''), ++ symbol=IBus.Text.new_from_string(''), ++ icon='', ++ tooltip=IBus.Text.new_from_string(''), ++ sensitive=True, ++ visible=True, ++ state=IBus.PropState.UNCHECKED, ++ sub_props=None): ++ self.mock_property_key = key ++ self.mock_property_prop_type = prop_type ++ self.mock_property_label = label.get_text() ++ self.mock_property_symbol = symbol.get_text() ++ self.mock_property_icon = icon ++ self.mock_property_tooltip = tooltip.get_text() ++ self.mock_property_sensitive = sensitive ++ self.mock_property_visible = visible ++ self.mock_property_state = state ++ self.mock_property_sub_props = sub_props ++ ++ def set_label(self, ibus_text): ++ self.mock_property_label = ibus_text.get_text() ++ ++ def set_symbol(self, ibus_text): ++ self.mock_property_symbol = ibus_text.get_text() ++ ++ def set_tooltip(self, ibus_text): ++ self.mock_property_tooltip = ibus_text.get_text() ++ ++ def set_sensitive(self, sensitive): ++ self.mock_property_sensitive = sensitive ++ ++ def set_visible(self, visible): ++ self.mock_property_visible = visible ++ ++ def set_state(self, state): ++ self.mock_property_state = state ++ ++ def set_sub_props(self, proplist): ++ self.mock_property_sub_props = proplist ++ ++ def get_key(self): ++ return self.mock_property_key ++ ++# -- Monkey patch the environment with the mock classes ---------------------- ++sys.modules["gi.repository.IBus"].Engine = MockEngine ++sys.modules["gi.repository.IBus"].LookupTable = MockLookupTable ++sys.modules["gi.repository.IBus"].Property = MockProperty ++sys.modules["gi.repository.IBus"].PropList = MockPropList ++ ++# -- Load and run our unit tests --------------------------------------------- ++os.environ['IBUS_TABLE_DEBUG_LEVEL'] = '255' ++loader = unittest.TestLoader() ++suite = loader.discover(".") ++runner = unittest.TextTestRunner(stream = sys.stderr, verbosity = 255) ++result = runner.run(suite) ++ ++if result.failures or result.errors: ++ sys.exit(1) +diff -Nru ibus-table-1.9.18.orig/tests/test_it.py ibus-table-1.9.18/tests/test_it.py +--- ibus-table-1.9.18.orig/tests/test_it.py 1970-01-01 01:00:00.000000000 +0100 ++++ ibus-table-1.9.18/tests/test_it.py 2020-07-22 14:43:51.908260925 +0200 +@@ -0,0 +1,403 @@ ++# -*- coding: utf-8 -*- ++# vim:et sts=4 sw=4 ++# ++# ibus-table - The Tables engine for IBus ++# ++# Copyright (c) 2018 Mike FABIAN ++# ++# This library is free software; you can redistribute it and/or ++# modify it under the terms of the GNU Lesser General Public ++# License as published by the Free Software Foundation; either ++# version 2.1 of the License, or (at your option) any later version. ++# ++# This library is distributed in the hope that it will be useful, ++# but WITHOUT ANY WARRANTY; without even the implied warranty of ++# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ++# Lesser General Public License for more details. ++# ++# You should have received a copy of the GNU Lesser General Public ++# License along with this library; if not, write to the Free Software ++# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA ++# ++ ++''' ++This file implements the test cases for the unit tests of ibus-table ++''' ++ ++import sys ++import os ++import unicodedata ++import unittest ++import subprocess ++ ++from gi import require_version ++require_version('IBus', '1.0') ++from gi.repository import IBus ++ ++sys.path.insert(0, "../engine") ++from table import * ++import tabsqlitedb ++import it_util ++#sys.path.pop(0) ++ ++ENGINE = None ++TABSQLITEDB = None ++ORIG_INPUT_MODE = None ++ORIG_CHINESE_MODE = None ++ORIG_LETTER_WIDTH = None ++ORIG_PUNCTUATION_WIDTH = None ++ORIG_ALWAYS_SHOW_LOOKUP = None ++ORIG_LOOKUP_TABLE_ORIENTATION = None ++ORIG_PAGE_SIZE = None ++ORIG_ONECHAR_MODE = None ++ORIG_AUTOSELECT_MODE = None ++ORIG_AUTOCOMMIT_MODE = None ++ORIG_SPACE_KEY_BEHAVIOR_MODE = None ++ORIG_AUTOWILDCARD_MODE = None ++ORIG_SINGLE_WILDCARD_CHAR = None ++ORIG_MULTI_WILDCARD_CHAR = None ++ ++def backup_original_settings(): ++ global ENGINE ++ global ORIG_INPUT_MODE ++ global ORIG_CHINESE_MODE ++ global ORIG_LETTER_WIDTH ++ global ORIG_PUNCTUATION_WIDTH ++ global ORIG_ALWAYS_SHOW_LOOKUP ++ global ORIG_LOOKUP_TABLE_ORIENTATION ++ global ORIG_PAGE_SIZE ++ global ORIG_ONECHAR_MODE ++ global ORIG_AUTOSELECT_MODE ++ global ORIG_AUTOCOMMIT_MODE ++ global ORIG_SPACE_KEY_BEHAVIOR_MODE ++ global ORIG_AUTOWILDCARD_MODE ++ global ORIG_SINGLE_WILDCARD_CHAR ++ global ORIG_MULTI_WILDCARD_CHAR ++ ORIG_INPUT_MODE = ENGINE.get_input_mode() ++ ORIG_CHINESE_MODE = ENGINE.get_chinese_mode() ++ ORIG_LETTER_WIDTH = ENGINE.get_letter_width() ++ ORIG_PUNCTUATION_WIDTH = ENGINE.get_punctuation_width() ++ ORIG_ALWAYS_SHOW_LOOKUP = ENGINE.get_always_show_lookup() ++ ORIG_LOOKUP_TABLE_ORIENTATION = ENGINE.get_lookup_table_orientation() ++ ORIG_PAGE_SIZE = ENGINE.get_page_size() ++ ORIG_ONECHAR_MODE = ENGINE.get_onechar_mode() ++ ORIG_AUTOSELECT_MODE = ENGINE.get_autoselect_mode() ++ ORIG_AUTOCOMMIT_MODE = ENGINE.get_autocommit_mode() ++ ORIG_SPACE_KEY_BEHAVIOR_MODE = ENGINE.get_space_key_behavior_mode() ++ ORIG_AUTOWILDCARD_MODE = ENGINE.get_autowildcard_mode() ++ ORIG_SINGLE_WILDCARD_CHAR = ENGINE.get_single_wildcard_char() ++ ORIG_MULTI_WILDCARD_CHAR = ENGINE.get_multi_wildcard_char() ++ ++def restore_original_settings(): ++ global ENGINE ++ global ORIG_INPUT_MODE ++ global ORIG_CHINESE_MODE ++ global ORIG_LETTER_WIDTH ++ global ORIG_PUNCTUATION_WIDTH ++ global ORIG_ALWAYS_SHOW_LOOKUP ++ global ORIG_LOOKUP_TABLE_ORIENTATION ++ global ORIG_PAGE_SIZE ++ global ORIG_ONECHAR_MODE ++ global ORIG_AUTOSELECT_MODE ++ global ORIG_AUTOCOMMIT_MODE ++ global ORIG_SPACE_KEY_BEHAVIOR_MODE ++ global ORIG_AUTOWILDCARD_MODE ++ global ORIG_SINGLE_WILDCARD_CHAR ++ global ORIG_MULTI_WILDCARD_CHAR ++ ENGINE.set_input_mode(ORIG_INPUT_MODE) ++ ENGINE.set_chinese_mode(ORIG_CHINESE_MODE) ++ ENGINE.set_letter_width(ORIG_LETTER_WIDTH[0], input_mode=0) ++ ENGINE.set_letter_width(ORIG_LETTER_WIDTH[1], input_mode=1) ++ ENGINE.set_punctuation_width(ORIG_PUNCTUATION_WIDTH[0], input_mode=0) ++ ENGINE.set_punctuation_width(ORIG_PUNCTUATION_WIDTH[1], input_mode=1) ++ ENGINE.set_always_show_lookup(ORIG_ALWAYS_SHOW_LOOKUP) ++ ENGINE.set_lookup_table_orientation(ORIG_LOOKUP_TABLE_ORIENTATION) ++ ENGINE.set_page_size(ORIG_PAGE_SIZE) ++ ENGINE.set_onechar_mode(ORIG_ONECHAR_MODE) ++ ENGINE.set_autoselect_mode(ORIG_AUTOSELECT_MODE) ++ ENGINE.set_autocommit_mode(ORIG_AUTOCOMMIT_MODE) ++ ENGINE.set_space_key_behavior_mode(ORIG_SPACE_KEY_BEHAVIOR_MODE) ++ ENGINE.set_autowildcard_mode(ORIG_AUTOWILDCARD_MODE) ++ ENGINE.set_single_wildcard_char(ORIG_SINGLE_WILDCARD_CHAR) ++ ENGINE.set_multi_wildcard_char(ORIG_MULTI_WILDCARD_CHAR) ++ ++def set_default_settings(): ++ global ENGINE ++ global TABSQLITEDB ++ ENGINE.set_input_mode(mode=1) ++ chinese_mode = 0 ++ language_filter = TABSQLITEDB.ime_properties.get('language_filter') ++ if language_filter in ['cm0', 'cm1', 'cm2', 'cm3', 'cm4']: ++ chinese_mode = int(language_filter[-1]) ++ ENGINE.set_chinese_mode(mode=chinese_mode) ++ ++ letter_width_mode = False ++ def_full_width_letter = TABSQLITEDB.ime_properties.get( ++ 'def_full_width_letter') ++ if def_full_width_letter: ++ letter_width_mode = (def_full_width_letter.lower() == u'true') ++ ENGINE.set_letter_width(mode=letter_width_mode, input_mode=0) ++ ENGINE.set_letter_width(mode=letter_width_mode, input_mode=1) ++ ++ punctuation_width_mode = False ++ def_full_width_punct = TABSQLITEDB.ime_properties.get( ++ 'def_full_width_punct') ++ if def_full_width_punct: ++ punctuation_width_mode = (def_full_width_punct.lower() == u'true') ++ ENGINE.set_punctuation_width(mode=punctuation_width_mode, input_mode=0) ++ ENGINE.set_punctuation_width(mode=punctuation_width_mode, input_mode=1) ++ ++ always_show_lookup_mode = True ++ always_show_lookup = TABSQLITEDB.ime_properties.get( ++ 'always_show_lookup') ++ if always_show_lookup: ++ always_show_lookup_mode = (always_show_lookup.lower() == u'true') ++ ENGINE.set_always_show_lookup(always_show_lookup_mode) ++ ++ orientation = TABSQLITEDB.get_orientation() ++ ENGINE.set_lookup_table_orientation(orientation) ++ ++ page_size = 6 ++ select_keys_csv = TABSQLITEDB.ime_properties.get('select_keys') ++ # select_keys_csv is something like: "1,2,3,4,5,6,7,8,9,0" ++ if select_keys_csv: ++ ENGINE.set_page_size(len(select_keys_csv.split(","))) ++ ++ onechar = False ++ ENGINE.set_onechar_mode(onechar) ++ ++ auto_select_mode = False ++ auto_select = TABSQLITEDB.ime_properties.get('auto_select') ++ if auto_select: ++ auto_select_mode = (auto_select.lower() == u'true') ++ ENGINE.set_autoselect_mode(auto_select_mode) ++ ++ auto_commit_mode = False ++ auto_commit = TABSQLITEDB.ime_properties.get('auto_commit') ++ if auto_commit: ++ auto_commit_mode = (auto_commit.lower() == u'true') ++ ENGINE.set_autocommit_mode(auto_commit_mode) ++ ++ space_key_behavior_mode = False ++ # if space is a page down key, set the option ++ # “spacekeybehavior” to “True”: ++ page_down_keys_csv = TABSQLITEDB.ime_properties.get( ++ 'page_down_keys') ++ if page_down_keys_csv: ++ page_down_keys = [ ++ IBus.keyval_from_name(x) ++ for x in page_down_keys_csv.split(',')] ++ if IBus.KEY_space in page_down_keys: ++ space_key_behavior_mode = True ++ # if space is a commit key, set the option ++ # “spacekeybehavior” to “False” (overrides if space is ++ # also a page down key): ++ commit_keys_csv = TABSQLITEDB.ime_properties.get('commit_keys') ++ if commit_keys_csv: ++ commit_keys = [ ++ IBus.keyval_from_name(x) ++ for x in commit_keys_csv.split(',')] ++ if IBus.KEY_space in commit_keys: ++ space_key_behavior_mode = False ++ ENGINE.set_space_key_behavior_mode(space_key_behavior_mode) ++ ++ auto_wildcard_mode = True ++ auto_wildcard = TABSQLITEDB.ime_properties.get('auto_wildcard') ++ if auto_wildcard: ++ auto_wildcard_mode = (auto_wildcard.lower() == u'true') ++ ENGINE.set_autowildcard_mode(auto_wildcard_mode) ++ ++ single_wildcard_char = TABSQLITEDB.ime_properties.get( ++ 'single_wildcard_char') ++ if not single_wildcard_char: ++ single_wildcard_char = u'' ++ if len(single_wildcard_char) > 1: ++ single_wildcard_char = single_wildcard_char[0] ++ ENGINE.set_single_wildcard_char(single_wildcard_char) ++ ++ multi_wildcard_char = TABSQLITEDB.ime_properties.get( ++ 'multi_wildcard_char') ++ if not multi_wildcard_char: ++ multi_wildcard_char = u'' ++ if len(multi_wildcard_char) > 1: ++ multi_wildcard_char = multi_wildcard_char[0] ++ ENGINE.set_multi_wildcard_char(multi_wildcard_char) ++ ++def set_up(engine_name): ++ global TABSQLITEDB ++ global ENGINE ++ bus = IBus.Bus() ++ db_dir = '/usr/share/ibus-table/tables' ++ db_file = os.path.join(db_dir, engine_name + '.db') ++ TABSQLITEDB = tabsqlitedb.tabsqlitedb( ++ filename=db_file, user_db=':memory:') ++ ENGINE = tabengine( ++ bus, ++ '/com/redhat/IBus/engines/table/%s/engine/0' %engine_name, ++ TABSQLITEDB, ++ unit_test = True) ++ backup_original_settings() ++ set_default_settings() ++ ++def tear_down(): ++ restore_original_settings() ++ ++class Wubi_Jidian86TestCase(unittest.TestCase): ++ def setUp(self): ++ set_up('wubi-jidian86') ++ ++ def tearDown(self): ++ tear_down() ++ ++ def test_dummy(self): ++ self.assertEqual(True, True) ++ ++ def test_single_char_commit_with_space(self): ++ ENGINE.do_process_key_event(IBus.KEY_a, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_space, 0, 0) ++ self.assertEqual(ENGINE.mock_committed_text, '工') ++ ++ def test_commit_to_preedit_and_switching_to_pinyin_and_defining_a_phrase(self): ++ ENGINE.do_process_key_event(IBus.KEY_a, 0, 0) ++ # commit to preëdit needs a press and release of either ++ # the left or the right shift key: ++ ENGINE.do_process_key_event( ++ IBus.KEY_Shift_L, 0, ++ IBus.ModifierType.SHIFT_MASK) ++ ENGINE.do_process_key_event( ++ IBus.KEY_Shift_L, 0, ++ IBus.ModifierType.SHIFT_MASK | IBus.ModifierType.RELEASE_MASK) ++ self.assertEqual(ENGINE.mock_preedit_text, '工') ++ ENGINE.do_process_key_event(IBus.KEY_b, 0, 0) ++ ENGINE.do_process_key_event( ++ IBus.KEY_Shift_R, 0, ++ IBus.ModifierType.SHIFT_MASK) ++ ENGINE.do_process_key_event( ++ IBus.KEY_Shift_R, 0, ++ IBus.ModifierType.SHIFT_MASK | IBus.ModifierType.RELEASE_MASK) ++ self.assertEqual(ENGINE.mock_preedit_text, '工了') ++ ENGINE.do_process_key_event(IBus.KEY_c, 0, 0) ++ ENGINE.do_process_key_event( ++ IBus.KEY_Shift_R, 0, ++ IBus.ModifierType.SHIFT_MASK) ++ ENGINE.do_process_key_event( ++ IBus.KEY_Shift_R, 0, ++ IBus.ModifierType.SHIFT_MASK | IBus.ModifierType.RELEASE_MASK) ++ self.assertEqual(ENGINE.mock_preedit_text, '工了以') ++ ENGINE.do_process_key_event(IBus.KEY_d, 0, 0) ++ ENGINE.do_process_key_event( ++ IBus.KEY_Shift_L, 0, ++ IBus.ModifierType.SHIFT_MASK) ++ ENGINE.do_process_key_event( ++ IBus.KEY_Shift_L, 0, ++ IBus.ModifierType.SHIFT_MASK | IBus.ModifierType.RELEASE_MASK) ++ self.assertEqual(ENGINE.mock_preedit_text, '工了以在') ++ # Move left two characters in the preëdit: ++ ENGINE.do_process_key_event(IBus.KEY_Left, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_Left, 0, 0) ++ # Switch to pinyin mode by pressing and releasing the right ++ # shift key: ++ ENGINE.do_process_key_event( ++ IBus.KEY_Shift_R, 0, ++ IBus.ModifierType.SHIFT_MASK) ++ ENGINE.do_process_key_event( ++ IBus.KEY_Shift_R, 0, ++ IBus.ModifierType.SHIFT_MASK | IBus.ModifierType.RELEASE_MASK) ++ ENGINE.do_process_key_event(IBus.KEY_n, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_i, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_numbersign, 0, 0) ++ ENGINE.do_process_key_event( ++ IBus.KEY_Shift_L, 0, ++ IBus.ModifierType.SHIFT_MASK) ++ ENGINE.do_process_key_event( ++ IBus.KEY_Shift_L, 0, ++ IBus.ModifierType.SHIFT_MASK | IBus.ModifierType.RELEASE_MASK) ++ self.assertEqual(ENGINE.mock_preedit_text, '工了你以在') ++ ENGINE.do_process_key_event(IBus.KEY_h, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_a, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_o, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_numbersign, 0, 0) ++ ENGINE.do_process_key_event( ++ IBus.KEY_Shift_L, 0, ++ IBus.ModifierType.SHIFT_MASK) ++ ENGINE.do_process_key_event( ++ IBus.KEY_Shift_L, 0, ++ IBus.ModifierType.SHIFT_MASK | IBus.ModifierType.RELEASE_MASK) ++ self.assertEqual(ENGINE.mock_preedit_text, '工了你好以在') ++ # Move right two characters in the preëdit (triggers a commit to preëdit): ++ ENGINE.do_process_key_event(IBus.KEY_Right, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_Right, 0, 0) ++ self.assertEqual(ENGINE.mock_auxiliary_text, 'd dhf dhfd\t#: abwd') ++ # commit the preëdit: ++ ENGINE.do_process_key_event(IBus.KEY_space, 0, 0) ++ self.assertEqual(ENGINE.mock_committed_text, '工了你好以在') ++ # Switch out of pinyin mode: ++ ENGINE.do_process_key_event( ++ IBus.KEY_Shift_R, 0, ++ IBus.ModifierType.SHIFT_MASK) ++ ENGINE.do_process_key_event( ++ IBus.KEY_Shift_R, 0, ++ IBus.ModifierType.SHIFT_MASK | IBus.ModifierType.RELEASE_MASK) ++ # “abwd” shown on the right of the auxiliary text above shows the ++ # newly defined shortcut for this phrase. Let’s try to type ++ # the same phrase again using the new shortcut: ++ ENGINE.do_process_key_event(IBus.KEY_a, 0, 0) ++ self.assertEqual(ENGINE.mock_preedit_text, '工') ++ ENGINE.do_process_key_event(IBus.KEY_b, 0, 0) ++ self.assertEqual(ENGINE.mock_preedit_text, '节') ++ ENGINE.do_process_key_event(IBus.KEY_w, 0, 0) ++ self.assertEqual(ENGINE.mock_preedit_text, '工了你好以在') ++ ENGINE.do_process_key_event(IBus.KEY_d, 0, 0) ++ self.assertEqual(ENGINE.mock_preedit_text, '工了你好以在') ++ # commit the preëdit: ++ ENGINE.do_process_key_event(IBus.KEY_space, 0, 0) ++ self.assertEqual(ENGINE.mock_committed_text, '工了你好以在工了你好以在') ++ ++class Stroke5TestCase(unittest.TestCase): ++ def setUp(self): ++ set_up('stroke5') ++ ++ def tearDown(self): ++ tear_down() ++ ++ def test_dummy(self): ++ self.assertEqual(True, True) ++ ++ def test_single_char_commit_with_space(self): ++ ENGINE.do_process_key_event(IBus.KEY_comma, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_slash, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_n, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_m, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_m, 0, 0) ++ ENGINE.do_process_key_event(IBus.KEY_space, 0, 0) ++ self.assertEqual(ENGINE.mock_committed_text, '的') ++ ++class TranslitTestCase(unittest.TestCase): ++ def setUp(self): ++ set_up('translit') ++ ++ def tearDown(self): ++ tear_down() ++ ++ def test_dummy(self): ++ self.assertEqual(True, True) ++ ++ def test_sh_multiple_match(self): ++ ENGINE.do_process_key_event(IBus.KEY_s, 0, 0) ++ self.assertEqual(ENGINE.mock_preedit_text, 'с') ++ ENGINE.do_process_key_event(IBus.KEY_h, 0, 0) ++ self.assertEqual(ENGINE.mock_preedit_text, 'ш') ++ ENGINE.do_process_key_event(IBus.KEY_s, 0, 0) ++ self.assertEqual(ENGINE.mock_committed_text, 'ш') ++ self.assertEqual(ENGINE.mock_preedit_text, 'с') ++ ENGINE.do_process_key_event(IBus.KEY_h, 0, 0) ++ self.assertEqual(ENGINE.mock_committed_text, 'ш') ++ self.assertEqual(ENGINE.mock_preedit_text, 'ш') ++ ENGINE.do_process_key_event(IBus.KEY_h, 0, 0) ++ self.assertEqual(ENGINE.mock_committed_text, 'шщ') ++ self.assertEqual(ENGINE.mock_preedit_text, '') ++ ENGINE.do_process_key_event(IBus.KEY_s, 0, 0) ++ self.assertEqual(ENGINE.mock_committed_text, 'шщ') ++ self.assertEqual(ENGINE.mock_preedit_text, 'с') ++ ENGINE.do_process_key_event(IBus.KEY_space, 0, 0) ++ self.assertEqual(ENGINE.mock_committed_text, 'шщс ') diff --git a/SOURCES/fix-test-cases-for-old-ibus-table.patch b/SOURCES/fix-test-cases-for-old-ibus-table.patch new file mode 100644 index 0000000..b025655 --- /dev/null +++ b/SOURCES/fix-test-cases-for-old-ibus-table.patch @@ -0,0 +1,166 @@ +diff -Nru ibus-table-1.9.18/engine/table.py ibus-table-1.9.18.new/engine/table.py +--- ibus-table-1.9.18/engine/table.py 2020-07-23 17:21:31.934904346 +0200 ++++ ibus-table-1.9.18.new/engine/table.py 2020-07-23 17:28:19.342746241 +0200 +@@ -1752,6 +1752,9 @@ + self._input_mode) + self._update_ui() + ++ def get_pinyin_mode(self): ++ return self._editor._py_mode ++ + def set_onechar_mode(self, mode=False, update_dconf=True): + if mode == self._editor._onechar: + return +diff -Nru ibus-table-1.9.18/tests/test_0_gtk.py ibus-table-1.9.18.new/tests/test_0_gtk.py +--- ibus-table-1.9.18/tests/test_0_gtk.py 2020-07-23 17:21:31.940904285 +0200 ++++ ibus-table-1.9.18.new/tests/test_0_gtk.py 2020-07-23 17:37:23.234210681 +0200 +@@ -205,14 +205,14 @@ + object_path = '%s/%d' % (self.ENGINE_PATH, self.__id) + db_dir = '/usr/share/ibus-table/tables' + db_file = os.path.join(db_dir, engine_name + '.db') +- database = tabsqlitedb.TabSqliteDb(filename=db_file, user_db=':memory:') +- self.__engine = table.TabEngine( ++ database = tabsqlitedb.tabsqlitedb(filename=db_file, user_db=':memory:') ++ self.__engine = table.tabengine( + self.__bus, + object_path, + database) + self.__engine.connect('focus-in', self.__engine_focus_in) + self.__engine.connect('focus-out', self.__engine_focus_out) +- # FIXME: Need to connect 'reset' after TabEngine.clear_all_input_and_preedit() ++ # FIXME: Need to connect 'reset' after tabengine.clear_all_input_and_preedit() + # is called. + self.__engine.connect_after('reset', self.__engine_reset) + self.__bus.get_connection().signal_subscribe( +diff -Nru ibus-table-1.9.18/tests/test_it.py ibus-table-1.9.18.new/tests/test_it.py +--- ibus-table-1.9.18/tests/test_it.py 2020-07-23 17:21:31.940904285 +0200 ++++ ibus-table-1.9.18.new/tests/test_it.py 2020-07-23 17:29:52.032800325 +0200 +@@ -78,7 +78,6 @@ + ORIG_SINGLE_WILDCARD_CHAR = None + ORIG_MULTI_WILDCARD_CHAR = None + ORIG_PINYIN_MODE = None +-ORIG_SUGGESTION_MODE = None + + def backup_original_settings(): + global ENGINE +@@ -97,7 +96,6 @@ + global ORIG_SINGLE_WILDCARD_CHAR + global ORIG_MULTI_WILDCARD_CHAR + global ORIG_PINYIN_MODE +- global ORIG_SUGGESTION_MODE + ORIG_INPUT_MODE = ENGINE.get_input_mode() + ORIG_CHINESE_MODE = ENGINE.get_chinese_mode() + ORIG_LETTER_WIDTH = ENGINE.get_letter_width() +@@ -113,7 +111,6 @@ + ORIG_SINGLE_WILDCARD_CHAR = ENGINE.get_single_wildcard_char() + ORIG_MULTI_WILDCARD_CHAR = ENGINE.get_multi_wildcard_char() + ORIG_PINYIN_MODE = ENGINE.get_pinyin_mode() +- ORIG_SUGGESTION_MODE = ENGINE.get_suggestion_mode() + + def restore_original_settings(): + global ENGINE +@@ -132,7 +129,6 @@ + global ORIG_SINGLE_WILDCARD_CHAR + global ORIG_MULTI_WILDCARD_CHAR + global ORIG_PINYIN_MODE +- global ORIG_SUGGESTION_MODE + ENGINE.set_input_mode(ORIG_INPUT_MODE) + ENGINE.set_chinese_mode(ORIG_CHINESE_MODE) + ENGINE.set_letter_width(ORIG_LETTER_WIDTH[0], input_mode=0) +@@ -150,7 +146,6 @@ + ENGINE.set_single_wildcard_char(ORIG_SINGLE_WILDCARD_CHAR) + ENGINE.set_multi_wildcard_char(ORIG_MULTI_WILDCARD_CHAR) + ENGINE.set_pinyin_mode(ORIG_PINYIN_MODE) +- ENGINE.set_suggestion_mode(ORIG_SUGGESTION_MODE) + + def set_default_settings(): + global ENGINE +@@ -256,7 +251,6 @@ + ENGINE.set_multi_wildcard_char(multi_wildcard_char) + + ENGINE.set_pinyin_mode(False) +- ENGINE.set_suggestion_mode(False) + + def set_up(engine_name): + ''' +@@ -290,7 +284,7 @@ + assert IBus.PropList is not IBUS_PROP_LIST + assert IBus.PropList is MockPropList + # Reload the table module so that the patches +- # are applied to TabEngine: ++ # are applied to tabengine: + sys.path.insert(0, '../engine') + importlib.reload(table) + sys.path.pop(0) +@@ -302,9 +296,9 @@ + ENGINE = None + tear_down() + return False +- TABSQLITEDB = tabsqlitedb.TabSqliteDb( ++ TABSQLITEDB = tabsqlitedb.tabsqlitedb( + filename=db_file, user_db=':memory:') +- ENGINE = table.TabEngine( ++ ENGINE = table.tabengine( + bus, + '/com/redhat/IBus/engines/table/%s/engine/0' %engine_name, + TABSQLITEDB, +@@ -380,59 +374,6 @@ + self.assertEqual(ENGINE.mock_preedit_text, '') + self.assertEqual(ENGINE.mock_committed_text, '工爱工') + +- def test_suggestion_mode(self): +- if not ENGINE._ime_sg: +- self.skipTest("This engine does not have a suggestion mode.") +- # Suggestion mode is False by default: +- self.assertEqual(ENGINE.get_suggestion_mode(), False) +- self.assertEqual(ENGINE.get_pinyin_mode(), False) +- ENGINE.do_process_key_event(IBus.KEY_a, 0, 0) +- self.assertEqual(ENGINE.mock_preedit_text, '工') +- ENGINE.do_process_key_event(IBus.KEY_space, 0, 0) +- self.assertEqual(ENGINE.mock_preedit_text, '') +- self.assertEqual(ENGINE.mock_committed_text, '工') +- self.assertEqual(ENGINE._editor._lookup_table.mock_candidates, []) +- ENGINE.set_suggestion_mode(True) +- ENGINE.do_process_key_event(IBus.KEY_a, 0, 0) +- self.assertEqual(ENGINE.mock_preedit_text, '工') +- ENGINE.do_process_key_event(IBus.KEY_space, 0, 0) +- self.assertEqual(ENGINE.mock_preedit_text, '') +- self.assertEqual(ENGINE.mock_committed_text, '工工') +- self.assertEqual(ENGINE._editor._lookup_table.mock_candidates, +- ['工作人员 673 0', +- '工作会议 310 0', +- '工作报告 267 0', +- '工人阶级 146 0', +- '工作重点 78 0', +- '工作小组 73 0', +- '工业企业 71 0', +- '工业大学 69 0', +- '工作单位 61 0', +- '工业生产 58 0']) +- ENGINE.do_process_key_event(IBus.KEY_space, 0, 0) +- self.assertEqual(ENGINE.mock_preedit_text, '') +- self.assertEqual(ENGINE.mock_committed_text, '工工作人员') +- ENGINE.set_pinyin_mode(True) +- ENGINE.do_process_key_event(IBus.KEY_a, 0, 0) +- self.assertEqual(ENGINE.mock_preedit_text, '爱') +- ENGINE.do_process_key_event(IBus.KEY_space, 0, 0) +- self.assertEqual(ENGINE.mock_preedit_text, '') +- self.assertEqual(ENGINE.mock_committed_text, '工工作人员爱') +- self.assertEqual(ENGINE._editor._lookup_table.mock_candidates, +- ['爱因斯坦 1109 0', +- '爱情故事 519 0', +- '爱国主义 191 0', +- '爱尔兰语 91 0', +- '爱好和平 62 0', +- '爱情小说 58 0', +- '爱不释手 39 0', +- '爱国热情 35 0', +- '爱莫能助 34 0', +- '爱理不理 32 0']) +- ENGINE.do_process_key_event(IBus.KEY_space, 0, 0) +- self.assertEqual(ENGINE.mock_preedit_text, '') +- self.assertEqual(ENGINE.mock_committed_text, '工工作人员爱因斯坦') +- + def test_commit_to_preedit_switching_to_pinyin_defining_a_phrase(self): + ENGINE.do_process_key_event(IBus.KEY_a, 0, 0) + # commit to preëdit needs a press and release of either diff --git a/SOURCES/use-config-not-gsettings-for-gui-test.patch b/SOURCES/use-config-not-gsettings-for-gui-test.patch new file mode 100644 index 0000000..5643bf3 --- /dev/null +++ b/SOURCES/use-config-not-gsettings-for-gui-test.patch @@ -0,0 +1,62 @@ +diff -ru ibus-table-1.9.18.orig/tests/test_0_gtk.py ibus-table-1.9.18/tests/test_0_gtk.py +--- ibus-table-1.9.18.orig/tests/test_0_gtk.py 2020-07-27 09:41:25.799087549 +0200 ++++ ibus-table-1.9.18/tests/test_0_gtk.py 2020-07-28 12:23:43.127130148 +0200 +@@ -68,6 +68,7 @@ + + DONE_EXIT = True + ENGINE_NAME = 'wubi-jidian86' ++CONFIG_SECTION = 'engine/table/%s' % ENGINE_NAME + + from gtkcases import TestCases + +@@ -96,16 +97,18 @@ + class SimpleGtkTestCase(unittest.TestCase): + global DONE_EXIT + global ENGINE_NAME ++ global CONFIG_SECTION + ENGINE_PATH = '/com/redhat/IBus/engines/table/Test/Engine' + + @classmethod + def setUpClass(cls): + cls._flag = False + IBus.init() +- cls._gsettings = Gio.Settings( +- schema='org.freedesktop.ibus.engine.table', +- path='/org/freedesktop/ibus/engine/table/%s/' % ENGINE_NAME) +- cls._orig_chinesemode = cls._gsettings.get_int('chinesemode') ++ cls.__bus = IBus.Bus() ++ cls._config = cls.__bus.get_config() ++ cls._orig_chinesemode = table.variant_to_value( ++ cls._config.get_value( ++ CONFIG_SECTION, 'chinesemode')) + signums = [getattr(signal, s, None) for s in + 'SIGINT SIGTERM SIGHUP'.split()] + for signum in filter(None, signums): +@@ -116,7 +119,11 @@ + (signum, original_handler)) + @classmethod + def tearDownClass(cls): +- cls._gsettings.set_int('chinesemode', cls._orig_chinesemode) ++ if cls._orig_chinesemode: ++ cls._config.set_value( ++ CONFIG_SECTION, ++ 'chinesemode', ++ GLib.Variant.new_int32(cls._orig_chinesemode)) + + @classmethod + def signal_handler(cls, user_data): +@@ -136,10 +142,12 @@ + self.__inserted_text = '' + self.__commit_done = False + self.__reset_coming = False +- self._gsettings.set_int('chinesemode', 4) ++ self._config.set_value( ++ CONFIG_SECTION, ++ 'chinesemode', ++ GLib.Variant.new_int32(4)) + + def register_ibus_engine(self): +- self.__bus = IBus.Bus() + if not self.__bus.is_connected(): + self.fail('ibus-daemon is not running') + return False diff --git a/SPECS/ibus-table.spec b/SPECS/ibus-table.spec index bfeb27e..5996bc2 100644 --- a/SPECS/ibus-table.spec +++ b/SPECS/ibus-table.spec @@ -1,17 +1,34 @@ Name: ibus-table Version: 1.9.18 -Release: 3%{?dist} +Release: 6%{?dist} Summary: The Table engine for IBus platform License: LGPLv2+ Group: System Environment/Libraries URL: http://code.google.com/p/ibus/ Source0: http://mfabian.fedorapeople.org/ibus-table/%{name}-%{version}.tar.gz +Patch0: add-a-test-suite.patch +Patch1: add-a-gui-test.patch +Patch2: use-config-not-gsettings-for-gui-test.patch +Patch3: fix-test-cases-for-old-ibus-table.patch Requires: ibus > 1.3.0 Requires: python(abi) >= 3.3 Requires: %{__python3} BuildRequires: ibus-devel > 1.3.0 BuildRequires: python3-devel +# for the unit tests +BuildRequires: automake +BuildRequires: autoconf +BuildRequires: gettext-devel BuildRequires: libappstream-glib +BuildRequires: desktop-file-utils +BuildRequires: python3-mock +BuildRequires: python3-gobject +BuildRequires: python3-gobject-base +BuildRequires: dbus-x11 +BuildRequires: xorg-x11-server-Xvfb +BuildRequires: ibus-table-chinese-wubi-jidian +BuildRequires: ibus-table-chinese-cangjie +BuildRequires: ibus-table-chinese-stroke5 Obsoletes: ibus-table-additional < 1.2.0.20100111-5 @@ -29,12 +46,25 @@ Requires: %{name} = %{version}-%{release}, pkgconfig %description -n %{name}-devel Development files for %{name}. +%package tests +Summary: Tests for the %{name} package +Requires: %{name} = %{version}-%{release} + +%description tests +The %{name}-tests package contains tests that can be used to verify +the functionality of the installed %{name} package. + %prep %setup -q +%patch0 -p1 -b .add-a-test-suite +%patch1 -p1 -b .add-a-gui-test +%patch2 -p1 -b .use-config-not-gsettings-for-gui-test +%patch3 -p1 -b .fix-test-cases-for-old-ibus-table %build export PYTHON=%{__python3} -%configure --disable-static --disable-additional +./autogen.sh --prefix=/usr +%configure --enable-installed-tests %__make %{?_smp_mflags} %install @@ -46,6 +76,38 @@ export PYTHON=%{__python3} %check appstream-util validate-relax --nonet %{buildroot}/%{_datadir}/metainfo/*.appdata.xml +pushd engine +# run doctests + python3 table.py + python3 it_util.py +popd +export DISPLAY=:1 +Xvfb $DISPLAY -screen 0 1024x768x16 & +ibus-daemon -drx # not needed on Fedora 32 +# A window manager and and ibus-daemon are needed to run the GUI +# test tests/test_gtk.py, for example i3 can be used. +# +# To debug what is going on if there is a problem with the GUI test +# add BuildRequires: x11vnc and start a vnc server: +# +# x11vnc -display $DISPLAY -unixsock /tmp/mysock -bg -nopw -listen localhost -xkb +# +# Then one can view what is going on outside of the chroot with vncviewer: +# +# vncviewer /var/lib/mock/fedora-32-x86_64/root/tmp/mysock +# +# The GUI test will be skipped if XDG_SESSION_TYPE is not x11 or wayland. +# +#ibus-daemon -drx +#touch /tmp/i3config +#i3 -c /tmp/i3config & +#export XDG_SESSION_TYPE=x11 + +make check && rc=0 || rc=1 +cat tests/*.log +if [ $rc != 0 ] ; then + exit $rc +fi %clean %__rm -rf $RPM_BUILD_ROOT @@ -67,7 +129,25 @@ appstream-util validate-relax --nonet %{buildroot}/%{_datadir}/metainfo/*.appdat %defattr(-, root, root, -) %{_datadir}/pkgconfig/%{name}.pc +%files tests +%dir %{_libexecdir}/installed-tests +%{_libexecdir}/installed-tests/%{name} +%dir %{_datadir}/installed-tests +%{_datadir}/installed-tests/%{name} + %changelog +* Tue Feb 16 2021 Mike FABIAN - 1.9.18-6 +- Fix a covscan error +- Related: rhbz#1929102 + +* Tue Feb 16 2021 Mike FABIAN - 1.9.18-5 +- Bump release number to add the new subpackage ibus-table-tests +- Resolves: rhbz#1929102 + +* Sat Jul 25 2020 Mike FABIAN - 1.9.18-4 +- Add gating tests to ibus-table +- Resolves: rhbz#1682163 + * Thu Sep 20 2018 Tomas Orsava - 1.9.18-3 - Require the Python interpreter directly instead of using the package name - Related: rhbz#1619153