34 Commits

Author SHA1 Message Date
Storm Dragon 20e61d6259 Merge branch 'testing' 2026-04-04 21:44:47 -04:00
Storm Dragon a750761f61 Steam UI improvements. 2026-04-04 20:22:54 -04:00
Storm Dragon 03b3880622 Merge branch 'testing' 2026-04-04 19:05:35 -04:00
Storm Dragon f4af54228a Tightened up Steam notifications. Fixed a system notifications regression.? 2026-04-04 19:04:45 -04:00
Storm Dragon 518d2b3bb6 Attempt to make the sound system more robust. Improve some web component detection. Fixed a place where Cthulhu gets stuck on netgear's web interface. 2026-04-04 18:31:11 -04:00
Storm Dragon 7043f08dab Simplify GStreamer sound playback 2026-03-23 13:51:42 -04:00
Storm Dragon d0bc7d8a3a I tested these changes for so long I actually forgot what they were, but they seem to work, so committing. 2026-03-23 12:46:27 -04:00
Storm Dragon 438fae4fef Fix script manager resets and isolate startup sound 2026-03-23 12:44:12 -04:00
Storm Dragon a7cd1d033a Speculative fix on preferences weirdness. 2026-03-09 01:14:01 -04:00
Storm Dragon c4890f31af Fix 2026.03.02 release versioning 2026-03-02 17:56:45 -05:00
Storm Dragon c44245189a Merge branch 'testing' 2026-03-02 15:42:15 -05:00
Storm Dragon c7b8e4a30d Clean stale autostart source references from translations 2026-03-02 15:42:04 -05:00
Storm Dragon 20fe1a7e30 Remove autogenerated autostart desktop template 2026-03-02 15:38:29 -05:00
Storm Dragon 25931758f7 Merge branch 'testing'
# Conflicts:
#	distro-packages/Arch-Linux/PKGBUILD
#	meson.build
#	src/cthulhu/cthulhuVersion.py
2026-03-02 15:37:50 -05:00
Storm Dragon daf57ef22c Removed the autogenerated autostart desktop file. Moved the desktop file to contrib so it's still available if people want it. 2026-02-25 23:20:05 -05:00
Storm Dragon 1d2d727fa2 Requested feature added. It is now possible to customize speech used for echo settings including speech module, voice, rate, volume, pitch. 2026-02-22 01:44:19 -05:00
Storm Dragon 11bd7107d2 Attempt to stop plugins from spontaneously re-enabling themselves. 2026-02-19 02:40:46 -05:00
Storm Dragon 95d33047fa Fix prefs state churn and sound-only role speech
Some settings were being reset when tab changed and a key was pressed, e.g. control.

Radio buttons were still announced even with sound only set.

Controls for AI assistant tab were vanishing after being set.
2026-02-18 07:03:12 -05:00
Storm Dragon 523b896053 Couple minor bug fixes, new speech code added in prefs. 2026-02-18 06:10:57 -05:00
Storm Dragon 9152455227 Speech in settings now uses the voice when switched to it. 2026-02-18 06:07:55 -05:00
Storm Dragon c0fdaca4d0 Merge remote-tracking branch 'origin/web-and-punctuation-fixes' into testing 2026-02-17 14:50:01 -05:00
destructatron 8312a842c1 Fix duplicate role name when aria-roledescription is present
When an element has aria-roledescription (e.g. Discord messages use
'Message'), _generateRoleName was adding both the roledescription AND
the standard localized role name (e.g. 'article'). This resulted in
the role being announced twice.

The aria-roledescription attribute is intended to replace the standard
role name, not supplement it. Return early after adding the
roledescription to match Orca's behavior.
2026-02-17 19:21:36 +00:00
destructatron 07138197cb Fix web content editable label, line boundary, and punctuation bugs
Bug 1: Content editable entry label not announced on caret-nav focus
- When navigating into a content editable via caret navigation (e.g. down
  arrow from message list to message entry in Discord), the label and role
  were suppressed because the code treated entering the field the same as
  navigating within it.
- Fixed locus_of_focus_changed in web/script.py to detect when focus enters
  a content editable from outside and generate full object speech.
- Fixed _generateLabelOrName in web/speech_generator.py to preserve the
  label when the prior object was outside the content editable.

Bug 2: Adjacent button included in line contents on first line
- When reading line contents inside a content editable, the layout-mode
  expansion could walk outside the content editable boundary and include
  adjacent UI elements (e.g. a 'More options' button) that happened to
  share the same visual line.
- Fixed _getLineContentsAtOffset in web/script_utilities.py to stop
  expanding at content editable boundaries in both directions.

Bug 3: Punctuation stripped from live regions and AT-SPI announcements
- presentMessage uses resetStyles=True by default, which sets punctuation
  to NONE for system voice messages. This is correct for generated text
  but wrong for user content in live regions and AT-SPI announcements.
- Fixed liveregions.py pumpMessages and default.py onAnnouncement to pass
  resetStyles=False, preserving the user's punctuation settings.
2026-02-17 19:07:41 +00:00
Storm Dragon ed78ffc248 Visual speech monitor added to speech history plugin. Toggle with cthulhu+shift+D 2026-02-17 08:40:51 -05:00
Storm Dragon 4add36f5ca latest changed merged, seems to be reasonably stable. 2026-02-17 08:01:47 -05:00
Storm Dragon 40e63150a6 Read a list of applications that should always be started in sleep mode from sleep.toml. 2026-02-17 07:57:14 -05:00
Storm Dragon 4dba0ec0cd Fixed a bug with sleep mode, was not suspending review keys for sleeping applications. 2026-02-16 14:55:56 -05:00
Storm Dragon e6f780c38b New code tested and seems pretty stable so merged and bumped version. 2026-02-15 12:10:36 -05:00
Storm Dragon 0f7f73a6a0 Fixed flat review bug affecting some sites in chrome. 2026-02-13 13:23:14 -05:00
Storm Dragon aa71d02036 Merge remote-tracking branch 'origin/keyboard-monitoring-api' into testing 2026-02-13 11:19:40 -05:00
destructatron 51ef3de672 Port keyboard monitoring API from Orca, and fix flat review bug in GTK apps 2026-02-13 10:14:04 +00:00
Storm Dragon c6049ef5f3 Preparing for release. 2026-01-26 13:07:33 -05:00
Storm Dragon 78ef51d01f Latest code, hopefully stable. 2026-01-19 05:24:48 -05:00
Storm Dragon 30a40f6974 Merged dbus implementation for plugins. 2026-01-12 11:54:42 -05:00
137 changed files with 4759 additions and 848 deletions
+15
View File
@@ -14,6 +14,16 @@ This repository is a screen reader. Prioritize accessibility, correctness, and s
- `meson compile -C _build`
- `meson install -C _build`
## Runtime target and testing rules
- Make source changes in this repo, not in `~/.local/lib/python*/site-packages/cthulhu/`, unless the user explicitly asks for an installed-package hotfix.
- If you confirm the active import comes from `~/.local/...`, that does **not** mean you should edit there. It means you should update the repo and then run `./build-local.sh` to replace the installed copy for testing.
- Default test/apply workflow for local Cthulhu fixes:
- edit repo files
- run `./build-local.sh`
- reproduce/test against the refreshed `~/.local` install
- If repo and installed behavior differ, prefer rebuilding with `./build-local.sh` over patching the installed package directly.
- Treat direct edits under `~/.local/.../cthulhu/` as an exception path that requires explicit user approval.
## Coding guidelines
- **When modifying existing code:** follow the surrounding codes conventions.
- **When writing new code from scratch:** prefer
@@ -55,3 +65,8 @@ This repository is a screen reader. Prioritize accessibility, correctness, and s
## Meson install reminder (important)
- If you add new Python modules under `src/cthulhu/`, update `src/cthulhu/meson.build` so they get installed (otherwise imports can fail after install).
- If you add a new plugin directory, update `src/cthulhu/plugins/meson.build` and add a `meson.build` in the plugin directory.
## Common Cthulhu agent mistakes
- Checking the import origin, seeing `~/.local/...`, and then editing the installed package instead of the repo.
- Forgetting that `./build-local.sh` is the normal way to apply repo changes into the installed copy for testing.
- Making repo fixes and then diagnosing the old installed copy without rebuilding.
-8
View File
@@ -8,13 +8,11 @@ set -e # Exit on error
# Colors for output (only if stdout is a terminal)
if [[ -t 1 ]]; then
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
else
RED=''
GREEN=''
YELLOW=''
BLUE=''
@@ -56,12 +54,6 @@ if [[ $REPLY =~ ^[Yy]$ ]]; then
# Remove data files
# Remove desktop files
if [[ -f "$HOME/.local/share/applications/cthulhu-autostart.desktop" ]]; then
rm -f "$HOME/.local/share/applications/cthulhu-autostart.desktop"
echo " Removed: ~/.local/share/applications/cthulhu-autostart.desktop"
fi
# Remove icons
for size in 16x16 22x22 24x24 32x32 48x48 256x256 scalable symbolic; do
icon_path="$HOME/.local/share/icons/hicolor/$size/apps"
-11
View File
@@ -1,11 +0,0 @@
[Desktop Entry]
Type=Application
Name=Cthulhu Screen Reader
Exec=cthulhu
NoDisplay=true
# Desktop-neutral autostart - no GNOME-specific conditions
# Users can enable/disable via their desktop environment's accessibility settings
# or by adding/removing this file from ~/.config/autostart/
X-GNOME-AutoRestart=true
Categories=Accessibility;
Keywords=screen;reader;accessibility;speech;braille;
+2 -7
View File
@@ -1,7 +1,7 @@
# Maintainer: Storm Dragon <storm_dragon@stormux.org>
pkgname=cthulhu
pkgver=2026.01.19
pkgver=2026.03.02
pkgrel=1
pkgdesc="Desktop-agnostic screen reader with plugin system, forked from Orca"
url="https://git.stormux.org/storm/cthulhu"
@@ -84,7 +84,7 @@ makedepends=(
)
install=cthulhu.install
source=(
"git+https://git.stormux.org/storm/cthulhu.git"
"git+https://git.stormux.org/storm/cthulhu.git#tag=${pkgver}"
"cthulhu.install"
)
b2sums=(
@@ -92,11 +92,6 @@ b2sums=(
'SKIP'
)
pkgver() {
cd cthulhu
grep "^version = " src/cthulhu/cthulhuVersion.py | sed 's/version = "\(.*\)"/\1/'
}
build() {
cd cthulhu
arch-meson _build
+1 -11
View File
@@ -1,5 +1,5 @@
project('cthulhu',
version: '2026.01.19-testing',
version: '2026.03.02-master',
meson_version: '>= 1.0.0',
)
@@ -93,16 +93,6 @@ else
summary += {'sound support': 'no (missing gstreamer)'}
endif
# Integration with session startup
i18n.merge_file(
input: 'cthulhu-autostart.desktop.in',
output: '@BASENAME@',
type: 'desktop',
po_dir: meson.project_source_root() / 'po',
install: true,
install_dir: get_option('sysconfdir') / 'xdg' / 'autostart',
)
# Update icon cache manually (desktop-neutral) - optional, ignore failures
gtk_update_icon_cache = find_program('gtk4-update-icon-cache', required: false)
if gtk_update_icon_cache.found()
-1
View File
@@ -1,6 +1,5 @@
# List of source files containing translatable strings.
# Please keep this file sorted alphabetically.
cthulhu-autostart.desktop.in
src/cthulhu/braille_rolenames.py
src/cthulhu/brltablenames.py
src/cthulhu/chnames.py
-2
View File
@@ -13,11 +13,9 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-DamnedLies-Scope: partial\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr ""
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr ""
-2
View File
@@ -18,11 +18,9 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 1.6.10\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Lector de pantalla Cthulhu"
#: cthulhu-autostart.desktop.in:11
#, fuzzy
msgid "screen;reader;accessibility;speech;braille;"
msgstr "lector de pantalla; fabla; braille;"
-2
View File
@@ -24,11 +24,9 @@ msgstr ""
"&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n"
"X-Poedit-Language: Arabic\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "قارئ الشاشة Cthulhu"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr "شاشة;قارئ;إمكانية الوصول;كلام;برايل;"
-2
View File
@@ -19,11 +19,9 @@ msgstr ""
"X-Poedit-Language: Asturian\n"
"X-Poedit-SourceCharset: utf-8\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Llector de pantalla d'Cthulhu"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr ""
-2
View File
@@ -17,11 +17,9 @@ msgstr ""
"X-Generator: Poedit 3.3.2\n"
"X-Project-Style: gnome\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Чытач з экрана Cthulhu"
#: cthulhu-autostart.desktop.in:11
#, fuzzy
msgid "screen;reader;accessibility;speech;braille;"
msgstr "чытач з экрана;экран;голас;маўленне;Брайль;"
-2
View File
@@ -23,11 +23,9 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Cthulhu — екранен четец"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr "екран;четец;достъпност;говор;брайл;"
-2
View File
@@ -21,12 +21,10 @@ msgstr ""
"X-Generator: KBabel 1.9.1\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#: cthulhu-autostart.desktop.in:3
#, fuzzy
msgid "Cthulhu Screen Reader"
msgstr "অর্কা স্ক্রীন রিডার / বিবর্ধন"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr ""
-2
View File
@@ -18,12 +18,10 @@ msgstr ""
"X-Generator: KBabel 1.9.1\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: cthulhu-autostart.desktop.in:3
#, fuzzy
msgid "Cthulhu Screen Reader"
msgstr "Cthulhu'র বৈশিষ্ট্য"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr ""
-2
View File
@@ -15,11 +15,9 @@ msgstr ""
"X-Launchpad-Export-Date: 2015-02-05 06:46+0000\n"
"X-Generator: Poedit 1.7.4\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Cthulhu čitač ekrana"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr "čitač ekrana;govor;brailleovo pismo;"
-2
View File
@@ -27,11 +27,9 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 3.3.2\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Lector de pantalla Cthulhu"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr "pantalla;lector;accessibilitat;veu;braille;"
-2
View File
@@ -27,11 +27,9 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Poedit 1.8.11\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Lector de pantalla Cthulhu"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr "lector de pantalla;pronunciació;braille;"
-2
View File
@@ -19,12 +19,10 @@ msgstr ""
"X-Launchpad-Export-Date: 2020-05-04 21:32+0000\n"
"X-Generator: Poedit 2.3\n"
#: cthulhu-autostart.desktop.in:3
#, fuzzy
msgid "Cthulhu Screen Reader"
msgstr "خوێنەری پەردەی ئۆرکا"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr ""
-2
View File
@@ -24,11 +24,9 @@ msgstr ""
"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
"X-Generator: Poedit 3.3.2\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Čtečka obrazovky Cthulhu"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr "obrazovka;čtečka;přístupnost;řeč;braille;"
-2
View File
@@ -17,12 +17,10 @@ msgstr ""
"Plural-Forms: nplurals=4; plural= (n==1) ? 0 : (n==2) ? 1 : (n != 8 && n != "
"11) ? 2 : 3;\n"
#: cthulhu-autostart.desktop.in:3
#, fuzzy
msgid "Cthulhu Screen Reader"
msgstr "Hoffterau Cthulhu"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr ""
-2
View File
@@ -32,11 +32,9 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 3.0.1\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Skærmlæseren Cthulhu"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr "skærm;læser;tilgængelighed;tale;braille;"
-2
View File
@@ -37,11 +37,9 @@ msgstr ""
"X-Generator: Poedit 3.3.2\n"
"X-Project-Style: gnome\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Cthulhu-Bildschirmleser"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr "bildschirm;leser;barrierefreiheit;sprache;braille;"
-2
View File
@@ -20,12 +20,10 @@ msgstr ""
"X-Poedit-SourceCharset: utf-8\n"
"Plural-Forms: nplurals=2; plural=(n!=1)\n"
#: cthulhu-autostart.desktop.in:3
#, fuzzy
msgid "Cthulhu Screen Reader"
msgstr "ཨོར་ཀ་གསལ་གཞི་ལྷག་བྱེད་/ཆེ་ཤེལ་"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr ""
-2
View File
@@ -28,11 +28,9 @@ msgstr ""
"X-Generator: Poedit 2.3\n"
"X-Project-Style: gnome\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Αναγνώστης οθόνης Cthulhu"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr "οθόνη;αναγνώστης;προσβασιμότητα;ομιλία;μπράιγ;"
-2
View File
@@ -17,11 +17,9 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr ""
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr ""
-2
View File
@@ -21,11 +21,9 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Gtranslator 45.3\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Cthulhu Screen Reader"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr "screen reader;speech;braille;"
-2
View File
@@ -21,11 +21,9 @@ msgstr ""
"X-Generator: Poedit 3.2.2\n"
"X-Project-Style: gnome\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Ekranlegilo Orko"
#: cthulhu-autostart.desktop.in:11
#, fuzzy
msgid "screen;reader;accessibility;speech;braille;"
msgstr "ekranlegilo;parolo;brajlo;"
-2
View File
@@ -28,11 +28,9 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"X-Generator: Gtranslator 45.alpha0\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Lector de pantalla Cthulhu"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr "lector;pantalla;accesibilidad;voz;braille;"
-2
View File
@@ -20,12 +20,10 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: cthulhu-autostart.desktop.in:3
#, fuzzy
msgid "Cthulhu Screen Reader"
msgstr "Cthulhu ekraanilugeja ja luup"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr ""
-2
View File
@@ -20,11 +20,9 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Cthulhu pantaila-irakurlea"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr "pantaila;irakurlea;irisgarritasuna;mintzamena;braillea;"
-2
View File
@@ -18,11 +18,9 @@ msgstr ""
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Poedit 3.4\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "صفحه‌خوان اورکا"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr ""
-2
View File
@@ -27,11 +27,9 @@ msgstr ""
"X-Generator: Poedit 3.3.2\n"
"X-Poedit-Bookmarks: 814,-1,-1,-1,-1,-1,-1,-1,-1,-1\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Cthulhu-näytönlukija"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr "näyttö;lukija;saavutettavuus;puhe;pistekirjoitus;"
-2
View File
@@ -31,11 +31,9 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
"X-Generator: Poedit 3.3.2\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Lecteur d’écran Cthulhu"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr "lecteur;écran;accessibilité;parole;braille;"
-2
View File
@@ -18,11 +18,9 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 2.0.3\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr ""
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr ""
-2
View File
@@ -18,12 +18,10 @@ msgstr ""
"Plural-Forms: nplurals=5; plural=n==1 ? 0 : (n%10==1 || n%10==2) ? 1 : "
"(n%10>=3 && n%10<= 6) ? 2 : ((n%10>=7 && n%10<=9) || n==10) ? 3 : 4;\n"
#: cthulhu-autostart.desktop.in:3
#, fuzzy
msgid "Cthulhu Screen Reader"
msgstr "Léitheoir Scáileáin agus Formhéadaitheoir Cthulhu"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr ""
-2
View File
@@ -34,11 +34,9 @@ msgstr ""
"X-DL-Domain: po\n"
"X-DL-State: Translating\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Lector da pantalla Cthulhu"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr "pantalla;lector;accesibilidade;fala;braille;"
-2
View File
@@ -33,12 +33,10 @@ msgstr ""
"\n"
"\n"
#: cthulhu-autostart.desktop.in:3
#, fuzzy
msgid "Cthulhu Screen Reader"
msgstr "Cthulhu સ્ક્રીન વાંચક અને વિસ્તૃતકારક"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr ""
-2
View File
@@ -14,11 +14,9 @@ msgstr ""
"2 : 3);\n"
"X-Generator: Poedit 3.4.1\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "מקריא המסך Cthulhu"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr "מסך;קורא;נגישות;דיבור;ברייל;"
-2
View File
@@ -22,11 +22,9 @@ msgstr ""
"\n"
"X-Generator: Lokalize 1.5\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Cthulhu स्क्रीन वाचक"
#: cthulhu-autostart.desktop.in:11
#, fuzzy
msgid "screen;reader;accessibility;speech;braille;"
msgstr "स्क्रीन रीडर;भाषण;ब्रेल;"
-2
View File
@@ -20,11 +20,9 @@ msgstr ""
"X-Launchpad-Export-Date: 2016-09-15 12:40+0000\n"
"X-Generator: Poedit 1.6.10\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Cthulhu čitač zaslona"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr "čitač zaslona;govor;brajica;"
-2
View File
@@ -22,11 +22,9 @@ msgstr ""
"X-Generator: Poedit 2.3\n"
# Megjegyzés: Mivel mindenhol régóta Gáborral egyeztetve az Cthulhu nevet magyarosan Orkaként írjuk a fordításban, így a fordítást megváltoztattam Orka képernyőolvasóra az Cthulhu képernyőolvasó helyett.
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Orka képernyőolvasó"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr "képernyő;olvasó;akadálymentesítés;beszéd;braille;"
-2
View File
@@ -20,11 +20,9 @@ msgstr ""
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Poedit 3.3.2\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Pembaca Layar Cthulhu"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr "layar;pembaca;aksesibilitas;ucapan;braille;"
-2
View File
@@ -17,11 +17,9 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Lokalize 1.5\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Cthulhu skjálestur"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr ""
-2
View File
@@ -39,11 +39,9 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"X-Generator: Gtranslator 42.0\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Lettore schermo Cthulhu"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr "lettore;schermo;accessibilità;voce;braille;"
-2
View File
@@ -25,11 +25,9 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Cthulhu スクリーンリーダー"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr "スクリーン;リーダー;アクセシビリティ;音声;点字;"
-2
View File
@@ -18,11 +18,9 @@ msgstr ""
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Poedit 3.3.2\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Cthulhu - ეკრანის მკითხველი"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr "ეკრანი;მკითხველი;ხელმისაწვდომობა;მეტყველება;ბრაილი;"
-2
View File
@@ -18,11 +18,9 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
"X-Generator: Poedit 3.0\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr ""
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr ""
-2
View File
@@ -18,11 +18,9 @@ msgstr ""
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Poedit 3.3.2\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Cthulhu экраннан оқитын қолданбасы"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr ""
-2
View File
@@ -18,12 +18,10 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: KBabel 1.11.4\n"
#: cthulhu-autostart.desktop.in:3
#, fuzzy
msgid "Cthulhu Screen Reader"
msgstr "ಆದ್ಯತೆಗಳು"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr ""
-2
View File
@@ -15,11 +15,9 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Cthulhu 화면 낭독기"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr "화면;리더;접근성;음성;점자;"
-2
View File
@@ -25,11 +25,9 @@ msgstr ""
"(n%100<10 || n%100>=20) ? 1 : 2);\n"
"X-Generator: Poedit 3.3.2\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Cthulhu ekrano skaityklė"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr "ekrano skaityklė;kalba;brailio raštas;"
-2
View File
@@ -23,11 +23,9 @@ msgstr ""
"2);\n"
"X-Generator: Lokalize 21.12.3\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Cthulhu ekrāna lasītājs"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr "ekrāna lasītājs;runa;Brails;"
-2
View File
@@ -19,12 +19,10 @@ msgstr ""
"\n"
"\n"
#: cthulhu-autostart.desktop.in:3
#, fuzzy
msgid "Cthulhu Screen Reader"
msgstr "Cthulhu स्क्रीन पाठक / आवर्द्धक"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr ""
-2
View File
@@ -18,11 +18,9 @@ msgstr ""
"Plural-Forms: nplurals=2; plural= n==1 || n%10==1 ? 0 : 1\n"
"X-Generator: KBabel 1.11.4\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Cthulhu читач на екранот"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr ""
-2
View File
@@ -22,12 +22,10 @@ msgstr ""
"X-Generator: Poedit 2.0.1\n"
"X-Project-Style: gnome\n"
#: cthulhu-autostart.desktop.in:3
#, fuzzy
msgid "Cthulhu Screen Reader"
msgstr "സ്ക്രീന്‍ വായന അവസാനിപ്പിക്കാം."
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr ""
-2
View File
@@ -18,12 +18,10 @@ msgstr ""
"X-Generator: KBabel 1.9.1\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: cthulhu-autostart.desktop.in:3
#, fuzzy
msgid "Cthulhu Screen Reader"
msgstr "वर्धक भिंग"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr ""
-2
View File
@@ -18,11 +18,9 @@ msgstr ""
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Poedit 2.0.6\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Pembaca skrin Cthulhu"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr "skrin;pembaca;kebolehcapaian;pertuturan;braille;"
-2
View File
@@ -19,11 +19,9 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Cthulhu skjermleser"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr "skjerm;leser;tilgjengelighet;tale;blindeskrift;"
-2
View File
@@ -13,11 +13,9 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 3.0.1\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "ओर्का दृष्टि वाचक"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr "दृष्टि वाचक; वाचन;ब्रेल;"
-2
View File
@@ -31,11 +31,9 @@ msgstr ""
"X-Generator: Poedit 3.2.2\n"
# vergrootglas/loep
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Cthulhu schermlezer"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr "scherm;lezer;toegankelijkheid;spraak;braille;"
-2
View File
@@ -18,12 +18,10 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: cthulhu-autostart.desktop.in:3
#, fuzzy
msgid "Cthulhu Screen Reader"
msgstr "Cthulhu skjermlesar og forstørringsglas"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr ""
-2
View File
@@ -23,11 +23,9 @@ msgstr ""
"X-Launchpad-Export-Date: 2015-05-21 18:05+0000\n"
"X-Project-Style: gnome\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Lector d'ecran Cthulhu"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr ""
"lector d'ecran;oral;braille;votz;sintèsi vocala;sintesi vocala;lector "
-2
View File
@@ -22,12 +22,10 @@ msgstr ""
"\n"
"\n"
#: cthulhu-autostart.desktop.in:3
#, fuzzy
msgid "Cthulhu Screen Reader"
msgstr "ଓର୍କା ସ୍କ୍ରିନ ପାଠକ /ଛୋଟକୁ ବଡ ଦେଖଉଥିବା ଉପକରଣ "
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr ""
-2
View File
@@ -24,11 +24,9 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Poedit-Bookmarks: 589,-1,-1,-1,-1,-1,-1,-1,-1,-1\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "ਓਰਕਾ ਸਕਰੀਨ ਰੀਡਰ"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr ""
-2
View File
@@ -24,11 +24,9 @@ msgstr ""
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
"|| n%100>=20) ? 1 : 2);\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Czytnik ekranowy Cthulhu"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr "czytnik;ekran;dostępność;mowa;braille;"
-2
View File
@@ -28,11 +28,9 @@ msgstr ""
"X-Source-Language: C\n"
"X-Project-Style: gnome\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Leitor de ecrã Cthulhu"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr "leitor;ecrã;acessibilidade;voz;braille;"
-2
View File
@@ -49,11 +49,9 @@ msgstr ""
"X-DL-Domain: po\n"
"X-DL-State: Translating\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Leitor de tela Cthulhu"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr "leitor;tela;acessibilidade;voz;braille;"
-2
View File
@@ -24,11 +24,9 @@ msgstr ""
"X-Project-Style: gnome\n"
"X-Poedit-SourceCharset: UTF-8\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Cititorul de ecran Cthulhu"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr "ecran;cititor;accesibilitate;vorbire;braille;"
-2
View File
@@ -22,11 +22,9 @@ msgstr ""
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
"X-Generator: Poedit 3.3.2\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Экранный диктор Cthulhu"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr "экран;чтец;доступность;речь;брайль;"
-2
View File
@@ -24,11 +24,9 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr ""
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr ""
-2
View File
@@ -18,11 +18,9 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: KBabel 1.11.4\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr ""
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr ""
-2
View File
@@ -19,11 +19,9 @@ msgstr ""
"Plural-Forms: nplurals=3; plural=(n==1) ? 1 : (n>=2 && n<=4) ? 2 : 0;\n"
"X-Generator: Gtranslator 2.91.7\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Ukončí čítačku obrazovky"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr ""
-2
View File
@@ -22,11 +22,9 @@ msgstr ""
"X-Poedit-SourceCharset: utf-8\n"
"X-Generator: Poedit 3.0.1\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Zaslonski bralnik Orka"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr "govor;zaslonski bralnik;Braillova pisava;brajica;povečevalnik"
-2
View File
@@ -17,12 +17,10 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: cthulhu-autostart.desktop.in:3
#, fuzzy
msgid "Cthulhu Screen Reader"
msgstr "Ekrani Zmadhues (Lupë)"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr ""
-2
View File
@@ -21,11 +21,9 @@ msgstr ""
"X-Project-Style: gnome\n"
"X-Generator: Poedit 3.1.1\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Читач екрана Орка"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr "читач екрана;говор;брајева азбука;"
-2
View File
@@ -20,11 +20,9 @@ msgstr ""
"X-Project-Style: gnome\n"
"X-Generator: Poedit 2.0.3\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Čitač ekrana Orka"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr "čitač ekrana;govor;brajeva azbuka;"
-2
View File
@@ -20,11 +20,9 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 3.3.2\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Cthulhu skärmläsare"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr "skärm;läsare;tillgänglighet;tal;punktskrift;"
-2
View File
@@ -24,11 +24,9 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n!=1);\\n\n"
"\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "ஆர்கா திரை படிப்பி "
#: cthulhu-autostart.desktop.in:11
#, fuzzy
msgid "screen;reader;accessibility;speech;braille;"
msgstr "திரைபடிப்பான்;பேசுதல்;ப்ரெய்ல்;"
-2
View File
@@ -23,11 +23,9 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n!=1);\n"
"X-Generator: KBabel 1.11.4\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "ఓర్కా తెరచదువరి"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr ""
-2
View File
@@ -18,11 +18,9 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 1.6.3\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Хонандаи экрани Cthulhu"
#: cthulhu-autostart.desktop.in:11
#, fuzzy
msgid "screen;reader;accessibility;speech;braille;"
msgstr "хонандаи экран;нутқ;брайл;"
-2
View File
@@ -19,11 +19,9 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "โปรแกรมอ่านหน้าจอ Cthulhu"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr "หน้าจอ;อ่าน;การเข้าถึง;เสียงพูด;อักษรเบรลล์;"
-2
View File
@@ -27,11 +27,9 @@ msgstr ""
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Poedit 3.4\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Cthulhu Ekran Okuyucu"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr "ekran;okuyucu;erişilebilirlik;konuşma;braille;"
-2
View File
@@ -20,11 +20,9 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Cthulhu ئېكران ئوقۇغۇ"
#: cthulhu-autostart.desktop.in:11
#, fuzzy
msgid "screen;reader;accessibility;speech;braille;"
msgstr "screen reader;speech;braille;ئېكران ئوقۇغۇچ;تاۋۇش;ئەمالار ئېلىپبەسى;"
-2
View File
@@ -26,11 +26,9 @@ msgstr ""
"n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
"X-Generator: Lokalize 20.12.0\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Інструмент читання з екрана «Cthulhu»"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr "екран;читач;доступність;мовлення;брайль;"
-2
View File
@@ -18,11 +18,9 @@ msgstr ""
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: LocFactoryEditor 1.8\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Trình đọc màn hình Cthulhu"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr "màn hình;đọc;trợ năng;giọng nói;chữ nổi;"
-2
View File
@@ -29,11 +29,9 @@ msgstr ""
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Poedit 3.3.2\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Cthulhu 屏幕阅读器"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr "屏幕;阅读器;无障碍;语音;盲文;"
-2
View File
@@ -20,11 +20,9 @@ msgstr ""
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Poedit 1.6.5\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Cthulhu 螢幕閱讀器"
#: cthulhu-autostart.desktop.in:11
#, fuzzy
msgid "screen;reader;accessibility;speech;braille;"
msgstr "screen reader;speech;braille;螢幕閱讀器;語音;點字;"
-2
View File
@@ -20,11 +20,9 @@ msgstr ""
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Poedit 1.8.12\n"
#: cthulhu-autostart.desktop.in:3
msgid "Cthulhu Screen Reader"
msgstr "Cthulhu 螢幕閱讀器"
#: cthulhu-autostart.desktop.in:11
msgid "screen;reader;accessibility;speech;braille;"
msgstr "螢幕;閱讀器;無障礙;語音;點字;"
+37 -2
View File
@@ -204,10 +204,21 @@ class AXHypertext:
@staticmethod
def find_child_at_offset(obj: Atspi.Accessible, offset: int) -> Optional[Atspi.Accessible]:
"""Attempts to correct for off-by-one brokenness in implementations"""
"""Returns the child at offset, correcting for broken hypertext offset mappings."""
if child := AXHypertext.get_child_at_offset(obj, offset):
return child
offset_in_parent = AXHypertext.get_character_offset_in_parent(child)
if offset_in_parent == offset:
return child
tokens = [
f"AXHypertext: Child at offset {offset} in",
obj,
"is",
child,
f"but reports offset {offset_in_parent}",
]
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
if child_before := AXHypertext.get_child_at_offset(obj, offset - 1):
offset_in_parent = AXHypertext.get_character_offset_in_parent(child_before)
@@ -225,6 +236,30 @@ class AXHypertext:
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
return child_after
for i in range(AXHypertext._get_link_count(obj)):
link = AXHypertext._get_link_at_index(obj, i)
if link is None or AXHypertext.get_link_start_offset(link) != offset:
continue
try:
child = Atspi.Hyperlink.get_object(link, 0)
except GLib.GError as error:
msg = f"AXHypertext: Exception in find_child_at_offset: {error}"
debug.print_message(debug.LEVEL_INFO, msg, True)
continue
if child is None:
continue
tokens = [
f"AXHypertext: Child at offset {offset} in",
obj,
"found via link enumeration:",
child,
]
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
return child
return None
@staticmethod
+48 -2
View File
@@ -85,12 +85,16 @@ class Backend:
with open(fileName, 'w', encoding='utf-8') as settingsFile:
settingsFile.write(dumps(prefsDoc))
def _updateTable(self, targetTable, newValues):
def _updateTable(self, targetTable, newValues, preserveMissingKeys=None):
if not isinstance(newValues, dict):
return
preserveMissingKeys = set(preserveMissingKeys or [])
for key in list(targetTable.keys()):
if key not in newValues:
if key in preserveMissingKeys:
continue
del targetTable[key]
continue
newValue = newValues[key]
@@ -173,7 +177,12 @@ class Backend:
profiles[profile] = {}
profileTable = profiles[profile]
self._updateTable(profileTable, general)
# Keep plugin persistence keys when callers provide partial updates.
self._updateTable(
profileTable,
general,
preserveMissingKeys={"activePlugins", "pluginSources"},
)
self._writeDocument(self.settingsFile, prefsDoc)
def _getSettings(self):
@@ -213,12 +222,33 @@ class Backend:
return settingsDict
def _hasLikelyCustomEchoVoice(self, echoVoice, defaultVoice):
"""Return True when echoVoice appears intentionally customized."""
if not echoVoice:
return False
echoEstablished = bool(echoVoice.get('established', False))
if not defaultVoice:
return echoEstablished or bool(echoVoice.get(acss.ACSS.FAMILY))
for key in [acss.ACSS.RATE, acss.ACSS.AVERAGE_PITCH, acss.ACSS.GAIN, acss.ACSS.FAMILY]:
echoValue = echoVoice.get(key)
defaultValue = defaultVoice.get(key)
if echoValue is not None and echoValue != defaultValue:
return True
return echoEstablished and bool(echoVoice.get(acss.ACSS.FAMILY))
def getGeneral(self, profile=None):
""" Get general settings from default settings and
override with profile values. """
self._getSettings()
generalSettings = self.general.copy()
generalSettings = self._migrateSettings(generalSettings)
# Plugin state is profile-scoped; ignore legacy/global values.
generalSettings.pop('activePlugins', None)
generalSettings.pop('pluginSources', None)
defaultProfile = generalSettings.get('startingProfile',
['Default', 'default'])
if profile is None:
@@ -229,8 +259,24 @@ class Backend:
if key == 'voices':
for voiceType, voiceDef in value.items():
value[voiceType] = acss.ACSS(voiceDef)
if key == 'echoVoice' and isinstance(value, dict):
value = acss.ACSS(value)
if key not in ['startingProfile', 'activeProfile']:
generalSettings[key] = value
# Backward compatibility: recover custom echo behavior when legacy
# profiles have echoVoice but are missing custom-echo toggle keys.
if 'useCustomEchoVoice' not in profileSettings:
voices = generalSettings.get('voices') or {}
defaultVoice = voices.get(settings.DEFAULT_VOICE)
echoVoice = generalSettings.get('echoVoice')
if self._hasLikelyCustomEchoVoice(echoVoice, defaultVoice):
generalSettings['useCustomEchoVoice'] = True
if 'useCustomEchoForKey' not in profileSettings:
generalSettings['useCustomEchoForKey'] = True
if 'useCustomEchoForCharacter' not in profileSettings:
generalSettings['useCustomEchoForCharacter'] = True
try:
generalSettings['activeProfile'] = profileSettings['profile']
except KeyError:
+360 -8
View File
@@ -136,6 +136,24 @@
<property name="step_increment">0.10000000149</property>
<property name="page_increment">1</property>
</object>
<object class="GtkAdjustment" id="echoPitchAdjustment">
<property name="upper">10</property>
<property name="value">5</property>
<property name="step_increment">0.10000000149</property>
<property name="page_increment">1</property>
</object>
<object class="GtkAdjustment" id="echoRateAdjustment">
<property name="upper">100</property>
<property name="value">50</property>
<property name="step_increment">1</property>
<property name="page_increment">10</property>
</object>
<object class="GtkAdjustment" id="echoVolumeAdjustment">
<property name="upper">10</property>
<property name="value">10</property>
<property name="step_increment">0.10000000149</property>
<property name="page_increment">1</property>
</object>
<object class="GtkDialog" id="cthulhuSetupWindow">
<property name="can_focus">False</property>
<property name="title" translatable="yes">Cthulhu Preferences</property>
@@ -553,7 +571,7 @@
<property name="use_underline">True</property>
<property name="mnemonic_widget">timeFormatCombo</property>
<accessibility>
<relation type="label-for" target="availableProfilesComboBox1"/>
<relation type="label-for" target="timeFormatCombo"/>
</accessibility>
</object>
<packing>
@@ -570,7 +588,7 @@
<property name="use_underline">True</property>
<property name="mnemonic_widget">dateFormatCombo</property>
<accessibility>
<relation type="label-for" target="availableProfilesComboBox2"/>
<relation type="label-for" target="dateFormatCombo"/>
</accessibility>
</object>
<packing>
@@ -1035,6 +1053,51 @@
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<child>
<object class="GtkBox" id="soundSinkHBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">12</property>
<child>
<object class="GtkLabel" id="soundSinkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="label" translatable="yes" comments="Translators: This is the label for a combo box where users can choose which audio backend Cthulhu should use.">Audio _backend:</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">soundSinkCombo</property>
<accessibility>
<relation type="label-for" target="soundSinkCombo"/>
</accessibility>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkComboBoxText" id="soundSinkCombo">
<property name="visible">True</property>
<property name="can_focus">False</property>
<signal name="changed" handler="soundSinkComboChanged" swapped="no"/>
<accessibility>
<relation type="labelled-by" target="soundSinkLabel"/>
</accessibility>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="soundThemeHBox">
<property name="visible">True</property>
@@ -1077,7 +1140,7 @@
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
<property name="position">1</property>
</packing>
</child>
<child>
@@ -1122,7 +1185,7 @@
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
<property name="position">2</property>
</packing>
</child>
</object>
@@ -1133,7 +1196,7 @@
<object class="GtkLabel" id="soundThemeTitleLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes" comments="Translators: This is the title of a section in the preferences dialog containing sound theme options.">Sound Theme</property>
<property name="label" translatable="yes" comments="Translators: This is the title of a section in the preferences dialog containing sound options.">Sound</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
@@ -3037,6 +3100,295 @@
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkFrame" id="echoVoiceFrame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0</property>
<property name="shadow_type">none</property>
<child>
<object class="GtkAlignment" id="echoVoiceAlignment">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="left_padding">12</property>
<child>
<object class="GtkGrid" id="echoVoiceGrid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="row_spacing">10</property>
<property name="column_spacing">8</property>
<child>
<object class="GtkCheckButton" id="useCustomEchoVoiceCheckButton">
<property name="label" translatable="yes">Use c_ustom echo voice settings</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="useCustomEchoVoiceToggled" swapped="no"/>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
<property name="width">2</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="useCustomEchoSpeechServerCheckButton">
<property name="label" translatable="yes">Use custom _speech-dispatcher module for echo</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="useCustomEchoSpeechServerToggled" swapped="no"/>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
<property name="width">2</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="echoSpeechServersLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">1</property>
<property name="label" translatable="yes">Echo _module:</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">echoSpeechServers</property>
<accessibility>
<relation type="label-for" target="echoSpeechServers"/>
</accessibility>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<object class="GtkComboBox" id="echoSpeechServers">
<property name="visible">True</property>
<property name="can_focus">False</property>
<signal name="changed" handler="echoSpeechServersChanged" swapped="no"/>
<accessibility>
<relation type="labelled-by" target="echoSpeechServersLabel"/>
</accessibility>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="echoSpeechFamiliesLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">1</property>
<property name="label" translatable="yes">Echo _voice:</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">echoSpeechFamilies</property>
<accessibility>
<relation type="label-for" target="echoSpeechFamilies"/>
</accessibility>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">3</property>
</packing>
</child>
<child>
<object class="GtkComboBox" id="echoSpeechFamilies">
<property name="visible">True</property>
<property name="can_focus">False</property>
<signal name="changed" handler="echoSpeechFamiliesChanged" swapped="no"/>
<accessibility>
<relation type="labelled-by" target="echoSpeechFamiliesLabel"/>
</accessibility>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">3</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="echoRateLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">1</property>
<property name="label" translatable="yes">Echo ra_te:</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">echoRateScale</property>
<accessibility>
<relation type="label-for" target="echoRateScale"/>
</accessibility>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">4</property>
</packing>
</child>
<child>
<object class="GtkScale" id="echoRateScale">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="adjustment">echoRateAdjustment</property>
<property name="round_digits">0</property>
<property name="digits">0</property>
<property name="value_pos">right</property>
<signal name="value-changed" handler="echoRateValueChanged" swapped="no"/>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">4</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="echoPitchLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">1</property>
<property name="label" translatable="yes">Echo pi_tch:</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">echoPitchScale</property>
<accessibility>
<relation type="label-for" target="echoPitchScale"/>
</accessibility>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">5</property>
</packing>
</child>
<child>
<object class="GtkScale" id="echoPitchScale">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="adjustment">echoPitchAdjustment</property>
<property name="round_digits">1</property>
<property name="value_pos">right</property>
<signal name="value-changed" handler="echoPitchValueChanged" swapped="no"/>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">5</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="echoVolumeLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">1</property>
<property name="label" translatable="yes">Echo vo_lume:</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">echoVolumeScale</property>
<accessibility>
<relation type="label-for" target="echoVolumeScale"/>
</accessibility>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">6</property>
</packing>
</child>
<child>
<object class="GtkScale" id="echoVolumeScale">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="adjustment">echoVolumeAdjustment</property>
<property name="round_digits">1</property>
<property name="value_pos">right</property>
<signal name="value-changed" handler="echoVolumeValueChanged" swapped="no"/>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">6</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="useCustomEchoForKeyCheckButton">
<property name="label" translatable="yes">Use custom voice for _key echo</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="checkButtonToggled" swapped="no"/>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">7</property>
<property name="width">2</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="useCustomEchoForCharacterCheckButton">
<property name="label" translatable="yes">Use custom voice for _character echo</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="checkButtonToggled" swapped="no"/>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">8</property>
<property name="width">2</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="useCustomEchoForWordCheckButton">
<property name="label" translatable="yes">Use custom voice for _word echo</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="checkButtonToggled" swapped="no"/>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">9</property>
<property name="width">2</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="useCustomEchoForSentenceCheckButton">
<property name="label" translatable="yes">Use custom voice for _sentence echo</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="checkButtonToggled" swapped="no"/>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">10</property>
<property name="width">2</property>
</packing>
</child>
</object>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel" id="echoVoiceLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Echo Voice</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
</child>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="enableEchoByCharacterCheckButton">
<property name="label" translatable="yes" comments="Translators: When this option is enabled, inserted text of length 1 is spoken.">Enable echo by cha_racter</property>
@@ -3049,7 +3401,7 @@
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">2</property>
<property name="top_attach">3</property>
</packing>
</child>
<child>
@@ -3064,7 +3416,7 @@
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">3</property>
<property name="top_attach">4</property>
</packing>
</child>
<child>
@@ -3079,7 +3431,7 @@
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">4</property>
<property name="top_attach">5</property>
</packing>
</child>
</object>
+10 -2
View File
@@ -769,7 +769,7 @@ def shutdown(script: Optional[Any] = None, inputEvent: Optional[Any] = None) ->
cthulhu_state.activeScript.presentationInterrupt()
cthulhuApp.getSignalManager().emitSignal('stop-application-completed')
sound_theme_manager.getManager().playStopSound(wait=True)
sound_theme_manager.getManager().playStopSound(wait=True, timeoutSeconds=1)
cthulhuApp.getPluginSystemManager().unloadAllPlugins(ForceAllPlugins=True)
# Deactivate the event manager first so that it clears its queue and will not
@@ -893,7 +893,14 @@ def main() -> int:
debug.printMessage(debug.LEVEL_INFO, "CTHULHU: Initialized.", True)
script = cthulhu_state.activeScript
sound_theme_manager.getManager().playStartSound(wait=True)
sound_theme_manager.getManager().playStartSound(wait=False)
soundFailureReason = sound.getSoundSystemFailureReason()
if soundFailureReason:
debug.printMessage(
debug.LEVEL_INFO,
f"CTHULHU: Startup sound failed. Continuing without sound. Reason: {soundFailureReason}",
True
)
cthulhuApp.getSignalManager().emitSignal('start-application-completed')
if script:
window = script.utilities.activeWindow()
@@ -944,6 +951,7 @@ class Cthulhu(GObject.Object):
self.settingsManager: SettingsManager = settings_manager.SettingsManager(self) # Directly instantiate
self.eventManager: EventManager = event_manager.EventManager(self) # Directly instantiate
self.scriptManager: ScriptManager = script_manager.ScriptManager(self) # Directly instantiate
script_manager._manager = self.scriptManager
self.logger: logger.Logger = logger.Logger() # Directly instantiate
self.signalManager: SignalManager = signal_manager.SignalManager(self)
self.dynamicApiManager: DynamicApiManager = dynamic_api_manager.DynamicApiManager(self)
+2 -2
View File
@@ -23,5 +23,5 @@
# Forked from Orca screen reader.
# Cthulhu project: https://git.stormux.org/storm/cthulhu
version = "2026.01.19"
codeName = "testing"
version = "2026.03.02"
codeName = "master"
+756 -21
View File
@@ -38,6 +38,9 @@ gi.require_version("Gtk", "3.0")
from gi.repository import Atspi
import os
import json
import subprocess
import sys
import threading
from gi.repository import Gdk
from gi.repository import GLib
@@ -57,6 +60,7 @@ from . import cthulhu_gui_profile
from . import cthulhu_state
from . import settings
from . import settings_manager
from . import sound_sink
from . import input_event
from . import input_event_manager
from . import keybindings
@@ -68,6 +72,7 @@ from . import text_attribute_names
from . import sound_theme_manager
from . import script_manager
from .ax_object import AXObject
from .ax_utilities import AXUtilities
_settingsManager = None # Removed - use cthulhu.cthulhuApp.settingsManager
@@ -173,6 +178,16 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
self.speechSystemsChoice = None
self.speechSystemsChoices = None
self.speechSystemsModel = None
self.echoVoice = None
self.echoSpeechFamiliesChoice = None
self.echoSpeechFamiliesChoices = None
self.echoSpeechFamiliesModel = None
self.echoSpeechServersChoice = None
self.echoSpeechServersChoices = None
self.echoSpeechServersModel = None
self.initializingEchoSpeech = False
self._echoVoiceFetchToken = 0
self._updatingEchoSpeechFamilies = False
self.systemVoice = None
self.uppercaseVoice = None
self.window = None
@@ -181,13 +196,16 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
self.savedPitch = None
self.savedRate = None
self.soundThemeCombo = None
self.soundSinkCombo = None
self.roleSoundPresentationCombo = None
self._isInitialSetup = False
self._updatingSpeechFamilies = False
self.selectedFamilyChoices = {}
self.selectedLanguageChoices = {}
self.profilesCombo = None
self.profilesComboModel = None
self.startingProfileCombo = None
self._initialFocusSyncAttempts = 0
self._capturedKey = []
self.script = None
@@ -385,7 +403,12 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
self._initComboBox(self.get_widget("speechLanguages"))
self.speechFamiliesModel = \
self._initComboBox(self.get_widget("speechFamilies"))
self.echoSpeechServersModel = \
self._initComboBox(self.get_widget("echoSpeechServers"))
self.echoSpeechFamiliesModel = \
self._initComboBox(self.get_widget("echoSpeechFamilies"))
self._initSpeechState()
self._initEchoSpeechState()
# TODO - JD: Will this ever be the case??
self._isInitialSetup = \
@@ -1271,7 +1294,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
for familyChoice in self.speechFamiliesChoices:
name = familyChoice[speechserver.VoiceFamily.NAME]
if name == familyName:
self.get_widget("speechFamilies").set_active(i)
self._setSpeechFamiliesActive(i)
self.speechFamiliesChoice = self.speechFamiliesChoices[i]
familySet = True
break
@@ -1295,13 +1318,22 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
if not familySet:
tokens = ["PREFERENCES DIALOG: Could not find speech family match for", familyName]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
self.get_widget("speechFamilies").set_active(0)
self._setSpeechFamiliesActive(0)
self.speechFamiliesChoice = self.speechFamiliesChoices[0]
if familySet:
self.selectedFamilyChoices[self.speechServersChoice,
self.speechLanguagesChoice] = i
def _setSpeechFamiliesActive(self, index):
"""Sets speechFamilies active index while suppressing preview chatter."""
self._updatingSpeechFamilies = True
try:
self.get_widget("speechFamilies").set_active(index)
finally:
self._updatingSpeechFamilies = False
def _setupFamilies(self):
"""Gets the list of voice variants for the current speech server and
current language.
@@ -1353,7 +1385,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
selectedIndex = self.selectedFamilyChoices[self.speechServersChoice,
self.speechLanguagesChoice]
self.get_widget("speechFamilies").set_active(selectedIndex)
self._setSpeechFamiliesActive(selectedIndex)
def _setSpeechLanguagesChoice(self, languageName):
"""Sets the active item in the languages ("Language:") combo box
@@ -1478,6 +1510,16 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
#
if not serverInfo:
serverInfo = speech.getInfo()
if serverInfo and len(serverInfo) >= 2 and serverInfo[1] == 'default':
defaultFamily = None
voices = self.prefsDict.get("voices", {})
defaultVoice = voices.get(settings.DEFAULT_VOICE) if voices else None
if defaultVoice:
defaultFamily = acss.ACSS(defaultVoice).get(acss.ACSS.FAMILY)
resolved = self._resolveSpeechDispatcherServerForFamily(defaultFamily)
if resolved is not None:
serverInfo = resolved.getInfo()
valueSet = False
i = 0
@@ -1661,6 +1703,390 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
self._setupSpeechSystems(factories)
self.initializingSpeech = False
def _getSpeechDispatcherFactory(self):
"""Returns the Speech Dispatcher factory if available."""
factories = cthulhu.cthulhuApp.settingsManager.getSpeechServerFactories()
for factory in factories:
try:
if factory.SpeechServer.getFactoryName() == guilabels.SPEECH_DISPATCHER:
return factory
except Exception:
debug.printException(debug.LEVEL_FINEST)
return None
def _setEchoVoiceFamily(self, family):
"""Sets echo voice family from the selected server family object."""
if not family:
return
name = family.get(speechserver.VoiceFamily.NAME)
language = family.get(speechserver.VoiceFamily.LANG)
dialect = family.get(speechserver.VoiceFamily.DIALECT)
variant = family.get(speechserver.VoiceFamily.VARIANT)
self.echoVoice[acss.ACSS.FAMILY] = {
speechserver.VoiceFamily.NAME: name,
speechserver.VoiceFamily.LANG: language,
speechserver.VoiceFamily.DIALECT: dialect,
speechserver.VoiceFamily.VARIANT: variant,
}
self.echoVoice['established'] = True
def _resolveSpeechDispatcherServerForFamily(self, family):
"""Returns a concrete Speech Dispatcher server matching the voice family."""
if not family or not self.speechServersChoices:
return None
name = family.get(speechserver.VoiceFamily.NAME)
language = family.get(speechserver.VoiceFamily.LANG)
dialect = family.get(speechserver.VoiceFamily.DIALECT)
variant = family.get(speechserver.VoiceFamily.VARIANT)
if not name:
return None
for server in self.speechServersChoices:
info = server.getInfo()
if not info or len(info) < 2 or info[1] == 'default':
continue
try:
families = server.getVoiceFamilies()
except Exception:
debug.printException(debug.LEVEL_FINEST)
continue
for candidate in families:
if candidate.get(speechserver.VoiceFamily.NAME) != name:
continue
if candidate.get(speechserver.VoiceFamily.LANG) != language:
continue
if candidate.get(speechserver.VoiceFamily.DIALECT) != dialect:
continue
if candidate.get(speechserver.VoiceFamily.VARIANT) != variant:
continue
tokens = [
"PREFERENCES DIALOG: Resolved voice family",
name,
"to speech server",
info,
]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return server
tokens = [
"PREFERENCES DIALOG: Could not resolve speech server for family",
family,
]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return None
def _getSpeechServerChoiceForSave(self):
"""Returns the server choice that should be persisted for speech settings."""
server = self.speechServersChoice
if not server or self.speechSystemsChoice != self._getSpeechDispatcherFactory():
return server
info = server.getInfo()
if not info or len(info) < 2 or info[1] != 'default':
return server
defaultFamily = None
if self.defaultVoice is not None:
defaultFamily = self.defaultVoice.get(acss.ACSS.FAMILY)
resolved = self._resolveSpeechDispatcherServerForFamily(defaultFamily)
return resolved or server
def _getEchoSpeechServerFamilyForSave(self):
"""Returns the most specific echo family to use for server resolution."""
family = None
if self.echoVoice is not None:
family = self.echoVoice.get(acss.ACSS.FAMILY)
if family:
return family
if self.defaultVoice is not None:
return self.defaultVoice.get(acss.ACSS.FAMILY)
return None
def _getEchoSpeechServerChoiceForSave(self):
"""Returns the echo speech server choice that should be persisted."""
server = self.echoSpeechServersChoice
if server is None:
return None
info = server.getInfo()
if not info or len(info) < 2 or info[1] != 'default':
return server
family = self._getEchoSpeechServerFamilyForSave()
resolved = self._resolveSpeechDispatcherServerForFamily(family)
if resolved is not None:
return resolved
speechServer = self._getSpeechServerChoiceForSave()
if speechServer is not None:
speechInfo = speechServer.getInfo()
if speechInfo and len(speechInfo) >= 2 and speechInfo[1] != 'default':
tokens = [
"PREFERENCES DIALOG: Falling back to main speech server for echo",
speechInfo,
]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return speechServer
return server
def _populateEchoSpeechFamilies(self, families):
"""Populate the echo family combobox from the provided families list."""
combobox = self.get_widget("echoSpeechFamilies")
combobox.set_model(None)
self.echoSpeechFamiliesModel.clear()
self.echoSpeechFamiliesChoices = list(families or [])
self.echoSpeechFamiliesChoice = None
selectedIndex = 0
selectedMatchFound = False
selectedFamily = self.echoVoice.get(acss.ACSS.FAMILY)
selectedName = selectedLanguage = selectedDialect = selectedVariant = None
if selectedFamily:
selectedName = selectedFamily.get(speechserver.VoiceFamily.NAME)
selectedLanguage = selectedFamily.get(speechserver.VoiceFamily.LANG)
selectedDialect = selectedFamily.get(speechserver.VoiceFamily.DIALECT)
selectedVariant = selectedFamily.get(speechserver.VoiceFamily.VARIANT)
for i, family in enumerate(self.echoSpeechFamiliesChoices):
name = family.get(speechserver.VoiceFamily.NAME) or ""
language = family.get(speechserver.VoiceFamily.LANG) or ""
dialect = family.get(speechserver.VoiceFamily.DIALECT) or ""
variant = family.get(speechserver.VoiceFamily.VARIANT)
display = name
locale = language
if dialect:
locale = f"{language}-{dialect}" if language else dialect
if locale:
display = f"{name} ({locale})"
self.echoSpeechFamiliesModel.append((i, display))
if selectedName == name and selectedLanguage == language \
and selectedDialect == dialect and selectedVariant == variant:
selectedIndex = i
selectedMatchFound = True
combobox.set_model(self.echoSpeechFamiliesModel)
if self.echoSpeechFamiliesChoices:
self._updatingEchoSpeechFamilies = True
try:
combobox.set_active(selectedIndex)
finally:
self._updatingEchoSpeechFamilies = False
self.echoSpeechFamiliesChoice = self.echoSpeechFamiliesChoices[selectedIndex]
def _fetchEchoSpeechFamiliesWorker(self, serverInfo, requestToken):
"""Fetch module-specific voices in a subprocess and apply asynchronously."""
queryScript = """
import json
import sys
from cthulhu import speechdispatcherfactory
from cthulhu import speechserver
serverInfo = json.loads(sys.argv[1])
if isinstance(serverInfo, list):
serverInfo = tuple(serverInfo)
server = speechdispatcherfactory.SpeechServer.getSpeechServer(serverInfo)
families = server.getVoiceFamilies() if server else []
result = []
for family in families:
result.append({
"name": family.get(speechserver.VoiceFamily.NAME),
"lang": family.get(speechserver.VoiceFamily.LANG),
"dialect": family.get(speechserver.VoiceFamily.DIALECT),
"variant": family.get(speechserver.VoiceFamily.VARIANT),
})
print(json.dumps(result))
"""
familyDefs = None
try:
completed = subprocess.run(
[sys.executable, "-c", queryScript, json.dumps(serverInfo)],
capture_output=True,
text=True,
timeout=4.0,
check=False,
)
if completed.returncode == 0 and completed.stdout.strip():
familyDefs = json.loads(completed.stdout.strip())
else:
tokens = [
"PREFERENCES DIALOG: Echo voice fetch failed for",
serverInfo,
"rc=",
completed.returncode,
]
debug.printTokens(debug.LEVEL_WARNING, tokens, True)
except subprocess.TimeoutExpired:
tokens = ["PREFERENCES DIALOG: Echo voice fetch timed out for", serverInfo]
debug.printTokens(debug.LEVEL_WARNING, tokens, True)
except Exception:
debug.printException(debug.LEVEL_WARNING)
GLib.idle_add(self._applyFetchedEchoSpeechFamilies, requestToken, familyDefs)
def _applyFetchedEchoSpeechFamilies(self, requestToken, familyDefs):
"""Apply fetched echo families if this is still the active request."""
if requestToken != self._echoVoiceFetchToken:
return False
if not isinstance(familyDefs, list):
return False
families = []
for familyDef in familyDefs:
if not isinstance(familyDef, dict):
continue
families.append(
speechserver.VoiceFamily({
speechserver.VoiceFamily.NAME: familyDef.get("name"),
speechserver.VoiceFamily.LANG: familyDef.get("lang"),
speechserver.VoiceFamily.DIALECT: familyDef.get("dialect"),
speechserver.VoiceFamily.VARIANT: familyDef.get("variant"),
})
)
if not families:
return False
self._populateEchoSpeechFamilies(families)
return False
def _setupEchoSpeechFamilies(self):
"""Populate echo voice family list for the current echo server."""
self._echoVoiceFetchToken += 1
requestToken = self._echoVoiceFetchToken
useCustomModule = self.get_widget("useCustomEchoSpeechServerCheckButton").get_active()
fallbackFamilies = list(self.speechFamiliesChoices or [])
self._populateEchoSpeechFamilies(fallbackFamilies)
if useCustomModule and self.echoSpeechServersChoice:
serverInfo = self.echoSpeechServersChoice.getInfo()
thread = threading.Thread(
target=self._fetchEchoSpeechFamiliesWorker,
args=(serverInfo, requestToken),
daemon=True,
)
thread.start()
def _setEchoSpeechServersChoice(self, serverInfo):
"""Set the active echo module based on stored server info."""
if not self.echoSpeechServersChoices:
self.echoSpeechServersChoice = None
self._setupEchoSpeechFamilies()
return
if serverInfo and len(serverInfo) >= 2 and serverInfo[1] == 'default':
family = self._getEchoSpeechServerFamilyForSave()
resolved = self._resolveSpeechDispatcherServerForFamily(family)
if resolved is None:
speechServer = self._getSpeechServerChoiceForSave()
if speechServer is not None:
speechInfo = speechServer.getInfo()
if speechInfo and len(speechInfo) >= 2 and speechInfo[1] != 'default':
resolved = speechServer
tokens = [
"PREFERENCES DIALOG: Reusing main speech server for echo",
speechInfo,
]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
if resolved is not None:
serverInfo = resolved.getInfo()
valueSet = False
for i, server in enumerate(self.echoSpeechServersChoices):
info = server.getInfo()
if serverInfo and info == serverInfo:
self.get_widget("echoSpeechServers").set_active(i)
self.echoSpeechServersChoice = server
valueSet = True
break
if not valueSet:
self.get_widget("echoSpeechServers").set_active(0)
self.echoSpeechServersChoice = self.echoSpeechServersChoices[0]
self._setupEchoSpeechFamilies()
def _setupEchoSpeechServers(self):
"""Populate available speech-dispatcher modules for echo."""
combobox = self.get_widget("echoSpeechServers")
combobox.set_model(None)
self.echoSpeechServersModel.clear()
self.echoSpeechServersChoices = []
self.echoSpeechServersChoice = None
if not self.prefsDict.get('enableSpeech', True):
combobox.set_model(self.echoSpeechServersModel)
self._setupEchoSpeechFamilies()
return
factory = self._getSpeechDispatcherFactory()
if factory is None:
combobox.set_model(self.echoSpeechServersModel)
self._setupEchoSpeechFamilies()
return
try:
self.echoSpeechServersChoices = factory.SpeechServer.getSpeechServers()
except Exception:
debug.printException(debug.LEVEL_WARNING)
self.echoSpeechServersChoices = []
for i, server in enumerate(self.echoSpeechServersChoices):
name = server.getInfo()[0]
self.echoSpeechServersModel.append((i, name))
combobox.set_model(self.echoSpeechServersModel)
self._setEchoSpeechServersChoice(self.prefsDict.get("echoSpeechServerInfo"))
def _initEchoSpeechState(self):
"""Initialize echo voice controls and choices."""
self.echoVoice = acss.ACSS(
self.prefsDict.get("echoVoice", settings.echoVoice) or {})
baseVoice = self.defaultVoice or acss.ACSS(
self.prefsDict.get("voices", {}).get(settings.DEFAULT_VOICE, {}))
rate = self.echoVoice.get(acss.ACSS.RATE, baseVoice.get(acss.ACSS.RATE, 50.0))
pitch = self.echoVoice.get(acss.ACSS.AVERAGE_PITCH, baseVoice.get(acss.ACSS.AVERAGE_PITCH, 5.0))
volume = self.echoVoice.get(acss.ACSS.GAIN, baseVoice.get(acss.ACSS.GAIN, 10.0))
self.get_widget("echoRateScale").set_value(rate)
self.get_widget("echoPitchScale").set_value(pitch)
self.get_widget("echoVolumeScale").set_value(volume)
self.initializingEchoSpeech = True
self._setupEchoSpeechServers()
self.initializingEchoSpeech = False
self._setEchoVoiceItems()
def _setSpokenTextAttributes(self, view, setAttributes,
state, moveToTop=False):
"""Given a set of spoken text attributes, update the model used by the
@@ -2394,6 +2820,19 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
prefs["enableEchoByWord"])
self.get_widget("enableEchoBySentenceCheckButton").set_active( \
prefs["enableEchoBySentence"])
self.get_widget("useCustomEchoVoiceCheckButton").set_active(
prefs.get("useCustomEchoVoice", settings.useCustomEchoVoice))
self.get_widget("useCustomEchoSpeechServerCheckButton").set_active(
prefs.get("useCustomEchoSpeechServer", settings.useCustomEchoSpeechServer))
self.get_widget("useCustomEchoForKeyCheckButton").set_active(
prefs.get("useCustomEchoForKey", settings.useCustomEchoForKey))
self.get_widget("useCustomEchoForCharacterCheckButton").set_active(
prefs.get("useCustomEchoForCharacter", settings.useCustomEchoForCharacter))
self.get_widget("useCustomEchoForWordCheckButton").set_active(
prefs.get("useCustomEchoForWord", settings.useCustomEchoForWord))
self.get_widget("useCustomEchoForSentenceCheckButton").set_active(
prefs.get("useCustomEchoForSentence", settings.useCustomEchoForSentence))
self._setEchoVoiceItems()
# Text attributes pane.
#
@@ -2553,22 +2992,20 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
self._updateAIControlsState(enabled)
def _updateAIControlsState(self, enabled):
"""Enable or disable AI controls based on AI enabled state."""
self.aiProviderCombo.set_sensitive(enabled)
self.aiApiKeyEntry.set_sensitive(enabled)
self.aiOllamaModelEntry.set_sensitive(enabled)
self.aiOllamaEndpointEntry.set_sensitive(enabled)
self.aiConfirmationCheckButton.set_sensitive(enabled)
self.aiScreenshotQualityCombo.set_sensitive(enabled)
"""Refresh AI controls while keeping configuration fields editable."""
_ = enabled # kept for signal/call compatibility
# Keep settings editable even when AI assistant is disabled so users
# can configure providers/keys before enabling it.
self.aiProviderCombo.set_sensitive(True)
self.aiConfirmationCheckButton.set_sensitive(True)
self.aiScreenshotQualityCombo.set_sensitive(True)
try:
self.get_widget("aiGetClaudeKeyButton").set_sensitive(enabled)
self.get_widget("aiGetClaudeKeyButton").set_sensitive(True)
except:
pass # Button might not exist in older UI files
# Update provider-specific controls if AI is enabled
if enabled:
current_provider = self.prefsDict.get("aiProvider", settings.aiProvider)
self._updateProviderControls(current_provider)
current_provider = self.prefsDict.get("aiProvider", settings.aiProvider)
self._updateProviderControls(current_provider)
def _initIndentationState(self):
"""Initialize Indentation widgets with current settings."""
@@ -2667,15 +3104,37 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
self.ocrCopyToClipboardCheckButton.set_active(copyToClipboard)
def _initSoundThemeState(self):
"""Initialize Sound Theme widgets with current settings."""
"""Initialize sound widgets with current settings."""
prefs = self.prefsDict
# Get widget references
self.soundSinkCombo = self.get_widget("soundSinkCombo")
self.soundThemeCombo = self.get_widget("soundThemeCombo")
self.roleSoundPresentationCombo = self.get_widget("roleSoundPresentationCombo")
self.soundSinkCombo.set_can_focus(False)
self.soundThemeCombo.set_can_focus(False)
self.roleSoundPresentationCombo.set_can_focus(False)
self._soundSinkChoices = [
(settings.SOUND_SINK_AUTO, guilabels.SOUND_BACKEND_AUTO),
(settings.SOUND_SINK_PIPEWIRE, guilabels.SOUND_BACKEND_PIPEWIRE),
(settings.SOUND_SINK_PULSE, guilabels.SOUND_BACKEND_PULSE),
(settings.SOUND_SINK_ALSA, guilabels.SOUND_BACKEND_ALSA),
]
self.soundSinkCombo.remove_all()
for _, label in self._soundSinkChoices:
self.soundSinkCombo.append_text(label)
runtimeSoundSink = cthulhu.cthulhuApp.settingsManager.getSetting("soundSink")
currentSink = sound_sink.normalize_sound_sink_choice(
prefs.get("soundSink", runtimeSoundSink if runtimeSoundSink is not None else settings.soundSink)
)
sinkIndex = 0
for index, (value, _) in enumerate(self._soundSinkChoices):
if value == currentSink:
sinkIndex = index
break
self.soundSinkCombo.set_active(sinkIndex)
# Populate sound theme combo box
themeManager = sound_theme_manager.getManager()
availableThemes = themeManager.getAvailableThemes()
@@ -2724,6 +3183,14 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
if activeText:
self.prefsDict["soundTheme"] = activeText
def soundSinkComboChanged(self, widget):
"""Signal handler for the sound backend combo box."""
activeIndex = widget.get_active()
if activeIndex < 0:
return
value = self._soundSinkChoices[activeIndex][0]
self.prefsDict["soundSink"] = value
def roleSoundPresentationComboChanged(self, widget):
"""Signal handler for the role sound presentation combo box."""
activeIndex = widget.get_active()
@@ -2854,6 +3321,68 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
cthulhuSetupWindow.set_title(title)
cthulhuSetupWindow.show()
self._initialFocusSyncAttempts = 0
GLib.idle_add(self._set_initial_window_state)
GLib.idle_add(self._set_initial_gtk_focus)
GLib.timeout_add(50, self._set_initial_window_state)
def _set_initial_gtk_focus(self):
"""Give GTK focus to a real preferences control as soon as the dialog appears."""
candidateIds = [
"generalDesktopButton",
"generalLaptopButton",
"availableProfilesComboBox1",
"speechSupportCheckButton",
"notebook",
]
cthulhuSetupWindow = self.get_widget("cthulhuSetupWindow")
for widgetId in candidateIds:
widget = self.get_widget(widgetId)
if not widget.get_visible() or not widget.get_sensitive():
continue
if not widget.get_can_focus():
continue
debug.printMessage(
debug.LEVEL_INFO,
f"PREFERENCES DIALOG: Setting initial GTK focus to {widgetId}",
True,
)
cthulhuSetupWindow.set_focus(widget)
widget.grab_focus()
return False
debug.printMessage(
debug.LEVEL_INFO,
"PREFERENCES DIALOG: No focusable initial GTK widget found",
True,
)
return False
def _set_initial_window_state(self):
"""Sync Cthulhu's active window to preferences without forcing dialog focus."""
self._initialFocusSyncAttempts += 1
activeWindow = AXUtilities.find_active_window()
if activeWindow is None:
return self._initialFocusSyncAttempts < 5
app = AXObject.get_application(activeWindow)
appName = (AXObject.get_name(app) or "").lower()
if appName != "cthulhu":
return self._initialFocusSyncAttempts < 5
debug.printTokens(
debug.LEVEL_INFO,
["PREFERENCES DIALOG: Syncing active window to", activeWindow],
True,
)
cthulhu.setActiveWindow(activeWindow, notifyScript=False)
tokens = ["PREFERENCES DIALOG: Synced initial window state to", activeWindow]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return False
def _initComboBox(self, combobox):
"""Initialize the given combo box to take a list of int/str pairs.
@@ -2898,22 +3427,91 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
self.get_widget("enableNavigationKeysCheckButton").set_sensitive(enable)
self.get_widget("enableDiacriticalKeysCheckButton").set_sensitive( \
enable)
self._setEchoVoiceItems()
def _presentMessage(self, text, interrupt=False):
def _setEchoVoiceItems(self):
"""[In]sensitize echo voice controls based on current state."""
useCustomVoice = self.get_widget("useCustomEchoVoiceCheckButton").get_active()
useCustomModule = self.get_widget("useCustomEchoSpeechServerCheckButton").get_active()
keyEchoEnabled = self.get_widget("keyEchoCheckButton").get_active()
charEchoEnabled = self.get_widget("enableEchoByCharacterCheckButton").get_active()
wordEchoEnabled = self.get_widget("enableEchoByWordCheckButton").get_active()
sentenceEchoEnabled = self.get_widget("enableEchoBySentenceCheckButton").get_active()
speechEnabled = self.get_widget("speechSupportCheckButton").get_active()
speechSystemIsDispatcher = False
if self.speechSystemsChoice:
try:
speechSystemIsDispatcher = \
self.speechSystemsChoice.SpeechServer.getFactoryName() == guilabels.SPEECH_DISPATCHER
except Exception:
speechSystemIsDispatcher = False
voiceControlsEnabled = useCustomVoice and speechEnabled
moduleOverrideAvailable = voiceControlsEnabled and speechSystemIsDispatcher
moduleControlsEnabled = moduleOverrideAvailable and useCustomModule
self.get_widget("useCustomEchoSpeechServerCheckButton").set_sensitive(moduleOverrideAvailable)
self.get_widget("echoSpeechServers").set_sensitive(moduleControlsEnabled)
self.get_widget("echoSpeechFamilies").set_sensitive(voiceControlsEnabled)
self.get_widget("echoRateScale").set_sensitive(useCustomVoice)
self.get_widget("echoPitchScale").set_sensitive(useCustomVoice)
self.get_widget("echoVolumeScale").set_sensitive(useCustomVoice)
self.get_widget("useCustomEchoForKeyCheckButton").set_sensitive(useCustomVoice and keyEchoEnabled)
self.get_widget("useCustomEchoForCharacterCheckButton").set_sensitive(
useCustomVoice and charEchoEnabled)
self.get_widget("useCustomEchoForWordCheckButton").set_sensitive(useCustomVoice and wordEchoEnabled)
self.get_widget("useCustomEchoForSentenceCheckButton").set_sensitive(
useCustomVoice and sentenceEchoEnabled)
def _presentMessage(self, text, interrupt=False, voice=None):
"""If the text field is not None, presents the given text, optionally
interrupting anything currently being spoken.
Arguments:
- text: the text to present
- interrupt: if True, interrupt any speech currently being spoken
- voice: the voice to use; if None, use the default system voice
"""
self.script.speakMessage(text, interrupt=interrupt)
self.script.speakMessage(text, voice=voice, interrupt=interrupt)
try:
self.script.displayBrailleMessage(text, flashTime=-1)
except Exception:
pass
def _previewVoiceSelection(self, voiceType, name):
"""Present a short preview in the currently selected voice."""
if not name:
return
voice = self._getACSSForVoiceType(voiceType)
if not voice:
return
previewVoice = acss.ACSS(voice)
if self.speechSystemsChoice \
and self.speechSystemsChoice.SpeechServer.getFactoryName() == guilabels.SPEECH_DISPATCHER:
previewMessage = messages.SPEECH_VOICE_VALUE % name
else:
previewMessage = messages.SPEECH_VOICE_VALUE_GENERIC % name
# Speak through the currently-selected preferences speech server so
# preview uses the chosen module/voice, not the globally-active one.
server = self.speechServersChoice
if server and hasattr(server, "speak"):
try:
server.speak(previewMessage, previewVoice, interrupt=True)
return
except Exception:
debug.printException(debug.LEVEL_WARNING)
self._presentMessage(previewMessage, interrupt=True, voice=previewVoice)
def _createNode(self, appName):
"""Create a new root node in the TreeStore model with the name of the
application.
@@ -3194,6 +3792,8 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
enable = widget.get_active()
self.prefsDict["enableSpeech"] = enable
self.get_widget("speechOptionsGrid").set_sensitive(enable)
self._setupEchoSpeechServers()
self._setEchoVoiceItems()
def onlySpeakDisplayedTextToggled(self, widget):
"""Signal handler for the "toggled" signal for the GtkCheckButton
@@ -3225,6 +3825,8 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
selectedIndex = widget.get_active()
self.speechSystemsChoice = self.speechSystemsChoices[selectedIndex]
self._setupSpeechServers()
self._setupEchoSpeechServers()
self._setEchoVoiceItems()
def speechServersChanged(self, widget):
"""Signal handler for the "changed" signal for the speechServers
@@ -3257,6 +3859,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
pass
self._setupVoices()
self._setEchoVoiceItems()
def speechLanguagesChanged(self, widget):
"""Signal handler for the "value_changed" signal for the languages
@@ -3316,6 +3919,8 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
variant = family[speechserver.VoiceFamily.VARIANT]
voiceType = self.get_widget("voiceTypesCombo").get_active()
self._setFamilyNameForVoiceType(voiceType, name, language, dialect, variant)
if not self._updatingSpeechFamilies:
self._previewVoiceSelection(voiceType, name)
except Exception:
debug.printException(debug.LEVEL_SEVERE)
@@ -3393,6 +3998,75 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
voices.get(settings.DEFAULT_VOICE, {})[acss.ACSS.GAIN] = volume
cthulhu.cthulhuApp.settingsManager.setSetting('voices', voices)
def useCustomEchoVoiceToggled(self, widget):
"""Signal handler for enabling/disabling custom echo voice settings."""
self.prefsDict["useCustomEchoVoice"] = widget.get_active()
self._setEchoVoiceItems()
self._setupEchoSpeechFamilies()
def useCustomEchoSpeechServerToggled(self, widget):
"""Signal handler for enabling/disabling custom echo module override."""
self.prefsDict["useCustomEchoSpeechServer"] = widget.get_active()
self._setEchoVoiceItems()
self._setupEchoSpeechFamilies()
def echoSpeechServersChanged(self, widget):
"""Signal handler for selecting the echo speech-dispatcher module."""
if self.initializingEchoSpeech:
return
selectedIndex = widget.get_active()
if selectedIndex < 0:
self.echoSpeechServersChoice = None
self.prefsDict["echoSpeechServerInfo"] = None
self._setupEchoSpeechFamilies()
return
self.echoSpeechServersChoice = self.echoSpeechServersChoices[selectedIndex]
self.prefsDict["echoSpeechServerInfo"] = self.echoSpeechServersChoice.getInfo()
self._setupEchoSpeechFamilies()
def echoSpeechFamiliesChanged(self, widget):
"""Signal handler for selecting the echo voice family."""
if self.initializingEchoSpeech or self._updatingEchoSpeechFamilies:
return
selectedIndex = widget.get_active()
if selectedIndex < 0:
self.echoSpeechFamiliesChoice = None
return
self.echoSpeechFamiliesChoice = self.echoSpeechFamiliesChoices[selectedIndex]
self._setEchoVoiceFamily(self.echoSpeechFamiliesChoice)
def echoRateValueChanged(self, widget):
"""Signal handler for changing custom echo rate."""
if self.echoVoice is None:
self.echoVoice = acss.ACSS({})
self.echoVoice[acss.ACSS.RATE] = widget.get_value()
self.echoVoice['established'] = True
def echoPitchValueChanged(self, widget):
"""Signal handler for changing custom echo pitch."""
if self.echoVoice is None:
self.echoVoice = acss.ACSS({})
self.echoVoice[acss.ACSS.AVERAGE_PITCH] = widget.get_value()
self.echoVoice['established'] = True
def echoVolumeValueChanged(self, widget):
"""Signal handler for changing custom echo volume."""
if self.echoVoice is None:
self.echoVoice = acss.ACSS({})
self.echoVoice[acss.ACSS.GAIN] = widget.get_value()
self.echoVoice['established'] = True
def checkButtonToggled(self, widget):
"""Signal handler for "toggled" signal for basic GtkCheckButton
widgets. The user has altered the state of the checkbox.
@@ -3410,6 +4084,15 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
# strip "CheckButton" from the end.
settingName = settingName[:-11]
self.prefsDict[settingName] = widget.get_active()
if settingName in [
"enableEchoByCharacter",
"enableEchoByWord",
"enableEchoBySentence",
"useCustomEchoForKey",
"useCustomEchoForCharacter",
"useCustomEchoForWord",
"useCustomEchoForSentence"]:
self._setEchoVoiceItems()
def keyEchoChecked(self, widget):
"""Signal handler for the "toggled" signal for the
@@ -4193,6 +4876,11 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
if not self._isInitialSetup:
self.restoreSettings()
if self.soundSinkCombo is not None:
activeIndex = self.soundSinkCombo.get_active()
if activeIndex >= 0:
self.prefsDict["soundSink"] = self._soundSinkChoices[activeIndex][0]
enable = self.get_widget("speechSupportCheckButton").get_active()
self.prefsDict["enableSpeech"] = enable
@@ -4200,9 +4888,24 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
self.prefsDict["speechServerFactory"] = \
self.speechSystemsChoice.__name__
if self.speechServersChoice:
speechServerChoice = self._getSpeechServerChoiceForSave()
if speechServerChoice:
self.prefsDict["speechServerInfo"] = \
self.speechServersChoice.getInfo()
speechServerChoice.getInfo()
else:
activeSpeechInfo = speech.getInfo()
if activeSpeechInfo:
self.prefsDict["speechServerInfo"] = activeSpeechInfo
runtimeSpeechServerInfo = cthulhu.cthulhuApp.settingsManager.getSetting(
"speechServerInfo"
)
if runtimeSpeechServerInfo and not self.prefsDict.get("speechServerInfo"):
self.prefsDict["speechServerInfo"] = runtimeSpeechServerInfo
existingSpeechServerInfo = self.prefsDict.get("speechServerInfo")
if existingSpeechServerInfo:
self.prefsDict["speechServerInfo"] = existingSpeechServerInfo
if self.defaultVoice is not None:
self.prefsDict["voices"] = {
@@ -4212,6 +4915,36 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
settings.SYSTEM_VOICE: acss.ACSS(self.systemVoice),
}
self.prefsDict["useCustomEchoVoice"] = \
self.get_widget("useCustomEchoVoiceCheckButton").get_active()
self.prefsDict["useCustomEchoSpeechServer"] = \
self.get_widget("useCustomEchoSpeechServerCheckButton").get_active()
self.prefsDict["useCustomEchoForKey"] = \
self.get_widget("useCustomEchoForKeyCheckButton").get_active()
self.prefsDict["useCustomEchoForCharacter"] = \
self.get_widget("useCustomEchoForCharacterCheckButton").get_active()
self.prefsDict["useCustomEchoForWord"] = \
self.get_widget("useCustomEchoForWordCheckButton").get_active()
self.prefsDict["useCustomEchoForSentence"] = \
self.get_widget("useCustomEchoForSentenceCheckButton").get_active()
if self.echoVoice is None:
self.echoVoice = acss.ACSS({})
# Persist slider values directly so saving does not depend on
# value-changed signal timing.
self.echoVoice[acss.ACSS.RATE] = self.get_widget("echoRateScale").get_value()
self.echoVoice[acss.ACSS.AVERAGE_PITCH] = self.get_widget("echoPitchScale").get_value()
self.echoVoice[acss.ACSS.GAIN] = self.get_widget("echoVolumeScale").get_value()
self.echoVoice['established'] = True
self.prefsDict["echoVoice"] = acss.ACSS(self.echoVoice)
echoSpeechServerChoice = self._getEchoSpeechServerChoiceForSave()
if echoSpeechServerChoice:
self.prefsDict["echoSpeechServerInfo"] = echoSpeechServerChoice.getInfo()
else:
self.prefsDict["echoSpeechServerInfo"] = None
def applyButtonClicked(self, widget):
"""Signal handler for the "clicked" signal for the applyButton
GtkButton widget. The user has clicked the Apply button.
@@ -4244,6 +4977,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
self._refresh_dynamic_plugin_tabs()
braille.checkBrailleSetting()
self._initSpeechState()
self._initEchoSpeechState()
self._populateKeyBindings()
self.__initProfileCombo()
@@ -4506,6 +5240,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
braille.checkBrailleSetting()
self._initSpeechState()
self._initEchoSpeechState()
self._populateKeyBindings()
+5
View File
@@ -47,6 +47,11 @@ __license__ = "LGPL"
#
locusOfFocus: Optional[Any] = None # Actually: Optional[Atspi.Accessible]
# A pending focused object from a Cthulhu-owned window that should be used
# for keyboard-event context until the queued focus event is processed.
#
pendingSelfHostedFocus: Optional[Any] = None # Actually: Optional[Atspi.Accessible]
# The currently active window.
#
activeWindow: Optional[Any] = None # Actually: Optional[Atspi.Accessible]
+141 -1
View File
@@ -70,7 +70,9 @@ class EventManager:
self._dequeueCount: int = 0
self._cmdlineCache: Dict[int, str] = {}
self._eventQueue: queue.Queue[Any] = queue.Queue(0)
self._prioritizedEvent: Optional[Atspi.Event] = None
self._gidleId: int = 0
self._prioritizedIdleId: int = 0
self._gidleLock: threading.Lock = threading.Lock()
self._gilSleepTime: float = 0.00001
self._synchronousToolkits: List[str] = ['VCL']
@@ -284,6 +286,12 @@ class EventManager:
if self._isDuplicateEvent(event):
return _ignore_with_reason("duplicate", "duplicate event")
if self._isSelfHostedFocusClearedEvent(event):
return _ignore_with_reason("self-hosted-focus-cleared", "self-hosted focused=false event")
if self._isRedundantSelfHostedPropertyEvent(event):
return _ignore_with_reason("self-hosted-redundant-property", "self-hosted redundant property event")
# Thunderbird spams us with these when a message list thread is expanded or collapsed.
if event.type.endswith('system') \
and AXObject.get_name(app).lower().startswith('thunderbird'):
@@ -596,6 +604,11 @@ class EventManager:
self._enqueueCount -= 1
return
if isObjectEvent and self._prioritizeSelfHostedFocusedEvent(e):
if debug.debugEventQueue:
self._enqueueCount -= 1
return
self._queuePrintln(e)
if self._inFlood() and self._prioritizeDuringFlood(e):
@@ -628,6 +641,73 @@ class EventManager:
if debug.debugEventQueue:
self._enqueueCount -= 1
def _isSelfHostedFocusedEvent(self, event: Atspi.Event) -> bool:
"""Returns True if the event is a newly-focused event from Cthulhu."""
if not event.type.startswith("object:state-changed:focused") or not event.detail1:
return False
app = AXObject.get_application(event.source)
appName = (AXObject.get_name(app) or "").lower()
return appName == "cthulhu"
def _isSelfHostedFocusClearedEvent(self, event: Atspi.Event) -> bool:
"""Returns True if the event is a focus-lost event from Cthulhu."""
if not event.type.startswith("object:state-changed:focused") or event.detail1:
return False
app = AXObject.get_application(event.source)
appName = (AXObject.get_name(app) or "").lower()
return appName == "cthulhu"
def _isRedundantSelfHostedPropertyEvent(self, event: Atspi.Event) -> bool:
"""Returns True if the event is redundant prefs startup noise from Cthulhu."""
app = AXObject.get_application(event.source)
appName = (AXObject.get_name(app) or "").lower()
if appName != "cthulhu":
return False
if event.type.startswith("object:property-change:accessible-name"):
return AXUtilities.is_combo_box(event.source) or AXUtilities.is_table_cell(event.source)
if event.type.startswith("object:property-change:accessible-value"):
return AXObject.get_role(event.source) == Atspi.Role.SLIDER \
and event.source != cthulhu_state.locusOfFocus
return False
def _prioritizeSelfHostedFocusedEvent(self, event: Atspi.Event) -> bool:
"""Schedules the latest focused-child event from Cthulhu ahead of the normal queue."""
if not self._isSelfHostedFocusedEvent(event):
return False
tokens = ["EVENT MANAGER: Prioritizing self-hosted focused event for", event.source]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
script = self._get_scriptForEvent(event)
if script is not None:
script.eventCache[event.type] = (event, time.time())
self._gidleLock.acquire()
try:
replaced = self._prioritizedEvent is not None
self._prioritizedEvent = event
cthulhu_state.pendingSelfHostedFocus = event.source
if not self._prioritizedIdleId:
self._prioritizedIdleId = GLib.idle_add(
self._dequeuePrioritizedEvent,
priority=GLib.PRIORITY_HIGH_IDLE,
)
finally:
self._gidleLock.release()
msg = f"EVENT MANAGER: Prioritized self-hosted focused event. Replaced pending event: {replaced}"
debug.printMessage(debug.LEVEL_INFO, msg, True)
return True
def _isNoFocus(self) -> bool:
if cthulhu_state.locusOfFocus or cthulhu_state.activeWindow or cthulhu_state.activeScript:
return False
@@ -645,6 +725,60 @@ class EventManager:
defaultScript.idleMessage()
return False
def _dequeuePrioritizedEvent(self) -> bool:
"""Handles prioritized focused events from Cthulhu-owned windows."""
self._gidleLock.acquire()
try:
event = self._prioritizedEvent
self._prioritizedEvent = None
self._prioritizedIdleId = 0
if event is not None and cthulhu_state.pendingSelfHostedFocus == event.source:
cthulhu_state.pendingSelfHostedFocus = None
finally:
self._gidleLock.release()
if event is None:
return False
tokens = ["EVENT MANAGER: Dequeued prioritized event", event]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
debugging = not debug.eventDebugFilter or debug.eventDebugFilter.match(event.type)
if debugging:
startTime = time.time()
msg = (
f"\nvvvvv PROCESS OBJECT EVENT {event.type} "
f"(prioritized) vvvvv"
)
debug.printMessage(debug.eventDebugLevel, msg, False)
try:
self._processObjectEvent(event)
if self._didSuspendEventsFor(event):
self._unsuspendEvents(event)
elif self._eventsSuspended and self._shouldUnsuspendEventsFor(event):
self._unsuspendEvents(event, force=True)
except Exception:
debug.printException(debug.LEVEL_SEVERE)
if debugging:
msg = (
f"TOTAL PROCESSING TIME: {time.time() - startTime:.4f}"
f"\n^^^^^ PROCESS OBJECT EVENT {event.type} ^^^^^\n"
)
debug.printMessage(debug.eventDebugLevel, msg, False)
self._gidleLock.acquire()
try:
hasMore = self._prioritizedEvent is not None
if not hasMore and self._eventQueue.qsize() and not self._gidleId:
self._gidleId = GLib.idle_add(self._dequeue)
finally:
self._gidleLock.release()
return hasMore
def _dequeue(self) -> bool:
"""Handles all events destined for scripts. Called by the GTK
idle thread."""
@@ -657,7 +791,13 @@ class EventManager:
self._dequeueCount += 1
try:
event = self._eventQueue.get_nowait()
fromPriority = False
self._gidleLock.acquire()
try:
event = self._eventQueue.get_nowait()
finally:
self._gidleLock.release()
self._queuePrintln(event, isEnqueue=False)
inputEvents = (input_event.KeyboardEvent, input_event.BrailleEvent)
if isinstance(event, inputEvents):
+34 -6
View File
@@ -116,6 +116,13 @@ class FocusManager:
_log_tokens(["Focused object in", self._window, "is", result])
return result
def active_window_is_cthulhu(self) -> bool:
"""Returns True if the active window belongs to Cthulhu itself."""
app = AXObject.get_application(self._window)
appName = (AXObject.get_name(app) or "").lower()
return appName == "cthulhu"
def focus_and_window_are_unknown(self) -> bool:
"""Returns True if we have no knowledge about what is focused."""
@@ -350,18 +357,39 @@ class FocusManager:
self._window = frame
cthulhu_state.activeWindow = frame
contextObject = self._focus
if set_window_as_focus:
self.set_locus_of_focus(None, self._window, notify_script)
elif not (self.focus_is_active_window() or self.focus_is_in_active_window()):
_log_tokens(["Focus", self._focus, "is not in", self._window], stack=True)
# Don't update the focus to the active window if we can't get to the active window
# from the focused object. https://bugreports.qt.io/browse/QTBUG-130116
if not AXObject.has_broken_ancestry(self._focus):
self.set_locus_of_focus(None, self._window, notify_script=True)
if self.active_window_is_cthulhu():
_log_tokens(
["Skipping focused-object lookup and script activation for self-hosted window", self._window],
"self-window",
)
return
else:
focusedObject = self.find_focused_object()
if focusedObject is not None and focusedObject != self._window:
_log_tokens(["Using focused object", focusedObject, "from active window", self._window])
self.set_locus_of_focus(None, focusedObject, notify_script=True)
contextObject = focusedObject
elif self._focus is None:
_log_tokens(["No previous focus. Falling back to active window", self._window])
self.set_locus_of_focus(None, self._window, notify_script=True)
contextObject = self._window
# Don't update the focus to the active window if we can't get to the active window
# from the focused object. https://bugreports.qt.io/browse/QTBUG-130116
elif not AXObject.has_broken_ancestry(self._focus):
self.set_locus_of_focus(None, self._window, notify_script=True)
contextObject = self._window
app = _get_ax_utilities().get_application(self._focus)
self.app.scriptManager.activate_script_for_context(app, self._focus, "focus: active-window")
if contextObject is None:
contextObject = self._window
app = _get_ax_utilities().get_application(contextObject)
self.app.scriptManager.activate_script_for_context(app, contextObject, "focus: active-window")
@dbus_service.command
def toggle_presentation_mode(
+19 -2
View File
@@ -938,9 +938,26 @@ USE_STRUCTURAL_NAVIGATION = _("Enable _structural navigation")
# audio files that Cthulhu plays for various events.
SOUND_THEME = _("Sound _theme:")
# Translators: This is the label for a combo box in the preferences dialog
# where users can choose which audio backend Cthulhu should use.
SOUND_BACKEND = _("Audio _backend:")
# Translators: This is a sound backend option which lets Cthulhu choose a
# backend automatically.
SOUND_BACKEND_AUTO = _("Automatic")
# Translators: This is a sound backend option which forces PipeWire.
SOUND_BACKEND_PIPEWIRE = _("PipeWire")
# Translators: This is a sound backend option which forces PulseAudio.
SOUND_BACKEND_PULSE = _("PulseAudio")
# Translators: This is a sound backend option which forces ALSA.
SOUND_BACKEND_ALSA = _("ALSA")
# Translators: This is the title of a frame in the preferences dialog
# containing sound theme options.
SOUND_THEME_TITLE = _("Sound Theme")
# containing sound options.
SOUND_THEME_TITLE = _("Sound")
# Translators: This refers to the amount of information Cthulhu provides about a
# particular object that receives focus.
+47 -2
View File
@@ -844,6 +844,11 @@ class KeyboardEvent(InputEvent):
"shouldConsume: No handler found",
reason="no-handler", timestamp=True)
if self._isSleepModeActive():
if self._isSleepModeToggleHandler():
return True, 'Sleep mode toggle command'
return False, 'Sleep mode active'
self._script.updateKeyboardEventState(self, self._handler)
scriptConsumes = self._script.shouldConsumeKeyboardEvent(self, self._handler)
if globalHandlerUsed:
@@ -865,6 +870,8 @@ class KeyboardEvent(InputEvent):
return True, 'Cthulhu modifier'
if not self._handler:
if scriptConsumes:
return True, 'Script consumed without handler'
return False, 'No handler'
return scriptConsumes, 'Script indication'
@@ -881,6 +888,35 @@ class KeyboardEvent(InputEvent):
return None
return global_bindings.getInputHandler(self)
def _isSleepModeActive(self):
"""Returns True if the script for this event is in sleep mode."""
if not self._script:
return False
if "scripts.sleepmode" in self._script.__module__:
return True
app = getattr(self._script, "app", None)
if app is None:
return False
try:
from . import sleep_mode_manager
manager = sleep_mode_manager.getManager()
return bool(manager and manager.isActiveForApp(app))
except Exception:
return False
def _isSleepModeToggleHandler(self):
"""Returns True if the resolved handler toggles sleep mode."""
if not self._handler or not self._handler.function:
return False
functionName = getattr(self._handler.function, "__name__", "")
return "toggleSleepMode" in functionName
def didConsume(self):
"""Returns True if this event was consumed."""
@@ -1010,7 +1046,7 @@ class KeyboardEvent(InputEvent):
return False, 'Should not consume'
if not (self._consumer or self._handler):
return False, 'No consumer or handler'
return True, 'Consumed during shouldConsume'
if self._consumer or self._handler.function:
GLib.timeout_add(1, self._consume)
@@ -1139,7 +1175,7 @@ class RemoteControllerEvent(InputEvent):
class InputEventHandler:
def __init__(self, function, description, learnModeEnabled=True):
def __init__(self, function, description, learnModeEnabled=True, enabled=True):
"""Creates a new InputEventHandler instance. All bindings
(e.g., key bindings and braille bindings) will be handled
by an instance of an InputEventHandler.
@@ -1159,6 +1195,15 @@ class InputEventHandler:
self.function = function
self.description = description
self.learnModeEnabled = learnModeEnabled
self._enabled = enabled
def is_enabled(self):
"""Returns True if this handler is enabled."""
return self._enabled
def set_enabled(self, enabled):
"""Sets enabled state of this handler."""
self._enabled = enabled
def __eq__(self, other):
"""Compares one input handler to another."""

Some files were not shown because too many files have changed in this diff Show More