|
1 | 1 | import appModuleHandler |
| 2 | +import os |
2 | 3 | import globalPluginHandler |
| 4 | +import api |
| 5 | +from typing import Iterable, List, Any |
| 6 | +import speech |
| 7 | +from logHandler import log |
| 8 | + |
| 9 | + |
| 10 | +TARGET_EXE = "spcsadm.exe" |
| 11 | +TEXT_TO_SUPPRESS = "ruta" |
| 12 | + |
| 13 | + |
| 14 | +def _is_target_context() -> bool: |
| 15 | + f = api.getFocusObject() |
| 16 | + if not f: |
| 17 | + return False |
| 18 | + # App check |
| 19 | + am = getattr(f, "appModule", None) |
| 20 | + app_ok = False |
| 21 | + if am: |
| 22 | + appPath = (getattr(am, "appPath", "") or "") |
| 23 | + app_ok = os.path.basename(appPath).lower() == TARGET_EXE.lower() |
| 24 | + if not app_ok: |
| 25 | + appName = (getattr(am, "appName", "") or "").lower() |
| 26 | + app_ok = appName in {TARGET_EXE.lower(), TARGET_EXE.lower().removesuffix(".exe")} |
| 27 | + return app_ok |
| 28 | + |
| 29 | + |
| 30 | +def _item_text(item: Any) -> str | None: |
| 31 | + """Best-effort: speech sequences can contain strings and command objects.""" |
| 32 | + if isinstance(item, str): |
| 33 | + return item |
| 34 | + # Many speech command objects expose .text (e.g. TextCommand). |
| 35 | + txt = getattr(item, "text", None) |
| 36 | + return txt if isinstance(txt, str) else None |
| 37 | + |
| 38 | +def _filter_sequence(seq: Iterable[Any]) -> List[Any]: |
| 39 | + if not _is_target_context(): |
| 40 | + return list(seq) |
| 41 | + out: List[Any] = [] |
| 42 | + suppress = TEXT_TO_SUPPRESS.lower() |
| 43 | + for item in seq: |
| 44 | + txt = _item_text(item) |
| 45 | + if txt is not None and txt.strip().lower() == suppress: |
| 46 | + # Skip this piece entirely. |
| 47 | + continue |
| 48 | + |
| 49 | + |
| 50 | + out.append(item) |
| 51 | + return out |
3 | 52 |
|
4 | 53 |
|
5 | 54 | class GlobalPlugin(globalPluginHandler.GlobalPlugin): |
6 | 55 |
|
7 | 56 | def __init__(self, *args, **kwargs): |
8 | 57 | super().__init__(*args, **kwargs) |
| 58 | + # Defensive: confirm we're bound to NVDA's real module |
| 59 | + try: |
| 60 | + modpath = getattr(speech, "__file__", "") |
| 61 | + if not modpath or "nvda" not in modpath.lower(): |
| 62 | + raise RuntimeError(f"Unexpected 'speech' module import: {modpath}") |
| 63 | + except Exception: |
| 64 | + # If this triggers, there’s still a name collision. |
| 65 | + pass |
| 66 | + self._reg = speech.filter_speechSequence.register(_filter_sequence) |
9 | 67 | appModuleHandler.registerExecutableWithAppModule("spcsadm", "vismaAdmin") |
10 | 68 | appModuleHandler.registerExecutableWithAppModule("spcsfkt", "vismaAdmin") |
11 | 69 | appModuleHandler.registerExecutableWithAppModule("spcsfor", "vismaAdmin") |
12 | 70 |
|
| 71 | + |
13 | 72 | def terminate(self, *args, **kwargs): |
14 | 73 | super().terminate(*args, **kwargs) |
15 | 74 | appModuleHandler.unregisterExecutable("spcsadm") |
16 | 75 | appModuleHandler.unregisterExecutable("spcsfkt") |
17 | 76 | appModuleHandler.unregisterExecutable("spcsfor") |
| 77 | + try: |
| 78 | + self._reg.remove() |
| 79 | + except: |
| 80 | + pass |
| 81 | + |
0 commit comments