From 68bc571a835c7222e538728cde7c38ffb12af7d3 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Thu, 14 Aug 2025 20:53:03 -0400 Subject: [PATCH] Fixed sleep mode after a long and fierce battle. --- distro-packages/Arch-Linux/PKGBUILD | 2 +- src/cthulhu/braille.py | 20 ++ src/cthulhu/common_keyboardmap.py | 3 +- src/cthulhu/cthulhuVersion.py | 2 +- src/cthulhu/meson.build | 1 + src/cthulhu/script.py | 5 + src/cthulhu/script_manager.py | 13 + src/cthulhu/scripts/default.py | 11 +- .../scripts/sleepmode/braille_generator.py | 43 +++ src/cthulhu/scripts/sleepmode/meson.build | 2 + src/cthulhu/scripts/sleepmode/script.py | 263 +++++++----------- .../scripts/sleepmode/speech_generator.py | 62 +++++ src/cthulhu/sleep_mode_manager.py | 196 +++++++++++++ src/cthulhu/speech.py | 38 +++ 14 files changed, 484 insertions(+), 177 deletions(-) create mode 100644 src/cthulhu/scripts/sleepmode/braille_generator.py create mode 100644 src/cthulhu/scripts/sleepmode/speech_generator.py create mode 100644 src/cthulhu/sleep_mode_manager.py diff --git a/distro-packages/Arch-Linux/PKGBUILD b/distro-packages/Arch-Linux/PKGBUILD index a538a87..f4c597b 100644 --- a/distro-packages/Arch-Linux/PKGBUILD +++ b/distro-packages/Arch-Linux/PKGBUILD @@ -1,7 +1,7 @@ # Maintainer: Storm Dragon pkgname=cthulhu -pkgver=2025.08.11 +pkgver=2025.08.12 pkgrel=1 pkgdesc="Desktop-agnostic screen reader with plugin system, forked from Orca" url="https://git.stormux.org/storm/cthulhu" diff --git a/src/cthulhu/braille.py b/src/cthulhu/braille.py index 8297cd5..aa86f6b 100644 --- a/src/cthulhu/braille.py +++ b/src/cthulhu/braille.py @@ -1576,6 +1576,16 @@ def displayRegions(regionInfo, flashTime=0): comes along or the user presses a cursor routing key. """ + # Block braille for applications in sleep mode + from . import cthulhu_state + from . import sleep_mode_manager + if cthulhu_state.activeScript and hasattr(cthulhu_state.activeScript, 'app'): + sleepModeManager = sleep_mode_manager.getManager() + if sleepModeManager.isActiveForApp(cthulhu_state.activeScript.app): + from . import debug + debug.printMessage(debug.LEVEL_INFO, "BRAILLE: Blocked by sleep mode", True) + return + _initFlash(flashTime) regions = regionInfo[0] focusedRegion = regionInfo[1] @@ -1602,6 +1612,16 @@ def displayMessage(message, cursor=-1, flashTime=0): comes along or the user presses a cursor routing key. """ + # Block braille for applications in sleep mode + from . import cthulhu_state + from . import sleep_mode_manager + if cthulhu_state.activeScript and hasattr(cthulhu_state.activeScript, 'app'): + sleepModeManager = sleep_mode_manager.getManager() + if sleepModeManager.isActiveForApp(cthulhu_state.activeScript.app): + from . import debug + debug.printMessage(debug.LEVEL_INFO, f"BRAILLE: Blocked by sleep mode: '{message}'", True) + return + _initFlash(flashTime) clear() region = Region(message, cursor) diff --git a/src/cthulhu/common_keyboardmap.py b/src/cthulhu/common_keyboardmap.py index d768a86..cfc054e 100644 --- a/src/cthulhu/common_keyboardmap.py +++ b/src/cthulhu/common_keyboardmap.py @@ -44,6 +44,7 @@ CTHULHU_SHIFT_MODIFIER_MASK = keybindings.CTHULHU_SHIFT_MODIFIER_MASK CTHULHU_CTRL_MODIFIER_MASK = keybindings.CTHULHU_CTRL_MODIFIER_MASK CTHULHU_ALT_MODIFIER_MASK = keybindings.CTHULHU_ALT_MODIFIER_MASK CTHULHU_CTRL_ALT_MODIFIER_MASK = keybindings.CTHULHU_CTRL_ALT_MODIFIER_MASK +SHIFT_MODIFIER_MASK = keybindings.SHIFT_MODIFIER_MASK SHIFT_ALT_MODIFIER_MASK = keybindings.SHIFT_ALT_MODIFIER_MASK keymap = ( @@ -57,7 +58,7 @@ keymap = ( ("BackSpace", defaultModifierMask, CTHULHU_MODIFIER_MASK, "bypassNextCommandHandler"), - ("q", defaultModifierMask, CTHULHU_CTRL_ALT_MODIFIER_MASK | CTHULHU_SHIFT_MODIFIER_MASK, + ("q", defaultModifierMask, CTHULHU_CTRL_MODIFIER_MASK | SHIFT_MODIFIER_MASK, "toggleSleepModeHandler"), ("q", defaultModifierMask, CTHULHU_MODIFIER_MASK, diff --git a/src/cthulhu/cthulhuVersion.py b/src/cthulhu/cthulhuVersion.py index 44267ae..13b7723 100644 --- a/src/cthulhu/cthulhuVersion.py +++ b/src/cthulhu/cthulhuVersion.py @@ -23,5 +23,5 @@ # Fork of Orca Screen Reader (GNOME) # Original source: https://gitlab.gnome.org/GNOME/orca -version = "2025.08.12" +version = "2025.08.14" codeName = "testing" diff --git a/src/cthulhu/meson.build b/src/cthulhu/meson.build index b720764..582d143 100644 --- a/src/cthulhu/meson.build +++ b/src/cthulhu/meson.build @@ -71,6 +71,7 @@ cthulhu_python_sources = files([ 'settings.py', 'settings_manager.py', 'signal_manager.py', + 'sleep_mode_manager.py', 'sound.py', 'sound_generator.py', 'speech_and_verbosity_manager.py', diff --git a/src/cthulhu/script.py b/src/cthulhu/script.py index 459e30f..f6c7c0a 100644 --- a/src/cthulhu/script.py +++ b/src/cthulhu/script.py @@ -68,6 +68,7 @@ from . import script_manager from . import script_utilities from . import settings from . import settings_manager +from . import sleep_mode_manager from . import sound_generator from . import speech_and_verbosity_manager from . import speech_generator @@ -638,5 +639,9 @@ class Script: """Called when this script is deactivated.""" pass + def getSleepModeManager(self): + """Returns the sleep mode manager for this script.""" + return sleep_mode_manager.getManager() + def getTransferableAttributes(self): return {} diff --git a/src/cthulhu/script_manager.py b/src/cthulhu/script_manager.py index a8508d2..f01c409 100644 --- a/src/cthulhu/script_manager.py +++ b/src/cthulhu/script_manager.py @@ -44,6 +44,7 @@ class ScriptManager: self.appScripts = {} self.toolkitScripts = {} self.customScripts = {} + self._sleepModeScripts = {} self._appModules = apps.__all__ self._toolkitModules = toolkits.__all__ self._defaultScript = None @@ -300,6 +301,18 @@ class ScriptManager: return appScript + def getOrCreateSleepModeScript(self, app): + """Gets or creates the sleep mode script.""" + script = self._sleepModeScripts.get(app) + if script is not None: + return script + + # Import sleepmode dynamically to avoid circular imports + from .scripts import sleepmode + script = sleepmode.Script(app) + self._sleepModeScripts[app] = script + return script + def setActiveScript(self, newScript, reason=None): """Set the new active script. diff --git a/src/cthulhu/scripts/default.py b/src/cthulhu/scripts/default.py index 86b3192..8c38dfc 100644 --- a/src/cthulhu/scripts/default.py +++ b/src/cthulhu/scripts/default.py @@ -785,14 +785,9 @@ class Script(script.Script): def toggleSleepMode(self, input_event=None): """Toggles between sleep mode and regular mode.""" - script_manager = _scriptManager - debug.printMessage(debug.LEVEL_INFO, f"SLEEP: Attempting to create sleepmode script for app: {self.app}", True) - sleepScript = script_manager._newNamedScript(self.app, "sleepmode") - debug.printMessage(debug.LEVEL_INFO, f"SLEEP: Result of _newNamedScript: {sleepScript}", True) - if sleepScript: - script_manager.setActiveScript(sleepScript, "Sleep mode toggled") - else: - self.presentMessage("Could not activate sleep mode") + # Sleep mode is now handled by the sleep mode manager + sleepModeManager = self.getSleepModeManager() + sleepModeManager.toggleSleepMode(self) return True def bypassNextCommand(self, inputEvent=None): diff --git a/src/cthulhu/scripts/sleepmode/braille_generator.py b/src/cthulhu/scripts/sleepmode/braille_generator.py new file mode 100644 index 0000000..abe134d --- /dev/null +++ b/src/cthulhu/scripts/sleepmode/braille_generator.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2024 Stormux +# Copyright (c) 2023 Igalia, S.L. +# Author: Joanmarie Diggs +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., Franklin Street, Fifth Floor, +# Boston MA 02110-1301 USA. +# +# Fork of Orca Screen Reader (GNOME) +# Original source: https://gitlab.gnome.org/GNOME/orca + +"""Braille Generator for Sleep Mode. Does nothing.""" + +__id__ = "$Id$" +__version__ = "$Revision$" +__date__ = "$Date$" +__copyright__ = "Copyright (c) 2024 Stormux" +__license__ = "LGPL" + +import cthulhu.debug as debug +import cthulhu.braille_generator as braille_generator + +class BrailleGenerator(braille_generator.BrailleGenerator): + """Braille Generator for Sleep Mode. Does nothing.""" + + def generateBraille(self, obj, **args): + """Generates braille for the given object. Returns empty list in sleep mode.""" + msg = "SLEEP MODE BRAILLE GENERATOR: Generating nothing." + debug.printMessage(debug.LEVEL_INFO, msg, True) + return [] \ No newline at end of file diff --git a/src/cthulhu/scripts/sleepmode/meson.build b/src/cthulhu/scripts/sleepmode/meson.build index 3e04407..aa90dfb 100644 --- a/src/cthulhu/scripts/sleepmode/meson.build +++ b/src/cthulhu/scripts/sleepmode/meson.build @@ -1,7 +1,9 @@ sleepmode_python_sources = files([ '__init__.py', + 'braille_generator.py', 'script.py', 'script_utilities.py', + 'speech_generator.py', ]) python3.install_sources( diff --git a/src/cthulhu/scripts/sleepmode/script.py b/src/cthulhu/scripts/sleepmode/script.py index c118105..58f8b3e 100644 --- a/src/cthulhu/scripts/sleepmode/script.py +++ b/src/cthulhu/scripts/sleepmode/script.py @@ -1,9 +1,8 @@ #!/usr/bin/env python3 # # Copyright (c) 2024 Stormux -# Copyright (c) 2010-2012 The Orca Team -# Copyright (c) 2012 Igalia, S.L. -# Copyright (c) 2005-2010 Sun Microsystems Inc. +# Copyright (c) 2023 Igalia, S.L. +# Author: Joanmarie Diggs # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -23,7 +22,12 @@ # Fork of Orca Screen Reader (GNOME) # Original source: https://gitlab.gnome.org/GNOME/orca -"""Script for sleep mode where Cthulhu ignores events and commands.""" +"""Script for sleep mode where Cthulhu ignores events and commands. + +This script is now a minimal implementation that relies on the global +sleep mode flag in cthulhu_state.sleepMode to block speech and braille +at the system level. +""" __id__ = "$Id$" __version__ = "$Revision$" @@ -31,195 +35,122 @@ __date__ = "$Date$" __copyright__ = "Copyright (c) 2024 Stormux" __license__ = "LGPL" -import cthulhu.debug as debug import cthulhu.scripts.default as default -import cthulhu.input_event as input_event -import cthulhu.keybindings as keybindings -import cthulhu.messages as messages -import cthulhu.script_manager as script_manager -from cthulhu.ax_object import AXObject -from cthulhu.ax_utilities import AXUtilities - -_scriptManager = script_manager.getManager() +import cthulhu.debug as debug +import cthulhu.sleep_mode_manager as sleep_mode_manager class Script(default.Script): - """The sleep-mode script.""" + """The sleep mode script. + + This script now relies on the global sleep mode flag for blocking + speech and braille output at the system level. It only handles + keybinding management and basic event blocking. + """ def __init__(self, app): super().__init__(app) - self.presentIfInactive = True def activate(self): """Called when this script is activated.""" - tokens = ["SLEEP MODE: Activating script for", self.app] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - # Only keep the sleep mode toggle binding active - self.removeKeyGrabs() - self.addKeyGrabs() + debug.printMessage(debug.LEVEL_INFO, "SLEEP MODE SCRIPT: Activating", True) + super().activate() - # Present sleep mode status - self.clearBraille() - app_name = AXObject.get_name(self.app) if self.app else "unknown application" - message = messages.SLEEP_MODE_ENABLED_FOR % app_name - self.presentMessage(message) + # Get the manager and add its bindings and handlers + manager = sleep_mode_manager.getManager() + managerBindings = manager.getBindings() + if hasattr(managerBindings, 'keyBindings'): + for binding in managerBindings.keyBindings: + self.keyBindings.add(binding) + self.inputEventHandlers.update(manager.getHandlers()) + + # Remove most key grabs except sleep mode toggle + self.removeKeyGrabs() + + debug.printMessage(debug.LEVEL_INFO, "SLEEP MODE SCRIPT: Activated successfully", True) def deactivate(self): """Called when this script is deactivated.""" - tokens = ["SLEEP MODE: De-activating script for", self.app] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - self.removeKeyGrabs() - - def getKeyBindings(self): - """Only provide the key binding needed to exit sleep mode.""" - keyBindings = keybindings.KeyBindings() - keyBindings.load([("q", keybindings.defaultModifierMask, - keybindings.CTHULHU_CTRL_ALT_MODIFIER_MASK | keybindings.SHIFT_ALT_MODIFIER_MASK, - "toggleSleepModeHandler")], - self.inputEventHandlers) - return keyBindings + debug.printMessage(debug.LEVEL_INFO, "SLEEP MODE SCRIPT: Deactivating", True) + + # Restore key grabs + self.addKeyGrabs() + + super().deactivate() - def setupInputEventHandlers(self): - """Sets up the input event handlers for sleep mode.""" - super().setupInputEventHandlers() - self.inputEventHandlers["toggleSleepModeHandler"] = \ - input_event.InputEventHandler( - Script.toggleSleepMode, - "Toggles sleep mode on/off") + def removeKeyGrabs(self): + """Remove key grabs except for sleep mode toggle.""" + + try: + self.grab_ids = [] + for keyBinding in self.keyBindings: + if hasattr(keyBinding, 'handler') and hasattr(keyBinding.handler, 'function'): + if hasattr(keyBinding.handler.function, '__name__'): + if 'toggleSleepMode' in keyBinding.handler.function.__name__: + # Keep sleep mode toggle + try: + import cthulhu + grab_id = cthulhu.addKeyGrab(keyBinding) + if grab_id: + self.grab_ids.append(grab_id) + debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Kept sleep toggle key grab: {grab_id}", True) + except Exception as e: + debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Error keeping key grab: {e}", True) + else: + debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Skipping key grab for {keyBinding.handler.function.__name__}", True) + + except Exception as e: + debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Error in removeKeyGrabs: {e}", True) - def toggleSleepMode(self, input_event=None): - """Toggles between sleep mode and regular mode.""" - script_manager = _scriptManager - script_manager.setActiveScript(script_manager.getDefaultScript(), "Sleep mode toggled") - self.presentMessage(messages.SLEEP_MODE_DISABLED_FOR % AXObject.get_name(self.app)) - return True - - def locusOfFocusChanged(self, event, oldLocusOfFocus, newLocusOfFocus): - """Handles changes of focus of interest to the script.""" - tokens = ["SLEEP MODE: focus changed from", oldLocusOfFocus, "to", newLocusOfFocus] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - if oldLocusOfFocus is None and AXUtilities.is_application(AXObject.get_parent(newLocusOfFocus)): - self.clearBraille() - self.presentMessage(messages.SLEEP_MODE_ENABLED_FOR % AXObject.get_name(self.app)) - return - - msg = "SLEEP MODE: Ignoring event." - debug.printMessage(debug.LEVEL_INFO, msg, True) - - def presentKeyboardEvent(self, event): - """Prevents keyboard echo in sleep mode.""" - msg = "SLEEP MODE: Not presenting keyboard event." - debug.printMessage(debug.LEVEL_INFO, msg, True) - return True - - def updateBraille(self, obj, **args): - """Don't update braille in sleep mode.""" - msg = "SLEEP MODE: Not updating braille." - debug.printMessage(debug.LEVEL_INFO, msg, True) - - # Event handler overrides - all do nothing - def onActiveChanged(self, event): - msg = "SLEEP MODE: Ignoring event." - debug.printMessage(debug.LEVEL_INFO, msg, True) - - def onActiveDescendantChanged(self, event): - msg = "SLEEP MODE: Ignoring event." - debug.printMessage(debug.LEVEL_INFO, msg, True) - - def onBusyChanged(self, event): - msg = "SLEEP MODE: Ignoring event." - debug.printMessage(debug.LEVEL_INFO, msg, True) + def addKeyGrabs(self): + """Add back all key grabs.""" + + try: + # Remove our limited grabs first + if hasattr(self, 'grab_ids'): + import cthulhu + for grab_id in self.grab_ids: + try: + cthulhu.removeKeyGrab(grab_id) + debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Removed key grab: {grab_id}", True) + except Exception as e: + debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Error removing key grab {grab_id}: {e}", True) + self.grab_ids = [] + + # Let the parent class restore all grabs + super().addKeyGrabs() + + except Exception as e: + debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Error in addKeyGrabs: {e}", True) + # Block common event handlers as an additional layer of protection def onCaretMoved(self, event): - msg = "SLEEP MODE: Ignoring event." - debug.printMessage(debug.LEVEL_INFO, msg, True) - - def onCheckedChanged(self, event): - msg = "SLEEP MODE: Ignoring event." - debug.printMessage(debug.LEVEL_INFO, msg, True) - - def onChildrenAdded(self, event): - msg = "SLEEP MODE: Ignoring event." - debug.printMessage(debug.LEVEL_INFO, msg, True) - - def onChildrenRemoved(self, event): - msg = "SLEEP MODE: Ignoring event." - debug.printMessage(debug.LEVEL_INFO, msg, True) - - def onColumnReordered(self, event): - msg = "SLEEP MODE: Ignoring event." - debug.printMessage(debug.LEVEL_INFO, msg, True) - - def onDocumentLoadComplete(self, event): - msg = "SLEEP MODE: Ignoring event." - debug.printMessage(debug.LEVEL_INFO, msg, True) - - def onDocumentLoadStopped(self, event): - msg = "SLEEP MODE: Ignoring event." - debug.printMessage(debug.LEVEL_INFO, msg, True) - - def onDocumentReload(self, event): - msg = "SLEEP MODE: Ignoring event." - debug.printMessage(debug.LEVEL_INFO, msg, True) - - def onExpandedChanged(self, event): - msg = "SLEEP MODE: Ignoring event." - debug.printMessage(debug.LEVEL_INFO, msg, True) - - def onFocus(self, event): - msg = "SLEEP MODE: Ignoring event." - debug.printMessage(debug.LEVEL_INFO, msg, True) - - def onFocusedChanged(self, event): - msg = "SLEEP MODE: Ignoring event." - debug.printMessage(debug.LEVEL_INFO, msg, True) - - def onMouseButton(self, event): - msg = "SLEEP MODE: Ignoring event." - debug.printMessage(debug.LEVEL_INFO, msg, True) - - def onNameChanged(self, event): - msg = "SLEEP MODE: Ignoring event." - debug.printMessage(debug.LEVEL_INFO, msg, True) - - def onSelectedChanged(self, event): - msg = "SLEEP MODE: Ignoring event." - debug.printMessage(debug.LEVEL_INFO, msg, True) - - def onSelectionChanged(self, event): - msg = "SLEEP MODE: Ignoring event." - debug.printMessage(debug.LEVEL_INFO, msg, True) - - def onShowingChanged(self, event): - msg = "SLEEP MODE: Ignoring event." - debug.printMessage(debug.LEVEL_INFO, msg, True) - - def onTextAttributesChanged(self, event): - msg = "SLEEP MODE: Ignoring event." - debug.printMessage(debug.LEVEL_INFO, msg, True) + """Block caret movement events.""" + debug.printMessage(debug.LEVEL_INFO, "SLEEP MODE: Blocking onCaretMoved", True) + return True def onTextDeleted(self, event): - msg = "SLEEP MODE: Ignoring event." - debug.printMessage(debug.LEVEL_INFO, msg, True) + """Block text deletion events.""" + debug.printMessage(debug.LEVEL_INFO, "SLEEP MODE: Blocking onTextDeleted", True) + return True def onTextInserted(self, event): - msg = "SLEEP MODE: Ignoring event." - debug.printMessage(debug.LEVEL_INFO, msg, True) + """Block text insertion events.""" + debug.printMessage(debug.LEVEL_INFO, "SLEEP MODE: Blocking onTextInserted", True) + return True - def onTextSelectionChanged(self, event): - msg = "SLEEP MODE: Ignoring event." - debug.printMessage(debug.LEVEL_INFO, msg, True) + def onFocusChanged(self, event): + """Block focus change events.""" + debug.printMessage(debug.LEVEL_INFO, "SLEEP MODE: Blocking onFocusChanged", True) + return True def onWindowActivated(self, event): - """Callback for window:activate accessibility events.""" - self.clearBraille() - self.presentMessage(messages.SLEEP_MODE_ENABLED_FOR % AXObject.get_name(self.app)) + """Block window activation events.""" + debug.printMessage(debug.LEVEL_INFO, "SLEEP MODE: Blocking onWindowActivated", True) + return True - def onWindowDeactivated(self, event): - msg = "SLEEP MODE: Ignoring event." - debug.printMessage(debug.LEVEL_INFO, msg, True) def getScript(app): """Returns the script for the given application.""" - return Script(app) + return Script(app) \ No newline at end of file diff --git a/src/cthulhu/scripts/sleepmode/speech_generator.py b/src/cthulhu/scripts/sleepmode/speech_generator.py new file mode 100644 index 0000000..686d1cf --- /dev/null +++ b/src/cthulhu/scripts/sleepmode/speech_generator.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2024 Stormux +# Copyright (c) 2023 Igalia, S.L. +# Author: Joanmarie Diggs +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., Franklin Street, Fifth Floor, +# Boston MA 02110-1301 USA. +# +# Fork of Orca Screen Reader (GNOME) +# Original source: https://gitlab.gnome.org/GNOME/orca + +"""Speech Generator for Sleep Mode. Does nothing.""" + +__id__ = "$Id$" +__version__ = "$Revision$" +__date__ = "$Date$" +__copyright__ = "Copyright (c) 2024 Stormux" +__license__ = "LGPL" + +import cthulhu.debug as debug +import cthulhu.speech_generator as speech_generator + +class SpeechGenerator(speech_generator.SpeechGenerator): + """Speech Generator for Sleep Mode. Does nothing.""" + + def generateSpeech(self, obj, **args): + """Generates speech for the given object. Returns empty list in sleep mode.""" + msg = "SLEEP MODE SPEECH GENERATOR: Generating nothing." + debug.printMessage(debug.LEVEL_INFO, msg, True) + print("DEBUG: SLEEP MODE SPEECH GENERATOR CALLED!") + return [] + + def generateSelectedItems(self, obj, **args): + """Override selected items generation.""" + msg = "SLEEP MODE SPEECH GENERATOR: generateSelectedItems - generating nothing." + debug.printMessage(debug.LEVEL_INFO, msg, True) + return [] + + def generateContext(self, obj, **args): + """Override context generation.""" + msg = "SLEEP MODE SPEECH GENERATOR: generateContext - generating nothing." + debug.printMessage(debug.LEVEL_INFO, msg, True) + return [] + + def generateTitle(self, obj, **args): + """Override title generation.""" + msg = "SLEEP MODE SPEECH GENERATOR: generateTitle - generating nothing." + debug.printMessage(debug.LEVEL_INFO, msg, True) + return [] \ No newline at end of file diff --git a/src/cthulhu/sleep_mode_manager.py b/src/cthulhu/sleep_mode_manager.py new file mode 100644 index 0000000..0bc28c3 --- /dev/null +++ b/src/cthulhu/sleep_mode_manager.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2024 Stormux +# Copyright (c) 2024 Igalia, S.L. +# Author: Joanmarie Diggs +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., Franklin Street, Fifth Floor, +# Boston MA 02110-1301 USA. +# +# Fork of Orca Screen Reader (GNOME) +# Original source: https://gitlab.gnome.org/GNOME/orca + +"""Module for sleep mode management.""" + +__id__ = "$Id$" +__version__ = "$Revision$" +__date__ = "$Date$" +__copyright__ = "Copyright (c) 2024 Stormux" +__license__ = "LGPL" + +import time +from gi.repository import GLib +import cthulhu.braille as braille +import cthulhu.cmdnames as cmdnames +import cthulhu.debug as debug +import cthulhu.input_event as input_event +import cthulhu.keybindings as keybindings +import cthulhu.messages as messages +import cthulhu.script_manager as script_manager +from cthulhu.ax_object import AXObject + +class SleepModeManager: + """Provides sleep mode implementation.""" + + def __init__(self): + self._handlers = self.getHandlers(True) + self._bindings = keybindings.KeyBindings() + self._apps = [] + self._lastToggleTime = 0 + self._toggleDebounceDelay = 0.1 # 100ms debounce (reduced for better responsiveness) + + def getBindings(self, refresh=False, isDesktop=True): + """Returns the sleep-mode-manager keybindings.""" + + if refresh: + msg = f"SLEEP MODE MANAGER: Refreshing bindings. Is desktop: {isDesktop}" + debug.printMessage(debug.LEVEL_INFO, msg, True) + self._setupBindings() + elif len(self._bindings.keyBindings) == 0: + self._setupBindings() + + return self._bindings + + def getHandlers(self, refresh=False): + """Returns the sleep-mode-manager handlers.""" + + if refresh: + msg = "SLEEP MODE MANAGER: Refreshing handlers." + debug.printMessage(debug.LEVEL_INFO, msg, True) + self._setupHandlers() + + return self._handlers + + def isActiveForApp(self, app): + """Returns True if sleep mode is active for app.""" + + result = bool(app and hash(app) in self._apps) + if result: + tokens = ["SLEEP MODE MANAGER: Is active for", app] + debug.printTokens(debug.LEVEL_INFO, tokens, True) + return result + + def _setupHandlers(self): + """Sets up and returns the sleep-mode-manager input event handlers.""" + + self._handlers = {} + + self._handlers["toggleSleepMode"] = \ + input_event.InputEventHandler( + self.toggleSleepMode, + cmdnames.TOGGLE_SLEEP_MODE) + + msg = "SLEEP MODE MANAGER: Handlers set up." + debug.printMessage(debug.LEVEL_INFO, msg, True) + + def _setupBindings(self): + """Sets up and returns the sleep-mode-manager key bindings.""" + + self._bindings = keybindings.KeyBindings() + + self._bindings.add( + keybindings.KeyBinding( + "q", + keybindings.defaultModifierMask, + keybindings.CTHULHU_CTRL_MODIFIER_MASK | keybindings.SHIFT_MODIFIER_MASK, + self._handlers["toggleSleepMode"])) + + msg = "SLEEP MODE MANAGER: Bindings set up." + debug.printMessage(debug.LEVEL_INFO, msg, True) + + def toggleSleepMode(self, script, inputEvent=None, notifyUser=True): + """Toggles sleep mode for the active application.""" + + tokens = ["SLEEP MODE MANAGER: toggleSleepMode. Script:", script, + "Event:", inputEvent, "notifyUser:", notifyUser] + debug.printTokens(debug.LEVEL_INFO, tokens, True) + + # Debounce rapid key presses + currentTime = time.time() + timeSinceLastToggle = currentTime - self._lastToggleTime + debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Toggle attempt - time since last: {timeSinceLastToggle:.3f}s", True) + + if timeSinceLastToggle < self._toggleDebounceDelay: + debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Ignoring rapid toggle (delay: {timeSinceLastToggle:.3f}s < {self._toggleDebounceDelay}s)", True) + return True + + self._lastToggleTime = currentTime + debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Processing toggle - app: {AXObject.get_name(script.app) if script and script.app else 'None'}", True) + + if not (script and script.app): + return True + + from . import cthulhu_state + scriptManager = script_manager.getManager() + + if self.isActiveForApp(script.app): + # Turning OFF sleep mode + self._apps.remove(hash(script.app)) + newScript = scriptManager.getScript(script.app) + if notifyUser: + newScript.presentMessage( + messages.SLEEP_MODE_DISABLED_FOR % AXObject.get_name(script.app)) + scriptManager.setActiveScript(newScript, "Sleep mode toggled off") + debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Disabled for {AXObject.get_name(script.app)}", True) + # Reset debounce timer after successful toggle + self._lastToggleTime = 0 + return True + + # Turning ON sleep mode + braille.clear() + if notifyUser: + # Announce BEFORE switching to sleep mode script + script.presentMessage(messages.SLEEP_MODE_ENABLED_FOR % AXObject.get_name(script.app)) + debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Scheduling delayed activation for {AXObject.get_name(script.app)}", True) + # Use a small delay to ensure the message is spoken before switching scripts + GLib.timeout_add(250, self._enableSleepModeDelayed, script, scriptManager) + else: + debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Immediate activation for {AXObject.get_name(script.app)}", True) + self._enableSleepModeDelayed(script, scriptManager) + + return True + + def _enableSleepModeDelayed(self, script, scriptManager): + """Enable sleep mode after a small delay to allow announcement.""" + + debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: _enableSleepModeDelayed called for {AXObject.get_name(script.app) if script and script.app else 'None'}", True) + + try: + debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Getting sleep script for {AXObject.get_name(script.app)}", True) + sleepScript = scriptManager.getOrCreateSleepModeScript(script.app) + debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Got sleep script: {sleepScript}", True) + + debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Setting active script", True) + scriptManager.setActiveScript(sleepScript, "Sleep mode toggled on") + debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Active script set successfully", True) + + debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Adding app to sleep list", True) + self._apps.append(hash(script.app)) + debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Enabled for {AXObject.get_name(script.app)} (delayed)", True) + # Reset debounce timer after successful toggle + self._lastToggleTime = 0 + except Exception as e: + debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Error enabling delayed: {e}", True) + import traceback + debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Exception traceback: {traceback.format_exc()}", True) + + return False # Don't repeat the timeout + + +_manager = SleepModeManager() + +def getManager(): + """Returns the Sleep Mode Manager singleton.""" + return _manager \ No newline at end of file diff --git a/src/cthulhu/speech.py b/src/cthulhu/speech.py index 31ddc6d..be81c3e 100644 --- a/src/cthulhu/speech.py +++ b/src/cthulhu/speech.py @@ -153,6 +153,19 @@ def sayAll(utteranceIterator, progressCallback): def _speak(text, acss, interrupt): """Speaks the individual string using the given ACSS.""" + # Block speech for applications in sleep mode, except sleep mode status messages + from . import cthulhu_state + from . import sleep_mode_manager + if cthulhu_state.activeScript and hasattr(cthulhu_state.activeScript, 'app'): + sleepModeManager = sleep_mode_manager.getManager() + if sleepModeManager.isActiveForApp(cthulhu_state.activeScript.app): + # Allow sleep mode status messages to get through + if "Sleep mode enabled" in text or "Sleep mode disabled" in text: + debug.printMessage(debug.LEVEL_INFO, f"SPEECH: Allowing sleep mode status: '{text}'", True) + else: + debug.printMessage(debug.LEVEL_INFO, f"SPEECH: Blocked by sleep mode: '{text}'", True) + return + if not _speechserver: logLine = f"SPEECH OUTPUT: '{text}' {acss}" debug.printMessage(debug.LEVEL_INFO, logLine, True) @@ -179,6 +192,31 @@ def speak(content, acss=None, interrupt=True): if settings.silenceSpeech: return + # Block speech for applications in sleep mode, except sleep mode status messages + from . import cthulhu_state + from . import sleep_mode_manager + if cthulhu_state.activeScript and hasattr(cthulhu_state.activeScript, 'app'): + sleepModeManager = sleep_mode_manager.getManager() + if sleepModeManager.isActiveForApp(cthulhu_state.activeScript.app): + # Allow sleep mode status messages to get through + if isinstance(content, str): + if "Sleep mode enabled" in content or "Sleep mode disabled" in content: + debug.printMessage(debug.LEVEL_INFO, f"SPEECH: Allowing sleep mode status: '{content}'", True) + else: + debug.printMessage(debug.LEVEL_INFO, f"SPEECH: Blocked by sleep mode: '{content}'", True) + return + elif isinstance(content, list): + # Check if any element in the content list contains sleep mode messages + content_str = str(content) + if "Sleep mode enabled" in content_str or "Sleep mode disabled" in content_str: + debug.printMessage(debug.LEVEL_INFO, f"SPEECH: Allowing sleep mode status list: '{content_str}'", True) + else: + debug.printMessage(debug.LEVEL_INFO, f"SPEECH: Blocked by sleep mode list: '{content_str}'", True) + return + else: + debug.printMessage(debug.LEVEL_INFO, f"SPEECH: Blocked by sleep mode: '{content}'", True) + return + validTypes = (str, list, speech_generator.Pause, speech_generator.LineBreak, ACSS) error = "SPEECH: bad content sent to speak(): '%s'"