diff --git a/addon/appModules/line.py b/addon/appModules/line.py index 15a2f99..f1782ad 100644 --- a/addon/appModules/line.py +++ b/addon/appModules/line.py @@ -24,6 +24,7 @@ import re import time import configparser +import winreg from ._virtualWindow import VirtualWindow import addonHandler @@ -83,6 +84,69 @@ def _readLineLanguage(): return None +# --------------------------------------------------------------------------- +# Qt accessibility environment variable management +# --------------------------------------------------------------------------- + +_QT_ACCESSIBILITY_ENV_NAME = "QT_ACCESSIBILITY" +_HWND_BROADCAST = 0xFFFF +_WM_SETTINGCHANGE = 0x001A +_SMTO_ABORTIFHUNG = 0x0002 + + +def _isQtAccessibleSet(): + """Check if QT_ACCESSIBILITY=1 is set in user environment variables. + + Returns True if the variable is set to '1', False otherwise. + """ + try: + with winreg.OpenKey( + winreg.HKEY_CURRENT_USER, "Environment", 0, winreg.KEY_READ + ) as key: + value, _ = winreg.QueryValueEx(key, _QT_ACCESSIBILITY_ENV_NAME) + return str(value) == "1" + except FileNotFoundError: + return False + except Exception: + log.debugWarning("Failed to read QT_ACCESSIBILITY from registry", exc_info=True) + return False + + +def _setQtAccessible(enable=True): + """Set or remove QT_ACCESSIBILITY in user environment variables. + + Writes to HKCU\\Environment so the setting persists across reboots. + Broadcasts WM_SETTINGCHANGE so new processes pick up the change. + Returns True on success, False on failure. + """ + try: + with winreg.OpenKey( + winreg.HKEY_CURRENT_USER, "Environment", 0, + winreg.KEY_SET_VALUE | winreg.KEY_READ + ) as key: + if enable: + winreg.SetValueEx( + key, _QT_ACCESSIBILITY_ENV_NAME, 0, + winreg.REG_SZ, "1" + ) + log.info("QT_ACCESSIBILITY=1 set in user environment") + else: + try: + winreg.DeleteValue(key, _QT_ACCESSIBILITY_ENV_NAME) + log.info("QT_ACCESSIBILITY removed from user environment") + except FileNotFoundError: + pass + # Broadcast environment change to all windows + ctypes.windll.user32.SendMessageTimeoutW( + _HWND_BROADCAST, _WM_SETTINGCHANGE, 0, + "Environment", _SMTO_ABORTIFHUNG, 5000, None + ) + return True + except Exception: + log.warning("Failed to set QT_ACCESSIBILITY in registry", exc_info=True) + return False + + # Window type classification — # LINE has two main window modes: # "AllInOneWindow" — sidebar (chat list) + chat area in one window @@ -3481,11 +3545,13 @@ def __init__(self, *args, **kwargs): # Read and cache LINE installation info self._lineVersion = _readLineVersion() self._lineLanguage = _readLineLanguage() + self._qtAccessibleSet = _isQtAccessibleSet() log.info( f"LINE AppModule loaded for process: {self.processID}, " f"exe: {self.appName}, " f"lineVersion: {self._lineVersion}, " - f"lineLanguage: {self._lineLanguage}" + f"lineLanguage: {self._lineLanguage}, " + f"qtAccessible: {self._qtAccessibleSet}" ) def chooseNVDAObjectOverlayClasses(self, obj, clsList): @@ -6010,10 +6076,28 @@ def script_reportLineInfo(self, gesture): parts.append(_("視窗類型: {type}").format( type=typeNames.get(winType, winType) )) + qtA11y = _isQtAccessibleSet() + parts.append( + _("Qt 無障礙: 已啟用") if qtA11y else _("Qt 無障礙: 未啟用") + ) msg = ", ".join(parts) ui.message(msg) log.info(f"LINE info: {msg}") + def script_toggleQtAccessible(self, gesture): + """Toggle QT_ACCESSIBILITY=1 user environment variable.""" + currentlySet = _isQtAccessibleSet() + if currentlySet: + if _setQtAccessible(False): + ui.message(_("已移除 QT_ACCESSIBILITY 環境變數,重啟 LINE 後生效")) + else: + ui.message(_("移除 QT_ACCESSIBILITY 環境變數失敗")) + else: + if _setQtAccessible(True): + ui.message(_("已設定 QT_ACCESSIBILITY=1,重啟 LINE 後生效")) + else: + ui.message(_("設定 QT_ACCESSIBILITY 環境變數失敗")) + def _pollFileDialog(self): """Poll to detect when the file dialog closes, then resume addon. diff --git a/addon/globalPlugins/lineDesktopHelper.py b/addon/globalPlugins/lineDesktopHelper.py index 03ea34d..e464011 100644 --- a/addon/globalPlugins/lineDesktopHelper.py +++ b/addon/globalPlugins/lineDesktopHelper.py @@ -8,11 +8,65 @@ from logHandler import log import gui import wx +import winreg +import ctypes import addonHandler addonHandler.initTranslation() +# --------------------------------------------------------------------------- +# Qt accessibility environment variable helpers (duplicated from line.py +# so the global plugin can toggle the setting even when LINE is not running) +# --------------------------------------------------------------------------- + +_QT_ACCESSIBILITY_ENV_NAME = "QT_ACCESSIBILITY" +_HWND_BROADCAST = 0xFFFF +_WM_SETTINGCHANGE = 0x001A +_SMTO_ABORTIFHUNG = 0x0002 + + +def _isQtAccessibleSet(): + """Check if QT_ACCESSIBILITY=1 is set in user environment variables.""" + try: + with winreg.OpenKey( + winreg.HKEY_CURRENT_USER, "Environment", 0, winreg.KEY_READ + ) as key: + value, _ = winreg.QueryValueEx(key, _QT_ACCESSIBILITY_ENV_NAME) + return str(value) == "1" + except FileNotFoundError: + return False + except Exception: + return False + + +def _setQtAccessible(enable=True): + """Set or remove QT_ACCESSIBILITY in user environment variables.""" + try: + with winreg.OpenKey( + winreg.HKEY_CURRENT_USER, "Environment", 0, + winreg.KEY_SET_VALUE | winreg.KEY_READ + ) as key: + if enable: + winreg.SetValueEx( + key, _QT_ACCESSIBILITY_ENV_NAME, 0, + winreg.REG_SZ, "1" + ) + else: + try: + winreg.DeleteValue(key, _QT_ACCESSIBILITY_ENV_NAME) + except FileNotFoundError: + pass + ctypes.windll.user32.SendMessageTimeoutW( + _HWND_BROADCAST, _WM_SETTINGCHANGE, 0, + "Environment", _SMTO_ABORTIFHUNG, 5000, None + ) + return True + except Exception: + log.warning("Failed to set QT_ACCESSIBILITY in registry", exc_info=True) + return False + + def _getLineAppModule(): """Find and return the LINE appModule instance, or None.""" for app in appModuleHandler.runningTable.values(): @@ -144,6 +198,15 @@ def _createToolsMenu(self): _("跳到通話視窗(&F)") + "\tNVDA+Windows+F", ) + self._lineSubMenu.AppendSeparator() + + # ── Settings ── + self._qtAccessibleItem = self._lineSubMenu.Append( + wx.ID_ANY, + # Translators: Menu item for toggling Qt accessibility env var + _("切換 Qt 無障礙環境變數(&Q)"), + ) + # Bind events gui.mainFrame.sysTrayIcon.Bind( wx.EVT_MENU, self._onAllChats, self._allChatsItem @@ -187,6 +250,9 @@ def _createToolsMenu(self): gui.mainFrame.sysTrayIcon.Bind( wx.EVT_MENU, self._onFocusCallWindow, self._focusCallItem ) + gui.mainFrame.sysTrayIcon.Bind( + wx.EVT_MENU, self._onToggleQtAccessible, self._qtAccessibleItem + ) # Add the submenu to NVDA's Tools menu self._toolsMenu = gui.mainFrame.sysTrayIcon.toolsMenu @@ -429,6 +495,28 @@ def _announceCallWindow(): log.warning(f"LINE focusCallWindow error: {e}", exc_info=True) ui.message(_("跳到通話視窗功能錯誤: {error}").format(error=e)) + def _onToggleQtAccessible(self, evt): + wx.CallAfter(self._doToggleQtAccessible) + + def _doToggleQtAccessible(self): + import ui + lineApp = _getLineAppModule() + if lineApp and hasattr(lineApp, 'script_toggleQtAccessible'): + lineApp.script_toggleQtAccessible(None) + return + # LINE not running — toggle the env var directly + currentlySet = _isQtAccessibleSet() + if currentlySet: + if _setQtAccessible(False): + ui.message(_("已移除 QT_ACCESSIBILITY 環境變數,重啟 LINE 後生效")) + else: + ui.message(_("移除 QT_ACCESSIBILITY 環境變數失敗")) + else: + if _setQtAccessible(True): + ui.message(_("已設定 QT_ACCESSIBILITY=1,重啟 LINE 後生效")) + else: + ui.message(_("設定 QT_ACCESSIBILITY 環境變數失敗")) + def terminate(self, *args, **kwargs): self._removeToolsMenu() super().terminate(*args, **kwargs)