|
4 | 4 | gi.require_version("Adw", "1") |
5 | 5 | from gi.repository import Gtk, Adw, Gio, GObject, Gdk, GLib |
6 | 6 | import json |
| 7 | +import subprocess |
7 | 8 | import unicodedata |
8 | 9 | from urllib.parse import parse_qs, urlparse |
9 | 10 | from translations import _ |
@@ -173,8 +174,75 @@ def _create_filtered_model(self): |
173 | 174 | self.filter = Gtk.CustomFilter.new(self._filter_func, None) |
174 | 175 | self.filter_model = Gtk.FilterListModel(model=self._store, filter=self.filter) |
175 | 176 | selection_model = Gtk.SingleSelection(model=self.filter_model) |
| 177 | + selection_model.connect("selection-changed", self._on_selection_changed) |
| 178 | + self._espeak_proc = None |
| 179 | + self._speak_timeout_id = 0 |
| 180 | + # speech-dispatcher client for fast cancel (avoids subprocess overhead) |
| 181 | + self._spd_client = None |
| 182 | + self._spd_scope_all = None |
| 183 | + try: |
| 184 | + import speechd |
| 185 | + |
| 186 | + self._spd_client = speechd.SSIPClient("biglinux-wizard") |
| 187 | + self._spd_scope_all = speechd.Scope.ALL |
| 188 | + except Exception: |
| 189 | + pass |
176 | 190 | return selection_model |
177 | 191 |
|
| 192 | + def _cancel_orca(self): |
| 193 | + """Cancel ALL speech-dispatcher clients (including ORCA) instantly.""" |
| 194 | + if self._spd_client and self._spd_scope_all: |
| 195 | + try: |
| 196 | + self._spd_client.cancel(scope=self._spd_scope_all) |
| 197 | + except Exception: |
| 198 | + pass |
| 199 | + |
| 200 | + def _on_selection_changed(self, selection_model, position, n_items): |
| 201 | + """For pt_BR, let ORCA speak with the default LetĂcia voice. |
| 202 | + For other languages, cancel ORCA and use espeak-ng with the native voice.""" |
| 203 | + # Cancel any pending delayed speak |
| 204 | + if self._speak_timeout_id > 0: |
| 205 | + GLib.source_remove(self._speak_timeout_id) |
| 206 | + self._speak_timeout_id = 0 |
| 207 | + # Kill any ongoing espeak-ng process |
| 208 | + if self._espeak_proc and self._espeak_proc.poll() is None: |
| 209 | + self._espeak_proc.terminate() |
| 210 | + self._espeak_proc = None |
| 211 | + selected = selection_model.get_selected() |
| 212 | + if selected == Gtk.INVALID_LIST_POSITION: |
| 213 | + return |
| 214 | + item = selection_model.get_item(selected) |
| 215 | + if not item: |
| 216 | + return |
| 217 | + # For pt_BR: do nothing, let ORCA read with LetĂcia voice |
| 218 | + if item.code == "pt_BR": |
| 219 | + return |
| 220 | + # Immediately cancel ORCA speech via Python API (instant, no fork) |
| 221 | + self._cancel_orca() |
| 222 | + # Schedule espeak-ng after a brief delay to also cancel any ORCA re-queue |
| 223 | + parts = item.name.split(" - ", 1) |
| 224 | + country = parts[1] if len(parts) > 1 else "" |
| 225 | + native_name = _NATIVE_LANG_NAMES.get(item.code[:2], item.name_orig) |
| 226 | + text = f"{native_name}, {country}" if country else native_name |
| 227 | + voice = item.code.replace("_", "-") # "en_US" -> "en-US" |
| 228 | + self._speak_timeout_id = GLib.timeout_add(50, self._do_espeak, voice, text) |
| 229 | + |
| 230 | + def _do_espeak(self, voice, text): |
| 231 | + """Cancel ORCA speech and speak with espeak-ng in native voice.""" |
| 232 | + self._speak_timeout_id = 0 |
| 233 | + # Cancel any ORCA speech that was re-queued |
| 234 | + self._cancel_orca() |
| 235 | + # Speak with espeak-ng using the native voice |
| 236 | + try: |
| 237 | + self._espeak_proc = subprocess.Popen( |
| 238 | + ["espeak-ng", "-v", voice, "--", text], |
| 239 | + stdout=subprocess.DEVNULL, |
| 240 | + stderr=subprocess.DEVNULL, |
| 241 | + ) |
| 242 | + except FileNotFoundError: |
| 243 | + logger.debug("espeak-ng not found") |
| 244 | + return GLib.SOURCE_REMOVE |
| 245 | + |
178 | 246 | def _activate_item(self, item): |
179 | 247 | if not item: |
180 | 248 | return |
@@ -248,7 +316,7 @@ def _on_factory_setup(self, factory, list_item): |
248 | 316 | halign=Gtk.Align.START, |
249 | 317 | valign=Gtk.Align.CENTER, |
250 | 318 | ) |
251 | | - # Heading: ORCA reads this (native language name) |
| 319 | + # Heading: native name (ORCA reads this for pt_BR with LetĂcia voice) |
252 | 320 | name_label = Gtk.Label( |
253 | 321 | halign=Gtk.Align.START, |
254 | 322 | wrap=True, |
@@ -304,9 +372,9 @@ def _on_factory_bind(self, factory, list_item): |
304 | 372 | native_name = _NATIVE_LANG_NAMES.get(item.code[:2], item.name_orig) |
305 | 373 | heading_text = f"{native_name}, {country}" if country else native_name |
306 | 374 |
|
307 | | - # Heading: native name (ORCA reads this) |
| 375 | + # Heading: visual text |
308 | 376 | root_box._name_label.set_label(heading_text) |
309 | | - # Caption: English name (hidden from ORCA via PRESENTATION role) |
| 377 | + # Caption: English name (PRESENTATION â ORCA doesn't read) |
310 | 378 | root_box._orig_label.set_label(item.name) |
311 | 379 |
|
312 | 380 | root_box._flag.set_from_icon_name(item.flag_icon_name) |
|
0 commit comments