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 value > len(self._editor._select_keys): + value = len(self._editor._select_keys) + if value < 1: + value = 1 + 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() + if update_dconf: + self._config.set_value( + self._config_section, + 'lookuptablepagesize', + GLib.Variant.new_int32(value)) + + 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, 'шщс ')