Fixed sleep mode after a long and fierce battle.

This commit is contained in:
Storm Dragon
2025-08-14 20:53:03 -04:00
parent fb8c64a406
commit 68bc571a83
14 changed files with 484 additions and 177 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
# Maintainer: Storm Dragon <storm_dragon@stormux.org> # Maintainer: Storm Dragon <storm_dragon@stormux.org>
pkgname=cthulhu pkgname=cthulhu
pkgver=2025.08.11 pkgver=2025.08.12
pkgrel=1 pkgrel=1
pkgdesc="Desktop-agnostic screen reader with plugin system, forked from Orca" pkgdesc="Desktop-agnostic screen reader with plugin system, forked from Orca"
url="https://git.stormux.org/storm/cthulhu" url="https://git.stormux.org/storm/cthulhu"
+20
View File
@@ -1576,6 +1576,16 @@ def displayRegions(regionInfo, flashTime=0):
comes along or the user presses a cursor routing key. 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) _initFlash(flashTime)
regions = regionInfo[0] regions = regionInfo[0]
focusedRegion = regionInfo[1] focusedRegion = regionInfo[1]
@@ -1602,6 +1612,16 @@ def displayMessage(message, cursor=-1, flashTime=0):
comes along or the user presses a cursor routing key. 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) _initFlash(flashTime)
clear() clear()
region = Region(message, cursor) region = Region(message, cursor)
+2 -1
View File
@@ -44,6 +44,7 @@ CTHULHU_SHIFT_MODIFIER_MASK = keybindings.CTHULHU_SHIFT_MODIFIER_MASK
CTHULHU_CTRL_MODIFIER_MASK = keybindings.CTHULHU_CTRL_MODIFIER_MASK CTHULHU_CTRL_MODIFIER_MASK = keybindings.CTHULHU_CTRL_MODIFIER_MASK
CTHULHU_ALT_MODIFIER_MASK = keybindings.CTHULHU_ALT_MODIFIER_MASK CTHULHU_ALT_MODIFIER_MASK = keybindings.CTHULHU_ALT_MODIFIER_MASK
CTHULHU_CTRL_ALT_MODIFIER_MASK = keybindings.CTHULHU_CTRL_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 SHIFT_ALT_MODIFIER_MASK = keybindings.SHIFT_ALT_MODIFIER_MASK
keymap = ( keymap = (
@@ -57,7 +58,7 @@ keymap = (
("BackSpace", defaultModifierMask, CTHULHU_MODIFIER_MASK, ("BackSpace", defaultModifierMask, CTHULHU_MODIFIER_MASK,
"bypassNextCommandHandler"), "bypassNextCommandHandler"),
("q", defaultModifierMask, CTHULHU_CTRL_ALT_MODIFIER_MASK | CTHULHU_SHIFT_MODIFIER_MASK, ("q", defaultModifierMask, CTHULHU_CTRL_MODIFIER_MASK | SHIFT_MODIFIER_MASK,
"toggleSleepModeHandler"), "toggleSleepModeHandler"),
("q", defaultModifierMask, CTHULHU_MODIFIER_MASK, ("q", defaultModifierMask, CTHULHU_MODIFIER_MASK,
+1 -1
View File
@@ -23,5 +23,5 @@
# Fork of Orca Screen Reader (GNOME) # Fork of Orca Screen Reader (GNOME)
# Original source: https://gitlab.gnome.org/GNOME/orca # Original source: https://gitlab.gnome.org/GNOME/orca
version = "2025.08.12" version = "2025.08.14"
codeName = "testing" codeName = "testing"
+1
View File
@@ -71,6 +71,7 @@ cthulhu_python_sources = files([
'settings.py', 'settings.py',
'settings_manager.py', 'settings_manager.py',
'signal_manager.py', 'signal_manager.py',
'sleep_mode_manager.py',
'sound.py', 'sound.py',
'sound_generator.py', 'sound_generator.py',
'speech_and_verbosity_manager.py', 'speech_and_verbosity_manager.py',
+5
View File
@@ -68,6 +68,7 @@ from . import script_manager
from . import script_utilities from . import script_utilities
from . import settings from . import settings
from . import settings_manager from . import settings_manager
from . import sleep_mode_manager
from . import sound_generator from . import sound_generator
from . import speech_and_verbosity_manager from . import speech_and_verbosity_manager
from . import speech_generator from . import speech_generator
@@ -638,5 +639,9 @@ class Script:
"""Called when this script is deactivated.""" """Called when this script is deactivated."""
pass pass
def getSleepModeManager(self):
"""Returns the sleep mode manager for this script."""
return sleep_mode_manager.getManager()
def getTransferableAttributes(self): def getTransferableAttributes(self):
return {} return {}
+13
View File
@@ -44,6 +44,7 @@ class ScriptManager:
self.appScripts = {} self.appScripts = {}
self.toolkitScripts = {} self.toolkitScripts = {}
self.customScripts = {} self.customScripts = {}
self._sleepModeScripts = {}
self._appModules = apps.__all__ self._appModules = apps.__all__
self._toolkitModules = toolkits.__all__ self._toolkitModules = toolkits.__all__
self._defaultScript = None self._defaultScript = None
@@ -300,6 +301,18 @@ class ScriptManager:
return appScript 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): def setActiveScript(self, newScript, reason=None):
"""Set the new active script. """Set the new active script.
+3 -8
View File
@@ -785,14 +785,9 @@ class Script(script.Script):
def toggleSleepMode(self, input_event=None): def toggleSleepMode(self, input_event=None):
"""Toggles between sleep mode and regular mode.""" """Toggles between sleep mode and regular mode."""
script_manager = _scriptManager # Sleep mode is now handled by the sleep mode manager
debug.printMessage(debug.LEVEL_INFO, f"SLEEP: Attempting to create sleepmode script for app: {self.app}", True) sleepModeManager = self.getSleepModeManager()
sleepScript = script_manager._newNamedScript(self.app, "sleepmode") sleepModeManager.toggleSleepMode(self)
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")
return True return True
def bypassNextCommand(self, inputEvent=None): def bypassNextCommand(self, inputEvent=None):
@@ -0,0 +1,43 @@
#!/usr/bin/env python3
#
# Copyright (c) 2024 Stormux
# Copyright (c) 2023 Igalia, S.L.
# Author: Joanmarie Diggs <jdiggs@igalia.com>
#
# 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 []
@@ -1,7 +1,9 @@
sleepmode_python_sources = files([ sleepmode_python_sources = files([
'__init__.py', '__init__.py',
'braille_generator.py',
'script.py', 'script.py',
'script_utilities.py', 'script_utilities.py',
'speech_generator.py',
]) ])
python3.install_sources( python3.install_sources(
+97 -166
View File
@@ -1,9 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# #
# Copyright (c) 2024 Stormux # Copyright (c) 2024 Stormux
# Copyright (c) 2010-2012 The Orca Team # Copyright (c) 2023 Igalia, S.L.
# Copyright (c) 2012 Igalia, S.L. # Author: Joanmarie Diggs <jdiggs@igalia.com>
# Copyright (c) 2005-2010 Sun Microsystems Inc.
# #
# This library is free software; you can redistribute it and/or # This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public # modify it under the terms of the GNU Lesser General Public
@@ -23,7 +22,12 @@
# Fork of Orca Screen Reader (GNOME) # Fork of Orca Screen Reader (GNOME)
# Original source: https://gitlab.gnome.org/GNOME/orca # 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$" __id__ = "$Id$"
__version__ = "$Revision$" __version__ = "$Revision$"
@@ -31,195 +35,122 @@ __date__ = "$Date$"
__copyright__ = "Copyright (c) 2024 Stormux" __copyright__ = "Copyright (c) 2024 Stormux"
__license__ = "LGPL" __license__ = "LGPL"
import cthulhu.debug as debug
import cthulhu.scripts.default as default import cthulhu.scripts.default as default
import cthulhu.input_event as input_event import cthulhu.debug as debug
import cthulhu.keybindings as keybindings import cthulhu.sleep_mode_manager as sleep_mode_manager
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()
class Script(default.Script): 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): def __init__(self, app):
super().__init__(app) super().__init__(app)
self.presentIfInactive = True
def activate(self): def activate(self):
"""Called when this script is activated.""" """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 debug.printMessage(debug.LEVEL_INFO, "SLEEP MODE SCRIPT: Activating", True)
self.removeKeyGrabs() super().activate()
self.addKeyGrabs()
# Present sleep mode status # Get the manager and add its bindings and handlers
self.clearBraille() manager = sleep_mode_manager.getManager()
app_name = AXObject.get_name(self.app) if self.app else "unknown application" managerBindings = manager.getBindings()
message = messages.SLEEP_MODE_ENABLED_FOR % app_name if hasattr(managerBindings, 'keyBindings'):
self.presentMessage(message) 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): def deactivate(self):
"""Called when this script is deactivated.""" """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, debug.printMessage(debug.LEVEL_INFO, "SLEEP MODE SCRIPT: Deactivating", True)
keybindings.CTHULHU_CTRL_ALT_MODIFIER_MASK | keybindings.SHIFT_ALT_MODIFIER_MASK,
"toggleSleepModeHandler")], # Restore key grabs
self.inputEventHandlers) self.addKeyGrabs()
return keyBindings
super().deactivate()
def setupInputEventHandlers(self): def removeKeyGrabs(self):
"""Sets up the input event handlers for sleep mode.""" """Remove key grabs except for sleep mode toggle."""
super().setupInputEventHandlers()
self.inputEventHandlers["toggleSleepModeHandler"] = \ try:
input_event.InputEventHandler( self.grab_ids = []
Script.toggleSleepMode, for keyBinding in self.keyBindings:
"Toggles sleep mode on/off") 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): def addKeyGrabs(self):
"""Toggles between sleep mode and regular mode.""" """Add back all key grabs."""
script_manager = _scriptManager
script_manager.setActiveScript(script_manager.getDefaultScript(), "Sleep mode toggled") try:
self.presentMessage(messages.SLEEP_MODE_DISABLED_FOR % AXObject.get_name(self.app)) # Remove our limited grabs first
return True if hasattr(self, 'grab_ids'):
import cthulhu
def locusOfFocusChanged(self, event, oldLocusOfFocus, newLocusOfFocus): for grab_id in self.grab_ids:
"""Handles changes of focus of interest to the script.""" try:
tokens = ["SLEEP MODE: focus changed from", oldLocusOfFocus, "to", newLocusOfFocus] cthulhu.removeKeyGrab(grab_id)
debug.printTokens(debug.LEVEL_INFO, tokens, True) debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Removed key grab: {grab_id}", True)
if oldLocusOfFocus is None and AXUtilities.is_application(AXObject.get_parent(newLocusOfFocus)): except Exception as e:
self.clearBraille() debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Error removing key grab {grab_id}: {e}", True)
self.presentMessage(messages.SLEEP_MODE_ENABLED_FOR % AXObject.get_name(self.app)) self.grab_ids = []
return
# Let the parent class restore all grabs
msg = "SLEEP MODE: Ignoring event." super().addKeyGrabs()
debug.printMessage(debug.LEVEL_INFO, msg, True)
except Exception as e:
def presentKeyboardEvent(self, event): debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Error in addKeyGrabs: {e}", True)
"""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)
# Block common event handlers as an additional layer of protection
def onCaretMoved(self, event): def onCaretMoved(self, event):
msg = "SLEEP MODE: Ignoring event." """Block caret movement events."""
debug.printMessage(debug.LEVEL_INFO, msg, True) debug.printMessage(debug.LEVEL_INFO, "SLEEP MODE: Blocking onCaretMoved", True)
return 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)
def onTextDeleted(self, event): def onTextDeleted(self, event):
msg = "SLEEP MODE: Ignoring event." """Block text deletion events."""
debug.printMessage(debug.LEVEL_INFO, msg, True) debug.printMessage(debug.LEVEL_INFO, "SLEEP MODE: Blocking onTextDeleted", True)
return True
def onTextInserted(self, event): def onTextInserted(self, event):
msg = "SLEEP MODE: Ignoring event." """Block text insertion events."""
debug.printMessage(debug.LEVEL_INFO, msg, True) debug.printMessage(debug.LEVEL_INFO, "SLEEP MODE: Blocking onTextInserted", True)
return True
def onTextSelectionChanged(self, event): def onFocusChanged(self, event):
msg = "SLEEP MODE: Ignoring event." """Block focus change events."""
debug.printMessage(debug.LEVEL_INFO, msg, True) debug.printMessage(debug.LEVEL_INFO, "SLEEP MODE: Blocking onFocusChanged", True)
return True
def onWindowActivated(self, event): def onWindowActivated(self, event):
"""Callback for window:activate accessibility events.""" """Block window activation events."""
self.clearBraille() debug.printMessage(debug.LEVEL_INFO, "SLEEP MODE: Blocking onWindowActivated", True)
self.presentMessage(messages.SLEEP_MODE_ENABLED_FOR % AXObject.get_name(self.app)) return True
def onWindowDeactivated(self, event):
msg = "SLEEP MODE: Ignoring event."
debug.printMessage(debug.LEVEL_INFO, msg, True)
def getScript(app): def getScript(app):
"""Returns the script for the given application.""" """Returns the script for the given application."""
return Script(app) return Script(app)
@@ -0,0 +1,62 @@
#!/usr/bin/env python3
#
# Copyright (c) 2024 Stormux
# Copyright (c) 2023 Igalia, S.L.
# Author: Joanmarie Diggs <jdiggs@igalia.com>
#
# 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 []
+196
View File
@@ -0,0 +1,196 @@
#!/usr/bin/env python3
#
# Copyright (c) 2024 Stormux
# Copyright (c) 2024 Igalia, S.L.
# Author: Joanmarie Diggs <jdiggs@igalia.com>
#
# 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
+38
View File
@@ -153,6 +153,19 @@ def sayAll(utteranceIterator, progressCallback):
def _speak(text, acss, interrupt): def _speak(text, acss, interrupt):
"""Speaks the individual string using the given ACSS.""" """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: if not _speechserver:
logLine = f"SPEECH OUTPUT: '{text}' {acss}" logLine = f"SPEECH OUTPUT: '{text}' {acss}"
debug.printMessage(debug.LEVEL_INFO, logLine, True) debug.printMessage(debug.LEVEL_INFO, logLine, True)
@@ -179,6 +192,31 @@ def speak(content, acss=None, interrupt=True):
if settings.silenceSpeech: if settings.silenceSpeech:
return 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, validTypes = (str, list, speech_generator.Pause,
speech_generator.LineBreak, ACSS) speech_generator.LineBreak, ACSS)
error = "SPEECH: bad content sent to speak(): '%s'" error = "SPEECH: bad content sent to speak(): '%s'"