#!/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.
#
# 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.
#
# Forked from Orca screen reader.
# Cthulhu project: https://git.stormux.org/storm/cthulhu

"""Module for configuring speech and verbosity settings."""

__id__        = "$Id$"
__version__   = "$Revision$"
__date__      = "$Date$"
__copyright__ = "Copyright (c) 2005-2008 Sun Microsystems Inc." \
                "Copyright (c) 2016-2023 Igalia, S.L."
__license__   = "LGPL"

from . import cmdnames
from . import cthulhu
from . import dbus_service
from . import debug
from . import input_event
from . import keybindings
from . import messages
from . import cthulhu_state
from . import script_manager
from . import speechserver
from . import settings
from . import settings_manager
from . import speech

# Removed global _settings_manager

class SpeechAndVerbosityManager:
    """Configures speech and verbosity settings."""

    def __init__(self, app):
        self.app = app
        self._speech_settings_order = [
            "rate",
            "pitch",
            "volume",
            "module",
            "voice",
        ]
        self._current_speech_setting_index = 0
        self._rate_step = 1
        self._pitch_step = 0.1
        self._volume_step = 0.1
        self._handlers = self._setup_handlers()
        self._bindings = self._setup_bindings()

        # Register with D-Bus
        msg = "SPEECH AND VERBOSITY MANAGER: Registering D-Bus commands."
        debug.printMessage(debug.LEVEL_INFO, msg, True)
        controller = dbus_service.get_remote_controller()
        if controller:
            controller.register_decorated_module("SpeechAndVerbosityManager", self)

    def get_bindings(self):
        """Returns the notification-presenter keybindings."""

        return self._bindings

    def get_handlers(self):
        """Returns the notification-presenter handlers."""

        return self._handlers

    def _setup_handlers(self):
        """Sets up and returns the speech and verbosity input event handlers."""

        handlers = {}

        handlers["cycleCapitalizationStyleHandler"] = \
            input_event.InputEventHandler(
                self.cycle_capitalization_style,
                cmdnames.CYCLE_CAPITALIZATION_STYLE)

        handlers["cycleSpeakingPunctuationLevelHandler"] = \
            input_event.InputEventHandler(
                self.cycle_punctuation_level,
                cmdnames.CYCLE_PUNCTUATION_LEVEL)

        handlers["cycleSynthesizerHandler"] = \
            input_event.InputEventHandler(
                self.cycle_synthesizer,
                cmdnames.CYCLE_SYNTHESIZER)

        handlers["cycleKeyEchoHandler"] = \
            input_event.InputEventHandler(
                self.cycle_key_echo,
                cmdnames.CYCLE_KEY_ECHO)

        handlers["changeNumberStyleHandler"] = \
            input_event.InputEventHandler(
                self.change_number_style,
                cmdnames.CHANGE_NUMBER_STYLE)

        handlers["toggleSilenceSpeechHandler"] = \
            input_event.InputEventHandler(
                self.toggle_speech,
                cmdnames.TOGGLE_SPEECH)

        handlers["toggleSpeechVerbosityHandler"] = \
            input_event.InputEventHandler(
                self.toggle_verbosity,
                cmdnames.TOGGLE_SPEECH_VERBOSITY)

        handlers["toggleSpeakingIndentationJustificationHandler"] = \
            input_event.InputEventHandler(
                self.toggle_indentation_and_justification,
                cmdnames.TOGGLE_SPOKEN_INDENTATION_AND_JUSTIFICATION)

        handlers["toggleTableCellReadModeHandler"] = \
            input_event.InputEventHandler(
                self.toggle_table_cell_reading_mode,
                cmdnames.TOGGLE_TABLE_CELL_READ_MODE)

        handlers["decreaseSpeechRateHandler"] = \
            input_event.InputEventHandler(
                self.decrease_rate,
                cmdnames.DECREASE_SPEECH_RATE)

        handlers["increaseSpeechRateHandler"] = \
            input_event.InputEventHandler(
                self.increase_rate,
                cmdnames.INCREASE_SPEECH_RATE)

        handlers["decreaseSpeechPitchHandler"] = \
            input_event.InputEventHandler(
                self.decrease_pitch,
                cmdnames.DECREASE_SPEECH_PITCH)

        handlers["increaseSpeechPitchHandler"] = \
            input_event.InputEventHandler(
                self.increase_pitch,
                cmdnames.INCREASE_SPEECH_PITCH)

        handlers["decreaseSpeechVolumeHandler"] = \
            input_event.InputEventHandler(
                self.decrease_volume,
                cmdnames.DECREASE_SPEECH_VOLUME)

        handlers["increaseSpeechVolumeHandler"] = \
            input_event.InputEventHandler(
                self.increase_volume,
                cmdnames.INCREASE_SPEECH_VOLUME)

        handlers["selectPreviousSpeechSettingHandler"] = \
            input_event.InputEventHandler(
                self.select_previous_speech_setting,
                cmdnames.SPEECH_SETTING_PREVIOUS)

        handlers["selectNextSpeechSettingHandler"] = \
            input_event.InputEventHandler(
                self.select_next_speech_setting,
                cmdnames.SPEECH_SETTING_NEXT)

        handlers["decreaseCurrentSpeechSettingHandler"] = \
            input_event.InputEventHandler(
                self.decrease_current_speech_setting,
                cmdnames.SPEECH_SETTING_DECREASE)

        handlers["increaseCurrentSpeechSettingHandler"] = \
            input_event.InputEventHandler(
                self.increase_current_speech_setting,
                cmdnames.SPEECH_SETTING_INCREASE)

        handlers["saveSpeechSettingsHandler"] = \
            input_event.InputEventHandler(
                self.save_speech_settings,
                cmdnames.SPEECH_SETTINGS_SAVE)

        return handlers

    def _setup_bindings(self):
        """Sets up and returns the speech and verbosity key bindings."""

        bindings = keybindings.KeyBindings()

        bindings.add(
            keybindings.KeyBinding(
                "",
                keybindings.defaultModifierMask,
                keybindings.NO_MODIFIER_MASK,
                self._handlers.get("cycleCapitalizationStyleHandler")))

        bindings.add(
            keybindings.KeyBinding(
                "",
                keybindings.defaultModifierMask,
                keybindings.NO_MODIFIER_MASK,
                self._handlers.get("cycleSpeakingPunctuationLevelHandler")))

        bindings.add(
            keybindings.KeyBinding(
                "",
                keybindings.defaultModifierMask,
                keybindings.NO_MODIFIER_MASK,
                self._handlers.get("cycleSynthesizerHandler")))

        bindings.add(
            keybindings.KeyBinding(
                "",
                keybindings.defaultModifierMask,
                keybindings.NO_MODIFIER_MASK,
                self._handlers.get("cycleKeyEchoHandler")))

        bindings.add(
            keybindings.KeyBinding(
                "",
                keybindings.defaultModifierMask,
                keybindings.NO_MODIFIER_MASK,
                self._handlers.get("changeNumberStyleHandler")))

        bindings.add(
            keybindings.KeyBinding(
                "",
                keybindings.defaultModifierMask,
                keybindings.NO_MODIFIER_MASK,
                self._handlers.get("decreaseSpeechRateHandler")))

        bindings.add(
            keybindings.KeyBinding(
                "",
                keybindings.defaultModifierMask,
                keybindings.NO_MODIFIER_MASK,
                self._handlers.get("increaseSpeechRateHandler")))

        bindings.add(
            keybindings.KeyBinding(
                "",
                keybindings.defaultModifierMask,
                keybindings.NO_MODIFIER_MASK,
                self._handlers.get("decreaseSpeechPitchHandler")))

        bindings.add(
            keybindings.KeyBinding(
                "",
                keybindings.defaultModifierMask,
                keybindings.NO_MODIFIER_MASK,
                self._handlers.get("increaseSpeechPitchHandler")))

        bindings.add(
            keybindings.KeyBinding(
                "",
                keybindings.defaultModifierMask,
                keybindings.NO_MODIFIER_MASK,
                self._handlers.get("decreaseSpeechVolumeHandler")))

        bindings.add(
            keybindings.KeyBinding(
                "",
                keybindings.defaultModifierMask,
                keybindings.NO_MODIFIER_MASK,
                self._handlers.get("increaseSpeechVolumeHandler")))

        bindings.add(
            keybindings.KeyBinding(
                "Left",
                keybindings.defaultModifierMask,
                keybindings.CTHULHU_MODIFIER_MASK,
                self._handlers.get("selectPreviousSpeechSettingHandler")))

        bindings.add(
            keybindings.KeyBinding(
                "Right",
                keybindings.defaultModifierMask,
                keybindings.CTHULHU_MODIFIER_MASK,
                self._handlers.get("selectNextSpeechSettingHandler")))

        bindings.add(
            keybindings.KeyBinding(
                "Down",
                keybindings.defaultModifierMask,
                keybindings.CTHULHU_MODIFIER_MASK,
                self._handlers.get("decreaseCurrentSpeechSettingHandler")))

        bindings.add(
            keybindings.KeyBinding(
                "Up",
                keybindings.defaultModifierMask,
                keybindings.CTHULHU_MODIFIER_MASK,
                self._handlers.get("increaseCurrentSpeechSettingHandler")))

        bindings.add(
            keybindings.KeyBinding(
                "s",
                keybindings.defaultModifierMask,
                keybindings.CTHULHU_CTRL_MODIFIER_MASK | keybindings.SHIFT_MODIFIER_MASK,
                self._handlers.get("saveSpeechSettingsHandler")))

        bindings.add(
            keybindings.KeyBinding(
                "",
                keybindings.defaultModifierMask,
                keybindings.NO_MODIFIER_MASK,
                self._handlers.get("toggleSpeakingIndentationJustificationHandler")))

        bindings.add(
            keybindings.KeyBinding(
                "s",
                keybindings.defaultModifierMask,
                keybindings.CTHULHU_MODIFIER_MASK,
                self._handlers.get("toggleSilenceSpeechHandler")))

        bindings.add(
            keybindings.KeyBinding(
                "v",
                keybindings.defaultModifierMask,
                keybindings.CTHULHU_MODIFIER_MASK,
                self._handlers.get("toggleSpeechVerbosityHandler")))

        bindings.add(
            keybindings.KeyBinding(
                "F11",
                keybindings.defaultModifierMask,
                keybindings.CTHULHU_MODIFIER_MASK,
                self._handlers.get("toggleTableCellReadModeHandler")))

        return bindings

    def _get_server(self):
        return speech.getSpeechServer()

    def _format_number(self, value):
        if isinstance(value, float):
            if value.is_integer():
                return str(int(value))
            return f"{value:.1f}".rstrip("0").rstrip(".")
        return str(value)

    def _present_message(self, script, message, voice=None):
        if script:
            script.presentMessage(message, voice=voice)
        else:
            speech.speak(message, voice)

    def _get_default_voice(self):
        from . import acss
        default_voice = settings.voices.get(settings.DEFAULT_VOICE)
        if default_voice is None:
            default_voice = acss.ACSS({})
            settings.voices[settings.DEFAULT_VOICE] = default_voice
        return default_voice

    def _set_default_voice_family(self, family):
        from . import acss
        default_voice = self._get_default_voice()
        if family is None:
            default_voice.pop(acss.ACSS.FAMILY, None)
        else:
            family_dict = {
                key: value for key, value in dict(family).items()
                if value is not None
            }
            default_voice[acss.ACSS.FAMILY] = family_dict
        default_voice['established'] = True

    def _get_current_speech_setting(self):
        order = self._get_speech_settings_order()
        if not order:
            return ""
        if self._current_speech_setting_index < 0:
            self._current_speech_setting_index = 0
        elif self._current_speech_setting_index >= len(order):
            self._current_speech_setting_index = len(order) - 1
        return order[self._current_speech_setting_index]

    def _get_speech_settings_order(self):
        order = ["rate", "pitch", "volume"]
        server = self._get_server()
        if server and hasattr(server, "list_output_modules") and hasattr(server, "getOutputModule"):
            order.append("module")
        if server and hasattr(server, "getVoiceFamilies"):
            order.append("voice")
        return order

    def _get_rate_value(self):
        from . import acss
        default_voice = self._get_default_voice()
        return int(default_voice.get(acss.ACSS.RATE, 50))

    def _set_rate_value(self, value):
        from . import acss
        default_voice = self._get_default_voice()
        default_voice[acss.ACSS.RATE] = int(value)
        default_voice['established'] = True

    def _get_pitch_value(self):
        from . import acss
        default_voice = self._get_default_voice()
        return float(default_voice.get(acss.ACSS.AVERAGE_PITCH, 5.0))

    def _set_pitch_value(self, value):
        from . import acss
        default_voice = self._get_default_voice()
        default_voice[acss.ACSS.AVERAGE_PITCH] = float(value)
        default_voice['established'] = True

    def _get_volume_value(self):
        from . import acss
        default_voice = self._get_default_voice()
        return float(default_voice.get(acss.ACSS.GAIN, 10.0))

    def _set_volume_value(self, value):
        from . import acss
        default_voice = self._get_default_voice()
        default_voice[acss.ACSS.GAIN] = float(value)
        default_voice['established'] = True

    def _get_current_voice_name(self, server=None):
        from . import acss
        default_voice = self._get_default_voice()
        family = default_voice.get(acss.ACSS.FAMILY, {}) or {}
        name = family.get(speechserver.VoiceFamily.NAME)
        if name and server:
            voices = self._get_available_voices(server)
            if voices:
                for voice in voices:
                    if voice.get(speechserver.VoiceFamily.NAME) == name:
                        return name
                self._set_default_voice_family(voices[0])
                return voices[0].get(speechserver.VoiceFamily.NAME)
        if name:
            return name
        if server:
            voices = self._get_available_voices(server)
            if voices:
                self._set_default_voice_family(voices[0])
                return voices[0].get(speechserver.VoiceFamily.NAME)
        return ""

    def _get_voice_messages(self, server):
        if server and hasattr(server, "list_output_modules"):
            return messages.SPEECH_VOICE_VALUE, messages.SPEECH_VOICES_UNAVAILABLE
        return messages.SPEECH_VOICE_VALUE_GENERIC, messages.SPEECH_VOICES_UNAVAILABLE_GENERIC

    def _get_available_modules(self, server):
        if server is None or not hasattr(server, 'list_output_modules'):
            return []
        try:
            modules = list(server.list_output_modules() or [])
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"Error getting output modules: {e}", True)
            return []
        return modules

    def _get_available_voices(self, server):
        if server is None or not hasattr(server, 'getVoiceFamilies'):
            return []
        try:
            voices = server.getVoiceFamilies() or []
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"Error getting voices: {e}", True)
            return []
        filtered = []
        for voice in voices:
            if voice.get(speechserver.VoiceFamily.NAME):
                filtered.append(voice)
        if not filtered:
            return []

        if not self._should_filter_voices_by_locale(server):
            return filtered

        language, dialect = self._get_system_language_and_dialect()
        if not language:
            return filtered

        language_filtered = []
        for voice in filtered:
            voice_lang = (voice.get(speechserver.VoiceFamily.LANG) or "").lower()
            voice_dialect = (voice.get(speechserver.VoiceFamily.DIALECT) or "").lower()
            if voice_lang != language:
                continue
            if dialect and voice_dialect and voice_dialect != dialect:
                continue
            language_filtered.append(voice)

        if language_filtered:
            msg = (
                "SPEECH AND VERBOSITY MANAGER: "
                f"Filtered voices to locale {language}"
                f"{('-' + dialect) if dialect else ''}: "
                f"{len(language_filtered)} of {len(filtered)}"
            )
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            return language_filtered

        msg = (
            "SPEECH AND VERBOSITY MANAGER: "
            f"No voices for locale {language}"
            f"{('-' + dialect) if dialect else ''}; using all voices."
        )
        debug.printMessage(debug.LEVEL_INFO, msg, True)
        return filtered

    def _should_filter_voices_by_locale(self, server):
        if server is None or not hasattr(server, "getOutputModule"):
            return False
        try:
            module = (server.getOutputModule() or "").lower()
        except Exception:
            return False
        return "espeak-ng" in module

    def _get_system_language_and_dialect(self):
        import locale
        locale_value = None
        try:
            locale_value = locale.getlocale(locale.LC_MESSAGES)[0]
        except Exception:
            locale_value = None
        if not locale_value:
            try:
                locale_value = locale.getdefaultlocale()[0]
            except Exception:
                locale_value = None

        if not locale_value:
            return "", ""

        if "_" in locale_value:
            language, dialect = locale_value.split("_", 1)
        elif "-" in locale_value:
            language, dialect = locale_value.split("-", 1)
        else:
            language, dialect = locale_value, ""

        return language.lower(), dialect.lower()

    def _announce_current_speech_setting(self, script=None):
        setting = self._get_current_speech_setting()
        debug.printMessage(debug.LEVEL_INFO,
                           f"SPEECH AND VERBOSITY MANAGER: Current setting {setting}",
                           True)
        if setting == "rate":
            value = self._get_rate_value()
            message = messages.SPEECH_RATE_VALUE % value
        elif setting == "pitch":
            value = self._format_number(self._get_pitch_value())
            message = messages.SPEECH_PITCH_VALUE % value
        elif setting == "volume":
            value = self._format_number(self._get_volume_value())
            message = messages.SPEECH_VOLUME_VALUE % value
        elif setting == "module":
            server = self._get_server()
            if server is None or not hasattr(server, 'getOutputModule'):
                message = messages.SPEECH_MODULES_UNAVAILABLE
            else:
                module = server.getOutputModule() or ""
                if module:
                    message = messages.SPEECH_MODULE_VALUE % module
                else:
                    message = messages.SPEECH_MODULES_UNAVAILABLE
        elif setting == "voice":
            server = self._get_server()
            voices = self._get_available_voices(server)
            voice_value_message, voice_unavailable_message = self._get_voice_messages(server)
            if not voices:
                message = voice_unavailable_message
            else:
                name = self._get_current_voice_name(server)
                message = voice_value_message % name
        else:
            message = ""

        if message:
            voice = self._get_default_voice() if setting in ("voice", "module") else None
            self._present_message(script, message, voice=voice)

    @dbus_service.command
    def select_previous_speech_setting(self, script=None, event=None):
        order = self._get_speech_settings_order()
        if not order:
            return True
        if self._current_speech_setting_index >= len(order):
            self._current_speech_setting_index = len(order) - 1
        elif self._current_speech_setting_index > 0:
            self._current_speech_setting_index -= 1
        self._announce_current_speech_setting(script)
        return True

    @dbus_service.command
    def select_next_speech_setting(self, script=None, event=None):
        order = self._get_speech_settings_order()
        if not order:
            return True
        if self._current_speech_setting_index < len(order) - 1:
            self._current_speech_setting_index += 1
        self._announce_current_speech_setting(script)
        return True

    @dbus_service.command
    def decrease_current_speech_setting(self, script=None, event=None):
        return self._adjust_current_speech_setting(script, decrease=True)

    @dbus_service.command
    def increase_current_speech_setting(self, script=None, event=None):
        return self._adjust_current_speech_setting(script, decrease=False)

    def _adjust_current_speech_setting(self, script, decrease=False):
        setting = self._get_current_speech_setting()
        if setting == "rate":
            return self._adjust_rate(script, decrease)
        if setting == "pitch":
            return self._adjust_pitch(script, decrease)
        if setting == "volume":
            return self._adjust_volume(script, decrease)
        if setting == "module":
            return self._adjust_module(script, decrease)
        if setting == "voice":
            return self._adjust_voice(script, decrease)
        return True

    def _adjust_rate(self, script, decrease):
        delta = -self._rate_step if decrease else self._rate_step
        value = self._get_rate_value() + delta
        value = max(0, min(100, value))
        self._set_rate_value(value)
        msg = f"SPEECH AND VERBOSITY MANAGER: Rate set to {value}"
        debug.printMessage(debug.LEVEL_INFO, msg, True)
        self._announce_current_speech_setting(script)
        return True

    def _adjust_pitch(self, script, decrease):
        delta = -self._pitch_step if decrease else self._pitch_step
        value = round(self._get_pitch_value() + delta, 1)
        value = max(0.0, min(10.0, value))
        self._set_pitch_value(value)
        msg = f"SPEECH AND VERBOSITY MANAGER: Pitch set to {value}"
        debug.printMessage(debug.LEVEL_INFO, msg, True)
        self._announce_current_speech_setting(script)
        return True

    def _adjust_volume(self, script, decrease):
        delta = -self._volume_step if decrease else self._volume_step
        value = round(self._get_volume_value() + delta, 1)
        value = max(0.0, min(10.0, value))
        self._set_volume_value(value)
        msg = f"SPEECH AND VERBOSITY MANAGER: Volume set to {value}"
        debug.printMessage(debug.LEVEL_INFO, msg, True)
        self._announce_current_speech_setting(script)
        return True

    def _adjust_module(self, script, decrease):
        server = self._get_server()
        if server is None:
            msg = "SPEECH AND VERBOSITY MANAGER: Cannot get speech server."
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            return True

        modules = self._get_available_modules(server)
        if not modules:
            self._present_message(script, messages.SPEECH_MODULES_UNAVAILABLE)
            return True

        current = server.getOutputModule() if hasattr(server, 'getOutputModule') else ""
        try:
            index = modules.index(current)
        except ValueError:
            index = 0

        delta = -1 if decrease else 1
        new_index = max(0, min(len(modules) - 1, index + delta))
        new_module = modules[new_index]

        if new_module != current:
            try:
                server.setOutputModule(new_module)
                msg = f"SPEECH AND VERBOSITY MANAGER: Output module set to {new_module}"
                debug.printMessage(debug.LEVEL_INFO, msg, True)
            except Exception as e:
                debug.printMessage(debug.LEVEL_WARNING, f"Error setting output module: {e}", True)
                return True
        settings.speechServerInfo = [new_module, new_module]
        msg = f"SPEECH AND VERBOSITY MANAGER: speechServerInfo set to {settings.speechServerInfo}"
        debug.printMessage(debug.LEVEL_INFO, msg, True)

        voices = self._get_available_voices(server)
        if voices:
            self._set_default_voice_family(voices[0])
        else:
            self._set_default_voice_family({})

        self._present_message(
            script,
            messages.SPEECH_MODULE_VALUE % new_module,
            voice=self._get_default_voice(),
        )
        return True

    def _adjust_voice(self, script, decrease):
        server = self._get_server()
        voices = self._get_available_voices(server)
        if not voices:
            _, voice_unavailable_message = self._get_voice_messages(server)
            self._present_message(script, voice_unavailable_message)
            return True

        current_name = self._get_current_voice_name(server)
        index = 0
        for idx, voice in enumerate(voices):
            if voice.get(speechserver.VoiceFamily.NAME) == current_name:
                index = idx
                break

        delta = -1 if decrease else 1
        new_index = max(0, min(len(voices) - 1, index + delta))
        new_voice = voices[new_index]
        self._set_default_voice_family(new_voice)
        name = new_voice.get(speechserver.VoiceFamily.NAME, "")
        msg = f"SPEECH AND VERBOSITY MANAGER: Voice set to {name}"
        debug.printMessage(debug.LEVEL_INFO, msg, True)
        voice_value_message, _ = self._get_voice_messages(server)
        self._present_message(script, voice_value_message % name, voice=self._get_default_voice())
        return True

    @dbus_service.command
    def save_speech_settings(self, script=None, event=None):
        try:
            server = self._get_server()
            if server and hasattr(server, "getOutputModule"):
                module = server.getOutputModule() or ""
                if module:
                    settings.speechServerInfo = [module, module]
                    msg = f"SPEECH AND VERBOSITY MANAGER: Saving speechServerInfo {settings.speechServerInfo}"
                    debug.printMessage(debug.LEVEL_INFO, msg, True)

            general = {}
            for key in settings.userCustomizableSettings:
                general[key] = getattr(settings, key)

            current_profile = self.app.getSettingsManager().profile
            pronunciations = self.app.getSettingsManager().getPronunciations(current_profile)
            keybindings = self.app.getSettingsManager().getKeybindings(current_profile)

            default_script = self.app.getScriptManager().get_default_script()
            self.app.getSettingsManager().saveSettings(default_script,
                                           general,
                                           pronunciations,
                                           keybindings)
            self._present_message(script, messages.SPEECH_SETTINGS_SAVED)
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"Error saving speech settings: {e}", True)
            self._present_message(script, messages.SPEECH_SETTINGS_SAVE_FAILED)
        return True

    # ========================================================================
    # D-Bus Speech Server Controls
    # ========================================================================

    @dbus_service.getter
    def get_current_server(self) -> str:
        """Returns the name of the current speech server (Speech Dispatcher or Spiel)."""
        server = self._get_server()
        if server is None:
            return ""
        return server.getFactoryName() if hasattr(server, 'getFactoryName') else ""

    @dbus_service.getter
    def get_available_servers(self) -> list:
        """Returns a list of available speech servers."""
        # TODO: Implement multi-server support if needed
        return ["speechdispatcher"]

    @dbus_service.getter
    def get_current_synthesizer(self) -> str:
        """Returns the current synthesizer of the speech server."""
        server = self._get_server()
        if server is None:
            return ""
        return server.getOutputModule() if hasattr(server, 'getOutputModule') else ""

    @dbus_service.setter
    def set_current_synthesizer(self, value: str) -> bool:
        """Sets the current synthesizer of the active speech server."""
        server = self._get_server()
        if server is None:
            return False
        try:
            if hasattr(server, 'setOutputModule'):
                server.setOutputModule(value)
                return True
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"Error setting synthesizer: {e}", True)
        return False

    @dbus_service.getter
    def get_available_synthesizers(self) -> list:
        """Returns a list of available synthesizers of the speech server."""
        server = self._get_server()
        if server is None:
            return []
        try:
            if hasattr(server, 'list_output_modules'):
                return server.list_output_modules()
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"Error getting synthesizers: {e}", True)
        return []

    # ========================================================================
    # D-Bus Voice Controls
    # ========================================================================

    @dbus_service.getter
    def get_available_voices(self) -> list:
        """Returns a list of available voices for the current synthesizer."""
        server = self._get_server()
        if server is None:
            return []
        try:
            if hasattr(server, 'getVoiceFamilies'):
                voices = server.getVoiceFamilies()
                return [v.get('name', '') for v in voices if v.get('name')]
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"Error getting voices: {e}", True)
        return []

    @dbus_service.getter
    def get_current_voice(self) -> str:
        """Returns the current voice name."""
        server = self._get_server()
        if server is None:
            return ""
        try:
            if hasattr(server, 'getVoiceFamily'):
                family = server.getVoiceFamily()
                if family:
                    return family.get('name', '')
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"Error getting current voice: {e}", True)
        return ""

    @dbus_service.setter
    def set_current_voice(self, voice_name: str) -> bool:
        """Sets the current voice for the active synthesizer."""
        server = self._get_server()
        if server is None:
            return False
        try:
            if hasattr(server, 'getVoiceFamilies') and hasattr(server, 'setVoiceFamily'):
                voices = server.getVoiceFamilies()
                for voice_family in voices:
                    if voice_family.get('name') == voice_name:
                        server.setVoiceFamily(voice_family)
                        return True
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"Error setting voice: {e}", True)
        return False

    # ========================================================================
    # D-Bus Rate/Pitch/Volume Controls
    # ========================================================================

    @dbus_service.getter
    def get_rate(self) -> int:
        """Returns the current speech rate."""
        from . import acss
        result = 50
        default_voice = settings.voices.get(settings.DEFAULT_VOICE)
        if default_voice and acss.ACSS.RATE in default_voice:
            result = default_voice[acss.ACSS.RATE]
        return result

    @dbus_service.setter
    def set_rate(self, value: int) -> bool:
        """Sets the current speech rate (0-100, default: 50)."""
        try:
            from . import acss
            default_voice = settings.voices.get(settings.DEFAULT_VOICE)
            if default_voice and acss.ACSS.RATE in default_voice:
                default_voice[acss.ACSS.RATE] = value
                return True
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"Error setting rate: {e}", True)
        return False

    @dbus_service.getter
    def get_pitch(self) -> float:
        """Returns the current speech pitch."""
        from . import acss
        result = 5.0
        default_voice = settings.voices.get(settings.DEFAULT_VOICE)
        if default_voice and acss.ACSS.AVERAGE_PITCH in default_voice:
            result = default_voice[acss.ACSS.AVERAGE_PITCH]
        return result

    @dbus_service.setter
    def set_pitch(self, value: float) -> bool:
        """Sets the current speech pitch (0.0-10.0, default: 5.0)."""
        try:
            from . import acss
            default_voice = settings.voices.get(settings.DEFAULT_VOICE)
            if default_voice and acss.ACSS.AVERAGE_PITCH in default_voice:
                default_voice[acss.ACSS.AVERAGE_PITCH] = value
                return True
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"Error setting pitch: {e}", True)
        return False

    @dbus_service.getter
    def get_volume(self) -> float:
        """Returns the current speech volume."""
        from . import acss
        result = 10.0
        default_voice = settings.voices.get(settings.DEFAULT_VOICE)
        if default_voice and acss.ACSS.GAIN in default_voice:
            result = default_voice[acss.ACSS.GAIN]
        return result

    @dbus_service.setter
    def set_volume(self, value: float) -> bool:
        """Sets the current speech volume (0.0-10.0, default: 10.0)."""
        try:
            from . import acss
            default_voice = settings.voices.get(settings.DEFAULT_VOICE)
            if default_voice and acss.ACSS.GAIN in default_voice:
                default_voice[acss.ACSS.GAIN] = value
                return True
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"Error setting volume: {e}", True)
        return False

    # ========================================================================
    # D-Bus Speech Enable/Mute Controls
    # ========================================================================

    @dbus_service.getter
    def get_speech_is_muted(self) -> bool:
        """Returns whether speech output is temporarily muted."""
        return self.app.getSettingsManager().getSetting('silenceSpeech')

    @dbus_service.setter
    def set_speech_is_muted(self, value: bool) -> bool:
        """Sets whether speech output is temporarily muted."""
        try:
            cthulhu.cthulhuApp.settingsManager.setSetting('silenceSpeech', value)
            return True
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"Error setting speech mute: {e}", True)
            return False

    @dbus_service.getter
    def get_verbosity_level(self) -> str:
        """Returns the current speech verbosity level."""
        level = cthulhu.cthulhuApp.settingsManager.getSetting('speechVerbosityLevel')
        if level == settings.VERBOSITY_LEVEL_BRIEF:
            return "brief"
        else:
            return "verbose"

    @dbus_service.setter
    def set_verbosity_level(self, value: str) -> bool:
        """Sets the speech verbosity level."""
        try:
            if value.lower() == "brief":
                cthulhu.cthulhuApp.settingsManager.setSetting('speechVerbosityLevel', settings.VERBOSITY_LEVEL_BRIEF)
            elif value.lower() == "verbose":
                cthulhu.cthulhuApp.settingsManager.setSetting('speechVerbosityLevel', settings.VERBOSITY_LEVEL_VERBOSE)
            else:
                return False
            return True
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"Error setting verbosity level: {e}", True)
            return False

    @dbus_service.getter
    def get_speak_numbers_as_digits(self) -> bool:
        """Returns whether numbers are spoken as digits."""
        return cthulhu.cthulhuApp.settingsManager.getSetting('speakNumbersAsDigits')

    @dbus_service.setter
    def set_speak_numbers_as_digits(self, value: bool) -> bool:
        """Sets whether numbers are spoken as digits."""
        try:
            cthulhu.cthulhuApp.settingsManager.setSetting('speakNumbersAsDigits', value)
            return True
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"Error setting speak numbers as digits: {e}", True)
            return False

    @dbus_service.getter
    def get_only_speak_displayed_text(self) -> bool:
        """Returns whether only displayed text should be spoken."""
        return cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText')

    @dbus_service.setter
    def set_only_speak_displayed_text(self, value: bool) -> bool:
        """Sets whether only displayed text should be spoken."""
        try:
            cthulhu.cthulhuApp.settingsManager.setSetting('onlySpeakDisplayedText', value)
            return True
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"Error setting only speak displayed text: {e}", True)
            return False

    @dbus_service.getter
    def get_speak_indentation_and_justification(self) -> bool:
        """Returns whether speaking of indentation and justification is enabled."""
        return cthulhu.cthulhuApp.settingsManager.getSetting('enableSpeechIndentation')

    def _sync_indentation_presentation_mode(self, enable_speech):
        mode = self.app.getSettingsManager().getSetting('indentationPresentationMode') \
            or settings.indentationPresentationMode
        if enable_speech:
            if mode == settings.INDENTATION_PRESENTATION_OFF:
                mode = settings.INDENTATION_PRESENTATION_SPEECH
            elif mode == settings.INDENTATION_PRESENTATION_BEEPS:
                mode = settings.INDENTATION_PRESENTATION_SPEECH_AND_BEEPS
        else:
            if mode == settings.INDENTATION_PRESENTATION_SPEECH:
                mode = settings.INDENTATION_PRESENTATION_OFF
            elif mode == settings.INDENTATION_PRESENTATION_SPEECH_AND_BEEPS:
                mode = settings.INDENTATION_PRESENTATION_BEEPS

        self.app.getSettingsManager().setSetting('indentationPresentationMode', mode)

    @dbus_service.setter
    def set_speak_indentation_and_justification(self, value: bool) -> bool:
        """Sets whether speaking of indentation and justification is enabled."""
        try:
            cthulhu.cthulhuApp.settingsManager.setSetting('enableSpeechIndentation', value)
            self._sync_indentation_presentation_mode(value)
            return True
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"Error setting speak indentation: {e}", True)
            return False

    @dbus_service.getter
    def get_punctuation_level(self) -> str:
        """Returns the current punctuation level."""
        level = cthulhu.cthulhuApp.settingsManager.getSetting('verbalizePunctuationStyle')
        if level == settings.PUNCTUATION_STYLE_NONE:
            return "none"
        elif level == settings.PUNCTUATION_STYLE_SOME:
            return "some"
        elif level == settings.PUNCTUATION_STYLE_MOST:
            return "most"
        elif level == settings.PUNCTUATION_STYLE_ALL:
            return "all"
        else:
            return "some"

    @dbus_service.setter
    def set_punctuation_level(self, value: str) -> bool:
        """Sets the punctuation level."""
        try:
            value_lower = value.lower()
            if value_lower == "none":
                cthulhu.cthulhuApp.settingsManager.setSetting('verbalizePunctuationStyle', settings.PUNCTUATION_STYLE_NONE)
            elif value_lower == "some":
                cthulhu.cthulhuApp.settingsManager.setSetting('verbalizePunctuationStyle', settings.PUNCTUATION_STYLE_SOME)
            elif value_lower == "most":
                cthulhu.cthulhuApp.settingsManager.setSetting('verbalizePunctuationStyle', settings.PUNCTUATION_STYLE_MOST)
            elif value_lower == "all":
                cthulhu.cthulhuApp.settingsManager.setSetting('verbalizePunctuationStyle', settings.PUNCTUATION_STYLE_ALL)
            else:
                return False
            return True
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"Error setting punctuation level: {e}", True)
            return False

    @dbus_service.getter
    def get_capitalization_style(self) -> str:
        """Returns the current capitalization style."""
        style = cthulhu.cthulhuApp.settingsManager.getSetting('capitalizationStyle')
        if style == settings.CAPITALIZATION_STYLE_NONE:
            return "none"
        elif style == settings.CAPITALIZATION_STYLE_ICON:
            return "icon"
        elif style == settings.CAPITALIZATION_STYLE_SPELL:
            return "spell"
        else:
            return "none"

    @dbus_service.setter
    def set_capitalization_style(self, value: str) -> bool:
        """Sets the capitalization style."""
        try:
            value_lower = value.lower()
            if value_lower == "none":
                cthulhu.cthulhuApp.settingsManager.setSetting('capitalizationStyle', settings.CAPITALIZATION_STYLE_NONE)
            elif value_lower == "icon":
                cthulhu.cthulhuApp.settingsManager.setSetting('capitalizationStyle', settings.CAPITALIZATION_STYLE_ICON)
            elif value_lower == "spell":
                cthulhu.cthulhuApp.settingsManager.setSetting('capitalizationStyle', settings.CAPITALIZATION_STYLE_SPELL)
            else:
                return False
            self.update_capitalization_style()
            return True
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"Error setting capitalization style: {e}", True)
            return False

    # ========================================================================
    # D-Bus Verbosity Settings
    # ========================================================================

    @dbus_service.getter
    def get_speak_misspelled_indicator(self) -> bool:
        """Returns whether the misspelled indicator is spoken."""
        return cthulhu.cthulhuApp.settingsManager.getSetting('speakMisspelledIndicator')

    @dbus_service.setter
    def set_speak_misspelled_indicator(self, value: bool) -> bool:
        """Sets whether the misspelled indicator is spoken."""
        try:
            cthulhu.cthulhuApp.settingsManager.setSetting('speakMisspelledIndicator', value)
            return True
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"Error setting speak misspelled indicator: {e}", True)
            return False

    @dbus_service.getter
    def get_speak_description(self) -> bool:
        """Returns whether object descriptions are spoken."""
        return cthulhu.cthulhuApp.settingsManager.getSetting('speakDescription')

    @dbus_service.setter
    def set_speak_description(self, value: bool) -> bool:
        """Sets whether object descriptions are spoken."""
        try:
            cthulhu.cthulhuApp.settingsManager.setSetting('speakDescription', value)
            return True
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"Error setting speak description: {e}", True)
            return False

    @dbus_service.getter
    def get_speak_position_in_set(self) -> bool:
        """Returns whether the position and set size of objects are spoken."""
        return cthulhu.cthulhuApp.settingsManager.getSetting('enablePositionSpeaking')

    @dbus_service.setter
    def set_speak_position_in_set(self, value: bool) -> bool:
        """Sets whether the position and set size of objects are spoken."""
        try:
            cthulhu.cthulhuApp.settingsManager.setSetting('enablePositionSpeaking', value)
            return True
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"Error setting speak position in set: {e}", True)
            return False

    @dbus_service.getter
    def get_speak_widget_mnemonic(self) -> bool:
        """Returns whether widget mnemonics are spoken."""
        return cthulhu.cthulhuApp.settingsManager.getSetting('enableMnemonicSpeaking')

    @dbus_service.setter
    def set_speak_widget_mnemonic(self, value: bool) -> bool:
        """Sets whether widget mnemonics are spoken."""
        try:
            cthulhu.cthulhuApp.settingsManager.setSetting('enableMnemonicSpeaking', value)
            return True
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"Error setting speak widget mnemonic: {e}", True)
            return False

    @dbus_service.getter
    def get_speak_tutorial_messages(self) -> bool:
        """Returns whether tutorial messages are spoken."""
        return cthulhu.cthulhuApp.settingsManager.getSetting('enableTutorialMessages')

    @dbus_service.setter
    def set_speak_tutorial_messages(self, value: bool) -> bool:
        """Sets whether tutorial messages are spoken."""
        try:
            cthulhu.cthulhuApp.settingsManager.setSetting('enableTutorialMessages', value)
            return True
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"Error setting speak tutorial messages: {e}", True)
            return False

    @dbus_service.getter
    def get_insert_pauses_between_utterances(self) -> bool:
        """Returns whether pauses are inserted between utterances, e.g. between name and role."""
        return cthulhu.cthulhuApp.settingsManager.getSetting('enablePauseBreaks')

    @dbus_service.setter
    def set_insert_pauses_between_utterances(self, value: bool) -> bool:
        """Sets whether pauses are inserted between utterances, e.g. between name and role."""
        try:
            cthulhu.cthulhuApp.settingsManager.setSetting('enablePauseBreaks', value)
            return True
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"Error setting insert pauses: {e}", True)
            return False

    @dbus_service.getter
    def get_repeated_character_limit(self) -> int:
        """Returns the count at which repeated, non-alphanumeric symbols will be described."""
        return cthulhu.cthulhuApp.settingsManager.getSetting('repeatCharacterLimit')

    @dbus_service.setter
    def set_repeated_character_limit(self, value: int) -> bool:
        """Sets the count at which repeated, non-alphanumeric symbols will be described."""
        try:
            cthulhu.cthulhuApp.settingsManager.setSetting('repeatCharacterLimit', value)
            return True
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"Error setting repeated character limit: {e}", True)
            return False

    @dbus_service.getter
    def get_use_pronunciation_dictionary(self) -> bool:
        """Returns whether the user's pronunciation dictionary should be applied."""
        return cthulhu.cthulhuApp.settingsManager.getSetting('usePronunciationDictionary')

    @dbus_service.setter
    def set_use_pronunciation_dictionary(self, value: bool) -> bool:
        """Sets whether the user's pronunciation dictionary should be applied."""
        try:
            cthulhu.cthulhuApp.settingsManager.setSetting('usePronunciationDictionary', value)
            return True
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"Error setting use pronunciation dictionary: {e}", True)
            return False

    @dbus_service.getter
    def get_messages_are_detailed(self) -> bool:
        """Returns whether informative messages will be detailed or brief."""
        return cthulhu.cthulhuApp.settingsManager.getSetting('messagesAreDetailed')

    @dbus_service.setter
    def set_messages_are_detailed(self, value: bool) -> bool:
        """Sets whether informative messages will be detailed or brief."""
        try:
            cthulhu.cthulhuApp.settingsManager.setSetting('messagesAreDetailed', value)
            return True
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"Error setting messages are detailed: {e}", True)
            return False

    @dbus_service.getter
    def get_speak_indentation_only_if_changed(self) -> bool:
        """Returns whether indentation will be announced only if it has changed."""
        return cthulhu.cthulhuApp.settingsManager.getSetting('speakIndentationOnlyIfChanged')

    @dbus_service.setter
    def set_speak_indentation_only_if_changed(self, value: bool) -> bool:
        """Sets whether indentation will be announced only if it has changed."""
        try:
            cthulhu.cthulhuApp.settingsManager.setSetting('speakIndentationOnlyIfChanged', value)
            return True
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"Error setting speak indentation only if changed: {e}", True)
            return False

    # ========================================================================
    # D-Bus Table Reading Settings
    # ========================================================================

    @dbus_service.getter
    def get_speak_blank_lines(self) -> bool:
        """Returns whether blank lines will be spoken."""
        return cthulhu.cthulhuApp.settingsManager.getSetting('speakBlankLines')

    @dbus_service.setter
    def set_speak_blank_lines(self, value: bool) -> bool:
        """Sets whether blank lines will be spoken."""
        try:
            cthulhu.cthulhuApp.settingsManager.setSetting('speakBlankLines', value)
            return True
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"Error setting speak blank lines: {e}", True)
            return False

    @dbus_service.getter
    def get_speak_row_in_gui_table(self) -> bool:
        """Returns whether Up/Down in GUI tables speaks the row or just the cell."""
        return cthulhu.cthulhuApp.settingsManager.getSetting('readFullRowInGUITable')

    @dbus_service.setter
    def set_speak_row_in_gui_table(self, value: bool) -> bool:
        """Sets whether Up/Down in GUI tables speaks the row or just the cell."""
        try:
            cthulhu.cthulhuApp.settingsManager.setSetting('readFullRowInGUITable', value)
            return True
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"Error setting speak row in GUI table: {e}", True)
            return False

    @dbus_service.getter
    def get_speak_row_in_document_table(self) -> bool:
        """Returns whether Up/Down in text-document tables speaks the row or just the cell."""
        return cthulhu.cthulhuApp.settingsManager.getSetting('readFullRowInDocumentTable')

    @dbus_service.setter
    def set_speak_row_in_document_table(self, value: bool) -> bool:
        """Sets whether Up/Down in text-document tables speaks the row or just the cell."""
        try:
            cthulhu.cthulhuApp.settingsManager.setSetting('readFullRowInDocumentTable', value)
            return True
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"Error setting speak row in document table: {e}", True)
            return False

    @dbus_service.getter
    def get_speak_row_in_spreadsheet(self) -> bool:
        """Returns whether Up/Down in spreadsheets speaks the row or just the cell."""
        return cthulhu.cthulhuApp.settingsManager.getSetting('readFullRowInSpreadSheet')

    @dbus_service.setter
    def set_speak_row_in_spreadsheet(self, value: bool) -> bool:
        """Sets whether Up/Down in spreadsheets speaks the row or just the cell."""
        try:
            cthulhu.cthulhuApp.settingsManager.setSetting('readFullRowInSpreadSheet', value)
            return True
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"Error setting speak row in spreadsheet: {e}", True)
            return False

    @dbus_service.getter
    def get_announce_cell_span(self) -> bool:
        """Returns whether cell spans are announced when greater than 1."""
        return cthulhu.cthulhuApp.settingsManager.getSetting('speakCellSpan')

    @dbus_service.setter
    def set_announce_cell_span(self, value: bool) -> bool:
        """Sets whether cell spans are announced when greater than 1."""
        try:
            cthulhu.cthulhuApp.settingsManager.setSetting('speakCellSpan', value)
            return True
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"Error setting announce cell span: {e}", True)
            return False

    @dbus_service.getter
    def get_announce_cell_coordinates(self) -> bool:
        """Returns whether (non-spreadsheet) cell coordinates are announced."""
        return cthulhu.cthulhuApp.settingsManager.getSetting('speakCellCoordinates')

    @dbus_service.setter
    def set_announce_cell_coordinates(self, value: bool) -> bool:
        """Sets whether (non-spreadsheet) cell coordinates are announced."""
        try:
            cthulhu.cthulhuApp.settingsManager.setSetting('speakCellCoordinates', value)
            return True
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"Error setting announce cell coordinates: {e}", True)
            return False

    @dbus_service.getter
    def get_announce_spreadsheet_cell_coordinates(self) -> bool:
        """Returns whether spreadsheet cell coordinates are announced."""
        return cthulhu.cthulhuApp.settingsManager.getSetting('speakSpreadsheetCoordinates')

    @dbus_service.setter
    def set_announce_spreadsheet_cell_coordinates(self, value: bool) -> bool:
        """Sets whether spreadsheet cell coordinates are announced."""
        try:
            cthulhu.cthulhuApp.settingsManager.setSetting('speakSpreadsheetCoordinates', value)
            return True
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"Error setting announce spreadsheet cell coordinates: {e}", True)
            return False

    @dbus_service.getter
    def get_always_announce_selected_range_in_spreadsheet(self) -> bool:
        """Returns whether the selected range in spreadsheets is always announced."""
        return cthulhu.cthulhuApp.settingsManager.getSetting('alwaysSpeakSelectedSpreadsheetRange')

    @dbus_service.setter
    def set_always_announce_selected_range_in_spreadsheet(self, value: bool) -> bool:
        """Sets whether the selected range in spreadsheets is always announced."""
        try:
            cthulhu.cthulhuApp.settingsManager.setSetting('alwaysSpeakSelectedSpreadsheetRange', value)
            return True
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"Error setting always announce selected range: {e}", True)
            return False

    @dbus_service.getter
    def get_announce_cell_headers(self) -> bool:
        """Returns whether cell headers are announced."""
        return cthulhu.cthulhuApp.settingsManager.getSetting('speakCellHeaders')

    @dbus_service.setter
    def set_announce_cell_headers(self, value: bool) -> bool:
        """Sets whether cell headers are announced."""
        try:
            cthulhu.cthulhuApp.settingsManager.setSetting('speakCellHeaders', value)
            return True
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"Error setting announce cell headers: {e}", True)
            return False

    # ========================================================================
    # D-Bus Context Announcement Settings
    # ========================================================================

    @dbus_service.getter
    def get_announce_blockquote(self) -> bool:
        """Returns whether blockquotes are announced when entered."""
        return cthulhu.cthulhuApp.settingsManager.getSetting('speakContextBlockquote')

    @dbus_service.setter
    def set_announce_blockquote(self, value: bool) -> bool:
        """Sets whether blockquotes are announced when entered."""
        try:
            cthulhu.cthulhuApp.settingsManager.setSetting('speakContextBlockquote', value)
            return True
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"Error setting announce blockquote: {e}", True)
            return False

    @dbus_service.getter
    def get_announce_form(self) -> bool:
        """Returns whether non-landmark forms are announced when entered."""
        return cthulhu.cthulhuApp.settingsManager.getSetting('speakContextNonLandmarkForm')

    @dbus_service.setter
    def set_announce_form(self, value: bool) -> bool:
        """Sets whether non-landmark forms are announced when entered."""
        try:
            cthulhu.cthulhuApp.settingsManager.setSetting('speakContextNonLandmarkForm', value)
            return True
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"Error setting announce form: {e}", True)
            return False

    @dbus_service.getter
    def get_announce_grouping(self) -> bool:
        """Returns whether groupings are announced when entered."""
        return cthulhu.cthulhuApp.settingsManager.getSetting('speakContextPanel')

    @dbus_service.setter
    def set_announce_grouping(self, value: bool) -> bool:
        """Sets whether groupings are announced when entered."""
        try:
            cthulhu.cthulhuApp.settingsManager.setSetting('speakContextPanel', value)
            return True
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"Error setting announce grouping: {e}", True)
            return False

    @dbus_service.getter
    def get_announce_landmark(self) -> bool:
        """Returns whether landmarks are announced when entered."""
        return cthulhu.cthulhuApp.settingsManager.getSetting('speakContextLandmark')

    @dbus_service.setter
    def set_announce_landmark(self, value: bool) -> bool:
        """Sets whether landmarks are announced when entered."""
        try:
            cthulhu.cthulhuApp.settingsManager.setSetting('speakContextLandmark', value)
            return True
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"Error setting announce landmark: {e}", True)
            return False

    @dbus_service.getter
    def get_announce_list(self) -> bool:
        """Returns whether lists are announced when entered."""
        return cthulhu.cthulhuApp.settingsManager.getSetting('speakContextList')

    @dbus_service.setter
    def set_announce_list(self, value: bool) -> bool:
        """Sets whether lists are announced when entered."""
        try:
            cthulhu.cthulhuApp.settingsManager.setSetting('speakContextList', value)
            return True
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"Error setting announce list: {e}", True)
            return False

    @dbus_service.getter
    def get_announce_table(self) -> bool:
        """Returns whether tables are announced when entered."""
        return cthulhu.cthulhuApp.settingsManager.getSetting('speakContextTable')

    @dbus_service.setter
    def set_announce_table(self, value: bool) -> bool:
        """Sets whether tables are announced when entered."""
        try:
            cthulhu.cthulhuApp.settingsManager.setSetting('speakContextTable', value)
            return True
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"Error setting announce table: {e}", True)
            return False

    @dbus_service.getter
    def get_use_color_names(self) -> bool:
        """Returns whether colors are announced by name or as RGB values."""
        return cthulhu.cthulhuApp.settingsManager.getSetting('useColorNames')

    @dbus_service.setter
    def set_use_color_names(self, value: bool) -> bool:
        """Sets whether colors are announced by name or as RGB values."""
        try:
            cthulhu.cthulhuApp.settingsManager.setSetting('useColorNames', value)
            return True
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"Error setting use color names: {e}", True)
            return False

    # ========================================================================
    # D-Bus Speech Control Commands
    # ========================================================================

    @dbus_service.command
    def start_speech(self, script=None, event=None) -> bool:
        """Starts the speech server."""
        try:
            speech.init()
            return True
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"Error starting speech: {e}", True)
            return False

    @dbus_service.command
    def interrupt_speech(self, script=None, event=None) -> bool:
        """Interrupts the speech server."""
        try:
            server = self._get_server()
            if server:
                server.stop()
            return True
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"Error interrupting speech: {e}", True)
            return False

    @dbus_service.command
    def shutdown_speech(self, script=None, event=None) -> bool:
        """Shuts down the speech server."""
        try:
            server = self._get_server()
            if server:
                server.shutdownActiveServers()
            return True
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"Error shutting down speech: {e}", True)
            return False

    @dbus_service.command
    def refresh_speech(self, script=None, event=None) -> bool:
        """Shuts down and re-initializes speech."""
        try:
            self.shutdown_speech()
            self.start_speech()
            return True
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"Error refreshing speech: {e}", True)
            return False

    @dbus_service.setter
    def set_speech_is_enabled(self, value: bool) -> bool:
        """Sets whether the speech server is enabled. See also is-muted."""
        try:
            cthulhu.cthulhuApp.settingsManager.setSetting('enableSpeech', value)
            if value:
                self.start_speech()
            else:
                self.shutdown_speech()
            return True
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"Error setting speech enabled: {e}", True)
            return False

    @dbus_service.getter
    def get_speech_is_enabled(self) -> bool:
        """Returns whether the speech server is enabled. See also is-muted."""
        return cthulhu.cthulhuApp.settingsManager.getSetting('enableSpeech')

    @dbus_service.command
    def decrease_rate(self, script=None, event=None):
        """Decreases the speech rate"""

        server = self._get_server()
        if server is None:
            msg = "SPEECH AND VERBOSITY MANAGER: Cannot get speech server."
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            return True

        server.decreaseSpeechRate()
        if script:
            script.presentMessage(messages.SPEECH_SLOWER)
        return True

    @dbus_service.command
    def increase_rate(self, script=None, event=None):
        """Increases the speech rate"""

        server = self._get_server()
        if server is None:
            msg = "SPEECH AND VERBOSITY MANAGER: Cannot get speech server."
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            return True

        server.increaseSpeechRate()
        if script:
            script.presentMessage(messages.SPEECH_FASTER)
        return True

    @dbus_service.command
    def decrease_pitch(self, script=None, event=None):
        """Decreases the speech pitch"""

        server = self._get_server()
        if server is None:
            msg = "SPEECH AND VERBOSITY MANAGER: Cannot get speech server."
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            return True

        server.decreaseSpeechPitch()
        if script:
            script.presentMessage(messages.SPEECH_LOWER)
        return True

    @dbus_service.command
    def increase_pitch(self, script=None, event=None):
        """Increase the speech pitch"""

        server = self._get_server()
        if server is None:
            msg = "SPEECH AND VERBOSITY MANAGER: Cannot get speech server."
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            return True

        server.increaseSpeechPitch()
        if script:
            script.presentMessage(messages.SPEECH_HIGHER)
        return True

    @dbus_service.command
    def decrease_volume(self, script=None, event=None):
        """Decreases the speech volume"""

        server = self._get_server()
        if server is None:
            msg = "SPEECH AND VERBOSITY MANAGER: Cannot get speech server."
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            return True

        server.decreaseSpeechVolume()
        if script:
            script.presentMessage(messages.SPEECH_SOFTER)
        return True

    @dbus_service.command
    def increase_volume(self, script=None, event=None):
        """Increases the speech volume"""

        server = self._get_server()
        if server is None:
            msg = "SPEECH AND VERBOSITY MANAGER: Cannot get speech server."
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            return True

        server.increaseSpeechVolume()
        if script:
            script.presentMessage(messages.SPEECH_LOUDER)
        return True

    def update_capitalization_style(self):
        """Updates the capitalization style based on the value in settings."""

        server = self._get_server()
        if server is None:
            msg = "SPEECH AND VERBOSITY MANAGER: Cannot get speech server."
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            return True

        server.updateCapitalizationStyle()
        return True

    def update_punctuation_level(self):
        """Updates the punctuation level based on the value in settings."""

        server = self._get_server()
        if server is None:
            msg = "SPEECH AND VERBOSITY MANAGER: Cannot get speech server."
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            return True

        server.updatePunctuationLevel()
        return True

    @dbus_service.command
    def cycle_synthesizer(self, script=None, event=None):
        """Cycle through the speech-dispatcher's available output modules."""

        server = self._get_server()
        if server is None:
            msg = "SPEECH AND VERBOSITY MANAGER: Cannot get speech server."
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            return True

        available = server.list_output_modules()
        if not available:
            msg = "SPEECH AND VERBOSITY MANAGER: Cannot get output modules."
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            return True

        current = server.getOutputModule()
        if not current:
            msg = "SPEECH AND VERBOSITY MANAGER: Cannot get current output module."
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            return True

        index = available.index(current) + 1
        if index == len(available):
            index = 0

        server.setOutputModule(available[index])
        if script:
            script.presentMessage(available[index])
        return True

    @dbus_service.command
    def cycle_capitalization_style(self, script=None, event=None):
        """Cycle through the speech-dispatcher capitalization styles."""

        current_style = cthulhu.cthulhuApp.settingsManager.getSetting('capitalizationStyle')
        if current_style == settings.CAPITALIZATION_STYLE_NONE:
            new_style = settings.CAPITALIZATION_STYLE_SPELL
            full = messages.CAPITALIZATION_SPELL_FULL
            brief = messages.CAPITALIZATION_SPELL_BRIEF
        elif current_style == settings.CAPITALIZATION_STYLE_SPELL:
            new_style = settings.CAPITALIZATION_STYLE_ICON
            full = messages.CAPITALIZATION_ICON_FULL
            brief = messages.CAPITALIZATION_ICON_BRIEF
        else:
            new_style = settings.CAPITALIZATION_STYLE_NONE
            full = messages.CAPITALIZATION_NONE_FULL
            brief = messages.CAPITALIZATION_NONE_BRIEF

        cthulhu.cthulhuApp.settingsManager.setSetting('capitalizationStyle', new_style)
        if script:
            script.presentMessage(full, brief)
        self.update_capitalization_style()
        return True

    @dbus_service.command
    def cycle_punctuation_level(self, script=None, event=None):
        """Cycle through the punctuation levels for speech."""

        current_level = cthulhu.cthulhuApp.settingsManager.getSetting('verbalizePunctuationStyle')
        if current_level == settings.PUNCTUATION_STYLE_NONE:
            new_level = settings.PUNCTUATION_STYLE_SOME
            full = messages.PUNCTUATION_SOME_FULL
            brief = messages.PUNCTUATION_SOME_BRIEF
        elif current_level == settings.PUNCTUATION_STYLE_SOME:
            new_level = settings.PUNCTUATION_STYLE_MOST
            full = messages.PUNCTUATION_MOST_FULL
            brief = messages.PUNCTUATION_MOST_BRIEF
        elif current_level == settings.PUNCTUATION_STYLE_MOST:
            new_level = settings.PUNCTUATION_STYLE_ALL
            full = messages.PUNCTUATION_ALL_FULL
            brief = messages.PUNCTUATION_ALL_BRIEF
        else:
            new_level = settings.PUNCTUATION_STYLE_NONE
            full = messages.PUNCTUATION_NONE_FULL
            brief = messages.PUNCTUATION_NONE_BRIEF

        cthulhu.cthulhuApp.settingsManager.setSetting('verbalizePunctuationStyle', new_level)
        if script:
            script.presentMessage(full, brief)
        self.update_punctuation_level()
        return True

    @dbus_service.command
    def cycle_key_echo(self, script=None, event=None):
        """Cycle through the key echo levels."""
        (new_key, new_word, new_sentence) = (False, False, False)
        key = cthulhu.cthulhuApp.settingsManager.getSetting('enableKeyEcho')
        word = cthulhu.cthulhuApp.settingsManager.getSetting('enableEchoByWord')
        sentence = cthulhu.cthulhuApp.settingsManager.getSetting('enableEchoBySentence')

        if (key, word, sentence) == (False, False, False):
            (new_key, new_word, new_sentence) = (True, False, False)
            full = messages.KEY_ECHO_KEY_FULL
            brief = messages.KEY_ECHO_KEY_BRIEF
        elif (key, word, sentence) == (True, False, False):
            (new_key, new_word, new_sentence) = (False, True, False)
            full = messages.KEY_ECHO_WORD_FULL
            brief = messages.KEY_ECHO_WORD_BRIEF
        elif (key, word, sentence) == (False, True, False):
            (new_key, new_word, new_sentence) = (False, False, True)
            full = messages.KEY_ECHO_SENTENCE_FULL
            brief = messages.KEY_ECHO_SENTENCE_BRIEF
        elif (key, word, sentence) == (False, False, True):
            (new_key, new_word, new_sentence) = (True, True, False)
            full = messages.KEY_ECHO_KEY_AND_WORD_FULL
            brief = messages.KEY_ECHO_KEY_AND_WORD_BRIEF
        elif (key, word, sentence) == (True, True, False):
            (new_key, new_word, new_sentence) = (False, True, True)
            full = messages.KEY_ECHO_WORD_AND_SENTENCE_FULL
            brief = messages.KEY_ECHO_WORD_AND_SENTENCE_BRIEF
        else:
            (new_key, new_word, new_sentence) = (False, False, False)
            full = messages.KEY_ECHO_NONE_FULL
            brief = messages.KEY_ECHO_NONE_BRIEF

        cthulhu.cthulhuApp.settingsManager.setSetting('enableKeyEcho', new_key)
        cthulhu.cthulhuApp.settingsManager.setSetting('enableEchoByWord', new_word)
        cthulhu.cthulhuApp.settingsManager.setSetting('enableEchoBySentence', new_sentence)
        if script:
            script.presentMessage(full, brief)
        return True

    @dbus_service.command
    def change_number_style(self, script=None, event=None):
        """Changes spoken number style between digits and words."""

        speak_digits = cthulhu.cthulhuApp.settingsManager.getSetting('speakNumbersAsDigits')
        if speak_digits:
            brief = messages.NUMBER_STYLE_WORDS_BRIEF
            full = messages.NUMBER_STYLE_WORDS_FULL
        else:
            brief = messages.NUMBER_STYLE_DIGITS_BRIEF
            full = messages.NUMBER_STYLE_DIGITS_FULL

        cthulhu.cthulhuApp.settingsManager.setSetting('speakNumbersAsDigits', not speak_digits)
        if script:
            script.presentMessage(full, brief)
        return True

    @dbus_service.command
    def toggle_speech(self, script, event=None):
        """Toggles speech."""

        script.presentationInterrupt()
        if self.app.getSettingsManager().getSetting('silenceSpeech'):
            self.app.getSettingsManager().setSetting('silenceSpeech', False)
            script.presentMessage(messages.SPEECH_ENABLED)
        elif not self.app.getSettingsManager().getSetting('enableSpeech'):
            self.app.getSettingsManager().setSetting('enableSpeech', True)
            speech.init()
            script.presentMessage(messages.SPEECH_ENABLED)
        else:
            script.presentMessage(messages.SPEECH_DISABLED)
            self.app.getSettingsManager().setSetting('silenceSpeech', True)
        return True

    @dbus_service.command
    def toggle_verbosity(self, script, event=None):
        """Toggles speech verbosity level between verbose and brief."""

        value = self.app.getSettingsManager().getSetting('speechVerbosityLevel')
        if value == settings.VERBOSITY_LEVEL_BRIEF:
            script.presentMessage(messages.SPEECH_VERBOSITY_VERBOSE)
            self.app.getSettingsManager().setSetting('speechVerbosityLevel', settings.VERBOSITY_LEVEL_VERBOSE)
        else:
            script.presentMessage(messages.SPEECH_VERBOSITY_BRIEF)
            self.app.getSettingsManager().setSetting('speechVerbosityLevel', settings.VERBOSITY_LEVEL_BRIEF)
        return True

    @dbus_service.command
    def toggle_indentation_and_justification(self, script=None, event=None):
        """Toggles the speaking of indentation and justification."""

        value = self.app.getSettingsManager().getSetting('enableSpeechIndentation')
        self.app.getSettingsManager().setSetting('enableSpeechIndentation', not value)
        self._sync_indentation_presentation_mode(not value)
        if self.app.getSettingsManager().getSetting('enableSpeechIndentation'):
            full = messages.INDENTATION_JUSTIFICATION_ON_FULL
            brief = messages.INDENTATION_JUSTIFICATION_ON_BRIEF
        else:
            full = messages.INDENTATION_JUSTIFICATION_OFF_FULL
            brief = messages.INDENTATION_JUSTIFICATION_OFF_BRIEF
        if script:
            script.presentMessage(full, brief)
        return True

    @dbus_service.command
    def toggle_table_cell_reading_mode(self, script=None, event=None):
        """Toggles between speak cell and speak row."""

        if not script:
            return False

        table = script.utilities.getTable(cthulhu_state.locusOfFocus)
        if not table:
            script.presentMessage(messages.TABLE_NOT_IN_A)
            return True

        if not script.utilities.getDocumentForObject(table):
            setting_name = 'readFullRowInGUITable'
        elif script.utilities.isSpreadSheetTable(table):
            setting_name = 'readFullRowInSpreadSheet'
        else:
            setting_name = 'readFullRowInDocumentTable'

        speak_row = cthulhu.cthulhuApp.settingsManager.getSetting(setting_name)
        cthulhu.cthulhuApp.settingsManager.setSetting(setting_name, not speak_row)

        if not speak_row:
            msg = messages.TABLE_MODE_ROW
        else:
            msg = messages.TABLE_MODE_CELL

        script.presentMessage(msg)
        return True

_manager = None
def getManager():
    """Returns the Speech and Verbosity Manager"""

    global _manager
    if _manager is None:
        from . import cthulhu
        _manager = SpeechAndVerbosityManager(cthulhu.cthulhuApp)
    return _manager
