Initial sound support added.
This commit is contained in:
@@ -119,6 +119,7 @@ summary += {'Install dir': python.find_installation('python3').get_install_dir()
|
||||
subdir('docs')
|
||||
subdir('icons')
|
||||
subdir('po')
|
||||
subdir('sounds')
|
||||
subdir('src')
|
||||
|
||||
summary(summary)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -1018,6 +1018,105 @@
|
||||
<property name="height">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkFrame" id="soundThemeFrame">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label_xalign">0</property>
|
||||
<property name="shadow_type">none</property>
|
||||
<child>
|
||||
<object class="GtkAlignment" id="soundThemeAlignment">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="left_padding">12</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="soundThemeVBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="enableModeChangeSoundCheckButton">
|
||||
<property name="label" translatable="yes" comments="Translators: If this checkbox is checked, Cthulhu will play sounds when switching between browse mode and focus mode in web content.">Play sounds when switching between _browse and focus modes</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="xalign">0</property>
|
||||
<property name="draw_indicator">True</property>
|
||||
<signal name="toggled" handler="enableModeChangeSoundCheckButtonToggled" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="soundThemeHBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">12</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="soundThemeLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="xalign">0</property>
|
||||
<property name="label" translatable="yes" comments="Translators: This is the label for a combo box where users can select a sound theme.">Sound _theme:</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="mnemonic_widget">soundThemeCombo</property>
|
||||
<accessibility>
|
||||
<relation type="label-for" target="soundThemeCombo"/>
|
||||
</accessibility>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="soundThemeCombo">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<signal name="changed" handler="soundThemeComboChanged" swapped="no"/>
|
||||
<accessibility>
|
||||
<relation type="labelled-by" target="soundThemeLabel"/>
|
||||
</accessibility>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label">
|
||||
<object class="GtkLabel" id="soundThemeTitleLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes" comments="Translators: This is the title of a section in the preferences dialog containing sound theme options.">Sound Theme</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="bold"/>
|
||||
</attributes>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="tab">
|
||||
|
||||
@@ -63,6 +63,7 @@ from . import braille
|
||||
from . import speech
|
||||
from . import speechserver
|
||||
from . import text_attribute_names
|
||||
from . import sound_theme_manager
|
||||
from .ax_object import AXObject
|
||||
|
||||
_settingsManager = settings_manager.getManager()
|
||||
@@ -368,6 +369,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
|
||||
self.get_widget("notebook").append_page(appPage, label)
|
||||
|
||||
self._initGUIState()
|
||||
self._initSoundThemeState()
|
||||
|
||||
def _getACSSForVoiceType(self, voiceType):
|
||||
"""Return the ACSS value for the given voice type.
|
||||
@@ -1991,6 +1993,61 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
|
||||
copyToClipboard = prefs.get("ocrCopyToClipboard", settings.ocrCopyToClipboard)
|
||||
self.ocrCopyToClipboardCheckButton.set_active(copyToClipboard)
|
||||
|
||||
def _initSoundThemeState(self):
|
||||
"""Initialize Sound Theme widgets with current settings."""
|
||||
prefs = self.prefsDict
|
||||
|
||||
# Get widget references
|
||||
self.enableModeChangeSoundCheckButton = self.get_widget(
|
||||
"enableModeChangeSoundCheckButton")
|
||||
self.soundThemeCombo = self.get_widget("soundThemeCombo")
|
||||
|
||||
# Set enable mode change sound checkbox
|
||||
enabled = prefs.get("enableModeChangeSound", settings.enableModeChangeSound)
|
||||
self.enableModeChangeSoundCheckButton.set_active(enabled)
|
||||
|
||||
# Populate sound theme combo box
|
||||
themeManager = sound_theme_manager.getManager()
|
||||
availableThemes = themeManager.getAvailableThemes()
|
||||
|
||||
# Clear and populate combo - add "None" as first option
|
||||
self.soundThemeCombo.remove_all()
|
||||
self.soundThemeCombo.append_text(sound_theme_manager.THEME_NONE)
|
||||
for theme in availableThemes:
|
||||
self.soundThemeCombo.append_text(theme)
|
||||
|
||||
# Build the full list for index lookup
|
||||
allThemes = [sound_theme_manager.THEME_NONE] + availableThemes
|
||||
|
||||
# Set active theme
|
||||
currentTheme = prefs.get("soundTheme", settings.soundTheme)
|
||||
if currentTheme in allThemes:
|
||||
self.soundThemeCombo.set_active(allThemes.index(currentTheme))
|
||||
elif len(allThemes) > 1:
|
||||
# Default to first actual theme (skip "none")
|
||||
self.soundThemeCombo.set_active(1)
|
||||
else:
|
||||
self.soundThemeCombo.set_active(0)
|
||||
|
||||
# Update sensitivity based on checkbox
|
||||
self._updateSoundThemeWidgetSensitivity()
|
||||
|
||||
def _updateSoundThemeWidgetSensitivity(self):
|
||||
"""Update sound theme combo sensitivity based on enable checkbox."""
|
||||
enabled = self.enableModeChangeSoundCheckButton.get_active()
|
||||
self.soundThemeCombo.set_sensitive(enabled)
|
||||
|
||||
def enableModeChangeSoundCheckButtonToggled(self, widget):
|
||||
"""Signal handler for the enable mode change sound checkbox."""
|
||||
self.prefsDict["enableModeChangeSound"] = widget.get_active()
|
||||
self._updateSoundThemeWidgetSensitivity()
|
||||
|
||||
def soundThemeComboChanged(self, widget):
|
||||
"""Signal handler for the sound theme combo box."""
|
||||
activeText = widget.get_active_text()
|
||||
if activeText:
|
||||
self.prefsDict["soundTheme"] = activeText
|
||||
|
||||
def _updateCthulhuModifier(self):
|
||||
combobox = self.get_widget("cthulhuModifierComboBox")
|
||||
keystring = ", ".join(self.prefsDict["cthulhuModifierKeys"])
|
||||
|
||||
@@ -929,6 +929,20 @@ USE_CARET_NAVIGATION = _("Control caret navigation")
|
||||
# of a checkbox in which users can indicate their default preference.
|
||||
USE_STRUCTURAL_NAVIGATION = _("Enable _structural navigation")
|
||||
|
||||
# Translators: This is the label for a checkbox in the preferences dialog.
|
||||
# When enabled, Cthulhu will play sounds when switching between browse mode
|
||||
# and focus mode in web content.
|
||||
ENABLE_MODE_CHANGE_SOUND = _("Play sounds when switching between _browse and focus modes")
|
||||
|
||||
# Translators: This is the label for a combo box in the preferences dialog
|
||||
# where users can select a sound theme. A sound theme is a collection of
|
||||
# audio files that Cthulhu plays for various events.
|
||||
SOUND_THEME = _("Sound _theme:")
|
||||
|
||||
# Translators: This is the title of a frame in the preferences dialog
|
||||
# containing sound theme options.
|
||||
SOUND_THEME_TITLE = _("Sound Theme")
|
||||
|
||||
# Translators: This refers to the amount of information Cthulhu provides about a
|
||||
# particular object that receives focus.
|
||||
VERBOSITY_LEVEL_BRIEF = _("Brie_f")
|
||||
|
||||
@@ -85,6 +85,7 @@ cthulhu_python_sources = files([
|
||||
'sleep_mode_manager.py',
|
||||
'sound.py',
|
||||
'sound_generator.py',
|
||||
'sound_theme_manager.py',
|
||||
'speech_and_verbosity_manager.py',
|
||||
'speech_history.py',
|
||||
'speech.py',
|
||||
|
||||
@@ -54,6 +54,7 @@ from cthulhu import settings_manager
|
||||
from cthulhu import speech
|
||||
from cthulhu import speechserver
|
||||
from cthulhu import structural_navigation
|
||||
from cthulhu import sound_theme_manager
|
||||
from cthulhu.acss import ACSS
|
||||
from cthulhu.scripts import default
|
||||
from cthulhu.ax_object import AXObject
|
||||
@@ -1376,6 +1377,7 @@ class Script(default.Script):
|
||||
self.utilities.setCaretContext(AXObject.get_parent(parent), -1)
|
||||
if not self._loadingDocumentContent:
|
||||
self.presentMessage(messages.MODE_BROWSE)
|
||||
sound_theme_manager.getManager().playBrowseModeSound()
|
||||
else:
|
||||
if not self.utilities.grabFocusWhenSettingCaret(obj) \
|
||||
and (self._lastCommandWasCaretNav \
|
||||
@@ -1384,6 +1386,7 @@ class Script(default.Script):
|
||||
self.utilities.grabFocus(obj)
|
||||
|
||||
self.presentMessage(messages.MODE_FOCUS)
|
||||
sound_theme_manager.getManager().playFocusModeSound()
|
||||
self._inFocusMode = not self._inFocusMode
|
||||
self._focusModeIsSticky = False
|
||||
self._browseModeIsSticky = False
|
||||
|
||||
@@ -78,6 +78,8 @@ userCustomizableSettings = [
|
||||
"playSoundForState",
|
||||
"playSoundForPositionInSet",
|
||||
"playSoundForValue",
|
||||
"soundTheme",
|
||||
"enableModeChangeSound",
|
||||
"verbalizePunctuationStyle",
|
||||
"presentToolTips",
|
||||
"sayAllStyle",
|
||||
@@ -312,6 +314,8 @@ playSoundForRole = False
|
||||
playSoundForState = False
|
||||
playSoundForPositionInSet = False
|
||||
playSoundForValue = False
|
||||
soundTheme = "default"
|
||||
enableModeChangeSound = True
|
||||
|
||||
# Keyboard and Echo
|
||||
keyboardLayout = GENERAL_KEYBOARD_LAYOUT_DESKTOP
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright (c) 2024 Stormux
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# Cthulhu project: https://git.stormux.org/storm/cthulhu
|
||||
|
||||
"""Sound Theme Manager for Cthulhu screen reader.
|
||||
|
||||
Handles discovery and playback of sound theme files from:
|
||||
- System: /usr/share/cthulhu/sounds/{theme_name}/
|
||||
- Local: ~/.local/share/cthulhu/sounds/{theme_name}/
|
||||
- User themes: Same as local, user can add custom folders
|
||||
|
||||
Themes are simply folders containing sound files. The folder name is the theme name.
|
||||
"""
|
||||
|
||||
__id__ = "$Id$"
|
||||
__version__ = "$Revision$"
|
||||
__date__ = "$Date$"
|
||||
__copyright__ = "Copyright (c) 2024 Stormux"
|
||||
__license__ = "LGPL"
|
||||
|
||||
import os
|
||||
|
||||
from gi.repository import GLib
|
||||
|
||||
from . import debug
|
||||
from . import settings_manager
|
||||
from . import sound
|
||||
from .sound_generator import Icon
|
||||
|
||||
_settingsManager = settings_manager.getManager()
|
||||
|
||||
# Sound event constants - add new events here for easy extensibility
|
||||
SOUND_FOCUS_MODE = "editbox"
|
||||
SOUND_BROWSE_MODE = "browse_mode"
|
||||
SOUND_BUTTON = "button"
|
||||
|
||||
# Special theme name for no sounds
|
||||
THEME_NONE = "none"
|
||||
|
||||
|
||||
class SoundThemeManager:
|
||||
"""Manages sound themes for Cthulhu."""
|
||||
|
||||
_instance = None
|
||||
|
||||
def __new__(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
cls._instance._initialized = False
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
if self._initialized:
|
||||
return
|
||||
self._initialized = True
|
||||
self._systemSoundsDir = None
|
||||
self._userSoundsDir = None
|
||||
|
||||
def getSystemSoundsDir(self):
|
||||
"""Get system sounds directory from platform settings."""
|
||||
if self._systemSoundsDir is None:
|
||||
try:
|
||||
from . import cthulhu_platform
|
||||
datadir = getattr(cthulhu_platform, 'datadir', '/usr/share')
|
||||
except ImportError:
|
||||
datadir = '/usr/share'
|
||||
self._systemSoundsDir = os.path.join(datadir, 'cthulhu', 'sounds')
|
||||
return self._systemSoundsDir
|
||||
|
||||
def getUserSoundsDir(self):
|
||||
"""Get user sounds directory (XDG_DATA_HOME/cthulhu/sounds)."""
|
||||
if self._userSoundsDir is None:
|
||||
self._userSoundsDir = os.path.join(
|
||||
GLib.get_user_data_dir(), 'cthulhu', 'sounds'
|
||||
)
|
||||
return self._userSoundsDir
|
||||
|
||||
def getAvailableThemes(self):
|
||||
"""Discover all available sound themes.
|
||||
|
||||
Returns list of theme names (folder names) from both system and user dirs.
|
||||
User themes with same name as system themes will override system themes.
|
||||
"""
|
||||
themes = set()
|
||||
|
||||
for baseDir in [self.getSystemSoundsDir(), self.getUserSoundsDir()]:
|
||||
if os.path.isdir(baseDir):
|
||||
try:
|
||||
for entry in os.listdir(baseDir):
|
||||
entryPath = os.path.join(baseDir, entry)
|
||||
if os.path.isdir(entryPath):
|
||||
themes.add(entry)
|
||||
except OSError as e:
|
||||
tokens = ["SOUND THEME: Error listing directory:", baseDir, str(e)]
|
||||
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||
|
||||
return sorted(list(themes))
|
||||
|
||||
def getThemePath(self, themeName):
|
||||
"""Get the path to a theme directory.
|
||||
|
||||
User themes take precedence over system themes.
|
||||
"""
|
||||
userPath = os.path.join(self.getUserSoundsDir(), themeName)
|
||||
if os.path.isdir(userPath):
|
||||
return userPath
|
||||
|
||||
systemPath = os.path.join(self.getSystemSoundsDir(), themeName)
|
||||
if os.path.isdir(systemPath):
|
||||
return systemPath
|
||||
|
||||
return None
|
||||
|
||||
def getSoundPath(self, themeName, soundName):
|
||||
"""Get path to a specific sound file.
|
||||
|
||||
Checks for common audio file extensions: .wav, .ogg, .mp3, .flac
|
||||
"""
|
||||
themePath = self.getThemePath(themeName)
|
||||
if not themePath:
|
||||
return None
|
||||
|
||||
for ext in ['.wav', '.ogg', '.mp3', '.flac']:
|
||||
soundPath = os.path.join(themePath, soundName + ext)
|
||||
if os.path.isfile(soundPath):
|
||||
return soundPath
|
||||
|
||||
return None
|
||||
|
||||
def playSound(self, soundName, interrupt=True):
|
||||
"""Play a sound from the current theme if enabled.
|
||||
|
||||
Args:
|
||||
soundName: The name of the sound file (without extension)
|
||||
interrupt: Whether to interrupt currently playing sounds
|
||||
|
||||
Returns:
|
||||
True if sound was played, False otherwise
|
||||
"""
|
||||
if not _settingsManager.getSetting('enableModeChangeSound'):
|
||||
return False
|
||||
|
||||
themeName = _settingsManager.getSetting('soundTheme')
|
||||
if not themeName:
|
||||
themeName = 'default'
|
||||
|
||||
# "none" theme means no sounds
|
||||
if themeName == THEME_NONE:
|
||||
return False
|
||||
|
||||
soundPath = self.getSoundPath(themeName, soundName)
|
||||
if not soundPath:
|
||||
tokens = ["SOUND THEME: Sound not found:", soundName, "in theme:", themeName]
|
||||
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||
return False
|
||||
|
||||
try:
|
||||
icon = Icon(os.path.dirname(soundPath), os.path.basename(soundPath))
|
||||
if icon.isValid():
|
||||
player = sound.getPlayer()
|
||||
player.play(icon, interrupt=interrupt)
|
||||
return True
|
||||
except Exception as e:
|
||||
tokens = ["SOUND THEME: Error playing sound:", str(e)]
|
||||
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||
|
||||
return False
|
||||
|
||||
def playFocusModeSound(self):
|
||||
"""Play sound for entering focus mode."""
|
||||
return self.playSound(SOUND_FOCUS_MODE)
|
||||
|
||||
def playBrowseModeSound(self):
|
||||
"""Play sound for entering browse mode."""
|
||||
return self.playSound(SOUND_BROWSE_MODE)
|
||||
|
||||
def playButtonSound(self):
|
||||
"""Play sound for button focus (future use)."""
|
||||
return self.playSound(SOUND_BUTTON)
|
||||
|
||||
|
||||
_manager = None
|
||||
|
||||
|
||||
def getManager():
|
||||
"""Get the singleton SoundThemeManager instance."""
|
||||
global _manager
|
||||
if _manager is None:
|
||||
_manager = SoundThemeManager()
|
||||
return _manager
|
||||
Reference in New Issue
Block a user