From f17efc3da87a101950c57388a58fb388b3fb6649 Mon Sep 17 00:00:00 2001 From: Chrys Date: Mon, 21 Oct 2024 02:18:04 +0200 Subject: [PATCH] add basic plugin capability --- m4/build-to-host.m4 | 79 ++ m4/host-cpu-c-abi.m4 | 527 ++++++++++++ src/cthulhu/dynamic_api_manager.py | 62 ++ src/cthulhu/plugin.py | 160 ++++ src/cthulhu/plugin_system_manager.py | 520 ++++++++++++ .../plugins/ByeCthulhu/ByeCthulhu.plugin | 6 + src/cthulhu/plugins/ByeCthulhu/ByeCthulhu.py | 27 + src/cthulhu/plugins/ByeCthulhu/Makefile.am | 7 + src/cthulhu/plugins/ByeCthulhu/__init__.py | 0 .../plugins/CapsLockHack/CapsLockHack.plugin | 6 + .../plugins/CapsLockHack/CapsLockHack.py | 119 +++ src/cthulhu/plugins/CapsLockHack/Makefile.am | 7 + src/cthulhu/plugins/CapsLockHack/__init__.py | 0 .../plugins/Clipboard/Clipboard.plugin | 6 + src/cthulhu/plugins/Clipboard/Clipboard.py | 76 ++ src/cthulhu/plugins/Clipboard/Makefile.am | 7 + src/cthulhu/plugins/Clipboard/__init__.py | 0 src/cthulhu/plugins/Date/Date.plugin | 6 + src/cthulhu/plugins/Date/Date.py | 33 + src/cthulhu/plugins/Date/Makefile.am | 7 + src/cthulhu/plugins/Date/__init__.py | 0 .../plugins/HelloCthulhu/HelloCthulhu.plugin | 6 + .../plugins/HelloCthulhu/HelloCthulhu.py | 23 + src/cthulhu/plugins/HelloCthulhu/Makefile.am | 7 + src/cthulhu/plugins/HelloCthulhu/__init__.py | 0 .../plugins/HelloWorld/HelloWorld.plugin | 6 + src/cthulhu/plugins/HelloWorld/HelloWorld.py | 24 + src/cthulhu/plugins/HelloWorld/Makefile.am | 7 + src/cthulhu/plugins/HelloWorld/__init__.py | 0 src/cthulhu/plugins/Makefile.am | 4 + src/cthulhu/plugins/MouseReview/Makefile.am | 7 + .../plugins/MouseReview/MouseReview.plugin | 6 + .../plugins/MouseReview/MouseReview.py | 754 ++++++++++++++++++ src/cthulhu/plugins/MouseReview/__init__.py | 0 src/cthulhu/plugins/PluginManager/Makefile.am | 8 + .../PluginManager/PluginManager.plugin | 14 + .../plugins/PluginManager/PluginManager.py | 36 + .../plugins/PluginManager/PluginManagerUi.py | 283 +++++++ .../PluginManager/PluginManagerUiListBox.py | 83 ++ .../PluginManagerUiListBox_tut.py | 93 +++ src/cthulhu/plugins/PluginManager/__init__.py | 0 src/cthulhu/plugins/SelfVoice/Makefile.am | 7 + .../plugins/SelfVoice/SelfVoice.plugin | 6 + src/cthulhu/plugins/SelfVoice/SelfVoice.py | 117 +++ src/cthulhu/plugins/SelfVoice/__init__.py | 0 src/cthulhu/plugins/Time/Makefile.am | 7 + src/cthulhu/plugins/Time/Time.plugin | 6 + src/cthulhu/plugins/Time/Time.py | 35 + src/cthulhu/plugins/Time/__init__.py | 0 src/cthulhu/resource_manager.py | 349 ++++++++ src/cthulhu/signal_manager.py | 58 ++ src/cthulhu/translation_context.py | 93 +++ src/cthulhu/translation_manager.py | 52 ++ 53 files changed, 3746 insertions(+) create mode 100644 m4/build-to-host.m4 create mode 100644 m4/host-cpu-c-abi.m4 create mode 100644 src/cthulhu/dynamic_api_manager.py create mode 100644 src/cthulhu/plugin.py create mode 100644 src/cthulhu/plugin_system_manager.py create mode 100644 src/cthulhu/plugins/ByeCthulhu/ByeCthulhu.plugin create mode 100644 src/cthulhu/plugins/ByeCthulhu/ByeCthulhu.py create mode 100644 src/cthulhu/plugins/ByeCthulhu/Makefile.am create mode 100644 src/cthulhu/plugins/ByeCthulhu/__init__.py create mode 100644 src/cthulhu/plugins/CapsLockHack/CapsLockHack.plugin create mode 100644 src/cthulhu/plugins/CapsLockHack/CapsLockHack.py create mode 100644 src/cthulhu/plugins/CapsLockHack/Makefile.am create mode 100644 src/cthulhu/plugins/CapsLockHack/__init__.py create mode 100644 src/cthulhu/plugins/Clipboard/Clipboard.plugin create mode 100644 src/cthulhu/plugins/Clipboard/Clipboard.py create mode 100644 src/cthulhu/plugins/Clipboard/Makefile.am create mode 100644 src/cthulhu/plugins/Clipboard/__init__.py create mode 100644 src/cthulhu/plugins/Date/Date.plugin create mode 100644 src/cthulhu/plugins/Date/Date.py create mode 100644 src/cthulhu/plugins/Date/Makefile.am create mode 100644 src/cthulhu/plugins/Date/__init__.py create mode 100644 src/cthulhu/plugins/HelloCthulhu/HelloCthulhu.plugin create mode 100644 src/cthulhu/plugins/HelloCthulhu/HelloCthulhu.py create mode 100644 src/cthulhu/plugins/HelloCthulhu/Makefile.am create mode 100644 src/cthulhu/plugins/HelloCthulhu/__init__.py create mode 100644 src/cthulhu/plugins/HelloWorld/HelloWorld.plugin create mode 100644 src/cthulhu/plugins/HelloWorld/HelloWorld.py create mode 100644 src/cthulhu/plugins/HelloWorld/Makefile.am create mode 100644 src/cthulhu/plugins/HelloWorld/__init__.py create mode 100644 src/cthulhu/plugins/Makefile.am create mode 100644 src/cthulhu/plugins/MouseReview/Makefile.am create mode 100644 src/cthulhu/plugins/MouseReview/MouseReview.plugin create mode 100644 src/cthulhu/plugins/MouseReview/MouseReview.py create mode 100644 src/cthulhu/plugins/MouseReview/__init__.py create mode 100644 src/cthulhu/plugins/PluginManager/Makefile.am create mode 100644 src/cthulhu/plugins/PluginManager/PluginManager.plugin create mode 100644 src/cthulhu/plugins/PluginManager/PluginManager.py create mode 100755 src/cthulhu/plugins/PluginManager/PluginManagerUi.py create mode 100755 src/cthulhu/plugins/PluginManager/PluginManagerUiListBox.py create mode 100755 src/cthulhu/plugins/PluginManager/PluginManagerUiListBox_tut.py create mode 100644 src/cthulhu/plugins/PluginManager/__init__.py create mode 100644 src/cthulhu/plugins/SelfVoice/Makefile.am create mode 100644 src/cthulhu/plugins/SelfVoice/SelfVoice.plugin create mode 100644 src/cthulhu/plugins/SelfVoice/SelfVoice.py create mode 100644 src/cthulhu/plugins/SelfVoice/__init__.py create mode 100644 src/cthulhu/plugins/Time/Makefile.am create mode 100644 src/cthulhu/plugins/Time/Time.plugin create mode 100644 src/cthulhu/plugins/Time/Time.py create mode 100644 src/cthulhu/plugins/Time/__init__.py create mode 100644 src/cthulhu/resource_manager.py create mode 100644 src/cthulhu/signal_manager.py create mode 100644 src/cthulhu/translation_context.py create mode 100644 src/cthulhu/translation_manager.py diff --git a/m4/build-to-host.m4 b/m4/build-to-host.m4 new file mode 100644 index 0000000..f928e9a --- /dev/null +++ b/m4/build-to-host.m4 @@ -0,0 +1,79 @@ +# build-to-host.m4 serial 3 +dnl Copyright (C) 2023-2024 Free Software Foundation, Inc. +dnl This file is free software; the Free Software Foundation +dnl gives unlimited permission to copy and/or distribute it, +dnl with or without modifications, as long as this notice is preserved. + +dnl Written by Bruno Haible. + +dnl When the build environment ($build_os) is different from the target runtime +dnl environment ($host_os), file names may need to be converted from the build +dnl environment syntax to the target runtime environment syntax. This is +dnl because the Makefiles are executed (mostly) by build environment tools and +dnl therefore expect file names in build environment syntax, whereas the runtime +dnl expects file names in target runtime environment syntax. +dnl +dnl For example, if $build_os = cygwin and $host_os = mingw32, filenames need +dnl be converted from Cygwin syntax to native Windows syntax: +dnl /cygdrive/c/foo/bar -> C:\foo\bar +dnl /usr/local/share -> C:\cygwin64\usr\local\share +dnl +dnl gl_BUILD_TO_HOST([somedir]) +dnl This macro takes as input an AC_SUBSTed variable 'somedir', which must +dnl already have its final value assigned, and produces two additional +dnl AC_SUBSTed variables 'somedir_c' and 'somedir_c_make', that designate the +dnl same file name value, just in different syntax: +dnl - somedir_c is the file name in target runtime environment syntax, +dnl as a C string (starting and ending with a double-quote, +dnl and with escaped backslashes and double-quotes in +dnl between). +dnl - somedir_c_make is the same thing, escaped for use in a Makefile. + +AC_DEFUN([gl_BUILD_TO_HOST], +[ + AC_REQUIRE([AC_CANONICAL_BUILD]) + AC_REQUIRE([AC_CANONICAL_HOST]) + AC_REQUIRE([gl_BUILD_TO_HOST_INIT]) + + dnl Define somedir_c. + gl_final_[$1]="$[$1]" + dnl Translate it from build syntax to host syntax. + case "$build_os" in + cygwin*) + case "$host_os" in + mingw* | windows*) + gl_final_[$1]=`cygpath -w "$gl_final_[$1]"` ;; + esac + ;; + esac + dnl Convert it to C string syntax. + [$1]_c=`printf '%s\n' "$gl_final_[$1]" | sed -e "$gl_sed_double_backslashes" -e "$gl_sed_escape_doublequotes" | tr -d "$gl_tr_cr"` + [$1]_c='"'"$[$1]_c"'"' + AC_SUBST([$1_c]) + + dnl Define somedir_c_make. + [$1]_c_make=`printf '%s\n' "$[$1]_c" | sed -e "$gl_sed_escape_for_make_1" -e "$gl_sed_escape_for_make_2" | tr -d "$gl_tr_cr"` + dnl Use the substituted somedir variable, when possible, so that the user + dnl may adjust somedir a posteriori when there are no special characters. + if test "$[$1]_c_make" = '\"'"${gl_final_[$1]}"'\"'; then + [$1]_c_make='\"$([$1])\"' + fi + AC_SUBST([$1_c_make]) +]) + +dnl Some initializations for gl_BUILD_TO_HOST. +AC_DEFUN([gl_BUILD_TO_HOST_INIT], +[ + gl_sed_double_backslashes='s/\\/\\\\/g' + gl_sed_escape_doublequotes='s/"/\\"/g' +changequote(,)dnl + gl_sed_escape_for_make_1="s,\\([ \"&'();<>\\\\\`|]\\),\\\\\\1,g" +changequote([,])dnl + gl_sed_escape_for_make_2='s,\$,\\$$,g' + dnl Find out how to remove carriage returns from output. Solaris /usr/ucb/tr + dnl does not understand '\r'. + case `echo r | tr -d '\r'` in + '') gl_tr_cr='\015' ;; + *) gl_tr_cr='\r' ;; + esac +]) diff --git a/m4/host-cpu-c-abi.m4 b/m4/host-cpu-c-abi.m4 new file mode 100644 index 0000000..e860a19 --- /dev/null +++ b/m4/host-cpu-c-abi.m4 @@ -0,0 +1,527 @@ +# host-cpu-c-abi.m4 serial 17 +dnl Copyright (C) 2002-2024 Free Software Foundation, Inc. +dnl This file is free software; the Free Software Foundation +dnl gives unlimited permission to copy and/or distribute it, +dnl with or without modifications, as long as this notice is preserved. + +dnl From Bruno Haible and Sam Steingold. + +dnl Sets the HOST_CPU variable to the canonical name of the CPU. +dnl Sets the HOST_CPU_C_ABI variable to the canonical name of the CPU with its +dnl C language ABI (application binary interface). +dnl Also defines __${HOST_CPU}__ and __${HOST_CPU_C_ABI}__ as C macros in +dnl config.h. +dnl +dnl This canonical name can be used to select a particular assembly language +dnl source file that will interoperate with C code on the given host. +dnl +dnl For example: +dnl * 'i386' and 'sparc' are different canonical names, because code for i386 +dnl will not run on SPARC CPUs and vice versa. They have different +dnl instruction sets. +dnl * 'sparc' and 'sparc64' are different canonical names, because code for +dnl 'sparc' and code for 'sparc64' cannot be linked together: 'sparc' code +dnl contains 32-bit instructions, whereas 'sparc64' code contains 64-bit +dnl instructions. A process on a SPARC CPU can be in 32-bit mode or in 64-bit +dnl mode, but not both. +dnl * 'mips' and 'mipsn32' are different canonical names, because they use +dnl different argument passing and return conventions for C functions, and +dnl although the instruction set of 'mips' is a large subset of the +dnl instruction set of 'mipsn32'. +dnl * 'mipsn32' and 'mips64' are different canonical names, because they use +dnl different sizes for the C types like 'int' and 'void *', and although +dnl the instruction sets of 'mipsn32' and 'mips64' are the same. +dnl * The same canonical name is used for different endiannesses. You can +dnl determine the endianness through preprocessor symbols: +dnl - 'arm': test __ARMEL__. +dnl - 'mips', 'mipsn32', 'mips64': test _MIPSEB vs. _MIPSEL. +dnl - 'powerpc64': test _BIG_ENDIAN vs. _LITTLE_ENDIAN. +dnl * The same name 'i386' is used for CPUs of type i386, i486, i586 +dnl (Pentium), AMD K7, Pentium II, Pentium IV, etc., because +dnl - Instructions that do not exist on all of these CPUs (cmpxchg, +dnl MMX, SSE, SSE2, 3DNow! etc.) are not frequently used. If your +dnl assembly language source files use such instructions, you will +dnl need to make the distinction. +dnl - Speed of execution of the common instruction set is reasonable across +dnl the entire family of CPUs. If you have assembly language source files +dnl that are optimized for particular CPU types (like GNU gmp has), you +dnl will need to make the distinction. +dnl See . +AC_DEFUN([gl_HOST_CPU_C_ABI], +[ + AC_REQUIRE([AC_CANONICAL_HOST]) + AC_REQUIRE([gl_C_ASM]) + AC_CACHE_CHECK([host CPU and C ABI], [gl_cv_host_cpu_c_abi], + [case "$host_cpu" in + +changequote(,)dnl + i[34567]86 ) +changequote([,])dnl + gl_cv_host_cpu_c_abi=i386 + ;; + + x86_64 ) + # On x86_64 systems, the C compiler may be generating code in one of + # these ABIs: + # - 64-bit instruction set, 64-bit pointers, 64-bit 'long': x86_64. + # - 64-bit instruction set, 64-bit pointers, 32-bit 'long': x86_64 + # with native Windows (mingw, MSVC). + # - 64-bit instruction set, 32-bit pointers, 32-bit 'long': x86_64-x32. + # - 32-bit instruction set, 32-bit pointers, 32-bit 'long': i386. + AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#if (defined __x86_64__ || defined __amd64__ \ + || defined _M_X64 || defined _M_AMD64) + int ok; + #else + error fail + #endif + ]])], + [AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#if defined __ILP32__ || defined _ILP32 + int ok; + #else + error fail + #endif + ]])], + [gl_cv_host_cpu_c_abi=x86_64-x32], + [gl_cv_host_cpu_c_abi=x86_64])], + [gl_cv_host_cpu_c_abi=i386]) + ;; + +changequote(,)dnl + alphaev[4-8] | alphaev56 | alphapca5[67] | alphaev6[78] ) +changequote([,])dnl + gl_cv_host_cpu_c_abi=alpha + ;; + + arm* | aarch64 ) + # Assume arm with EABI. + # On arm64 systems, the C compiler may be generating code in one of + # these ABIs: + # - aarch64 instruction set, 64-bit pointers, 64-bit 'long': arm64. + # - aarch64 instruction set, 32-bit pointers, 32-bit 'long': arm64-ilp32. + # - 32-bit instruction set, 32-bit pointers, 32-bit 'long': arm or armhf. + AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#ifdef __aarch64__ + int ok; + #else + error fail + #endif + ]])], + [AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#if defined __ILP32__ || defined _ILP32 + int ok; + #else + error fail + #endif + ]])], + [gl_cv_host_cpu_c_abi=arm64-ilp32], + [gl_cv_host_cpu_c_abi=arm64])], + [# Don't distinguish little-endian and big-endian arm, since they + # don't require different machine code for simple operations and + # since the user can distinguish them through the preprocessor + # defines __ARMEL__ vs. __ARMEB__. + # But distinguish arm which passes floating-point arguments and + # return values in integer registers (r0, r1, ...) - this is + # gcc -mfloat-abi=soft or gcc -mfloat-abi=softfp - from arm which + # passes them in float registers (s0, s1, ...) and double registers + # (d0, d1, ...) - this is gcc -mfloat-abi=hard. GCC 4.6 or newer + # sets the preprocessor defines __ARM_PCS (for the first case) and + # __ARM_PCS_VFP (for the second case), but older GCC does not. + echo 'double ddd; void func (double dd) { ddd = dd; }' > conftest.c + # Look for a reference to the register d0 in the .s file. + AC_TRY_COMMAND(${CC-cc} $CFLAGS $CPPFLAGS $gl_c_asm_opt conftest.c) >/dev/null 2>&1 + if LC_ALL=C grep 'd0,' conftest.$gl_asmext >/dev/null; then + gl_cv_host_cpu_c_abi=armhf + else + gl_cv_host_cpu_c_abi=arm + fi + rm -f conftest* + ]) + ;; + + hppa1.0 | hppa1.1 | hppa2.0* | hppa64 ) + # On hppa, the C compiler may be generating 32-bit code or 64-bit + # code. In the latter case, it defines _LP64 and __LP64__. + AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#ifdef __LP64__ + int ok; + #else + error fail + #endif + ]])], + [gl_cv_host_cpu_c_abi=hppa64], + [gl_cv_host_cpu_c_abi=hppa]) + ;; + + ia64* ) + # On ia64 on HP-UX, the C compiler may be generating 64-bit code or + # 32-bit code. In the latter case, it defines _ILP32. + AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#ifdef _ILP32 + int ok; + #else + error fail + #endif + ]])], + [gl_cv_host_cpu_c_abi=ia64-ilp32], + [gl_cv_host_cpu_c_abi=ia64]) + ;; + + mips* ) + # We should also check for (_MIPS_SZPTR == 64), but gcc keeps this + # at 32. + AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#if defined _MIPS_SZLONG && (_MIPS_SZLONG == 64) + int ok; + #else + error fail + #endif + ]])], + [gl_cv_host_cpu_c_abi=mips64], + [# In the n32 ABI, _ABIN32 is defined, _ABIO32 is not defined (but + # may later get defined by ), and _MIPS_SIM == _ABIN32. + # In the 32 ABI, _ABIO32 is defined, _ABIN32 is not defined (but + # may later get defined by ), and _MIPS_SIM == _ABIO32. + AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#if (_MIPS_SIM == _ABIN32) + int ok; + #else + error fail + #endif + ]])], + [gl_cv_host_cpu_c_abi=mipsn32], + [gl_cv_host_cpu_c_abi=mips])]) + ;; + + powerpc* ) + # Different ABIs are in use on AIX vs. Mac OS X vs. Linux,*BSD. + # No need to distinguish them here; the caller may distinguish + # them based on the OS. + # On powerpc64 systems, the C compiler may still be generating + # 32-bit code. And on powerpc-ibm-aix systems, the C compiler may + # be generating 64-bit code. + AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#if defined __powerpc64__ || defined __LP64__ + int ok; + #else + error fail + #endif + ]])], + [# On powerpc64, there are two ABIs on Linux: The AIX compatible + # one and the ELFv2 one. The latter defines _CALL_ELF=2. + AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#if defined _CALL_ELF && _CALL_ELF == 2 + int ok; + #else + error fail + #endif + ]])], + [gl_cv_host_cpu_c_abi=powerpc64-elfv2], + [gl_cv_host_cpu_c_abi=powerpc64]) + ], + [gl_cv_host_cpu_c_abi=powerpc]) + ;; + + rs6000 ) + gl_cv_host_cpu_c_abi=powerpc + ;; + + riscv32 | riscv64 ) + # There are 2 architectures (with variants): rv32* and rv64*. + AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#if __riscv_xlen == 64 + int ok; + #else + error fail + #endif + ]])], + [cpu=riscv64], + [cpu=riscv32]) + # There are 6 ABIs: ilp32, ilp32f, ilp32d, lp64, lp64f, lp64d. + # Size of 'long' and 'void *': + AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#if defined __LP64__ + int ok; + #else + error fail + #endif + ]])], + [main_abi=lp64], + [main_abi=ilp32]) + # Float ABIs: + # __riscv_float_abi_double: + # 'float' and 'double' are passed in floating-point registers. + # __riscv_float_abi_single: + # 'float' are passed in floating-point registers. + # __riscv_float_abi_soft: + # No values are passed in floating-point registers. + AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#if defined __riscv_float_abi_double + int ok; + #else + error fail + #endif + ]])], + [float_abi=d], + [AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#if defined __riscv_float_abi_single + int ok; + #else + error fail + #endif + ]])], + [float_abi=f], + [float_abi='']) + ]) + gl_cv_host_cpu_c_abi="${cpu}-${main_abi}${float_abi}" + ;; + + s390* ) + # On s390x, the C compiler may be generating 64-bit (= s390x) code + # or 31-bit (= s390) code. + AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#if defined __LP64__ || defined __s390x__ + int ok; + #else + error fail + #endif + ]])], + [gl_cv_host_cpu_c_abi=s390x], + [gl_cv_host_cpu_c_abi=s390]) + ;; + + sparc | sparc64 ) + # UltraSPARCs running Linux have `uname -m` = "sparc64", but the + # C compiler still generates 32-bit code. + AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#if defined __sparcv9 || defined __arch64__ + int ok; + #else + error fail + #endif + ]])], + [gl_cv_host_cpu_c_abi=sparc64], + [gl_cv_host_cpu_c_abi=sparc]) + ;; + + *) + gl_cv_host_cpu_c_abi="$host_cpu" + ;; + esac + ]) + + dnl In most cases, $HOST_CPU and $HOST_CPU_C_ABI are the same. + HOST_CPU=`echo "$gl_cv_host_cpu_c_abi" | sed -e 's/-.*//'` + HOST_CPU_C_ABI="$gl_cv_host_cpu_c_abi" + AC_SUBST([HOST_CPU]) + AC_SUBST([HOST_CPU_C_ABI]) + + # This was + # AC_DEFINE_UNQUOTED([__${HOST_CPU}__]) + # AC_DEFINE_UNQUOTED([__${HOST_CPU_C_ABI}__]) + # earlier, but KAI C++ 3.2d doesn't like this. + sed -e 's/-/_/g' >> confdefs.h < takes exactly 2 arguments (1 given) but documentation doesnt say any parameter + #def hasPluginDependency(self, pluginInfo): + # return pluginInfo.has_dependency() + #def getPluginExternalData(self, pluginInfo): + # return pluginInfo.get_external_data() + def isPluginAvailable(self, pluginInfo): + try: + return pluginInfo.is_available() + except: + return False + def isPluginLoaded(self, pluginInfo): + try: + return pluginInfo.is_loaded() + except: + return False + + def getIgnoredPlugins(self): + return self._ignorePluginModulePath + def setIgnoredPlugins(self, pluginModulePath, ignored): + if pluginModulePath.endswith('/'): + pluginModulePath = pluginModulePath[:-1] + if ignored: + if not pluginModulePath in self.getIgnoredPlugins(): + self._ignorePluginModulePath.append(pluginModulePath) + else: + if pluginModulePath in self.getIgnoredPlugins(): + self._ignorePluginModulePath.remove(pluginModulePath) + + def setPluginActive(self, pluginInfo, active): + if self.isPluginBuildIn(pluginInfo): + active = True + pluginName = self.getPluginModuleName(pluginInfo) + if active: + if not pluginName in self.getActivePlugins(): + if self.loadPlugin(pluginInfo): + self._activePlugins.append(pluginName ) + else: + if pluginName in self.getActivePlugins(): + if self.unloadPlugin(pluginInfo): + self._activePlugins.remove(pluginName ) + def isPluginActive(self, pluginInfo): + if self.isPluginBuildIn(pluginInfo): + return True + if self.isPluginLoaded(pluginInfo): + return True + active_plugin_names = self.getActivePlugins() + return self.getPluginModuleName(pluginInfo) in active_plugin_names + def syncAllPluginsActive(self, ForceAllPlugins=False): + self.unloadAllPlugins(ForceAllPlugins) + self.loadAllPlugins(ForceAllPlugins) + + def loadAllPlugins(self, ForceAllPlugins=False): + """Loads plugins from settings.""" + for pluginInfo in self.plugins: + if self.isPluginActive(pluginInfo) or ForceAllPlugins: + self.loadPlugin(pluginInfo) + + def loadPlugin(self, pluginInfo): + resourceManager = self.getApp().getResourceManager() + moduleName = pluginInfo.get_module_name() + try: + if pluginInfo not in self.plugins: + print("Plugin missing: {}".format(moduleName)) + return False + resourceManager.addResourceContext(moduleName) + self.engine.load_plugin(pluginInfo) + except Exception as e: + print('loadPlugin:',e) + return False + return True + + def unloadAllPlugins(self, ForceAllPlugins=False): + """Loads plugins from settings.""" + for pluginInfo in self.plugins: + if not self.isPluginActive(pluginInfo) or ForceAllPlugins: + self.unloadPlugin(pluginInfo) + + def unloadPlugin(self, pluginInfo): + resourceManager = self.getApp().getResourceManager() + moduleName = pluginInfo.get_module_name() + try: + if pluginInfo not in self.plugins: + print("Plugin missing: {}".format(moduleName)) + return False + if self.isPluginBuildIn(pluginInfo): + return False + self.engine.unload_plugin(pluginInfo) + self.getApp().getResourceManager().removeResourceContext(moduleName) + self.engine.garbage_collect() + except Exception as e: + print('unloadPlugin:',e) + return False + return True + def installPlugin(self, pluginFilePath, pluginType=PluginType.USER): + if not self.isValidPluginFile(pluginFilePath): + return False + pluginFolder = pluginType.get_root_dir() + if not pluginFolder.endswith('/'): + pluginFolder += '/' + if not os.path.exists(pluginFolder): + os.mkdir(pluginFolder) + else: + if not os.path.isdir(pluginFolder): + return False + try: + with tarfile.open(pluginFilePath) as tar: + tar.extractall(path=pluginFolder) + except Exception as e: + print(e) + + pluginModulePath = self.getModuleDirByPluginFile(pluginFilePath) + if pluginModulePath != '': + pluginModulePath = pluginFolder + pluginModulePath + self.setIgnoredPlugins(pluginModulePath[:-1], False) # without ending / + print('install', pluginFilePath) + self.callPackageTriggers(pluginModulePath, 'onPostInstall') + self.rescanPlugins() + + return True + def getModuleDirByPluginFile(self, pluginFilePath): + if not isinstance(pluginFilePath, str): + return '' + if pluginFilePath == '': + return '' + if not os.path.exists(pluginFilePath): + return '' + try: + with tarfile.open(pluginFilePath) as tar: + tarMembers = tar.getmembers() + for tarMember in tarMembers: + if tarMember.isdir(): + return tarMember.name + except Exception as e: + print(e) + return '' + def isValidPluginFile(self, pluginFilePath): + if not isinstance(pluginFilePath, str): + return False + if pluginFilePath == '': + return False + if not os.path.exists(pluginFilePath): + return False + pluginFolder = '' + pluginFileExists = False + packageFileExists = False + try: + with tarfile.open(pluginFilePath) as tar: + tarMembers = tar.getmembers() + for tarMember in tarMembers: + if tarMember.isdir(): + if pluginFolder == '': + pluginFolder = tarMember.name + if tarMember.isfile(): + if tarMember.name.endswith('.plugin'): + pluginFileExists = True + if tarMember.name.endswith('package.py'): + pluginFileExists = True + if not tarMember.name.startswith(pluginFolder): + return False + except Exception as e: + print(e) + return False + return pluginFileExists + def uninstallPlugin(self, pluginInfo): + if self.isPluginBuildIn(pluginInfo): + return False + # do we want to allow removing system plugins? + if PluginSystemManager.getPluginType(pluginInfo) == PluginType.SYSTEM: + return False + pluginFolder = pluginInfo.get_data_dir() + if not pluginFolder.endswith('/'): + pluginFolder += '/' + if not os.path.isdir(pluginFolder): + return False + if self.isPluginActive(pluginInfo): + self.setPluginActive(pluginInfo, False) + SettingsManager = self.app.getSettingsManager() + # TODO SettingsManager.set_settings_value_list('active-plugins', self.getActivePlugins()) + self.callPackageTriggers(pluginFolder, 'onPreUninstall') + + try: + shutil.rmtree(pluginFolder, ignore_errors=True) + except Exception as e: + print(e) + return False + self.setIgnoredPlugins(pluginFolder, True) + self.rescanPlugins() + + return True + def callPackageTriggers(self, pluginPath, trigger): + if not os.path.exists(pluginPath): + return + if not pluginPath.endswith('/'): + pluginPath += '/' + packageModulePath = pluginPath + 'package.py' + if not os.path.isfile(packageModulePath): + return + if not os.access(packageModulePath, os.R_OK): + return + package = self.getApp().getAPIHelper().importModule('package', packageModulePath) + + if trigger == 'onPostInstall': + try: + package.onPostInstall(pluginPath, self.getApp()) + except Exception as e: + print(e) + elif trigger == 'onPreUninstall': + try: + package.onPreUninstall(pluginPath, self.getApp()) + except Exception as e: + print(e) + + def _setupExtensionSet(self): + plugin_iface = API(self.getApp()) + self.extension_set = Peas.ExtensionSet.new(self.engine, + Peas.Activatable, + ["object"], + [plugin_iface]) + self.extension_set.connect("extension-removed", + self.__extensionRemoved) + self.extension_set.connect("extension-added", + self.__extensionAdded) + + def _setupPluginsDir(self): + system_plugins_dir = PluginType.SYSTEM.get_root_dir() + user_plugins_dir = PluginType.USER.get_root_dir() + if os.path.exists(user_plugins_dir): + self.engine.add_search_path(user_plugins_dir) + if os.path.exists(system_plugins_dir): + self.engine.add_search_path(system_plugins_dir) + + def __extensionRemoved(self, unusedSet, pluginInfo, extension): + extension.deactivate() + + def __extensionAdded(self, unusedSet, pluginInfo, extension): + extension.setApp(self.getApp()) + extension.setPluginInfo(pluginInfo) + extension.activate() + def __loadedPlugins(self, engine, unusedSet): + """Handles the changing of the loaded plugin list.""" + self.getApp().settings.ActivePlugins = engine.get_property("loaded-plugins") + +class APIHelper(): + def __init__(self, app): + self.app = app + self.cthulhuKeyBindings = None + + ''' + _pluginAPIManager.seCthulhuAPI('Logger', _logger) + _pluginAPIManager.setCthulhuAPI('SettingsManager', _settingsManager) + _pluginAPIManager.setCthulhuAPI('ScriptManager', _scriptManager) + _pluginAPIManager.setCthulhuAPI('EventManager', _eventManager) + _pluginAPIManager.setCthulhuAPI('Speech', speech) + _pluginAPIManager.setCthulhuAPI('Sound', sound) + _pluginAPIManager.setCthulhuAPI('Braille', braille) + _pluginAPIManager.setCthulhuAPI('Debug', debug) + _pluginAPIManager.setCthulhuAPI('Messages', messages) + _pluginAPIManager.setCthulhuAPI('MouseReview', mouse_review) + _pluginAPIManager.setCthulhuAPI('NotificationMessages', notification_messages) + _pluginAPIManager.setCthulhuAPI('CthulhuState', cthulhu_state) + _pluginAPIManager.setCthulhuAPI('CthulhuPlatform', cthulhu_platform) + _pluginAPIManager.setCthulhuAPI('Settings', settings) + _pluginAPIManager.setCthulhuAPI('Keybindings', keybindings) + ''' + def outputMessage(self, Message, interrupt=False): + settings = self.app.getDynamicApiManager().getAPI('Settings') + braille = self.app.getDynamicApiManager().getAPI('Braille') + speech = self.app.getDynamicApiManager().getAPI('Speech') + if speech != None: + if (settings.enableSpeech): + if interrupt: + speech.cancel() + if Message != '': + speech.speak(Message) + if braille != None: + if (settings.enableBraille): + braille.displayMessage(Message) + def createInputEventHandler(self, function, name, learnModeEnabled=True): + EventManager = self.app.getDynamicApiManager().getAPI('EventManager') + newInputEventHandler = EventManager.input_event.InputEventHandler(function, name, learnModeEnabled) + return newInputEventHandler + def registerGestureByString(self, function, name, gestureString, profile, application, learnModeEnabled = True, contextName = None): + gestureList = gestureString.split(',') + registeredGestures = [] + for gesture in gestureList: + if gesture.startswith('kb:'): + shortcutString = gesture[3:] + registuredGesture = self.registerShortcutByString(function, name, shortcutString, profile, application, learnModeEnabled, contextName=contextName) + if registuredGesture: + registeredGestures.append(registuredGesture) + return registeredGestures + def registerShortcutByString(self, function, name, shortcutString, profile, application, learnModeEnabled = True, contextName = None): + keybindings = self.app.getDynamicApiManager().getAPI('Keybindings') + settings = self.app.getDynamicApiManager().getAPI('Settings') + resourceManager = self.app.getResourceManager() + + clickCount = 0 + cthulhuKey = False + shiftKey = False + ctrlKey = False + altKey = False + key = '' + shortcutList = shortcutString.split('+') + for shortcutElement in shortcutList: + shortcutElementLower = shortcutElement.lower() + if shortcutElementLower == 'press': + clickCount += 1 + elif shortcutElement == 'cthulhu': + cthulhuKey = True + elif shortcutElementLower == 'shift': + shiftKey = True + elif shortcutElementLower == 'control': + ctrlKey = True + elif shortcutElementLower == 'alt': + altKey = True + else: + key = shortcutElementLower + if clickCount == 0: + clickCount = 1 + + if self.cthulhuKeyBindings == None: + self.cthulhuKeyBindings = keybindings.KeyBindings() + + tryFunction = resource_manager.TryFunction(function) + newInputEventHandler = self.createInputEventHandler(tryFunction.runInputEvent, name, learnModeEnabled) + + currModifierMask = keybindings.NO_MODIFIER_MASK + if cthulhuKey: + currModifierMask = currModifierMask | 1 << keybindings.MODIFIER_ORCA + if shiftKey: + currModifierMask = currModifierMask | 1 << Atspi.ModifierType.SHIFT + if altKey: + currModifierMask = currModifierMask | 1 << Atspi.ModifierType.ALT + if ctrlKey: + currModifierMask = currModifierMask | 1 << Atspi.ModifierType.CONTROL + + newKeyBinding = keybindings.KeyBinding(key, keybindings.defaultModifierMask, currModifierMask, newInputEventHandler, clickCount) + self.cthulhuKeyBindings.add(newKeyBinding) + + settings.keyBindingsMap["default"] = self.cthulhuKeyBindings + + if contextName: + resourceContext = resourceManager.getResourceContext(contextName) + if resourceContext: + resourceEntry = resource_manager.ResourceEntry('keyboard', newKeyBinding, function, tryFunction, shortcutString) + resourceContext.addGesture(profile, application, newKeyBinding, resourceEntry) + + return newKeyBinding + + def unregisterShortcut(self, KeyBindingToRemove, contextName = None): + ok = False + keybindings = self.app.getDynamicApiManager().getAPI('Keybindings') + settings = self.app.getDynamicApiManager().getAPI('Settings') + resourceManager = self.app.getResourceManager() + + if self.cthulhuKeyBindings == None: + self.cthulhuKeyBindings = keybindings.KeyBindings() + try: + self.cthulhuKeyBindings.remove(KeyBindingToRemove) + settings.keyBindingsMap["default"] = self.cthulhuKeyBindings + ok = True + except KeyError: + pass + if contextName: + resourceContext = resourceManager.getResourceContext(contextName) + if resourceContext: + resourceContext.removeGesture(KeyBindingToRemove) + + return ok + def importModule(self, moduleName, moduleLocation): + if version in ["3.3","3.4"]: + return SourceFileLoader(moduleName, moduleLocation).load_module() + else: + spec = importlib.util.spec_from_file_location(moduleName, moduleLocation) + driver_mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(driver_mod) + return driver_mod diff --git a/src/cthulhu/plugins/ByeCthulhu/ByeCthulhu.plugin b/src/cthulhu/plugins/ByeCthulhu/ByeCthulhu.plugin new file mode 100644 index 0000000..6c63a12 --- /dev/null +++ b/src/cthulhu/plugins/ByeCthulhu/ByeCthulhu.plugin @@ -0,0 +1,6 @@ +[Plugin] +Module=ByeCthulhu +Loader=python3 +Name=Stop announcement for cthulhu +Description=Test plugin for cthulhu +Authors=Chrys chrys@linux-a11y.org diff --git a/src/cthulhu/plugins/ByeCthulhu/ByeCthulhu.py b/src/cthulhu/plugins/ByeCthulhu/ByeCthulhu.py new file mode 100644 index 0000000..034e4e3 --- /dev/null +++ b/src/cthulhu/plugins/ByeCthulhu/ByeCthulhu.py @@ -0,0 +1,27 @@ +from cthulhu import plugin + +import gi +gi.require_version('Peas', '1.0') +from gi.repository import GObject +from gi.repository import Peas +import time + + +class ByeOrca(GObject.Object, Peas.Activatable, plugin.Plugin): + #__gtype_name__ = 'ByeCthulhu' + + object = GObject.Property(type=GObject.Object) + def __init__(self): + plugin.Plugin.__init__(self) + def do_activate(self): + API = self.object + self.connectSignal("stop-application-completed", self.process) + def do_deactivate(self): + API = self.object + def do_update_state(self): + API = self.object + def process(self, app): + messages = app.getDynamicApiManager().getAPI('Messages') + activeScript = app.getDynamicApiManager().getAPI('CthulhuState').activeScript + activeScript.presentationInterrupt() + activeScript.presentMessage(messages.STOP_ORCA, resetStyles=False) diff --git a/src/cthulhu/plugins/ByeCthulhu/Makefile.am b/src/cthulhu/plugins/ByeCthulhu/Makefile.am new file mode 100644 index 0000000..79796ed --- /dev/null +++ b/src/cthulhu/plugins/ByeCthulhu/Makefile.am @@ -0,0 +1,7 @@ +orca_python_PYTHON = \ + __init__.py \ + ByeCthulhu.plugin \ + ByeCthulhu.py + +orca_pythondir=$(pkgpythondir)/plugins/ByeCthulhu + diff --git a/src/cthulhu/plugins/ByeCthulhu/__init__.py b/src/cthulhu/plugins/ByeCthulhu/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/cthulhu/plugins/CapsLockHack/CapsLockHack.plugin b/src/cthulhu/plugins/CapsLockHack/CapsLockHack.plugin new file mode 100644 index 0000000..6279400 --- /dev/null +++ b/src/cthulhu/plugins/CapsLockHack/CapsLockHack.plugin @@ -0,0 +1,6 @@ +[Plugin] +Module=CapsLockHack +Loader=python3 +Name=Caps Lock Hack +Description=Fix Capslock sometimes switch on / off when its used as modifier +Authors=Chrys chrys@linux-a11y.org diff --git a/src/cthulhu/plugins/CapsLockHack/CapsLockHack.py b/src/cthulhu/plugins/CapsLockHack/CapsLockHack.py new file mode 100644 index 0000000..97f9566 --- /dev/null +++ b/src/cthulhu/plugins/CapsLockHack/CapsLockHack.py @@ -0,0 +1,119 @@ +from cthulhu import plugin + +import gi +gi.require_version('Peas', '1.0') +from gi.repository import GObject +from gi.repository import Peas +from threading import Thread, Lock +import subprocess, time, re, os + +class CapsLockHack(GObject.Object, Peas.Activatable, plugin.Plugin): + __gtype_name__ = 'CapsLockHack' + + object = GObject.Property(type=GObject.Object) + def __init__(self): + plugin.Plugin.__init__(self) + self.lock = Lock() + self.active = False + self.workerThread = Thread(target=self.worker) + def do_activate(self): + API = self.object + """Enable or disable use of the caps lock key as an Cthulhu modifier key.""" + self.interpretCapsLineProg = re.compile( + r'^\s*interpret\s+Caps[_+]Lock[_+]AnyOfOrNone\s*\(all\)\s*{\s*$', re.I) + self.normalCapsLineProg = re.compile( + r'^\s*action\s*=\s*LockMods\s*\(\s*modifiers\s*=\s*Lock\s*\)\s*;\s*$', re.I) + self.interpretShiftLineProg = re.compile( + r'^\s*interpret\s+Shift[_+]Lock[_+]AnyOf\s*\(\s*Shift\s*\+\s*Lock\s*\)\s*{\s*$', re.I) + self.normalShiftLineProg = re.compile( + r'^\s*action\s*=\s*LockMods\s*\(\s*modifiers\s*=\s*Shift\s*\)\s*;\s*$', re.I) + self.disabledModLineProg = re.compile( + r'^\s*action\s*=\s*NoAction\s*\(\s*\)\s*;\s*$', re.I) + self.normalCapsLine = ' action= LockMods(modifiers=Lock);' + self.normalShiftLine = ' action= LockMods(modifiers=Shift);' + self.disabledModLine = ' action= NoAction();' + self.activateWorker() + def do_deactivate(self): + API = self.object + self.deactivateWorker() + def do_update_state(self): + API = self.object + def deactivateWorker(self): + with self.lock: + self.active = False + self.workerThread.join() + def activateWorker(self): + with self.lock: + self.active = True + self.workerThread.start() + def isActive(self): + with self.lock: + return self.active + def worker(self): + """Makes an Cthulhu-specific Xmodmap so that the keys behave as we + need them to do. This is especially the case for the Cthulhu modifier. + """ + API = self.object + capsLockCleared = False + settings = API.app.getDynamicApiManager().getAPI('Settings') + time.sleep(3) + while self.isActive(): + if "Caps_Lock" in settings.orcaModifierKeys \ + or "Shift_Lock" in settings.orcaModifierKeys: + self.setCapsLockAsOrcaModifier(True) + capsLockCleared = True + elif capsLockCleared: + self.setCapsLockAsOrcaModifier(False) + capsLockCleared = False + time.sleep(1) + + def setCapsLockAsOrcaModifier(self, enable): + originalXmodmap = None + lines = None + try: + originalXmodmap = subprocess.check_output(['xkbcomp', os.environ['DISPLAY'], '-']) + lines = originalXmodmap.decode('UTF-8').split('\n') + except: + return + foundCapsInterpretSection = False + foundShiftInterpretSection = False + modified = False + for i, line in enumerate(lines): + if not foundCapsInterpretSection and not foundShiftInterpretSection: + if self.interpretCapsLineProg.match(line): + foundCapsInterpretSection = True + elif self.interpretShiftLineProg.match(line): + foundShiftInterpretSection = True + elif foundCapsInterpretSection: + if enable: + if self.normalCapsLineProg.match(line): + lines[i] = self.disabledModLine + modified = True + else: + if self.disabledModLineProg.match(line): + lines[i] = self.normalCapsLine + modified = True + if line.find('}'): + foundCapsInterpretSection = False + else: # foundShiftInterpretSection + if enable: + if self.normalShiftLineProg.match(line): + lines[i] = self.disabledModLine + modified = True + else: + if self.disabledModLineProg.match(line): + lines[i] = self.normalShiftLine + modified = True + if line.find('}'): + foundShiftInterpretSection = False + if modified: + newXmodMap = bytes('\n'.join(lines), 'UTF-8') + self.setXmodmap(newXmodMap) + def setXmodmap(self, xkbmap): + """Set the keyboard map using xkbcomp.""" + try: + p = subprocess.Popen(['xkbcomp', '-w0', '-', os.environ['DISPLAY']], + stdin=subprocess.PIPE, stdout=None, stderr=None) + p.communicate(xkbmap) + except: + pass diff --git a/src/cthulhu/plugins/CapsLockHack/Makefile.am b/src/cthulhu/plugins/CapsLockHack/Makefile.am new file mode 100644 index 0000000..b177bb1 --- /dev/null +++ b/src/cthulhu/plugins/CapsLockHack/Makefile.am @@ -0,0 +1,7 @@ +orca_python_PYTHON = \ + __init__.py \ + CapsLockHack.plugin \ + CapsLockHack.py + +orca_pythondir=$(pkgpythondir)/plugins/CapsLockHack + diff --git a/src/cthulhu/plugins/CapsLockHack/__init__.py b/src/cthulhu/plugins/CapsLockHack/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/cthulhu/plugins/Clipboard/Clipboard.plugin b/src/cthulhu/plugins/Clipboard/Clipboard.plugin new file mode 100644 index 0000000..72fb0a3 --- /dev/null +++ b/src/cthulhu/plugins/Clipboard/Clipboard.plugin @@ -0,0 +1,6 @@ +[Plugin] +Module=Clipboard +Loader=python3 +Name=Clipboard +Description=Present the content of the current clipboard +Authors=Chrys chrys@linux-a11y.org diff --git a/src/cthulhu/plugins/Clipboard/Clipboard.py b/src/cthulhu/plugins/Clipboard/Clipboard.py new file mode 100644 index 0000000..de6fc8b --- /dev/null +++ b/src/cthulhu/plugins/Clipboard/Clipboard.py @@ -0,0 +1,76 @@ +from cthulhu import plugin + +import gi, os +gi.require_version('Peas', '1.0') +from gi.repository import GObject +from gi.repository import Peas +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk, Gdk + +class Clipboard(GObject.Object, Peas.Activatable, plugin.Plugin): + #__gtype_name__ = 'Clipboard' + + object = GObject.Property(type=GObject.Object) + def __init__(self): + plugin.Plugin.__init__(self) + def do_activate(self): + API = self.object + self.registerGestureByString(self.speakClipboard, _('clipboard'), 'kb:cthulhu+c') + def do_deactivate(self): + API = self.object + def do_update_state(self): + API = self.object + def speakClipboard(self, script=None, inputEvent=None): + API = self.object + Message = self.getClipboard() + API.app.getDynamicApiManager().getAPI('CthulhuState').activeScript.presentMessage(Message, resetStyles=False) + return True + def getClipboard(self): + Message = "" + FoundClipboardContent = False + # Get Clipboard + ClipboardObj = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) + + ClipboardText = ClipboardObj.wait_for_text() + ClipboardImage = ClipboardObj.wait_for_image() + ClipboardURI = ClipboardObj.wait_for_uris() + if (ClipboardText != None): + FoundClipboardContent = True + if (ClipboardObj.wait_is_uris_available()): + noOfObjects = 0 + noOfFolder = 0 + noOfFiles = 0 + noOfDisks = 0 + noOfLinks = 0 + for Uri in ClipboardURI: + if Uri == '': + continue + noOfObjects += 1 + uriWithoutProtocoll = Uri[Uri.find('://') + 3:] + Message += " " + Uri[Uri.rfind('/') + 1:] + " " + if (os.path.isdir(uriWithoutProtocoll)): + noOfFolder += 1 + Message = Message + _("Folder") #Folder + if (os.path.isfile(uriWithoutProtocoll)): + noOfFiles += 1 + Message = Message + _("File") #File + if (os.path.ismount(uriWithoutProtocoll)): + noOfDisks += 1 + Message = Message + _("Disk") #Mountpoint + if (os.path.islink(uriWithoutProtocoll)): + noOfLinks += 1 + Message = Message + _("Link") #Link + if (noOfObjects > 1): + Message = str(noOfObjects) + _(" Objects in clipboard ") + Message # X Objects in Clipboard Object Object + else: + Message = str(noOfObjects) + _(" Object in clipboard ") + Message # 1 Object in Clipboard Object + else: + Message = _("Text in clipboard ") + ClipboardText # Text in Clipboard + + if (ClipboardImage != None): + FoundClipboardContent = True + Message = _("The clipboard contains a image") # Image is in Clipboard + + if (not FoundClipboardContent): + Message = _("The clipboard is empty") + return Message diff --git a/src/cthulhu/plugins/Clipboard/Makefile.am b/src/cthulhu/plugins/Clipboard/Makefile.am new file mode 100644 index 0000000..3f8b236 --- /dev/null +++ b/src/cthulhu/plugins/Clipboard/Makefile.am @@ -0,0 +1,7 @@ +orca_python_PYTHON = \ + __init__.py \ + Clipboard.plugin \ + Clipboard.py + +orca_pythondir=$(pkgpythondir)/plugins/Clipboard + diff --git a/src/cthulhu/plugins/Clipboard/__init__.py b/src/cthulhu/plugins/Clipboard/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/cthulhu/plugins/Date/Date.plugin b/src/cthulhu/plugins/Date/Date.plugin new file mode 100644 index 0000000..12f6a7e --- /dev/null +++ b/src/cthulhu/plugins/Date/Date.plugin @@ -0,0 +1,6 @@ +[Plugin] +Module=Date +Loader=python3 +Name=Date +Description=Present the current date +Authors=Chrys chrys@linux-a11y.org diff --git a/src/cthulhu/plugins/Date/Date.py b/src/cthulhu/plugins/Date/Date.py new file mode 100644 index 0000000..7fc3d67 --- /dev/null +++ b/src/cthulhu/plugins/Date/Date.py @@ -0,0 +1,33 @@ +from cthulhu import plugin + +import gi, time +gi.require_version('Peas', '1.0') +from gi.repository import GObject +from gi.repository import Peas + +class Date(GObject.Object, Peas.Activatable, plugin.Plugin): + #__gtype_name__ = 'Date' + + object = GObject.Property(type=GObject.Object) + def __init__(self): + plugin.Plugin.__init__(self) + def do_activate(self): + API = self.object + self.connectSignal("setup-inputeventhandlers-completed", self.setupCompatBinding) + def setupCompatBinding(self, app): + cmdnames = app.getDynamicApiManager().getAPI('Cmdnames') + inputEventHandlers = app.getDynamicApiManager().getAPI('inputEventHandlers') + inputEventHandlers['presentDateHandler'] = app.getAPIHelper().createInputEventHandler(self.presentDate, cmdnames.PRESENT_CURRENT_DATE) + def do_deactivate(self): + API = self.object + inputEventHandlers = API.app.getDynamicApiManager().getAPI('inputEventHandlers') + del inputEventHandlers['presentDateHandler'] + def presentDate(self, script=None, inputEvent=None): + """ Presents the current time. """ + API = self.object + settings_manager = API.app.getDynamicApiManager().getAPI('SettingsManager') + _settingsManager = settings_manager.getManager() + dateFormat = _settingsManager.getSetting('presentDateFormat') + message = time.strftime(dateFormat, time.localtime()) + API.app.getDynamicApiManager().getAPI('CthulhuState').activeScript.presentMessage(message, resetStyles=False) + return True diff --git a/src/cthulhu/plugins/Date/Makefile.am b/src/cthulhu/plugins/Date/Makefile.am new file mode 100644 index 0000000..1447580 --- /dev/null +++ b/src/cthulhu/plugins/Date/Makefile.am @@ -0,0 +1,7 @@ +orca_python_PYTHON = \ + __init__.py \ + Date.plugin \ + Date.py + +orca_pythondir=$(pkgpythondir)/plugins/Date + diff --git a/src/cthulhu/plugins/Date/__init__.py b/src/cthulhu/plugins/Date/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/cthulhu/plugins/HelloCthulhu/HelloCthulhu.plugin b/src/cthulhu/plugins/HelloCthulhu/HelloCthulhu.plugin new file mode 100644 index 0000000..f45ef69 --- /dev/null +++ b/src/cthulhu/plugins/HelloCthulhu/HelloCthulhu.plugin @@ -0,0 +1,6 @@ +[Plugin] +Module=HelloCthulhu +Loader=python3 +Name=Cthulhu say hello +Description=startup announcement for Cthulhu +Authors=Chrys chrys@linux-a11y.org diff --git a/src/cthulhu/plugins/HelloCthulhu/HelloCthulhu.py b/src/cthulhu/plugins/HelloCthulhu/HelloCthulhu.py new file mode 100644 index 0000000..b4ed422 --- /dev/null +++ b/src/cthulhu/plugins/HelloCthulhu/HelloCthulhu.py @@ -0,0 +1,23 @@ +from cthulhu import plugin + +import gi +gi.require_version('Peas', '1.0') +from gi.repository import GObject +from gi.repository import Peas + +class HelloCthulhu(GObject.Object, Peas.Activatable, plugin.Plugin): + #__gtype_name__ = 'HelloCthulhu' + + object = GObject.Property(type=GObject.Object) + def __init__(self): + plugin.Plugin.__init__(self) + def do_activate(self): + API = self.object + self.connectSignal("start-application-completed", self.process) + def do_deactivate(self): + API = self.object + def do_update_state(self): + API = self.object + def process(self, app): + messages = app.getDynamicApiManager().getAPI('Messages') + app.getDynamicApiManager().getAPI('CthulhuState').activeScript.presentMessage(messages.START_ORCA, resetStyles=False) diff --git a/src/cthulhu/plugins/HelloCthulhu/Makefile.am b/src/cthulhu/plugins/HelloCthulhu/Makefile.am new file mode 100644 index 0000000..1938ac1 --- /dev/null +++ b/src/cthulhu/plugins/HelloCthulhu/Makefile.am @@ -0,0 +1,7 @@ +orca_python_PYTHON = \ + __init__.py \ + HelloCthulhu.plugin \ + HelloCthulhu.py + +orca_pythondir=$(pkgpythondir)/plugins/HelloCthulhu + diff --git a/src/cthulhu/plugins/HelloCthulhu/__init__.py b/src/cthulhu/plugins/HelloCthulhu/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/cthulhu/plugins/HelloWorld/HelloWorld.plugin b/src/cthulhu/plugins/HelloWorld/HelloWorld.plugin new file mode 100644 index 0000000..1282f99 --- /dev/null +++ b/src/cthulhu/plugins/HelloWorld/HelloWorld.plugin @@ -0,0 +1,6 @@ +[Plugin] +Module=HelloWorld +Loader=python3 +Name=Hello World (python3) +Description=Test plugin for orca +Authors=Chrys chrys@linux-a11y.org diff --git a/src/cthulhu/plugins/HelloWorld/HelloWorld.py b/src/cthulhu/plugins/HelloWorld/HelloWorld.py new file mode 100644 index 0000000..b647b6a --- /dev/null +++ b/src/cthulhu/plugins/HelloWorld/HelloWorld.py @@ -0,0 +1,24 @@ +from cthulhu import plugin + +import gi +gi.require_version('Peas', '1.0') +from gi.repository import GObject +from gi.repository import Peas + +class HelloWorld(GObject.Object, Peas.Activatable, plugin.Plugin): + __gtype_name__ = 'helloworld' + + object = GObject.Property(type=GObject.Object) + def __init__(self): + plugin.Plugin.__init__(self) + def do_activate(self): + API = self.object + self.registerGestureByString(self.speakTest, _('hello world'), 'kb:cthulhu+z') + print('activate hello world plugin') + def do_deactivate(self): + API = self.object + print('deactivate hello world plugin') + def speakTest(self, script=None, inputEvent=None): + API = self.object + API.app.getDynamicApiManager().getAPI('CthulhuState').activeScript.presentMessage('hello world', resetStyles=False) + return True diff --git a/src/cthulhu/plugins/HelloWorld/Makefile.am b/src/cthulhu/plugins/HelloWorld/Makefile.am new file mode 100644 index 0000000..5217982 --- /dev/null +++ b/src/cthulhu/plugins/HelloWorld/Makefile.am @@ -0,0 +1,7 @@ +orca_python_PYTHON = \ + __init__.py \ + HelloWorld.plugin \ + HelloWorld.py + +orca_pythondir=$(pkgpythondir)/plugins/HelloWorld + diff --git a/src/cthulhu/plugins/HelloWorld/__init__.py b/src/cthulhu/plugins/HelloWorld/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/cthulhu/plugins/Makefile.am b/src/cthulhu/plugins/Makefile.am new file mode 100644 index 0000000..7f8a52c --- /dev/null +++ b/src/cthulhu/plugins/Makefile.am @@ -0,0 +1,4 @@ +SUBDIRS = Clipboard HelloWorld SelfVoice Time MouseReview Date ByeCthulhu HelloCthulhu PluginManager CapsLockHack + +orca_pythondir=$(pkgpythondir)/plugins + diff --git a/src/cthulhu/plugins/MouseReview/Makefile.am b/src/cthulhu/plugins/MouseReview/Makefile.am new file mode 100644 index 0000000..bfe3ed0 --- /dev/null +++ b/src/cthulhu/plugins/MouseReview/Makefile.am @@ -0,0 +1,7 @@ +orca_python_PYTHON = \ + __init__.py \ + MouseReview.plugin \ + MouseReview.py + +orca_pythondir=$(pkgpythondir)/plugins/MouseReview + diff --git a/src/cthulhu/plugins/MouseReview/MouseReview.plugin b/src/cthulhu/plugins/MouseReview/MouseReview.plugin new file mode 100644 index 0000000..986dfad --- /dev/null +++ b/src/cthulhu/plugins/MouseReview/MouseReview.plugin @@ -0,0 +1,6 @@ +[Plugin] +Module=MouseReview +Loader=python3 +Name=Mouse Review +Description=Review whats below the mouse coursor +Authors=Chrys chrys@linux-a11y.org diff --git a/src/cthulhu/plugins/MouseReview/MouseReview.py b/src/cthulhu/plugins/MouseReview/MouseReview.py new file mode 100644 index 0000000..cad38da --- /dev/null +++ b/src/cthulhu/plugins/MouseReview/MouseReview.py @@ -0,0 +1,754 @@ +# Mouse reviewer for Orca +# +# Copyright 2008 Eitan Isaacson +# Copyright 2016 Igalia, S.L. +# +# 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., Franklin Street, Fifth Floor, +# Boston MA 02110-1301 USA. + +"""Mouse review mode.""" + +__id__ = "$Id$" +__version__ = "$Revision$" +__date__ = "$Date$" +__copyright__ = "Copyright (c) 2008 Eitan Isaacson" \ + "Copyright (c) 2016 Igalia, S.L." +__license__ = "LGPL" + +from cthulhu import plugin + +import gi, math, time +gi.require_version('Peas', '1.0') +from gi.repository import GObject +from gi.repository import Peas + +gi.require_version("Atspi", "2.0") +from gi.repository import Atspi + +from gi.repository import Gdk +try: + gi.require_version("Wnck", "3.0") + from gi.repository import Wnck + _mouseReviewCapable = True +except Exception: + _mouseReviewCapable = False + +# compatibility layer, see MouseReview.do_activate +debug = None +event_manager = None +cthulhu = None +cthulhu_state = None +script_manager = None +settings_manager = None +speech = None +messages = None +cmdnames = None +emitRegionChanged = None +_scriptManager = None +_settingsManager = None +AXObject = None +AXUtilities = None +keybindings = None +input_event = None + +class MouseReview(GObject.Object, Peas.Activatable, plugin.Plugin): + #__gtype_name__ = 'MouseReview' + + object = GObject.Property(type=GObject.Object) + def __init__(self): + plugin.Plugin.__init__(self) + def do_activate(self): + API = self.object + global _mouseReviewCapable + if not _mouseReviewCapable: + return + global debug + global event_manager + global cthulhu_state + global script_manager + global settings_manager + global speech + global _scriptManager + global _settingsManager + global emitRegionChanged + global messages + global cmdnames + global AXObject + global AXUtilities + global keybindings + global input_event + debug= API.app.getDynamicApiManager().getAPI('Debug') + event_manager = API.app.getDynamicApiManager().getAPI('EventManager') + messages = API.app.getDynamicApiManager().getAPI('Messages') + cmdnames = API.app.getDynamicApiManager().getAPI('Cmdnames') + cthulhu_state = API.app.getDynamicApiManager().getAPI('CthulhuState') + script_manager = API.app.getDynamicApiManager().getAPI('ScriptManager') + settings_manager = API.app.getDynamicApiManager().getAPI('SettingsManager') + speech = API.app.getDynamicApiManager().getAPI('Speech') + emitRegionChanged = API.app.getDynamicApiManager().getAPI('EmitRegionChanged') + _scriptManager = script_manager.getManager() + _settingsManager = settings_manager.getManager() + AXObject = API.app.getDynamicApiManager().getAPI('AXObject') + AXUtilities = API.app.getDynamicApiManager().getAPI('AXUtilities') + keybindings = API.app.getDynamicApiManager().getAPI('Keybindings') + input_event = API.app.getDynamicApiManager().getAPI('InputEvent') + mouse_review = MouseReviewer() + self.registerAPI('MouseReview', mouse_review) + self.Initialize(API.app) + self.connectSignal("setup-inputeventhandlers-completed", self.setupCompatBinding) + self.connectSignal("load-setting-completed", self.Initialize) + + def do_deactivate(self): + API = self.object + global _mouseReviewCapable + if not _mouseReviewCapable: + return + mouse_review = API.app.getDynamicApiManager().getAPI('MouseReview') + + mouse_review.deactivate() + def do_update_state(self): + API = self.object + def setupCompatBinding(self, app): + API = self.object + mouse_review = API.app.getDynamicApiManager().getAPI('MouseReview') + cmdnames = API.app.getDynamicApiManager().getAPI('Cmdnames') + inputEventHandlers = API.app.getDynamicApiManager().getAPI('inputEventHandlers') + inputEventHandlers['toggleMouseReviewHandler'] = API.app.getAPIHelper().createInputEventHandler(mouse_review.toggle, cmdnames.MOUSE_REVIEW_TOGGLE) + def Initialize(self, app): + mouse_review = app.getDynamicApiManager().getAPI('MouseReview') + settings_manager = app.getDynamicApiManager().getAPI('SettingsManager') + _settingsManager = settings_manager.getManager() + if _settingsManager.getSetting('enableMouseReview'): + mouse_review.activate() + else: + mouse_review.deactivate() + +class _StringContext: + """The textual information associated with an _ItemContext.""" + + def __init__(self, obj, script=None, string="", start=0, end=0): + """Initialize the _StringContext. + + Arguments: + - string: The human-consumable string + - obj: The accessible object associated with this string + - start: The start offset with respect to entire text, if one exists + - end: The end offset with respect to the entire text, if one exists + - script: The script associated with the accessible object + """ + + self._obj = obj + self._script = script + self._string = string + self._start = start + self._end = end + self._boundingBox = 0, 0, 0, 0 + if script: + self._boundingBox = script.utilities.getTextBoundingBox(obj, start, end) + + def __eq__(self, other): + return other is not None \ + and self._obj == other._obj \ + and self._string == other._string \ + and self._start == other._start \ + and self._end == other._end + + def isSubstringOf(self, other): + """Returns True if this is a substring of other.""" + + if other is None: + return False + + if not (self._obj and other._obj): + return False + + thisBox = self.getBoundingBox() + if thisBox == (0, 0, 0, 0): + return False + + otherBox = other.getBoundingBox() + if otherBox == (0, 0, 0, 0): + return False + + # We get various and sundry results for the bounding box if the implementor + # included newline characters as part of the word or line at offset. Try to + # detect this and adjust the bounding boxes before getting the intersection. + if thisBox[3] != otherBox[3] and self._obj == other._obj: + thisNewLineCount = self._string.count("\n") + if thisNewLineCount and thisBox[3] / thisNewLineCount == otherBox[3]: + thisBox = *thisBox[0:3], otherBox[3] + + if self._script.utilities.intersection(thisBox, otherBox) != thisBox: + return False + + if not (self._string and self._string.strip() in other._string): + return False + + msg = f"MOUSE REVIEW: '{self._string}' is substring of '{other._string}'" + debug.println(debug.LEVEL_INFO, msg, True) + return True + + def getBoundingBox(self): + """Returns the bounding box associated with this context's range.""" + + return self._boundingBox + + def getString(self): + """Returns the string associated with this context.""" + + return self._string + + def present(self): + """Presents this context to the user.""" + + if not self._script: + msg = "MOUSE REVIEW: Not presenting due to lack of script" + debug.println(debug.LEVEL_INFO, msg, True) + return False + + if not self._string: + msg = "MOUSE REVIEW: Not presenting due to lack of string" + debug.println(debug.LEVEL_INFO, msg, True) + return False + + voice = self._script.speechGenerator.voice(obj=self._obj, string=self._string) + string = self._script.utilities.adjustForRepeats(self._string) + # TODO + #orca.emitRegionChanged(self._obj, self._start, self._end, orca.MOUSE_REVIEW) + emitRegionChanged(self._obj, self._start, self._end, "mouse-review") + + + self._script.speakMessage(string, voice=voice, interrupt=False) + self._script.displayBrailleMessage(self._string, -1) + return True + + +class _ItemContext: + """Holds all the information of the item at a specified point.""" + + def __init__(self, x=0, y=0, obj=None, boundary=None, frame=None, script=None): + """Initialize the _ItemContext. + + Arguments: + - x: The X coordinate + - y: The Y coordinate + - obj: The accessible object of interest at that coordinate + - boundary: The accessible-text boundary type + - frame: The containing accessible object (often a top-level window) + - script: The script associated with the accessible object + """ + + self._x = x + self._y = y + self._obj = obj + self._boundary = boundary + self._frame = frame + self._script = script + self._string = self._getStringContext() + self._time = time.time() + self._boundingBox = 0, 0, 0, 0 + if script: + self._boundingBox = script.utilities.getBoundingBox(obj) + + def __eq__(self, other): + return other is not None \ + and self._frame == other._frame \ + and self._obj == other._obj \ + and self._string == other._string + + def _treatAsDuplicate(self, prior): + if self._obj != prior._obj or self._frame != prior._frame: + msg = "MOUSE REVIEW: Not a duplicate: different objects" + debug.println(debug.LEVEL_INFO, msg, True) + return False + + if self.getString() and prior.getString() and not self._isSubstringOf(prior): + msg = "MOUSE REVIEW: Not a duplicate: not a substring of" + debug.println(debug.LEVEL_INFO, msg, True) + return False + + if self._x == prior._x and self._y == prior._y: + msg = "MOUSE REVIEW: Treating as duplicate: mouse didn't move" + debug.println(debug.LEVEL_INFO, msg, True) + return True + + interval = self._time - prior._time + if interval > 0.5: + msg = f"MOUSE REVIEW: Not a duplicate: was {interval:.2f}s ago" + debug.println(debug.LEVEL_INFO, msg, True) + return False + + msg = "MOUSE REVIEW: Treating as duplicate" + debug.println(debug.LEVEL_INFO, msg, True) + return True + + def _treatAsSingleObject(self): + if not AXObject.supports_text(self._obj): + return True + + if not self._obj.queryText().characterCount: + return True + + return False + + def _getStringContext(self): + """Returns the _StringContext associated with the specified point.""" + + if not (self._script and self._obj): + return _StringContext(self._obj) + + if self._treatAsSingleObject(): + return _StringContext(self._obj, self._script) + + string, start, end = self._script.utilities.textAtPoint( + self._obj, self._x, self._y, boundary=self._boundary) + if string: + string = self._script.utilities.expandEOCs(self._obj, start, end) + + return _StringContext(self._obj, self._script, string, start, end) + + def _getContainer(self): + roles = [Atspi.Role.DIALOG, + Atspi.Role.FRAME, + Atspi.Role.LAYERED_PANE, + Atspi.Role.MENU, + Atspi.Role.PAGE_TAB, + Atspi.Role.TOOL_BAR, + Atspi.Role.WINDOW] + return AXObject.find_ancestor(self._obj, lambda x: AXObject.get_role(x) in roles) + + def _isSubstringOf(self, other): + """Returns True if this is a substring of other.""" + + return self._string.isSubstringOf(other._string) + + def getObject(self): + """Returns the accessible object associated with this context.""" + + return self._obj + + def getBoundingBox(self): + """Returns the bounding box associated with this context.""" + + x, y, width, height = self._string.getBoundingBox() + if not (width or height): + return self._boundingBox + + return x, y, width, height + + def getString(self): + """Returns the string associated with this context.""" + + return self._string.getString() + + def getTime(self): + """Returns the time associated with this context.""" + + return self._time + + def _isInlineChild(self, prior): + if not self._obj or not prior._obj: + return False + + if AXObject.get_parent(prior._obj) != self._obj: + return False + + if self._treatAsSingleObject(): + return False + + return AXUtilities.is_link(prior._obj) + + def present(self, prior): + """Presents this context to the user.""" + + if self == prior or self._treatAsDuplicate(prior): + msg = "MOUSE REVIEW: Not presenting due to no change" + debug.println(debug.LEVEL_INFO, msg, True) + return False + + interrupt = self._obj and self._obj != prior._obj \ + or math.sqrt((self._x - prior._x)**2 + (self._y - prior._y)**2) > 25 + + if interrupt: + self._script.presentationInterrupt() + + if self._frame and self._frame != prior._frame: + self._script.presentObject(self._frame, + alreadyFocused=True, + inMouseReview=True, + interrupt=True) + + if self._script.utilities.containsOnlyEOCs(self._obj): + msg = "MOUSE REVIEW: Not presenting object which contains only EOCs" + debug.println(debug.LEVEL_INFO, msg, True) + return False + + if self._obj and self._obj != prior._obj and not self._isInlineChild(prior): + priorObj = prior._obj or self._getContainer() + # TODO + #orca.emitRegionChanged(self._obj, mode=orca.MOUSE_REVIEW) + emitRegionChanged(self._obj, mode="mouse-review") + + self._script.presentObject(self._obj, priorObj=priorObj, inMouseReview=True) + if self._string.getString() == AXObject.get_name(self._obj): + return True + if not self._script.utilities.isEditableTextArea(self._obj): + return True + if AXUtilities.is_table_cell(self._obj) \ + and self._string.getString() == self._script.utilities.displayedText(self._obj): + return True + + if self._string != prior._string and self._string.present(): + return True + + return True + + +class MouseReviewer: + """Main class for the mouse-review feature.""" + + def __init__(self): + self._active = _settingsManager.getSetting("enableMouseReview") + self._currentMouseOver = _ItemContext() + self._pointer = None + self._workspace = None + self._windows = [] + self._all_windows = [] + self._handlerIds = {} + self._eventListener = Atspi.EventListener.new(self._listener) + self.inMouseEvent = False + self._handlers = self._setup_handlers() + self._bindings = self._setup_bindings() + + if not _mouseReviewCapable: + msg = "MOUSE REVIEW ERROR: Wnck is not available" + debug.println(debug.LEVEL_INFO, msg, True) + return + + display = Gdk.Display.get_default() + try: + seat = Gdk.Display.get_default_seat(display) + self._pointer = seat.get_pointer() + except AttributeError: + msg = "MOUSE REVIEW ERROR: Gtk+ 3.20 is not available" + debug.println(debug.LEVEL_INFO, msg, True) + return + except Exception: + msg = "MOUSE REVIEW ERROR: Exception getting pointer for default seat." + debug.println(debug.LEVEL_INFO, msg, True) + return + + if not self._pointer: + msg = "MOUSE REVIEW ERROR: No pointer for default seat." + debug.println(debug.LEVEL_INFO, msg, True) + return + + if not self._active: + return + + self.activate() + + def get_bindings(self): + """Returns the mouse-review keybindings.""" + + return self._bindings + + def get_handlers(self): + """Returns the mouse-review handlers.""" + + return self._handlers + + def _setup_handlers(self): + """Sets up and returns the mouse-review input event handlers.""" + + handlers = {} + + handlers["toggleMouseReviewHandler"] = \ + input_event.InputEventHandler( + self.toggle, + cmdnames.MOUSE_REVIEW_TOGGLE) + + return handlers + + def _setup_bindings(self): + """Sets up and returns the mouse-review key bindings.""" + + bindings = keybindings.KeyBindings() + + bindings.add( + keybindings.KeyBinding( + "", + keybindings.defaultModifierMask, + keybindings.NO_MODIFIER_MASK, + self._handlers.get("toggleMouseReviewHandler"))) + + return bindings + + def activate(self): + """Activates mouse review.""" + + if not _mouseReviewCapable: + msg = "MOUSE REVIEW ERROR: Wnck is not available" + debug.println(debug.LEVEL_INFO, msg, True) + return + + # Set up the initial object as the one with the focus to avoid + # presenting irrelevant info the first time. + obj = cthulhu_state.locusOfFocus + script = None + frame = None + if obj: + script = _scriptManager.getScript(AXObject.get_application(obj), obj) + if script: + frame = script.utilities.topLevelObject(obj) + self._currentMouseOver = _ItemContext(obj=obj, frame=frame, script=script) + + self._eventListener.register("mouse:abs") + screen = Wnck.Screen.get_default() + if screen: + # On first startup windows and workspace are likely to be None, + # but the signals we connect to will get emitted when proper values + # become available; but in case we got disabled and re-enabled we + # have to get the initial values manually. + stacked = screen.get_windows_stacked() + if stacked: + stacked.reverse() + self._all_windows = stacked + self._workspace = screen.get_active_workspace() + if self._workspace: + self._update_workspace_windows() + + i = screen.connect("window-stacking-changed", self._on_stacking_changed) + self._handlerIds[i] = screen + i = screen.connect("active-workspace-changed", self._on_workspace_changed) + self._handlerIds[i] = screen + + self._active = True + + def deactivate(self): + """Deactivates mouse review.""" + + self._eventListener.deregister("mouse:abs") + for key, value in self._handlerIds.items(): + value.disconnect(key) + self._handlerIds = {} + self._workspace = None + self._windows = [] + self._all_windows = [] + + self._active = False + + def getCurrentItem(self): + """Returns the accessible object being reviewed.""" + + if not _mouseReviewCapable: + return None + + if not self._active: + return None + + obj = self._currentMouseOver.getObject() + + if time.time() - self._currentMouseOver.getTime() > 0.1: + msg = f"MOUSE REVIEW: Treating {obj} as stale" + debug.println(debug.LEVEL_INFO, msg, True) + return None + + return obj + + def toggle(self, script=None, event=None): + """Toggle mouse reviewing on or off.""" + + if not _mouseReviewCapable: + return + + self._active = not self._active + _settingsManager.setSetting("enableMouseReview", self._active) + + if not self._active: + self.deactivate() + msg = messages.MOUSE_REVIEW_DISABLED + else: + self.activate() + msg = messages.MOUSE_REVIEW_ENABLED + + if cthulhu_state.activeScript: + cthulhu_state.activeScript.presentMessage(msg) + + def _update_workspace_windows(self): + self._windows = [w for w in self._all_windows + if w.is_on_workspace(self._workspace)] + + def _on_stacking_changed(self, screen): + """Callback for Wnck's window-stacking-changed signal.""" + + stacked = screen.get_windows_stacked() + stacked.reverse() + self._all_windows = stacked + if self._workspace: + self._update_workspace_windows() + + def _on_workspace_changed(self, screen, prev_ws=None): + """Callback for Wnck's active-workspace-changed signal.""" + + self._workspace = screen.get_active_workspace() + self._update_workspace_windows() + + def _contains_point(self, obj, x, y, coordType=None): + if coordType is None: + coordType = Atspi.CoordType.SCREEN + + try: + return obj.queryComponent().contains(x, y, coordType) + except Exception: + return False + + def _has_bounds(self, obj, bounds, coordType=None): + """Returns True if the bounding box of obj is bounds.""" + + if coordType is None: + coordType = Atspi.CoordType.SCREEN + + try: + extents = obj.queryComponent().getExtents(coordType) + except Exception: + return False + + return list(extents) == list(bounds) + + def _accessible_window_at_point(self, pX, pY): + """Returns the accessible window at the specified coordinates.""" + + window = None + for w in self._windows: + if w.is_minimized(): + continue + + x, y, width, height = w.get_geometry() + if x <= pX <= x + width and y <= pY <= y + height: + window = w + break + + if not window: + return None + + windowApp = window.get_application() + if not windowApp: + return None + + app = AXUtilities.get_application_with_pid(windowApp.get_pid()) + if not app: + return None + + candidates = [o for o in AXObject.iter_children( + app, lambda x: self._contains_point(x, pX, pY))] + if len(candidates) == 1: + return candidates[0] + + name = window.get_name() + matches = [o for o in candidates if AXObject.get_name(o) == name] + if len(matches) == 1: + return matches[0] + + bbox = window.get_client_window_geometry() + matches = [o for o in candidates if self._has_bounds(o, bbox)] + if len(matches) == 1: + return matches[0] + + return None + + def _on_mouse_moved(self, event): + """Callback for mouse:abs events.""" + + screen, pX, pY = self._pointer.get_position() + window = self._accessible_window_at_point(pX, pY) + msg = "MOUSE REVIEW: Window at (%i, %i) is %s" % (pX, pY, window) + debug.println(debug.LEVEL_INFO, msg, True) + if not window: + return + + script = _scriptManager.getScript(AXObject.get_application(window)) + if not script: + return + + if script.utilities.isDead(cthulhu_state.locusOfFocus): + menu = None + elif AXUtilities.is_menu(cthulhu_state.locusOfFocus): + menu = cthulhu_state.locusOfFocus + else: + menu = AXObject.find_ancestor(cthulhu_state.locusOfFocus, AXUtilities.is_menu) + + screen, nowX, nowY = self._pointer.get_position() + if (pX, pY) != (nowX, nowY): + msg = "MOUSE REVIEW: Pointer moved again: (%i, %i)" % (nowX, nowY) + debug.println(debug.LEVEL_INFO, msg, True) + return + + obj = script.utilities.descendantAtPoint(menu, pX, pY) \ + or script.utilities.descendantAtPoint(window, pX, pY) + msg = "MOUSE REVIEW: Object at (%i, %i) is %s" % (pX, pY, obj) + debug.println(debug.LEVEL_INFO, msg, True) + + script = _scriptManager.getScript(AXObject.get_application(window), obj) + if menu and obj and not AXObject.find_ancestor(obj, AXUtilities.is_menu): + if script.utilities.intersectingRegion(obj, menu) != (0, 0, 0, 0): + msg = f"MOUSE REVIEW: {obj} believed to be under {menu}" + debug.println(debug.LEVEL_INFO, msg, True) + return + + objDocument = script.utilities.getTopLevelDocumentForObject(obj) + if objDocument and script.utilities.inDocumentContent(): + document = script.utilities.activeDocument() + if document != objDocument: + msg = f"MOUSE REVIEW: {obj} is not in active document {document}" + debug.println(debug.LEVEL_INFO, msg, True) + return + + screen, nowX, nowY = self._pointer.get_position() + if (pX, pY) != (nowX, nowY): + msg = "MOUSE REVIEW: Pointer moved again: (%i, %i)" % (nowX, nowY) + debug.println(debug.LEVEL_INFO, msg, True) + return + + boundary = None + x, y, width, height = self._currentMouseOver.getBoundingBox() + if y <= pY <= y + height and self._currentMouseOver.getString(): + boundary = Atspi.TextBoundaryType.WORD_START + elif obj == self._currentMouseOver.getObject(): + boundary = Atspi.TextBoundaryType.LINE_START + elif AXUtilities.is_selectable(obj): + boundary = Atspi.TextBoundaryType.LINE_START + elif script.utilities.isMultiParagraphObject(obj): + boundary = Atspi.TextBoundaryType.LINE_START + + new = _ItemContext(pX, pY, obj, boundary, window, script) + if new.present(self._currentMouseOver): + self._currentMouseOver = new + + def _listener(self, event): + """Generic listener, mainly to output debugging info.""" + + startTime = time.time() + msg = f"\nvvvvv PROCESS OBJECT EVENT {event.type} vvvvv" + debug.println(debug.LEVEL_INFO, msg, False) + + if event.type.startswith("mouse:abs"): + self.inMouseEvent = True + self._on_mouse_moved(event) + self.inMouseEvent = False + + msg = f"TOTAL PROCESSING TIME: {time.time() - startTime:.4f}\n" + msg += f"^^^^^ PROCESS OBJECT EVENT {event.type} ^^^^^\n" + debug.println(debug.LEVEL_INFO, msg, False) diff --git a/src/cthulhu/plugins/MouseReview/__init__.py b/src/cthulhu/plugins/MouseReview/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/cthulhu/plugins/PluginManager/Makefile.am b/src/cthulhu/plugins/PluginManager/Makefile.am new file mode 100644 index 0000000..3975390 --- /dev/null +++ b/src/cthulhu/plugins/PluginManager/Makefile.am @@ -0,0 +1,8 @@ +orca_python_PYTHON = \ + __init__.py \ + PluginManager.plugin \ + PluginManager.py \ + PluginManagerUi.py + +orca_pythondir=$(pkgpythondir)/plugins/PluginManager + diff --git a/src/cthulhu/plugins/PluginManager/PluginManager.plugin b/src/cthulhu/plugins/PluginManager/PluginManager.plugin new file mode 100644 index 0000000..f992ba2 --- /dev/null +++ b/src/cthulhu/plugins/PluginManager/PluginManager.plugin @@ -0,0 +1,14 @@ +[Plugin] +Module=PluginManager +Loader=python3 +Name=Plugin Manager +Description=Activate and Deactivate plugins +Authors=Chrys chrys@linux-a11y.org +Website= +Version=1.0 +Copyright= +Builtin=true +Hidden=true +Depends= +Icon= +Help= diff --git a/src/cthulhu/plugins/PluginManager/PluginManager.py b/src/cthulhu/plugins/PluginManager/PluginManager.py new file mode 100644 index 0000000..086caed --- /dev/null +++ b/src/cthulhu/plugins/PluginManager/PluginManager.py @@ -0,0 +1,36 @@ +from cthulhu import plugin + +import gi +gi.require_version('Peas', '1.0') +from gi.repository import GObject +from gi.repository import Peas + +import PluginManagerUi + +class PluginManager(GObject.Object, Peas.Activatable, plugin.Plugin): + #__gtype_name__ = 'PluginManager' + + object = GObject.Property(type=GObject.Object) + def __init__(self): + plugin.Plugin.__init__(self) + self.pluginManagerUi = None + def do_activate(self): + API = self.object + self.registerGestureByString(self.startPluginManagerUi, _('plugin manager'), 'kb:cthulhu+e') + + def do_deactivate(self): + API = self.object + + def startPluginManagerUi(self, script=None, inputEvent=None): + self.showUI() + return True + def showUI(self): + API = self.object + if self.pluginManagerUi == None: + self.pluginManagerUi = PluginManagerUi.PluginManagerUi(API.app) + self.pluginManagerUi.setTranslationContext(self.getTranslationContext()) + self.pluginManagerUi.createUI() + self.pluginManagerUi.run() + self.pluginManagerUi = None + else: + self.pluginManagerUi.present() diff --git a/src/cthulhu/plugins/PluginManager/PluginManagerUi.py b/src/cthulhu/plugins/PluginManager/PluginManagerUi.py new file mode 100755 index 0000000..3766610 --- /dev/null +++ b/src/cthulhu/plugins/PluginManager/PluginManagerUi.py @@ -0,0 +1,283 @@ +#!/bin/python +import gi +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk, Gdk + +class PluginManagerUi(Gtk.ApplicationWindow): + def __init__(self, app, *args, **kwargs): + super().__init__(*args, **kwargs, title=_("Cthulhu Plugin Manager")) + self.app = app + self.translationContext = None + self.connect("destroy", self._onCancelButtonClicked) + self.connect('key-press-event', self._onKeyPressWindow) + def createUI(self): + self.set_default_size(650, 650) + self.set_position(Gtk.WindowPosition.CENTER_ALWAYS) + + # pluginInfo (object) = 0 + # name (str) = 1 + # active (bool) = 2 + # buildIn (bool) = 3 + # dataDir (str) = 4 + # moduleDir (str) = 5 + # dependencies (object) = 6 + # moduleName (str) = 7 + # description (str) = 8 + # authors (object) = 9 + # website (str) = 10 + # copyright (str) = 11 + # version (str) = 12 + # helpUri (str) = 13 + # iconName (str) = 14 + self.listStore = Gtk.ListStore(object,str, bool, bool, str, str,object,str,str,object,str,str,str,str,str) + + self.treeView = Gtk.TreeView(model=self.listStore) + self.treeView.connect("row-activated", self._rowActivated) + self.treeView.connect('key-press-event', self._onKeyPressTreeView) + + self.rendererText = Gtk.CellRendererText() + self.columnText = Gtk.TreeViewColumn(_("Name"), self.rendererText, text=1) + self.treeView.append_column(self.columnText) + + self.rendererToggle = Gtk.CellRendererToggle() + self.rendererToggle.connect("toggled", self._onCellToggled) + + self.columnToggle = Gtk.TreeViewColumn(_("Active"), self.rendererToggle, active=2) + self.treeView.append_column(self.columnToggle) + + + self.buttomBox = Gtk.Box(spacing=6) + self.mainVBox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) + self.mainVBox.pack_start(self.treeView, True, True, 0) + self.mainVBox.pack_start(self.buttomBox, False, True, 0) + + self.add(self.mainVBox) + self.oKButton = Gtk.Button.new_with_mnemonic(_("_Details")) + self.oKButton.connect("clicked", self._onDetailsButtonClicked) + self.buttomBox.pack_start(self.oKButton, True, True, 0) + + self.oKButton = Gtk.Button.new_with_mnemonic(_("_OK")) + self.oKButton.connect("clicked", self._onOkButtonClicked) + self.buttomBox.pack_start(self.oKButton, True, True, 0) + + self.applyButton = Gtk.Button.new_with_mnemonic(_("_Apply")) + self.applyButton.connect("clicked", self._onApplyButtonClicked) + self.buttomBox.pack_start(self.applyButton, True, True, 0) + + self.applyButton = Gtk.Button.new_with_mnemonic(_("_Install")) + self.applyButton.connect("clicked", self._onInstallButtonClicked) + self.buttomBox.pack_start(self.applyButton, True, True, 0) + + self.applyButton = Gtk.Button.new_with_mnemonic(_("_Uninstall")) + self.applyButton.connect("clicked", self._onUninstallButtonClicked) + self.buttomBox.pack_start(self.applyButton, True, True, 0) + + self.cancelButton = Gtk.Button.new_with_mnemonic(_("_Cancel")) + self.cancelButton.connect("clicked", self._onCancelButtonClicked) + self.buttomBox.pack_start(self.cancelButton, True, True, 0) + def setTranslationContext(self, translationContext): + self.translationContext = translationContext + global _ + _ = translationContext.gettext + def closeWindow(self): + Gtk.main_quit() + def uninstallPlugin(self): + selection = self.treeView.get_selection() + model, list_iter = selection.get_selected() + try: + if model.get_value(list_iter,0): + pluginInfo = model.get_value(list_iter,0) + pluginName = self.app.getPluginSystemManager().getPluginName(pluginInfo) + dialog = Gtk.MessageDialog(None, + Gtk.DialogFlags.MODAL, + type=Gtk.MessageType.INFO, + buttons=Gtk.ButtonsType.YES_NO) + + dialog.set_markup("%s" % _('Remove Plugin {}?').format(pluginName)) + dialog.format_secondary_markup(_('Do you really want to remove Plugin {}?').format(pluginName)) + response = dialog.run() + dialog.destroy() + if response != Gtk.ResponseType.YES: + return + self.app.getPluginSystemManager().uninstallPlugin(model.get_value(list_iter,0)) + self.refreshPluginList() + except: + pass + + def installPlugin(self): + ok, filePath = self.chooseFile() + if not ok: + return + self.app.getPluginSystemManager().installPlugin(filePath) + self.refreshPluginList() + + def _onKeyPressWindow(self, _, event): + _, key_val = event.get_keyval() + if key_val == Gdk.KEY_Escape: + self.closeWindow() + def _onKeyPressTreeView(self, _, event): + _, key_val = event.get_keyval() + if key_val == Gdk.KEY_Return: + self.applySettings() + self.closeWindow() + if key_val == Gdk.KEY_Escape: + self.closeWindow() + # CTRL + Q + #modifiers = event.get_state() + #if modifiers == Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.MOD2_MASK: + # if key_val == Gdk.KEY_q: + # self._on_scan() + def applySettings(self): + for row in self.listStore: + pluginInfo = row[0] + isActive = row[2] + self.app.getPluginSystemManager().setPluginActive(pluginInfo, isActive) + gsettingsManager = self.app.getGsettingsManager() + gsettingsManager.set_settings_value_list('active-plugins', self.app.getPluginSystemManager().getActivePlugins()) + + self.app.getPluginSystemManager().syncAllPluginsActive() + self.refreshPluginList() + + + def _rowActivated(self, tree_view, path, column): + print('rowActivated') + def showDetails(self): + selection = self.treeView.get_selection() + model, list_iter = selection.get_selected() + try: + if model.get_value(list_iter,0): + pluginInfo = model.get_value(list_iter,0) + name = self.app.getPluginSystemManager().getPluginName(pluginInfo) + description = self.app.getPluginSystemManager().getPluginDescription(pluginInfo) + authors = self.app.getPluginSystemManager().getPluginAuthors(pluginInfo) + website =self.app.getPluginSystemManager().getPluginWebsite(pluginInfo) + copyright = self.app.getPluginSystemManager().getPluginCopyright(pluginInfo) + license = '' #self.app.getPluginSystemManager().getPluginName(pluginInfo) + version = self.app.getPluginSystemManager().getPluginVersion(pluginInfo) + dialog = Gtk.AboutDialog(self) + dialog.set_authors(authors) + dialog.set_website(website) + dialog.set_copyright(copyright) + dialog.set_license(license) + dialog.set_version(version) + dialog.set_program_name(name) + dialog.set_comments(description) + dialog.run() + dialog.destroy() + except: + pass + + def _onDetailsButtonClicked(self, widget): + self.showDetails() + + def _onOkButtonClicked(self, widget): + self.applySettings() + self.closeWindow() + def _onApplyButtonClicked(self, widget): + self.applySettings() + def _onInstallButtonClicked(self, widget): + self.installPlugin() + def _onUninstallButtonClicked(self, widget): + self.uninstallPlugin() + def _onCancelButtonClicked(self, widget): + self.closeWindow() + def refreshPluginList(self): + self.clearPluginList() + pluginList = self.app.getPluginSystemManager().plugins + for pluginInfo in pluginList: + self.addPlugin(pluginInfo) + def clearPluginList(self): + self.listStore.clear() + + def addPlugin(self, pluginInfo): + ignoredPlugins = self.app.getPluginSystemManager().getIgnoredPlugins() + moduleDir = self.app.getPluginSystemManager().getPluginModuleDir(pluginInfo) + if moduleDir in ignoredPlugins: + return + + hidden = self.app.getPluginSystemManager().isPluginHidden(pluginInfo) + if hidden: + return + + moduleName = self.app.getPluginSystemManager().getPluginModuleName(pluginInfo) + name = self.app.getPluginSystemManager().getPluginName(pluginInfo) + version = self.app.getPluginSystemManager().getPluginVersion(pluginInfo) + website = self.app.getPluginSystemManager().getPluginWebsite(pluginInfo) + authors = self.app.getPluginSystemManager().getPluginAuthors(pluginInfo) + buildIn = self.app.getPluginSystemManager().isPluginBuildIn(pluginInfo) + description = self.app.getPluginSystemManager().getPluginDescription(pluginInfo) + iconName = self.app.getPluginSystemManager().getPluginIconName(pluginInfo) + copyright = self.app.getPluginSystemManager().getPluginCopyright(pluginInfo) + dependencies = self.app.getPluginSystemManager().getPluginDependencies(pluginInfo) + + #settings = self.app.getPluginSystemManager().getPluginSettings(pluginInfo) + #hasDependencies = self.app.getPluginSystemManager().hasPluginDependency(pluginInfo) + loaded = self.app.getPluginSystemManager().isPluginLoaded(pluginInfo) + available = self.app.getPluginSystemManager().isPluginAvailable(pluginInfo) + active = self.app.getPluginSystemManager().isPluginActive(pluginInfo) + + #externalData = self.app.getPluginSystemManager().getPluginExternalData(pluginInfo) + helpUri = self.app.getPluginSystemManager().getPlugingetHelpUri(pluginInfo) + dataDir = self.app.getPluginSystemManager().getPluginDataDir(pluginInfo) + + # pluginInfo (object) = 0 + # name (str) = 1 + # active (bool) = 2 + # buildIn (bool) = 3 + # dataDir (str) = 4 + # moduleDir (str) = 5 + # dependencies (object) = 6 + # moduleName (str) = 7 + # description (str) = 8 + # authors (object) = 9 + # website (str) = 10 + # copyright (str) = 11 + # version (str) = 12 + # helpUri (str) = 13 + # iconName (str) = 14 + self.listStore.append([pluginInfo, name, active, buildIn, dataDir, moduleDir, dependencies, moduleName, description, authors, website, copyright, version, helpUri, iconName]) + def chooseFile(self): + dialog = Gtk.FileChooserDialog( + title=_("Please choose a file"), parent=self, action=Gtk.FileChooserAction.OPEN + ) + dialog.add_buttons( + Gtk.STOCK_CANCEL, + Gtk.ResponseType.CANCEL, + Gtk.STOCK_OPEN, + Gtk.ResponseType.OK, + ) + + filter_plugin = Gtk.FileFilter() + filter_plugin.set_name(_("Plugin Archive")) + filter_plugin.add_mime_type("application/gzip") + dialog.add_filter(filter_plugin) + + response = dialog.run() + filePath = '' + ok = False + + if response == Gtk.ResponseType.OK: + ok = True + filePath = dialog.get_filename() + + dialog.destroy() + return ok, filePath + def _onCellToggled(self, widget, path): + + self.listStore[path][2] = not self.listStore[path][2] + def present(self): + cthulhu_state = self.app.getDynamicApiManager().getAPI('CthulhuState') + ts = 0 + try: + ts = cthulhu_state.lastInputEvent.timestamp + except: + pass + if ts == 0: + ts = Gtk.get_current_event_time() + self.present_with_time(ts) + def run(self): + self.refreshPluginList() + self.present() + self.show_all() + Gtk.main() + self.destroy() diff --git a/src/cthulhu/plugins/PluginManager/PluginManagerUiListBox.py b/src/cthulhu/plugins/PluginManager/PluginManagerUiListBox.py new file mode 100755 index 0000000..edb520c --- /dev/null +++ b/src/cthulhu/plugins/PluginManager/PluginManagerUiListBox.py @@ -0,0 +1,83 @@ +#!/bin/python +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + +class ListBoxRowWithData(Gtk.ListBoxRow): + def __init__(self, data): + super(Gtk.ListBoxRow, self).__init__() + self.data = data + self.add(Gtk.Label(label=data)) + +class PluginManagerUi(Gtk.Window): + def __init__(self): + Gtk.Window.__init__(self) + self.pluginList = [] + self.set_default_size(200, -1) + self.connect("destroy", Gtk.main_quit) + self.listBox = Gtk.ListBox() + + self.buttomBox = Gtk.Box(spacing=6) + self.mainVBox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=20) + self.mainVBox.pack_start(self.listBox, True, True, 0) + self.mainVBox.pack_start(self.buttomBox, True, True, 0) + + self.add(self.mainVBox) + + self.oKButton = Gtk.Button(label="OK") + self.oKButton.connect("clicked", self.on_oKButton_clicked) + self.buttomBox.pack_start(self.oKButton, True, True, 0) + + self.applyButton = Gtk.Button(label="Apply") + self.applyButton.connect("clicked", self.on_applyButton_clicked) + self.buttomBox.pack_start(self.applyButton, True, True, 0) + + self.cancelButton = Gtk.Button(label="Cancel") + self.cancelButton.connect("clicked", self.on_cancelButton_clicked) + self.buttomBox.pack_start(self.cancelButton, True, True, 0) + + self.listBox.connect("row-activated", self.on_row_activated) + + def on_row_activated(self, listBox, listboxrow): + print("Row %i activated" % (listboxrow.get_index())) + + def on_oKButton_clicked(self, widget): + print("OK") + + def on_applyButton_clicked(self, widget): + print("Apply") + + def on_cancelButton_clicked(self, widget): + print("Cancel") + + + def addPlugin(self, Name, Active, Description = ''): + self.pluginList.append([Name, Active, Description]) + + def run(self): + for plugin in self.pluginList: + print(plugin) + box = Gtk.Box(spacing=10) + pluginNameLabel = Gtk.Label(plugin[0]) + #pluginActiveCheckButton = Gtk.CheckButton(label="_Active", use_underline=True) + #pluginActiveCheckButton.set_active(plugin[1]) + pluginActiveSwitch = Gtk.Switch() + pluginActiveSwitch.set_active(plugin[1]) + + + pluginDescriptionLabel = Gtk.Label(plugin[2]) + + box.pack_start(pluginNameLabel, True, True, 0) + box.pack_start(pluginActiveSwitch, True, True, 0) + box.pack_start(pluginDescriptionLabel, True, True, 0) + + self.listBox.add(box) + self.show_all() + Gtk.main() + +if __name__ == "__main__": + ui = PluginManagerUi() + ui.addPlugin('plugin1', True, 'bla') + ui.addPlugin('plugin2', True, 'bla') + ui.addPlugin('plugin3', True, 'bla') + ui.run() diff --git a/src/cthulhu/plugins/PluginManager/PluginManagerUiListBox_tut.py b/src/cthulhu/plugins/PluginManager/PluginManagerUiListBox_tut.py new file mode 100755 index 0000000..7e07f30 --- /dev/null +++ b/src/cthulhu/plugins/PluginManager/PluginManagerUiListBox_tut.py @@ -0,0 +1,93 @@ +#!/bin/python +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk + + +class ListBoxRowWithData(Gtk.ListBoxRow): + def __init__(self, data): + super(Gtk.ListBoxRow, self).__init__() + self.data = data + self.add(Gtk.Label(label=data)) + + +class ListBoxWindow(Gtk.Window): + def __init__(self): + Gtk.Window.__init__(self, title="ListBox Demo") + self.set_border_width(10) + + box_outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) + self.add(box_outer) + + listbox = Gtk.ListBox() + listbox.set_selection_mode(Gtk.SelectionMode.NONE) + box_outer.pack_start(listbox, True, True, 0) + + row = Gtk.ListBoxRow() + hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50) + row.add(hbox) + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + hbox.pack_start(vbox, True, True, 0) + + label1 = Gtk.Label(label="Automatic Date & Time", xalign=0) + label2 = Gtk.Label(label="Requires internet access", xalign=0) + vbox.pack_start(label1, True, True, 0) + vbox.pack_start(label2, True, True, 0) + + switch = Gtk.Switch() + switch.props.valign = Gtk.Align.CENTER + hbox.pack_start(switch, False, True, 0) + + listbox.add(row) + + row = Gtk.ListBoxRow() + hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50) + row.add(hbox) + label = Gtk.Label(label="Enable Automatic Update", xalign=0) + check = Gtk.CheckButton() + hbox.pack_start(label, True, True, 0) + hbox.pack_start(check, False, True, 0) + + listbox.add(row) + + row = Gtk.ListBoxRow() + hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50) + row.add(hbox) + label = Gtk.Label(label="Date Format", xalign=0) + combo = Gtk.ComboBoxText() + combo.insert(0, "0", "24-hour") + combo.insert(1, "1", "AM/PM") + hbox.pack_start(label, True, True, 0) + hbox.pack_start(combo, False, True, 0) + + listbox.add(row) + + listbox_2 = Gtk.ListBox() + items = "This is a sorted ListBox Fail".split() + + for item in items: + listbox_2.add(ListBoxRowWithData(item)) + + def sort_func(row_1, row_2, data, notify_destroy): + return row_1.data.lower() > row_2.data.lower() + + def filter_func(row, data, notify_destroy): + return False if row.data == "Fail" else True + + listbox_2.set_sort_func(sort_func, None, False) + listbox_2.set_filter_func(filter_func, None, False) + + def on_row_activated(listbox_widget, row): + print(row.data) + + listbox_2.connect("row-activated", on_row_activated) + + box_outer.pack_start(listbox_2, True, True, 0) + listbox_2.show_all() + + +win = ListBoxWindow() +win.connect("destroy", Gtk.main_quit) +win.show_all() +Gtk.main() diff --git a/src/cthulhu/plugins/PluginManager/__init__.py b/src/cthulhu/plugins/PluginManager/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/cthulhu/plugins/SelfVoice/Makefile.am b/src/cthulhu/plugins/SelfVoice/Makefile.am new file mode 100644 index 0000000..1083ded --- /dev/null +++ b/src/cthulhu/plugins/SelfVoice/Makefile.am @@ -0,0 +1,7 @@ +orca_python_PYTHON = \ + __init__.py \ + SelfVoice.plugin \ + SelfVoice.py + +orca_pythondir=$(pkgpythondir)/plugins/SelfVoice + diff --git a/src/cthulhu/plugins/SelfVoice/SelfVoice.plugin b/src/cthulhu/plugins/SelfVoice/SelfVoice.plugin new file mode 100644 index 0000000..8cf18d2 --- /dev/null +++ b/src/cthulhu/plugins/SelfVoice/SelfVoice.plugin @@ -0,0 +1,6 @@ +[Plugin] +Module=SelfVoice +Loader=python3 +Name=Self Voice Plugin +Description=use cthulhu text / braile from using unix sockets +Authors=Chrys chrys@linux-a11y.org diff --git a/src/cthulhu/plugins/SelfVoice/SelfVoice.py b/src/cthulhu/plugins/SelfVoice/SelfVoice.py new file mode 100644 index 0000000..1b2af63 --- /dev/null +++ b/src/cthulhu/plugins/SelfVoice/SelfVoice.py @@ -0,0 +1,117 @@ +# Example usage: +# echo "This is a test." | socat - UNIX-CLIENT:/tmp/cthulhu-PID.sock +# Where PID is cthulhu's process id. +# Prepend text to be spoken with to make it not interrupt, for inaccessible windows. +# Append message to be spoken with <#PERSISTENT#> to present a persistent message in braille +# <#APPEND#> is only usable for a persistent message + +from cthulhu import plugin + +import gi +gi.require_version('Peas', '1.0') +from gi.repository import GObject +from gi.repository import Peas +import select, socket, os, os.path +from threading import Thread, Lock + +APPEND_CODE = '<#APPEND#>' +PERSISTENT_CODE = '<#PERSISTENT#>' + +class SelfVoice(GObject.Object, Peas.Activatable, plugin.Plugin): + __gtype_name__ = 'SelfVoice' + + object = GObject.Property(type=GObject.Object) + def __init__(self): + plugin.Plugin.__init__(self) + self.lock = Lock() + self.active = False + self.voiceThread = Thread(target=self.voiceWorker) + def do_activate(self): + API = self.object + self.activateWorker() + def do_deactivate(self): + API = self.object + self.deactivateWorker() + def do_update_state(self): + API = self.object + def deactivateWorker(self): + with self.lock: + self.active = False + self.voiceThread.join() + def activateWorker(self): + with self.lock: + self.active = True + self.voiceThread.start() + def isActive(self): + with self.lock: + return self.active + def outputMessage(self, Message): + # Prepare + API = self.object + append = Message.startswith(APPEND_CODE) + if append: + Message = Message[len(APPEND_CODE):] + if Message.endswith(PERSISTENT_CODE): + Message = Message[:len(Message)-len(PERSISTENT_CODE)] + API.app.getAPIHelper().outputMessage(Message, not append) + else: + script_manager = API.app.getDynamicApiManager().getAPI('ScriptManager') + scriptManager = script_manager.getManager() + scriptManager.getDefaultScript().presentMessage(Message, resetStyles=False) + return + try: + settings = API.app.getDynamicApiManager().getAPI('Settings') + braille = API.app.getDynamicApiManager().getAPI('Braille') + speech = API.app.getDynamicApiManager().getAPI('Speech') + # Speak + if speech != None: + if (settings.enableSpeech): + if not append: + speech.cancel() + if Message != '': + speech.speak(Message) + # Braille + if braille != None: + if (settings.enableBraille): + braille.displayMessage(Message) + except e as Exception: + print(e) + + def voiceWorker(self): + socketFile = '/tmp/cthulhu-' + str(os.getppid()) + '.sock' + # for testing purposes + #socketFile = '/tmp/cthulhu-plugin.sock' + + if os.path.exists(socketFile): + os.unlink(socketFile) + cthulhuSock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + cthulhuSock.bind(socketFile) + os.chmod(socketFile, 0o222) + cthulhuSock.listen(1) + while self.isActive(): + # Check if the client is still connected and if data is available: + try: + r, _, _ = select.select([cthulhuSock], [], [], 0.8) + except select.error: + break + if r == []: + continue + if cthulhuSock in r: + client_sock, client_addr = cthulhuSock.accept() + try: + rawdata = client_sock.recv(8129) + data = rawdata.decode("utf-8").rstrip().lstrip() + self.outputMessage(data) + except: + pass + try: + client_sock.close() + except: + pass + if cthulhuSock: + cthulhuSock.close() + cthulhuSock = None + if os.path.exists(socketFile): + os.unlink(socketFile) + + diff --git a/src/cthulhu/plugins/SelfVoice/__init__.py b/src/cthulhu/plugins/SelfVoice/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/cthulhu/plugins/Time/Makefile.am b/src/cthulhu/plugins/Time/Makefile.am new file mode 100644 index 0000000..a0024f8 --- /dev/null +++ b/src/cthulhu/plugins/Time/Makefile.am @@ -0,0 +1,7 @@ +orca_python_PYTHON = \ + __init__.py \ + Time.plugin \ + Time.py + +orca_pythondir=$(pkgpythondir)/plugins/Time + diff --git a/src/cthulhu/plugins/Time/Time.plugin b/src/cthulhu/plugins/Time/Time.plugin new file mode 100644 index 0000000..2f290b5 --- /dev/null +++ b/src/cthulhu/plugins/Time/Time.plugin @@ -0,0 +1,6 @@ +[Plugin] +Module=Time +Loader=python3 +Name=Time +Description=Present current time +Authors=Chrys chrys@linux-a11y.org diff --git a/src/cthulhu/plugins/Time/Time.py b/src/cthulhu/plugins/Time/Time.py new file mode 100644 index 0000000..ccab06f --- /dev/null +++ b/src/cthulhu/plugins/Time/Time.py @@ -0,0 +1,35 @@ +import gi, time +gi.require_version('Peas', '1.0') +from gi.repository import GObject +from gi.repository import Peas + +from cthulhu import plugin + +class Time(GObject.Object, Peas.Activatable, plugin.Plugin): + #__gtype_name__ = 'Time' + + object = GObject.Property(type=GObject.Object) + def __init__(self): + plugin.Plugin.__init__(self) + def do_activate(self): + API = self.object + self.connectSignal("setup-inputeventhandlers-completed", self.setupCompatBinding) + def setupCompatBinding(self, app): + cmdnames = app.getDynamicApiManager().getAPI('Cmdnames') + inputEventHandlers = app.getDynamicApiManager().getAPI('inputEventHandlers') + inputEventHandlers['presentTimeHandler'] = app.getAPIHelper().createInputEventHandler(self.presentTime, cmdnames.PRESENT_CURRENT_TIME) + def do_deactivate(self): + API = self.object + inputEventHandlers = API.app.getDynamicApiManager().getAPI('inputEventHandlers') + del inputEventHandlers['presentTimeHandler'] + def do_update_state(self): + API = self.object + def presentTime(self, script=None, inputEvent=None): + """ Presents the current time. """ + API = self.object + settings_manager = API.app.getDynamicApiManager().getAPI('SettingsManager') + _settingsManager = settings_manager.getManager() + timeFormat = _settingsManager.getSetting('presentTimeFormat') + message = time.strftime(timeFormat, time.localtime()) + API.app.getDynamicApiManager().getAPI('CthulhuState').activeScript.presentMessage(message, resetStyles=False) + return True diff --git a/src/cthulhu/plugins/Time/__init__.py b/src/cthulhu/plugins/Time/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/cthulhu/resource_manager.py b/src/cthulhu/resource_manager.py new file mode 100644 index 0000000..8bf4e5f --- /dev/null +++ b/src/cthulhu/resource_manager.py @@ -0,0 +1,349 @@ +import traceback + +class TryFunction(): + def __init__(self, function): + self.function = function + def runSignal(self, app): + try: + return self.function(app) + except Exception as e: + print('try signal',e , traceback.print_exc()) + def runInputEvent(self, script=None, inputEvent=None): + try: + return self.function(script, inputEvent) + except Exception as e: + print('try input event',e , traceback.print_exc()) + def getFunction(self): + return self.function + def setFunction(self, function): + self.function = function + +class ResourceEntry(): + def __init__(self, entryType, resource1 = None, function = None, tryFunction= None, resourceText = '', resource2 = None, resource3 = None, resource4 = None): + # function based init + self.entryType = entryType # 'keyboard' = Keyboard, 'subscription' = Subscription, 'signal' = Signal, 'api'= Dynamic API, 'settings' = gSetttings + self.resource1 = resource1 + self.resource2 = resource2 + self.resource3 = resource3 + self.resource4 = resource4 + self.function = function + self.tryFunction = tryFunction + self.resourceText = resourceText + + def getEntryType(self): + return self.entryType + def getResourceText(self): + return self.resourceText + def getResource1(self): + return self.resource1 + def getResource2(self): + return self.resource2 + def getResource3(self): + return self.resource3 + def getResource4(self): + return self.resource4 + def getFunction(self): + return self.function + def getTryFunction(self): + return self.tryFunction + +class ResourceContext(): + def __init__(self, app, name): + self.app = app + self.name = name + self.gestures = {} # gestures added by the context + self.subscriptions = {} # subscription to signals by the context + self.apis = {} + self.signals = {} + self.settings = {} + + def getName(self): + return self.name + def getGestures(self): + return self.gestures + def getSubscriptions(self): + return self.subscriptions + def getSignals(self): + return self.signals + def getAPIs(self): + return self.apis + def getSettings(self): + return self.settings + def hasSettings(self, profile, application, sub_setting_name): + try: + d = self.getSettings()[profile][application][sub_setting_name] + return True + except KeyError: + return False + def addSetting(self, profile, application, sub_setting_name, entry): + # add profile + try: + d = self.settings[profile] + except KeyError: + self.settings[profile]= {} + # add application + try: + d = self.settings[profile][application] + except KeyError: + self.settings[profile][application] = {} + # add entry + self.settings[profile][application][sub_setting_name] = entry + + print('add', 'settings', self.getName(), profile, application, entry.getResourceText()) + + + def hasAPI(self, application, api): + try: + d = self.getAPIs()[application][api] + return True + except KeyError: + return False + def addAPI(self, application, api, entry): + # add application + try: + d = self.apis[application] + except KeyError: + self.apis[application] = {} + # add entry + self.apis[application][api] = entry + print('add', 'api', self.getName(), application, api) + + def removeAPI(self, application, api): + try: + del self.apis[application][api] + except KeyError as e: + print(e) + try: + if len(self.getAPIs()[application]) == 0: + del self.apis[application] + except KeyError: + pass + print('remove', 'apis', self.getName(), application, api) + + def addGesture(self, profile, application, gesture, entry): + # add profile + try: + d = self.gestures[profile] + except KeyError: + self.gestures[profile]= {} + # add application + try: + d = self.gestures[profile][application] + except KeyError: + self.gestures[profile][application] = {} + # add entry + self.gestures[profile][application][gesture] = entry + print('add', 'gesture', self.getName(), profile, application, entry.getResourceText()) + def removeGesture(self, gesture): + gestureCopy = self.getGestures().copy() + for profile, applicationDict in gestureCopy.items(): + for application, keyDict in applicationDict.copy().items(): + try: + del self.getGestures()[profile][application][gesture] + if len(self.getGestures()[profile][application]) == 0: + del self.getGestures()[profile][application] + if len(self.getGestures()[profile]) == 0: + del self.getGestures()[profile] + except KeyError: + pass + + print('remove', 'gesture', self.getName(), profile, application, gesture) + + def addSubscription(self, signalName, function, entry): + # add entry + try: + e = self.subscriptions[signalName] + except KeyError: + self.subscriptions[signalName] = {} + self.subscriptions[signalName][function] = entry + print('add', 'subscription', self.getName(), entry.getResourceText(), entry.function) + + def removeSubscriptionByFunction(self, function): + for signalName, functionDict in self.getSubscriptions().copy().items(): + for functionKey, entry in functionDict.copy().items(): + try: + if function == entry.function: + del self.getSubscriptions()[signalName][entry.function] + elif function == entry.tryFunction: + del self.getSubscriptions()[signalName][entry.function] + except KeyError as e: + print(e) + try: + if len(self.getSubscriptions()[signalName]) == 0: + del self.getSubscriptions()[signalName] + except KeyError: + pass + print('remove', 'subscription', self.getName(), function) + def addSignal(self, signal, entry): + # add entry + self.signals[signal] = entry + print('add', 'signal', self.getName(), entry.getResourceText()) + def removeSignal(self, signal): + print('remove', 'signal', self.getName(), entry.getResourceText()) + + def unregisterAllResources(self): + try: + self.unregisterAllGestures() + self.unregisterAllSubscriptions() + self.unregisterAllSignals() + self.unregisterAllAPI() + except Exception as e: + print(e) + + def unregisterAllAPI(self): + dynamicApiManager = self.app.getDynamicApiManager() + for application, value in self.getAPIs().copy().items(): + for key, entry in value.copy().items(): + try: + dynamicApiManager.unregisterAPI(key, application, self.getName()) + except Exception as e: + print(e) + print('unregister api ', self.getName(), entry.getEntryType(), entry.getResourceText()) + def unregisterAllGestures(self): + APIHelper = self.app.getAPIHelper() + + for profile, profileValue in self.getGestures().copy().items(): + for application, applicationValue in profileValue.copy().items(): + for gesture, entry in applicationValue.copy().items(): + if entry.getEntryType() == 'keyboard': + try: + APIHelper.unregisterShortcut(entry.getResource1(), self.getName()) + except Exception as e: + print(e) + print('unregister gesture', self.getName(), entry.getEntryType(), profile, application, entry.getResourceText()) + def unregisterAllSignals(self): + pass + # how to remove signals???? + + def unregisterAllSubscriptions(self): + SignalManager = self.app.getSignalManager() + + for signalName, entryDict in self.getSubscriptions().copy().items(): + for function, entry in entryDict.copy().items(): + try: + SignalManager.disconnectSignalByFunction(entry.tryFunction, self.getName()) + except Exception as e: + print(e) + print('unregister subscription', self.getName(), entry.getEntryType(), entry.getResourceText()) + +class ResourceManager(): + def __init__(self, app): + self.app = app + self.resourceContextDict = {} + def getResourceContextDict(self): + return self.resourceContextDict + + def addResourceContext(self, contextName, overwrite = False): + if not contextName: + return + resourceContext = self.getResourceContext(contextName) + if resourceContext: + if not overwrite: + return + resourceContext = ResourceContext(self.app, contextName) + self.resourceContextDict[contextName] = resourceContext + print('add {}'.format(contextName)) + + def removeResourceContext(self, contextName): + if not contextName: + return + + try: + self.resourceContextDict[contextName].unregisterAllResources() + except: + pass + + # temp + try: + print('_________', 'summery', self.resourceContextDict[contextName].getName(), '_________') + print('api', self.resourceContextDict[contextName].getAPIs()) + print('signals', self.resourceContextDict[contextName].getSignals()) + print('subscriptions', self.resourceContextDict[contextName].getSubscriptions()) + print('gestrues ', self.resourceContextDict[contextName].getGestures()) + print('_________', self.resourceContextDict[contextName].getName(), '_________') + except: + pass + # temp + try: + del self.resourceContextDict[contextName] + except KeyError: + pass + print('rm {}'.format(contextName)) + + def getResourceContext(self, contextName): + if not contextName: + return None + try: + return self.resourceContextDict[contextName] + except KeyError: + return None + + def addAPI(self, application, api, contextName = None): + if not contextName: + return + resourceContext = self.getResourceContext(contextName) + if not resourceContext: + return + entry = None + resourceContext.addAPI(application, api, entry) + def removeAPI(self, application, api, contextName = None): + if not contextName: + return + resourceContext = self.getResourceContext(contextName) + if not resourceContext: + return + resourceContext.removeAPI(application, api) + def addGesture(self, profile, application, gesture, entry, contextName = None): + if not contextName: + return + resourceContext = self.getResourceContext(contextName) + if not resourceContext: + return + entry = None + resourceContext.addGesture(profile, application, gesture, entry) + def removeGesture(self, gesture, contextName = None): + if not contextName: + return + resourceContext = self.getResourceContext(contextName) + if not resourceContext: + return + resourceContext.removeGesture(gesture) + def addSubscription(self, subscription, entry, contextName = None): + if not contextName: + return + resourceContext = self.getResourceContext(contextName) + if not resourceContext: + return + entry = None + resourceContext.addSubscription(subscription, entry) + def removeSubscriptionByFunction(self, function, contextName = None): + if not contextName: + return + resourceContext = self.getResourceContext(contextName) + if not resourceContext: + return + resourceContext.removeSubscriptionByFunction(function) + def addSignal(self, signal, entry, contextName = None): + if not contextName: + return + resourceContext = self.getResourceContext(contextName) + if not resourceContext: + return + entry = None + resourceContext.addSignal(signal, entry) + def removeSignal(self, signal, contextName = None): + if not contextName: + return + resourceContext = self.getResourceContext(contextName) + if not resourceContext: + return + resourceContext.removeSignal(signal) + def printContext(self): + for k, v in self.resourceContextDict.items(): + print('plugin', k) + for k1, v1 in v.getGestures().items(): + print(' profile', k1) + for k2, v2 in v1.items(): + print(' application', k2) + for k3, v3 in v2.items(): + print(' value', k3, v3) diff --git a/src/cthulhu/signal_manager.py b/src/cthulhu/signal_manager.py new file mode 100644 index 0000000..b2a0d6e --- /dev/null +++ b/src/cthulhu/signal_manager.py @@ -0,0 +1,58 @@ +import gi +from gi.repository import GObject + +from cthulhu import resource_manager + +class SignalManager(): + def __init__(self, app): + self.app = app + self.resourceManager = self.app.getResourceManager() + + def registerSignal(self, signalName, signalFlag = GObject.SignalFlags.RUN_LAST, closure = GObject.TYPE_NONE, accumulator=(), contextName = None): + # register signal + ok = False + if not self.signalExist(signalName): + GObject.signal_new(signalName, self.app, signalFlag, closure,accumulator) + ok = True + resourceContext = self.resourceManager.getResourceContext(contextName) + if resourceContext: + resourceEntry = resource_manager.ResourceEntry('signal', signalName, signalName, signalName, signalName) + resourceContext.addSignal(signalName, resourceEntry) + return ok + + def signalExist(self, signalName): + return GObject.signal_lookup(signalName, self.app) != 0 + def connectSignal(self, signalName, function, profile, param = None, contextName = None): + signalID = None + try: + if self.signalExist(signalName): + tryFunction = resource_manager.TryFunction(function) + signalID = self.app.connect(signalName, tryFunction.runSignal) + resourceContext = self.resourceManager.getResourceContext(contextName) + if resourceContext: + resourceEntry = resource_manager.ResourceEntry('subscription', signalID, function, tryFunction, signalName) + resourceContext.addSubscription(signalName, function, resourceEntry) + else: + print('signal {} does not exist'.format(signalName)) + except Exception as e: + print(e) + + return signalID + def disconnectSignalByFunction(self, function, contextName = None): + ok = False + try: + self.app.disconnect_by_func(function) + ok = True + except: + pass + resourceContext = self.resourceManager.getResourceContext(contextName) + if resourceContext: + resourceContext.removeSubscriptionByFunction(function) + return ok + def emitSignal(self, signalName): + # emit an signal + try: + self.app.emit(signalName) + print('after Emit Signal: {}'.format(signalName)) + except: + print('Signal "{}" does not exist.'.format(signalName)) diff --git a/src/cthulhu/translation_context.py b/src/cthulhu/translation_context.py new file mode 100644 index 0000000..3c140e1 --- /dev/null +++ b/src/cthulhu/translation_context.py @@ -0,0 +1,93 @@ +import gi, os, locale, gettext +from gi.repository import GObject +import gettext + +from cthulhu import cthulhu_i18n + +class TranslationContext(): + def __init__(self, app): + self.app = app + self.localeDir = cthulhu_i18n.localedir + self.domain = 'cthulhu' + self.language = 'en' + self.fallbackToCthulhuTranslation = True + self.domainTranslation = None + self.cthulhuMainTranslation = None + + def setDomain(self, domain): + self.domain = domain + def setLanguage(self, language): + self.language = language + def setLocaleDir(self, localeDir): + self.localeDir = localeDir + def getCurrentDefaultLocale(self): + return locale.getdefaultlocale()[0] + def setFallbackToCthulhuTranslation(self, fallback): + self.fallback = fallback + def getFallbackToCthulhuTranslation(self): + return self.fallback + def getLocaleDir(self): + return self.localeDir + def getLanguage(self): + return self.language + def getDomain(self): + return self.domain + def getDomainTranslation(self): + return self.domainTranslation + def getCthulhuMainTranslation(self): + return self.cthulhuMainTranslation + def setDomainTranslation(self, domainTranslation): + self.domainTranslation = domainTranslation + def setCthulhuMainTranslation(self, cthulhuMainTranslation): + self.cthulhuMainTranslation = cthulhuMainTranslation + def updateTranslation(self): + self.setDomainTranslation(None) + self.setCthulhuMainTranslation(None) + + try: + self.setLanguage(self.getCurrentDefaultLocale()) + except: + self.setLanguage('en') + print(e) + if self.getFallbackToCthulhuTranslation(): + cthulhuaMainTranslation = cthulhu_i18n + self.setCthulhuMainTranslation(cthulhuaMainTranslation) + try: + domainTranslation = gettext.translation(self.getDomain(), self.getLocaleDir(), languages=[self.getLanguage()]) + self.setDomainTranslation(domainTranslation) + except Exception as e: + print(e) + + def gettext(self, text): + translatedText = text + if self.getDomainTranslation() != None: + try: + translatedText = self.getDomainTranslation().gettext(text) + except Exception as e: + print(e) + if translatedText == text: + if self.getFallbackToCthulhuTranslation() or self.getDomainTranslation() == None: + if self.getCthulhuMainTranslation() != None: + try: + translatedText = self.getCthulhuMainTranslation().cgettext(text) # gettext from cthulhu_i18n + except Exception as e: + print(e) + return translatedText + + def ngettext(self, singular, plural, n): + translatedText = singular + if n > 1: + translatedText = plural + if self.getDomainTranslation() != None: + try: + translatedText = self.getDomainTranslation().ngettext(singular, plural, n) + except: + pass + if translatedText in [singular, plural]: # not translated + if self.getFallbackToCthulhuTranslation() or self.getDomainTranslation() == None: + if self.getCthulhuMainTranslation() != None: + try: + translatedText = self.getCthulhuMainTranslation().ngettext(singular, plural, n) + except: + pass + return translatedText diff --git a/src/cthulhu/translation_manager.py b/src/cthulhu/translation_manager.py new file mode 100644 index 0000000..5137d09 --- /dev/null +++ b/src/cthulhu/translation_manager.py @@ -0,0 +1,52 @@ +import gi, os, locale, gettext +from gi.repository import GObject +import gettext + +from cthulhu import cthulhu_i18n +from cthulhu import translation_context + +class TranslationManager(): + def __init__(self, app): + self.app = app + self.translatioContextnDict = {} + def initTranslation(self, name, domain = None, localeDir = None, language = None, fallbackToCthulhuTranslation = True): + translationContext = translation_context.TranslationContext(self.app) + if os.path.isdir(localeDir): + translationContext.setDomain(domain) + translationContext.setLocaleDir(localeDir) + translationContext.setLanguage(language) + translationContext.setFallbackToCthulhuTranslation(fallbackToCthulhuTranslation) + translationContext.updateTranslation() + self.addTranslationDict(name, translationContext) + return translationContext + def addTranslationDict(self, name, translationContext): + self.translatioContextnDict[name] = translationContext + def getTranslationContextByName(name): + try: + return self.translatioContextnDict[name] + except KeyError: + return self.translatioContextnDict['cthulhu'] + #def getTranslationsInstance(self, domain='cthulhu'): + """ Gets the gettext translation instance for this add-on. + \\locale will be used to find .mo files, if exists. + If a translation file is not found the default fallback null translation is returned. + @param domain: the translation domain to retrieve. The 'Cthulhu' default should be used in most cases. + @returns: the gettext translation class. + """ + # localedir = os.path.join(self.path, "locale") + # return gettext.translation(domain, localedir=localedir, languages=[languageHandler.getLanguage()], fallback=True) +""" + def initTranslation(): + addon = getCodeAddon(frameDist=2) + translations = addon.getTranslationsInstance() + # Point _ to the translation object in the globals namespace of the caller frame + # FIXME: should we retrieve the caller module object explicitly? + try: + callerFrame = inspect.currentframe().f_back + callerFrame.f_globals['_'] = translations.gettext + # Install our pgettext function. + callerFrame.f_globals['pgettext'] = languageHandler.makePgettext(translations) + finally: + del callerFrame # Avoid reference problems with frames (per python docs) +""" +