#!/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

"""Displays a GUI for the user to set Cthulhu preferences."""

__id__        = "$Id$"
__version__   = "$Revision$"
__date__      = "$Date$"
__copyright__ = "Copyright (c) 2005-2009 Sun Microsystems Inc."
__license__   = "LGPL"

import gi
gi.require_version("Atspi", "2.0")
gi.require_version("Gdk", "3.0")
gi.require_version("Gtk", "3.0")
from gi.repository import Atspi

import os
import json
import subprocess
import sys
import threading
from gi.repository import Gdk
from gi.repository import GLib
from gi.repository import Gtk
from gi.repository import GObject
from gi.repository import Pango
import time

from . import acss
from . import debug
from . import event_manager
from . import guilabels
from . import messages
from . import cthulhu
from . import cthulhu_gtkbuilder
from . import cthulhu_gui_profile
from . import cthulhu_state
from . import settings
from . import settings_manager
from . import sound_sink
from . import input_event
from . import input_event_manager
from . import keybindings
from . import pronunciation_dict
from . import braille
from . import speech
from . import speechserver
from . import text_attribute_names
from . import sound_theme_manager
from . import script_manager
from .ax_object import AXObject
from .ax_utilities import AXUtilities

_settingsManager = None  # Removed - use cthulhu.cthulhuApp.settingsManager

try:
    import louis
except ImportError:
    louis = None
from .cthulhu_platform import tablesdir
if louis and not tablesdir:
    louis = None

(HANDLER, DESCRIP, MOD_MASK1, MOD_USED1, KEY1, CLICK_COUNT1, OLDTEXT1, \
 TEXT1, MODIF, EDITABLE) = list(range(10))

(NAME, IS_SPOKEN, IS_BRAILLED, VALUE) = list(range(4))

(ACTUAL, REPLACEMENT) = list(range(2))

# Must match the order of voice types in the GtkBuilder file.
#
(DEFAULT, UPPERCASE, HYPERLINK, SYSTEM) = list(range(4))

# Must match the order that the timeFormatCombo is populated.
#
(TIME_FORMAT_LOCALE, TIME_FORMAT_12_HM, TIME_FORMAT_12_HMS, TIME_FORMAT_24_HMS,
 TIME_FORMAT_24_HMS_WITH_WORDS, TIME_FORMAT_24_HM,
 TIME_FORMAT_24_HM_WITH_WORDS) = list(range(7))

# Must match the order that the dateFormatCombo is populated.
#
(DATE_FORMAT_LOCALE, DATE_FORMAT_NUMBERS_DM, DATE_FORMAT_NUMBERS_MD,
 DATE_FORMAT_NUMBERS_DMY, DATE_FORMAT_NUMBERS_MDY, DATE_FORMAT_NUMBERS_YMD,
 DATE_FORMAT_FULL_DM, DATE_FORMAT_FULL_MD, DATE_FORMAT_FULL_DMY,
 DATE_FORMAT_FULL_MDY, DATE_FORMAT_FULL_YMD, DATE_FORMAT_ABBREVIATED_DM,
 DATE_FORMAT_ABBREVIATED_MD, DATE_FORMAT_ABBREVIATED_DMY,
 DATE_FORMAT_ABBREVIATED_MDY, DATE_FORMAT_ABBREVIATED_YMD) = list(range(16))

class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
    PLUGIN_COL_ENABLED = 0
    PLUGIN_COL_DISPLAY = 1
    PLUGIN_COL_CAN_TOGGLE = 2
    PLUGIN_COL_NAME = 3

    def __init__(self, fileName, windowName, prefsDict):
        """Initialize the Cthulhu configuration GUI.

        Arguments:
        - fileName: name of the GtkBuilder file.
        - windowName: name of the component to get from the GtkBuilder file.
        - prefsDict: dictionary of preferences to use during initialization
        """

        cthulhu_gtkbuilder.GtkBuilderWrapper.__init__(self, fileName, windowName)
        self.prefsDict = prefsDict

        self._defaultProfile = ['Default', 'default']
        self.bbindings = None
        self.cellRendererText = None
        self.defaultVoice = None
        self.disableKeyGrabPref = None
        self.getTextAttributesView = None
        self.hyperlinkVoice = None
        self.initializingSpeech = None
        self.kbindings = None
        self.keyBindingsModel = None
        self.keyBindView = None
        self.newBinding = None
        self.pendingKeyBindings = None
        self.planeCellRendererText = None
        self.pronunciationModel = None
        self.pronunciationView = None
        self._plugin_treeview = None
        self._plugin_model = None
        self._plugin_iters = {}
        self._plugin_enabled_iter = None
        self._plugin_disabled_iter = None
        self._plugin_sources = []
        self._plugin_sources_entry = None
        self._plugin_sources_listbox = None
        self._plugin_sources_original = []
        self._available_plugins = set()
        self._plugin_canonical_map = {}
        self._plugin_group_map = {}
        self._plugin_update_button = None
        self._plugin_update_progress = None
        self._plugin_update_status = None
        self._plugin_update_in_progress = False
        self._plugin_tabs = {}
        self._plugin_tabs_cached = False
        self._dynamic_plugin_tabs = {}
        self.screenHeight = None
        self.screenWidth = None
        self.speechFamiliesChoice = None
        self.speechFamiliesChoices = None
        self.speechFamiliesModel = None
        self.speechLanguagesChoice = None
        self.speechLanguagesChoices = None
        self.speechLanguagesModel = None
        self.speechFamilies = []
        self.speechServersChoice = None
        self.speechServersChoices = None
        self.speechServersModel = None
        self.speechSystemsChoice = None
        self.speechSystemsChoices = None
        self.speechSystemsModel = None
        self.echoVoice = None
        self.echoSpeechFamiliesChoice = None
        self.echoSpeechFamiliesChoices = None
        self.echoSpeechFamiliesModel = None
        self.echoSpeechServersChoice = None
        self.echoSpeechServersChoices = None
        self.echoSpeechServersModel = None
        self.initializingEchoSpeech = False
        self._echoVoiceFetchToken = 0
        self._updatingEchoSpeechFamilies = False
        self.systemVoice = None
        self.uppercaseVoice = None
        self.window = None
        self.workingFactories = None
        self.savedGain = None
        self.savedPitch = None
        self.savedRate = None
        self.soundThemeCombo = None
        self.soundSinkCombo = None
        self.roleSoundPresentationCombo = None
        self.soundVolumeScale = None
        self.progressBarBeepIntervalSpinButton = None
        self._isInitialSetup = False
        self._updatingSpeechFamilies = False
        self.selectedFamilyChoices = {}
        self.selectedLanguageChoices = {}
        self.profilesCombo = None
        self.profilesComboModel = None
        self.startingProfileCombo = None
        self._initialFocusSyncAttempts = 0
        self._capturedKey = []
        self.script = None

    def init(self, script):
        """Initialize the Cthulhu configuration GUI. Read the users current
        set of preferences and set the GUI state to match. Setup speech
        support and populate the combo box lists on the Speech Tab pane
        accordingly.
        """

        self.script = script

        # Restore the default rate/pitch/gain,
        # in case the user played with the sliders.
        #        
        try:
            voices = cthulhu.cthulhuApp.settingsManager.getSetting('voices')
            defaultVoice = voices[settings.DEFAULT_VOICE]
        except KeyError:
            defaultVoice = {}
        try:
            self.savedGain = defaultVoice[acss.ACSS.GAIN]
        except KeyError:
            self.savedGain = 10.0
        try:
            self.savedPitch = defaultVoice[acss.ACSS.AVERAGE_PITCH]
        except KeyError:
            self.savedPitch = 5.0
        try:
            self.savedRate = defaultVoice[acss.ACSS.RATE]
        except KeyError:
            self.savedRate = 50.0

        # ***** Key Bindings treeview initialization *****

        self.keyBindView = self.get_widget("keyBindingsTreeview")

        if self.keyBindView.get_columns():
            for column in self.keyBindView.get_columns():
                self.keyBindView.remove_column(column)

        self.keyBindingsModel = Gtk.TreeStore(
            GObject.TYPE_STRING,  # Handler name
            GObject.TYPE_STRING,  # Human Readable Description
            GObject.TYPE_STRING,  # Modifier mask 1
            GObject.TYPE_STRING,  # Used Modifiers 1
            GObject.TYPE_STRING,  # Modifier key name 1
            GObject.TYPE_STRING,  # Click count 1
            GObject.TYPE_STRING,  # Original Text of the Key Binding Shown 1
            GObject.TYPE_STRING,  # Text of the Key Binding Shown 1
            GObject.TYPE_BOOLEAN, # Key Modified by User
            GObject.TYPE_BOOLEAN) # Row with fields editable or not

        self.planeCellRendererText = Gtk.CellRendererText()

        self.cellRendererText = Gtk.CellRendererText()
        self.cellRendererText.set_property("ellipsize", Pango.EllipsizeMode.END)

        # HANDLER - invisble column
        #
        column = Gtk.TreeViewColumn("Handler",
                                    self.planeCellRendererText,
                                    text=HANDLER)
        column.set_resizable(True)
        column.set_visible(False)
        column.set_sort_column_id(HANDLER)
        self.keyBindView.append_column(column)

        # DESCRIP
        #
        column = Gtk.TreeViewColumn(guilabels.KB_HEADER_FUNCTION,
                                    self.cellRendererText,
                                    text=DESCRIP)
        column.set_resizable(True)
        column.set_min_width(380)
        column.set_sort_column_id(DESCRIP)
        self.keyBindView.append_column(column)

        # MOD_MASK1 - invisble column
        #
        column = Gtk.TreeViewColumn("Mod.Mask 1",
                                    self.planeCellRendererText,
                                    text=MOD_MASK1)
        column.set_visible(False)
        column.set_resizable(True)
        column.set_sort_column_id(MOD_MASK1)
        self.keyBindView.append_column(column)

        # MOD_USED1 - invisble column
        #
        column = Gtk.TreeViewColumn("Use Mod.1",
                                    self.planeCellRendererText,
                                    text=MOD_USED1)
        column.set_visible(False)
        column.set_resizable(True)
        column.set_sort_column_id(MOD_USED1)
        self.keyBindView.append_column(column)

        # KEY1 - invisble column
        #
        column = Gtk.TreeViewColumn("Key1",
                                    self.planeCellRendererText,
                                    text=KEY1)
        column.set_resizable(True)
        column.set_visible(False)
        column.set_sort_column_id(KEY1)
        self.keyBindView.append_column(column)

        # CLICK_COUNT1 - invisble column
        #
        column = Gtk.TreeViewColumn("ClickCount1",
                                    self.planeCellRendererText,
                                    text=CLICK_COUNT1)
        column.set_resizable(True)
        column.set_visible(False)
        column.set_sort_column_id(CLICK_COUNT1)
        self.keyBindView.append_column(column)

        # OLDTEXT1 - invisble column which will store a copy of the
        # original keybinding in TEXT1 prior to the Apply or OK
        # buttons being pressed.  This will prevent automatic
        # resorting each time a cell is edited.
        #
        column = Gtk.TreeViewColumn("OldText1",
                                    self.planeCellRendererText,
                                    text=OLDTEXT1)
        column.set_resizable(True)
        column.set_visible(False)
        column.set_sort_column_id(OLDTEXT1)
        self.keyBindView.append_column(column)

        # TEXT1
        #
        rendererText = Gtk.CellRendererText()
        rendererText.connect("editing-started",
                             self.editingKey,
                             self.keyBindingsModel)
        rendererText.connect("editing-canceled",
                             self.editingCanceledKey)
        rendererText.connect('edited',
                             self.editedKey,
                             self.keyBindingsModel,
                             MOD_MASK1, MOD_USED1, KEY1, CLICK_COUNT1, TEXT1)

        column = Gtk.TreeViewColumn(guilabels.KB_HEADER_KEY_BINDING,
                                    rendererText,
                                    text=TEXT1,
                                    editable=EDITABLE)

        column.set_resizable(True)
        column.set_sort_column_id(OLDTEXT1)
        self.keyBindView.append_column(column)

        # MODIF
        #
        rendererToggle = Gtk.CellRendererToggle()
        rendererToggle.connect('toggled',
                               self.keyModifiedToggle,
                               self.keyBindingsModel,
                               MODIF)
        column = Gtk.TreeViewColumn(guilabels.KB_MODIFIED,
                                    rendererToggle,
                                    active=MODIF,
                                    activatable=EDITABLE)
        #column.set_visible(False)
        column.set_resizable(True)
        column.set_sort_column_id(MODIF)
        self.keyBindView.append_column(column)

        # EDITABLE - invisble column
        #
        rendererToggle = Gtk.CellRendererToggle()
        rendererToggle.set_property('activatable', False)
        column = Gtk.TreeViewColumn("Modified",
                                    rendererToggle,
                                    active=EDITABLE)
        column.set_visible(False)
        column.set_resizable(True)
        column.set_sort_column_id(EDITABLE)
        self.keyBindView.append_column(column)

        # Populates the treeview with all the keybindings:
        #
        self._populateKeyBindings()

        self.window = self.get_widget("cthulhuSetupWindow")

        self._setKeyEchoItems()

        self.speechSystemsModel  = \
                        self._initComboBox(self.get_widget("speechSystems"))
        self.speechServersModel  = \
                        self._initComboBox(self.get_widget("speechServers"))
        self.speechLanguagesModel = \
                        self._initComboBox(self.get_widget("speechLanguages"))
        self.speechFamiliesModel = \
                        self._initComboBox(self.get_widget("speechFamilies"))
        self.echoSpeechServersModel = \
                        self._initComboBox(self.get_widget("echoSpeechServers"))
        self.echoSpeechFamiliesModel = \
                        self._initComboBox(self.get_widget("echoSpeechFamilies"))
        self._initSpeechState()
        self._initEchoSpeechState()

        # TODO - JD: Will this ever be the case??
        self._isInitialSetup = \
            not os.path.exists(cthulhu.cthulhuApp.settingsManager.getPrefsDir())

        try:
            self._initPluginsPage()
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"PREFERENCES DIALOG: Plugin page init failed: {e}", True)

        appPage = self.script.getAppPreferencesGUI()
        if appPage:
            label = Gtk.Label(label=AXObject.get_name(self.script.app))
            self.get_widget("notebook").append_page(appPage, label)

        self._cache_plugin_tabs()
        self._update_plugin_tabs()
        self._refresh_dynamic_plugin_tabs()
        self._initGUIState()
        self._initSoundThemeState()

    def _initPluginsPage(self):
        self._plugin_sources = list(self.prefsDict.get("pluginSources", settings.pluginSources) or [])
        self._plugin_sources_original = list(self._plugin_sources)

        pluginsPage = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
        pluginsPage.set_border_width(12)

        infoLabel = Gtk.Label(label="Enable or disable plugins and manage plugin sources.")
        infoLabel.set_line_wrap(True)
        infoLabel.set_halign(Gtk.Align.START)
        pluginsPage.pack_start(infoLabel, False, False, 0)

        pluginsFrame = Gtk.Frame(label="Plugins")
        try:
            pluginsFrame.set_label_align(0.0, 0.5)
        except Exception:
            try:
                pluginsFrame.set_label_xalign(0.0)
            except Exception:
                pass
        pluginsFrame.set_shadow_type(Gtk.ShadowType.NONE)
        pluginsFrameBox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
        pluginsFrameBox.set_border_width(6)
        pluginsFrame.add(pluginsFrameBox)

        pluginsScrolled = Gtk.ScrolledWindow()
        pluginsScrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
        pluginsScrolled.set_size_request(-1, 200)
        self._plugin_model = Gtk.TreeStore(
            GObject.TYPE_BOOLEAN,  # enabled
            GObject.TYPE_STRING,   # display text
            GObject.TYPE_BOOLEAN,  # can toggle
            GObject.TYPE_STRING,   # plugin name
        )
        self._plugin_treeview = Gtk.TreeView(model=self._plugin_model)
        self._plugin_treeview.set_headers_visible(True)
        self._plugin_treeview.set_enable_search(False)
        self._plugin_treeview.connect("key-press-event", self._on_plugin_tree_key_press)
        self._plugin_treeview.connect("row-activated", self._on_plugin_tree_row_activated)

        toggle_renderer = Gtk.CellRendererToggle()
        toggle_renderer.set_activatable(True)
        toggle_renderer.connect("toggled", self._on_plugin_tree_toggled)
        toggle_column = Gtk.TreeViewColumn(
            "Enabled",
            toggle_renderer,
            active=self.PLUGIN_COL_ENABLED,
            activatable=self.PLUGIN_COL_CAN_TOGGLE
        )
        toggle_column.add_attribute(toggle_renderer, "visible", self.PLUGIN_COL_CAN_TOGGLE)
        self._plugin_treeview.append_column(toggle_column)

        text_renderer = Gtk.CellRendererText()
        text_renderer.set_property("ellipsize", Pango.EllipsizeMode.END)
        text_column = Gtk.TreeViewColumn(
            "Plugin",
            text_renderer,
            text=self.PLUGIN_COL_DISPLAY
        )
        text_column.set_expand(True)
        self._plugin_treeview.append_column(text_column)

        pluginsScrolled.add(self._plugin_treeview)
        pluginsFrameBox.pack_start(pluginsScrolled, True, True, 0)

        pluginsPage.pack_start(pluginsFrame, True, True, 0)

        sourcesFrame = Gtk.Frame(label="Plugin Sources")
        try:
            sourcesFrame.set_label_align(0.0, 0.5)
        except Exception:
            try:
                sourcesFrame.set_label_xalign(0.0)
            except Exception:
                pass
        sourcesFrame.set_shadow_type(Gtk.ShadowType.NONE)
        sourcesFrameBox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
        sourcesFrameBox.set_border_width(6)
        sourcesFrame.add(sourcesFrameBox)

        sourcesGrid = Gtk.Grid(row_spacing=6, column_spacing=6)
        sourcesLabel = Gtk.Label(label="_Source URL:")
        sourcesLabel.set_use_underline(True)
        sourcesLabel.set_halign(Gtk.Align.START)
        self._plugin_sources_entry = Gtk.Entry()
        self._plugin_sources_entry.set_placeholder_text("https://example.com/repo.git")
        sourcesLabel.set_mnemonic_widget(self._plugin_sources_entry)
        addButton = Gtk.Button(label="Add Source")
        addButton.connect("clicked", self._on_add_plugin_source)
        sourcesGrid.attach(sourcesLabel, 0, 0, 1, 1)
        sourcesGrid.attach(self._plugin_sources_entry, 1, 0, 1, 1)
        sourcesGrid.attach(addButton, 2, 0, 1, 1)
        sourcesFrameBox.pack_start(sourcesGrid, False, False, 0)

        updateRow = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
        self._plugin_update_button = Gtk.Button(label="Update Plugins")
        self._plugin_update_button.connect("clicked", self._on_update_plugins_clicked)
        self._plugin_update_progress = Gtk.ProgressBar()
        self._plugin_update_progress.set_show_text(True)
        self._plugin_update_progress.set_text("Idle")
        updateRow.pack_start(self._plugin_update_button, False, False, 0)
        updateRow.pack_start(self._plugin_update_progress, True, True, 0)
        sourcesFrameBox.pack_start(updateRow, False, False, 0)

        sourcesScrolled = Gtk.ScrolledWindow()
        sourcesScrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
        sourcesScrolled.set_size_request(-1, 120)
        self._plugin_sources_listbox = Gtk.ListBox()
        self._plugin_sources_listbox.set_selection_mode(Gtk.SelectionMode.NONE)
        sourcesScrolled.add(self._plugin_sources_listbox)
        sourcesFrameBox.pack_start(sourcesScrolled, True, True, 0)

        sourcesNote = Gtk.Label(label="Use Update Plugins to install or update sources, then Apply or OK to save settings.")
        sourcesNote.set_line_wrap(True)
        sourcesNote.set_halign(Gtk.Align.START)
        sourcesFrameBox.pack_start(sourcesNote, False, False, 0)

        self._plugin_update_status = Gtk.Label(label="")
        self._plugin_update_status.set_line_wrap(True)
        self._plugin_update_status.set_halign(Gtk.Align.START)
        sourcesFrameBox.pack_start(self._plugin_update_status, False, False, 0)

        pluginsPage.pack_start(sourcesFrame, True, True, 0)

        notebook = self.get_widget("notebook")
        notebook.append_page(pluginsPage, Gtk.Label(label="Plugins"))

        self._populate_plugin_list()
        self._populate_plugin_sources_list()
        pluginsPage.show_all()

    def _clear_listbox(self, listbox):
        for child in listbox.get_children():
            listbox.remove(child)

    def _populate_plugin_list(self):
        if not self._plugin_treeview:
            return

        try:
            self._plugin_model.clear()
            self._plugin_iters = {}
            self._available_plugins = set()
            self._plugin_canonical_map = {}
            self._plugin_group_map = {}

            manager = cthulhu.cthulhuApp.getPluginSystemManager()
            if manager:
                try:
                    manager.rescanPlugins()
                except Exception as e:
                    debug.printMessage(debug.LEVEL_WARNING, f"PREFERENCES DIALOG: Plugin rescan failed: {e}", True)
                    manager = None

            active_plugins = list(self.prefsDict.get("activePlugins", settings.activePlugins) or [])
            active_plugins_lower = {name.lower() for name in active_plugins}

            plugin_infos = manager.plugins if manager else []
            self._available_plugins = {info.get_module_name() for info in plugin_infos}
            canonical_counts = {}
            canonical_builtins = {}
            for info in plugin_infos:
                canonical = info.get_canonical_name()
                canonical_counts[canonical] = canonical_counts.get(canonical, 0) + 1
                if info.builtin:
                    canonical_builtins[canonical] = True

            self._plugin_enabled_iter = self._plugin_model.append(
                None,
                [False, "Enabled plugins", False, ""]
            )
            self._plugin_disabled_iter = self._plugin_model.append(
                None,
                [False, "Disabled plugins", False, ""]
            )

            for plugin_info in sorted(plugin_infos, key=lambda item: (item.get_name() or item.get_module_name()).lower()):
                plugin_name = plugin_info.get_module_name()
                canonical_name = plugin_info.get_canonical_name()
                if plugin_info.hidden or canonical_name == "PluginManager":
                    continue

                self._plugin_canonical_map[plugin_name] = canonical_name

                is_active = (
                    plugin_name in active_plugins
                    or plugin_name.lower() in active_plugins_lower
                    or (plugin_info.preferred_alias and (canonical_name in active_plugins or canonical_name.lower() in active_plugins_lower))
                    or plugin_info.builtin
                )

                can_toggle = True
                if canonical_builtins.get(canonical_name) and not plugin_info.builtin:
                    is_active = False
                    can_toggle = False

                if plugin_info.builtin:
                    can_toggle = False

                display_name = plugin_info.get_name() or plugin_name
                info_text = display_name
                description = plugin_info.get_description()
                if description:
                    info_text += f" - {description}"
                version = plugin_info.get_version()
                if version:
                    info_text += f" (v{version})"
                if canonical_counts.get(canonical_name, 0) > 1:
                    info_text += f" - Source: {plugin_info.get_source_label()}"
                    if canonical_builtins.get(canonical_name) and not plugin_info.builtin:
                        info_text += " - Disabled because a builtin plugin uses this name."

                parent_iter = self._plugin_enabled_iter if is_active else self._plugin_disabled_iter
                tree_iter = self._plugin_model.append(
                    parent_iter,
                    [is_active, info_text, can_toggle, plugin_name]
                )
                self._plugin_iters[plugin_name] = tree_iter
                self._plugin_group_map.setdefault(canonical_name, []).append(plugin_name)

            self._plugin_treeview.collapse_all()
            self._plugin_treeview.show_all()
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"PREFERENCES DIALOG: Plugin list build failed: {e}", True)

    def _populate_plugin_sources_list(self):
        if not self._plugin_sources_listbox:
            return

        self._clear_listbox(self._plugin_sources_listbox)

        for source in self._plugin_sources:
            row = Gtk.ListBoxRow()
            row.set_activatable(False)
            hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
            hbox.set_border_width(5)

            label = Gtk.Label(label=source)
            label.set_halign(Gtk.Align.START)
            label.set_line_wrap(True)

            remove_button = Gtk.Button(label="Remove")
            remove_button.connect("clicked", self._on_remove_plugin_source, source, row)

            hbox.pack_start(label, True, True, 0)
            hbox.pack_start(remove_button, False, False, 0)
            row.add(hbox)
            self._plugin_sources_listbox.add(row)

        self._plugin_sources_listbox.show_all()

    def _on_add_plugin_source(self, widget):
        if not self._plugin_sources_entry:
            return
        source = self._plugin_sources_entry.get_text().strip()
        if not source:
            return
        if source in self._plugin_sources:
            return
        self._plugin_sources.append(source)
        self._plugin_sources_entry.set_text("")
        self._populate_plugin_sources_list()

    def _on_remove_plugin_source(self, widget, source, row):
        if source in self._plugin_sources:
            self._plugin_sources.remove(source)
        if self._plugin_sources_listbox and row:
            self._plugin_sources_listbox.remove(row)

    def _on_update_plugins_clicked(self, widget):
        if self._plugin_update_in_progress:
            return

        sources = list(self._plugin_sources)
        if not sources:
            self._set_plugin_update_status("No sources to update.", done=True)
            return

        self._plugin_update_in_progress = True
        if self._plugin_update_button:
            self._plugin_update_button.set_sensitive(False)
        self._set_plugin_update_progress(0, len(sources), "Starting updates...")

        def _run_updates():
            manager = cthulhu.cthulhuApp.getPluginSystemManager()
            if not manager:
                GLib.idle_add(self._finish_plugin_updates, "Plugin manager unavailable.")
                return

            def _progress_callback(index, total, message):
                GLib.idle_add(self._set_plugin_update_progress, index, total, message)

            manager.syncPluginSources(sources, progress_callback=_progress_callback)
            try:
                manager.rescanPlugins()
            except Exception as e:
                GLib.idle_add(self._finish_plugin_updates, f"Update finished with errors: {e}")
                return

            GLib.idle_add(self._finish_plugin_updates, "Update complete.")

        thread = threading.Thread(target=_run_updates, daemon=True)
        thread.start()

    def _set_plugin_update_progress(self, index, total, message):
        if not self._plugin_update_progress:
            return
        fraction = 0.0 if total <= 0 else min(1.0, float(index) / float(total))
        self._plugin_update_progress.set_fraction(fraction)
        self._plugin_update_progress.set_text(message)
        self._set_plugin_update_status(message)

    def _set_plugin_update_status(self, message, done=False):
        if self._plugin_update_status:
            self._plugin_update_status.set_text(message)
        if done and self._plugin_update_progress:
            self._plugin_update_progress.set_fraction(0.0)
            self._plugin_update_progress.set_text("Idle")

    def _finish_plugin_updates(self, message):
        self._plugin_update_in_progress = False
        if self._plugin_update_button:
            self._plugin_update_button.set_sensitive(True)
        if message:
            self._set_plugin_update_status(message)
        self._populate_plugin_list()

    def _on_plugin_tree_toggled(self, renderer, path):
        if not self._plugin_model:
            return

        tree_iter = self._plugin_model.get_iter(path)
        if not tree_iter:
            return

        can_toggle = self._plugin_model.get_value(tree_iter, self.PLUGIN_COL_CAN_TOGGLE)
        if not can_toggle:
            return

        plugin_name = self._plugin_model.get_value(tree_iter, self.PLUGIN_COL_NAME)
        if not plugin_name:
            return

        current_active = self._plugin_model.get_value(tree_iter, self.PLUGIN_COL_ENABLED)
        new_active = not current_active

        if new_active:
            canonical_name = self._plugin_canonical_map.get(plugin_name)
            for other_name in self._plugin_group_map.get(canonical_name, []):
                if other_name == plugin_name:
                    continue
                self._set_plugin_row_active(other_name, False)

        self._set_plugin_row_active(plugin_name, new_active)

    def _on_plugin_tree_key_press(self, widget, event):
        if event.keyval != Gdk.KEY_space:
            return False

        selection = self._plugin_treeview.get_selection()
        model, tree_iter = selection.get_selected()
        if not tree_iter:
            return False

        path = model.get_path(tree_iter)
        self._on_plugin_tree_toggled(None, path)
        return True

    def _on_plugin_tree_row_activated(self, treeview, path, column):
        self._on_plugin_tree_toggled(None, path)

    def _set_plugin_row_active(self, plugin_name, is_active):
        tree_iter = self._plugin_iters.get(plugin_name)
        if not tree_iter:
            return

        current_active = self._plugin_model.get_value(tree_iter, self.PLUGIN_COL_ENABLED)
        if current_active == is_active:
            return

        can_toggle = self._plugin_model.get_value(tree_iter, self.PLUGIN_COL_CAN_TOGGLE)
        display_text = self._plugin_model.get_value(tree_iter, self.PLUGIN_COL_DISPLAY)

        selection = self._plugin_treeview.get_selection()
        model, selected_iter = selection.get_selected()
        was_selected = selected_iter == tree_iter

        self._plugin_model.remove(tree_iter)

        parent_iter = self._plugin_enabled_iter if is_active else self._plugin_disabled_iter
        new_iter = self._plugin_model.append(
            parent_iter,
            [is_active, display_text, can_toggle, plugin_name]
        )
        self._plugin_iters[plugin_name] = new_iter

        if was_selected:
            path = self._plugin_model.get_path(new_iter)
            self._plugin_treeview.expand_to_path(path)
            selection.select_path(path)
        self._update_plugin_tabs()

    def _get_active_plugins_from_ui(self):
        existing_plugins = list(self.prefsDict.get("activePlugins", settings.activePlugins) or [])
        preserved_plugins = [
            name for name in existing_plugins
            if name not in self._plugin_iters and name in self._available_plugins
        ]
        selected_plugins = []
        for canonical_name, plugin_names in self._plugin_group_map.items():
            active_in_group = [
                name for name in plugin_names
                if self._plugin_iters.get(name)
                and self._plugin_model.get_value(self._plugin_iters[name], self.PLUGIN_COL_ENABLED)
            ]
            if active_in_group:
                selected_plugins.append(active_in_group[-1])
        return preserved_plugins + selected_plugins

    def _apply_plugin_changes(self):
        active_plugins = self._get_active_plugins_from_ui()
        plugin_sources = list(self._plugin_sources)

        self.prefsDict["activePlugins"] = active_plugins
        self.prefsDict["pluginSources"] = plugin_sources

        removed_sources = [source for source in self._plugin_sources_original if source not in plugin_sources]
        manager = cthulhu.cthulhuApp.getPluginSystemManager()
        if manager:
            try:
                manager.removePluginSources(removed_sources)
                manager.rescanPlugins()
            except Exception as e:
                debug.printMessage(debug.LEVEL_WARNING, f"PREFERENCES DIALOG: Plugin sync failed: {e}", True)

        self._plugin_sources_original = list(plugin_sources)
        self._populate_plugin_list()
        self._update_plugin_tabs(active_plugins)

    def _get_active_plugin_infos(self):
        manager = cthulhu.cthulhuApp.getPluginSystemManager()
        if not manager:
            return []
        return [info for info in manager.plugins if info.loaded and info.instance]

    def _get_plugin_preferences_page(self, plugin_info):
        plugin_instance = plugin_info.instance
        if not plugin_instance:
            return None

        if hasattr(plugin_instance, "getPreferencesGUI"):
            page = plugin_instance.getPreferencesGUI()
        elif hasattr(plugin_instance, "get_preferences_gui"):
            page = plugin_instance.get_preferences_gui()
        else:
            return None

        if not page:
            return None

        label = plugin_info.get_name() or plugin_info.get_module_name()
        if isinstance(page, (list, tuple)) and len(page) == 2:
            page, label = page

        if isinstance(label, str):
            label_widget = Gtk.Label(label=label)
        else:
            label_widget = label

        return page, label_widget

    def _refresh_dynamic_plugin_tabs(self):
        notebook = self.get_widget("notebook")
        if not notebook:
            return

        for tab in list(self._dynamic_plugin_tabs.values()):
            page = tab.get("page")
            if page:
                page_num = notebook.page_num(page)
                if page_num != -1:
                    notebook.remove_page(page_num)

        self._dynamic_plugin_tabs = {}

        plugin_infos = self._get_active_plugin_infos()
        for plugin_info in sorted(plugin_infos, key=lambda item: (item.get_name() or item.get_module_name()).lower()):
            result = self._get_plugin_preferences_page(plugin_info)
            if not result:
                continue

            page, label = result
            if not page or not label:
                continue

            notebook.append_page(page, label)
            page.show_all()
            label.show()
            self._dynamic_plugin_tabs[plugin_info.get_module_name()] = {
                "page": page,
                "label": label,
            }

        for plugin_name in self._dynamic_plugin_tabs:
            tab = self._plugin_tabs.get(plugin_name)
            if not tab:
                continue
            page = tab.get("page")
            if not page:
                continue
            page_num = notebook.page_num(page)
            if page_num != -1:
                notebook.remove_page(page_num)

    def _cache_plugin_tabs(self):
        if self._plugin_tabs_cached:
            return

        notebook = self.get_widget("notebook")
        if not notebook:
            return

        tab_specs = [
            ("AIAssistant", "aiPage", "aiTabLabel"),
            ("OCR", "ocrGrid", "ocrTabLabel"),
        ]

        for plugin_name, page_id, label_id in tab_specs:
            page = self.get_widget(page_id)
            label = self.get_widget(label_id)
            if not page or not label:
                continue
            position = notebook.page_num(page)
            self._plugin_tabs[plugin_name] = {
                "page": page,
                "label": label,
                "position": position,
            }

        self._plugin_tabs_cached = True

    def _get_active_plugins_for_tabs(self):
        if self._plugin_iters:
            return self._get_active_plugins_from_ui()
        return list(self.prefsDict.get("activePlugins", settings.activePlugins) or [])

    @staticmethod
    def _plugin_active(active_plugins, names):
        active_lower = {name.lower() for name in active_plugins}
        return any(name.lower() in active_lower for name in names)

    def _update_plugin_tabs(self, active_plugins=None):
        if not self._plugin_tabs:
            return

        notebook = self.get_widget("notebook")
        if not notebook:
            return

        if active_plugins is None:
            active_plugins = self._get_active_plugins_for_tabs()

        for plugin_name, tab in self._plugin_tabs.items():
            if plugin_name in self._dynamic_plugin_tabs:
                continue
            page = tab["page"]
            label = tab["label"]
            position = tab["position"]
            page_num = notebook.page_num(page)
            is_active = self._plugin_active(active_plugins, [plugin_name])

            if is_active and page_num == -1:
                insert_pos = min(max(position, 0), notebook.get_n_pages())
                notebook.insert_page(page, label, insert_pos)
                page.show_all()
                label.show()
            elif not is_active and page_num != -1:
                notebook.remove_page(page_num)

    def _getACSSForVoiceType(self, voiceType):
        """Return the ACSS value for the given voice type.

        Arguments:
        - voiceType: one of DEFAULT, UPPERCASE, HYPERLINK, SYSTEM

        Returns the voice dictionary for the given voice type.
        """

        if voiceType == DEFAULT:
            voiceACSS = self.defaultVoice
        elif voiceType == UPPERCASE:
            voiceACSS = self.uppercaseVoice
        elif voiceType == HYPERLINK:
            voiceACSS = self.hyperlinkVoice
        elif voiceType == SYSTEM:
            voiceACSS = self.systemVoice
        else:
            voiceACSS = self.defaultVoice

        return voiceACSS

    def writeUserPreferences(self):
        """Write out the user's generic Cthulhu preferences.
        """
        pronunciationDict = self.getModelDict(self.pronunciationModel)
        keyBindingsDict = self.getKeyBindingsModelDict(self.keyBindingsModel)
        self.prefsDict.update(self.script.getPreferencesFromGUI())
        self.prefsDict.update(self._get_plugin_preferences_from_gui())
        cthulhu.cthulhuApp.settingsManager.saveSettings(self.script,
                                      self.prefsDict,
                                      pronunciationDict,
                                      keyBindingsDict)

    def _get_plugin_preferences_from_gui(self):
        preferences = {}
        for plugin_info in self._get_active_plugin_infos():
            plugin_instance = plugin_info.instance
            if not plugin_instance:
                continue

            if hasattr(plugin_instance, "getPreferencesFromGUI"):
                plugin_prefs = plugin_instance.getPreferencesFromGUI()
            elif hasattr(plugin_instance, "get_preferences_from_gui"):
                plugin_prefs = plugin_instance.get_preferences_from_gui()
            else:
                continue

            if isinstance(plugin_prefs, dict):
                preferences.update(plugin_prefs)

        return preferences

    def _getKeyValueForVoiceType(self, voiceType, key, useDefault=True):
        """Look for the value of the given key in the voice dictionary
           for the given voice type.

        Arguments:
        - voiceType: one of DEFAULT, UPPERCASE, HYPERLINK, SYSTEM
        - key: the key to look for in the voice dictionary.
        - useDefault: if True, and the key isn't found for the given voice
                      type, the look for it in the default voice dictionary
                      as well.

        Returns the value of the given key, or None if it's not set.
        """

        if voiceType == DEFAULT:
            voice = self.defaultVoice
        elif voiceType == UPPERCASE:
            voice = self.uppercaseVoice
            if key not in voice:
                if not useDefault:
                    return None
                voice = self.defaultVoice
        elif voiceType == HYPERLINK:
            voice = self.hyperlinkVoice
            if key not in voice:
                if not useDefault:
                    return None
                voice = self.defaultVoice
        elif voiceType == SYSTEM:
            voice = self.systemVoice
            if key not in voice:
                if not useDefault:
                    return None
                voice = self.defaultVoice
        else:
            voice = self.defaultVoice

        if key in voice:
            return voice[key]
        else:
            return None

    def _getFamilyNameForVoiceType(self, voiceType):
        """Gets the name of the voice family for the given voice type.

        Arguments:
        - voiceType: one of DEFAULT, UPPERCASE, HYPERLINK, SYSTEM

        Returns the name of the voice family for the given voice type,
        or None if not set.
        """

        familyName = None
        family = self._getKeyValueForVoiceType(voiceType, acss.ACSS.FAMILY)

        if family and speechserver.VoiceFamily.NAME in family:
            familyName = family[speechserver.VoiceFamily.NAME]

        return familyName

    def _setFamilyNameForVoiceType(self, voiceType, name, language, dialect, variant):
        """Sets the name of the voice family for the given voice type.

        Arguments:
        - voiceType: one of DEFAULT, UPPERCASE, HYPERLINK, SYSTEM
        - name: the name of the voice family to set.
        - language: the locale of the voice family to set.
        - dialect: the dialect of the voice family to set.
        """

        family = self._getKeyValueForVoiceType(voiceType,
                                               acss.ACSS.FAMILY,
                                               False)

        voiceACSS = self._getACSSForVoiceType(voiceType)
        if family:
            family[speechserver.VoiceFamily.NAME] = name
            family[speechserver.VoiceFamily.LANG] = language
            family[speechserver.VoiceFamily.DIALECT] = dialect
            family[speechserver.VoiceFamily.VARIANT] = variant
        else:
            voiceACSS[acss.ACSS.FAMILY] = {}
            voiceACSS[acss.ACSS.FAMILY][speechserver.VoiceFamily.NAME] = name
            voiceACSS[acss.ACSS.FAMILY][speechserver.VoiceFamily.LANG] = language
            voiceACSS[acss.ACSS.FAMILY][speechserver.VoiceFamily.DIALECT] = dialect
            voiceACSS[acss.ACSS.FAMILY][speechserver.VoiceFamily.VARIANT] = variant
        voiceACSS['established'] = True

        #settings.voices[voiceType] = voiceACSS

    def _getRateForVoiceType(self, voiceType):
        """Gets the speaking rate value for the given voice type.

        Arguments:
        - voiceType: one of DEFAULT, UPPERCASE, HYPERLINK, SYSTEM

        Returns the rate value for the given voice type, or None if
        not set.
        """

        return self._getKeyValueForVoiceType(voiceType, acss.ACSS.RATE)

    def _setRateForVoiceType(self, voiceType, value):
        """Sets the speaking rate value for the given voice type.

        Arguments:
        - voiceType: one of DEFAULT, UPPERCASE, HYPERLINK, SYSTEM
        - value: the rate value to set.
        """

        voiceACSS = self._getACSSForVoiceType(voiceType)
        voiceACSS[acss.ACSS.RATE] = value
        voiceACSS['established'] = True
        #settings.voices[voiceType] = voiceACSS

    def _getPitchForVoiceType(self, voiceType):
        """Gets the pitch value for the given voice type.

        Arguments:
        - voiceType: one of DEFAULT, UPPERCASE, HYPERLINK, SYSTEM

        Returns the pitch value for the given voice type, or None if
        not set.
        """

        return self._getKeyValueForVoiceType(voiceType,
                                             acss.ACSS.AVERAGE_PITCH)

    def _setPitchForVoiceType(self, voiceType, value):
        """Sets the pitch value for the given voice type.

        Arguments:
        - voiceType: one of DEFAULT, UPPERCASE, HYPERLINK, SYSTEM
        - value: the pitch value to set.
        """

        voiceACSS = self._getACSSForVoiceType(voiceType)
        voiceACSS[acss.ACSS.AVERAGE_PITCH] = value
        voiceACSS['established'] = True
        #settings.voices[voiceType] = voiceACSS

    def _getVolumeForVoiceType(self, voiceType):
        """Gets the volume (gain) value for the given voice type.

        Arguments:
        - voiceType: one of DEFAULT, UPPERCASE, HYPERLINK, SYSTEM

        Returns the volume (gain) value for the given voice type, or
        None if not set.
        """

        return self._getKeyValueForVoiceType(voiceType, acss.ACSS.GAIN)

    def _setVolumeForVoiceType(self, voiceType, value):
        """Sets the volume (gain) value for the given voice type.

        Arguments:
        - voiceType: one of DEFAULT, UPPERCASE, HYPERLINK, SYSTEM
        - value: the volume (gain) value to set.
        """

        voiceACSS = self._getACSSForVoiceType(voiceType)
        voiceACSS[acss.ACSS.GAIN] = value
        voiceACSS['established'] = True
        #settings.voices[voiceType] = voiceACSS

    def _setVoiceSettingsForVoiceType(self, voiceType):
        """Sets the family, rate, pitch and volume GUI components based
        on the given voice type.

        Arguments:
        - voiceType: one of DEFAULT, UPPERCASE, HYPERLINK, SYSTEM
        """

        familyName = self._getFamilyNameForVoiceType(voiceType)
        self._setSpeechFamiliesChoice(familyName)

        rate = self._getRateForVoiceType(voiceType)
        if rate is not None:
            self.get_widget("rateScale").set_value(rate)
        else:
            self.get_widget("rateScale").set_value(50.0)

        pitch = self._getPitchForVoiceType(voiceType)
        if pitch is not None:
            self.get_widget("pitchScale").set_value(pitch)
        else:
            self.get_widget("pitchScale").set_value(5.0)

        volume = self._getVolumeForVoiceType(voiceType)
        if volume is not None:
            self.get_widget("volumeScale").set_value(volume)
        else:
            self.get_widget("volumeScale").set_value(10.0)

    def _setSpeechFamiliesChoice(self, familyName):
        """Sets the active item in the families ("Person:") combo box
        to the given family name.

        Arguments:
        - familyName: the family name to use to set the active combo box item.
        """

        if len(self.speechFamilies) == 0:
            return

        languageSet = False
        familySet = False
        for family in self.speechFamilies:
            name = family[speechserver.VoiceFamily.NAME]
            if name == familyName:
                lang = family[speechserver.VoiceFamily.LANG]
                dialect = family[speechserver.VoiceFamily.DIALECT]

                if dialect:
                    language = lang + '-' + dialect
                else:
                    language = lang

                i = 0
                for languageChoice in self.speechLanguagesChoices:
                    if languageChoice == language:
                        self.get_widget("speechLanguages").set_active(i)
                        self.speechLanguagesChoice = self.speechLanguagesChoices[i]
                        languageSet = True

                        self._setupFamilies()

                        i = 0
                        for familyChoice in self.speechFamiliesChoices:
                            name = familyChoice[speechserver.VoiceFamily.NAME]
                            if name == familyName:
                                self._setSpeechFamiliesActive(i)
                                self.speechFamiliesChoice = self.speechFamiliesChoices[i]
                                familySet = True
                                break
                            i += 1

                        break

                    i += 1

                break

        if not languageSet:
            tokens = ["PREFERENCES DIALOG: Could not find speech language match for", familyName]
            debug.printTokens(debug.LEVEL_INFO, tokens, True)
            self.get_widget("speechLanguages").set_active(0)
            self.speechLanguagesChoice = self.speechLanguagesChoices[0]

        if languageSet:
            self.selectedLanguageChoices[self.speechServersChoice] = i

        if not familySet:
            tokens = ["PREFERENCES DIALOG: Could not find speech family match for", familyName]
            debug.printTokens(debug.LEVEL_INFO, tokens, True)
            self._setSpeechFamiliesActive(0)
            self.speechFamiliesChoice = self.speechFamiliesChoices[0]

        if familySet:
            self.selectedFamilyChoices[self.speechServersChoice,
                    self.speechLanguagesChoice] = i

    def _setSpeechFamiliesActive(self, index):
        """Sets speechFamilies active index while suppressing preview chatter."""

        self._updatingSpeechFamilies = True
        try:
            self.get_widget("speechFamilies").set_active(index)
        finally:
            self._updatingSpeechFamilies = False

    def _setupFamilies(self):
        """Gets the list of voice variants for the current speech server and
        current language.
        If there are variants, get the information associated with
        each voice variant and add an entry for it to the variants
        GtkComboBox list.
        """

        combobox = self.get_widget("speechFamilies")
        combobox.set_model(None)
        self.speechFamiliesModel.clear()

        currentLanguage = self.speechLanguagesChoice

        i = 0
        self.speechFamiliesChoices = []
        for family in self.speechFamilies:
            lang = family[speechserver.VoiceFamily.LANG]
            dialect = family[speechserver.VoiceFamily.DIALECT]

            if dialect:
                language = lang + '-' + dialect
            else:
                language = lang

            if language != currentLanguage:
                continue

            name = family[speechserver.VoiceFamily.NAME]
            self.speechFamiliesChoices.append(family)
            self.speechFamiliesModel.append((i, name))
            i += 1

        combobox.set_model(self.speechFamiliesModel)
        if i == 0:
            tokens = ["No speech family was available for", str(currentLanguage), "."]
            debug.printTokens(debug.LEVEL_SEVERE, tokens, True)
            debug.printStack(debug.LEVEL_FINEST)
            self.speechFamiliesChoice = None
            return

        # If user manually selected a family for the current speech server
        # this choice it's restored. In other case the first family
        # (usually the default one) is selected
        #
        selectedIndex = 0
        if (self.speechServersChoice, self.speechLanguagesChoice) \
                in self.selectedFamilyChoices:
            selectedIndex = self.selectedFamilyChoices[self.speechServersChoice,
                    self.speechLanguagesChoice]

        self._setSpeechFamiliesActive(selectedIndex)

    def _setSpeechLanguagesChoice(self, languageName):
        """Sets the active item in the languages ("Language:") combo box
        to the given language name.

        Arguments:
        - languageName: the language name to use to set the active combo box item.
        """

        if len(self.speechLanguagesChoices) == 0:
            return

        valueSet = False
        i = 0
        for language in self.speechLanguagesChoices:
            if language == languageName:
                self.get_widget("speechLanguages").set_active(i)
                self.speechLanguagesChoice = self.speechLanguagesChoices[i]
                valueSet = True
                break
            i += 1

        if not valueSet:
            tokens = ["PREFERENCES DIALOG: Could not find speech language match for", languageName]
            debug.printTokens(debug.LEVEL_INFO, tokens, True)
            self.get_widget("speechLanguages").set_active(0)
            self.speechLanguagesChoice = self.speechLanguagesChoices[0]

        if valueSet:
            self.selectedLanguageChoices[self.speechServersChoice] = i

        self._setupFamilies()

    def _setupVoices(self):
        """Gets the list of voices for the current speech server.
        If there are families, get the information associated with
        each voice family and add an entry for it to the families
        GtkComboBox list.
        """

        combobox = self.get_widget("speechLanguages")
        combobox.set_model(None)
        self.speechLanguagesModel.clear()
        self.speechFamilies = self.speechServersChoice.getVoiceFamilies()
        self.speechLanguagesChoices = []

        if len(self.speechFamilies) == 0:
            debug.printMessage(debug.LEVEL_SEVERE, "No speech voice was available.")
            debug.printStack(debug.LEVEL_FINEST)
            self.speechLanguagesChoice = None
            return

        done = {}
        i = 0
        for family in self.speechFamilies:
            lang = family[speechserver.VoiceFamily.LANG]
            dialect = family[speechserver.VoiceFamily.DIALECT]
            if (lang,dialect) in done:
                continue
            done[lang,dialect] = True

            if dialect:
                language = lang + '-' + dialect
            else:
                language = lang

            # TODO: get translated language name from CLDR or such
            msg = language
            if msg == "":
                # Unsupported locale
                msg = "default language"

            self.speechLanguagesChoices.append(language)
            self.speechLanguagesModel.append((i, msg))
            i += 1

        # If user manually selected a language for the current speech server
        # this choice it's restored. In other case the first language
        # (usually the default one) is selected
        #
        selectedIndex = 0
        if self.speechServersChoice in self.selectedLanguageChoices:
            selectedIndex = self.selectedLanguageChoices[self.speechServersChoice]

        combobox.set_model(self.speechLanguagesModel)

        self.get_widget("speechLanguages").set_active(selectedIndex)
        if self.initializingSpeech:
            self.speechLanguagesChoice = self.speechLanguagesChoices[selectedIndex]

        self._setupFamilies()

        # The family name will be selected as part of selecting the
        # voice type.  Whenever the families change, we'll reset the
        # voice type selection to the first one ("Default").
        #
        comboBox = self.get_widget("voiceTypesCombo")
        types = [guilabels.SPEECH_VOICE_TYPE_DEFAULT,
                 guilabels.SPEECH_VOICE_TYPE_UPPERCASE,
                 guilabels.SPEECH_VOICE_TYPE_HYPERLINK,
                 guilabels.SPEECH_VOICE_TYPE_SYSTEM]
        self.populateComboBox(comboBox, types)
        comboBox.set_active(DEFAULT)
        voiceType = comboBox.get_active()
        self._setVoiceSettingsForVoiceType(voiceType)

    def _setSpeechServersChoice(self, serverInfo):
        """Sets the active item in the speech servers combo box to the
        given server.

        Arguments:
        - serversChoices: the list of available speech servers.
        - serverInfo: the speech server to use to set the active combo
        box item.
        """

        if len(self.speechServersChoices) == 0:
            return

        # We'll fallback to whatever we happen to be using in the event
        # that this preference has never been set.
        #
        if not serverInfo:
            serverInfo = speech.getInfo()
            if serverInfo and len(serverInfo) >= 2 and serverInfo[1] == 'default':
                defaultFamily = None
                voices = self.prefsDict.get("voices", {})
                defaultVoice = voices.get(settings.DEFAULT_VOICE) if voices else None
                if defaultVoice:
                    defaultFamily = acss.ACSS(defaultVoice).get(acss.ACSS.FAMILY)

                resolved = self._resolveSpeechDispatcherServerForFamily(defaultFamily)
                if resolved is not None:
                    serverInfo = resolved.getInfo()

        valueSet = False
        i = 0
        for server in self.speechServersChoices:
            if serverInfo == server.getInfo():
                self.get_widget("speechServers").set_active(i)
                self.speechServersChoice = server
                valueSet = True
                break
            i += 1

        if not valueSet:
            tokens = ["PREFERENCES DIALOG: Could not find speech server match for", serverInfo]
            debug.printTokens(debug.LEVEL_INFO, tokens, True)
            self.get_widget("speechServers").set_active(0)
            self.speechServersChoice = self.speechServersChoices[0]

        self._setupVoices()

    def _setupSpeechServers(self):
        """Gets the list of speech servers for the current speech factory.
        If there are servers, get the information associated with each
        speech server and add an entry for it to the speechServers
        GtkComboBox list.  Set the current choice to be the first item.
        """

        combobox = self.get_widget("speechServers")
        combobox.set_model(None)
        self.speechServersModel.clear()
        self.speechServersChoices = \
                self.speechSystemsChoice.SpeechServer.getSpeechServers()
        if len(self.speechServersChoices) == 0:
            debug.printMessage(debug.LEVEL_SEVERE, "Speech not available.")
            debug.printStack(debug.LEVEL_FINEST)
            self.speechServersChoice = None
            self.speechLanguagesChoices = []
            self.speechLanguagesChoice = None
            self.speechFamiliesChoices = []
            self.speechFamiliesChoice = None
            combobox.set_model(self.speechServersModel)
            return

        i = 0
        for server in self.speechServersChoices:
            name = server.getInfo()[0]
            self.speechServersModel.append((i, name))
            i += 1

        combobox.set_model(self.speechServersModel)
        self._setSpeechServersChoice(self.prefsDict["speechServerInfo"])

    def _setSpeechSystemsChoice(self, systemName):
        """Set the active item in the speech systems combo box to the
        given system name.

        Arguments:
        - factoryChoices: the list of available speech factories (systems).
        - systemName: the speech system name to use to set the active combo
        box item.
        """

        systemName = systemName.strip("'")

        if len(self.speechSystemsChoices) == 0:
            self.speechSystemsChoice = None
            return

        valueSet = False
        i = 0
        for speechSystem in self.speechSystemsChoices:
            name = speechSystem.__name__
            if name.endswith(systemName):
                self.get_widget("speechSystems").set_active(i)
                self.speechSystemsChoice = self.speechSystemsChoices[i]
                valueSet = True
                break
            i += 1

        if not valueSet:
            tokens = ["PREFERENCES DIALOG: Could not find speech system match for", systemName]
            debug.printTokens(debug.LEVEL_INFO, tokens, True)
            self.get_widget("speechSystems").set_active(0)
            self.speechSystemsChoice = self.speechSystemsChoices[0]

        self._setupSpeechServers()

    def _setupSpeechSystems(self, factories):
        """Sets up the speech systems combo box and sets the selection
        to the preferred speech system.

        Arguments:
        -factories: the list of known speech factories (working or not)
        """

        combobox = self.get_widget("speechSystems")
        combobox.set_model(None)
        self.speechSystemsModel.clear()
        self.workingFactories = []
        for factory in factories:
            try:
                servers = factory.SpeechServer.getSpeechServers()
                if len(servers):
                    self.workingFactories.append(factory)
            except Exception:
                debug.printException(debug.LEVEL_FINEST)

        self.speechSystemsChoices = []
        if len(self.workingFactories) == 0:
            debug.printMessage(debug.LEVEL_SEVERE, "Speech not available.")
            debug.printStack(debug.LEVEL_FINEST)
            self.speechSystemsChoice = None
            self.speechServersChoices = []
            self.speechServersChoice = None
            self.speechLanguagesChoices = []
            self.speechLanguagesChoice = None
            self.speechFamiliesChoices = []
            self.speechFamiliesChoice = None
            combobox.set_model(self.speechSystemsModel)
            return

        i = 0
        for workingFactory in self.workingFactories:
            self.speechSystemsChoices.append(workingFactory)
            name = workingFactory.SpeechServer.getFactoryName()
            self.speechSystemsModel.append((i, name))
            i += 1

        combobox.set_model(self.speechSystemsModel)
        if self.prefsDict["speechServerFactory"]:
            self._setSpeechSystemsChoice(self.prefsDict["speechServerFactory"])
        else:
            self.speechSystemsChoice = None

    def _initSpeechState(self):
        """Initialize the various speech components.
        """

        voices = self.prefsDict["voices"]
        self.defaultVoice   = acss.ACSS(voices.get(settings.DEFAULT_VOICE))
        self.uppercaseVoice = acss.ACSS(voices.get(settings.UPPERCASE_VOICE))
        self.hyperlinkVoice = acss.ACSS(voices.get(settings.HYPERLINK_VOICE))
        self.systemVoice    = acss.ACSS(voices.get(settings.SYSTEM_VOICE))

        # Just a note on general naming pattern:
        #
        # *        = The name of the combobox
        # *Model   = the name of the comobox model
        # *Choices = the Cthulhu/speech python objects
        # *Choice  = a value from *Choices
        #
        # Where * = speechSystems, speechServers, speechLanguages, speechFamilies
        #
        factories = cthulhu.cthulhuApp.settingsManager.getSpeechServerFactories()
        if len(factories) == 0 or not self.prefsDict.get('enableSpeech', True):
            self.workingFactories = []
            self.speechSystemsChoice = None
            self.speechServersChoices = []
            self.speechServersChoice = None
            self.speechLanguagesChoices = []
            self.speechLanguagesChoice = None
            self.speechFamiliesChoices = []
            self.speechFamiliesChoice = None
            return

        try:
            speech.init()
        except Exception:
            self.workingFactories = []
            self.speechSystemsChoice = None
            self.speechServersChoices = []
            self.speechServersChoice = None
            self.speechLanguagesChoices = []
            self.speechLanguagesChoice = None
            self.speechFamiliesChoices = []
            self.speechFamiliesChoice = None
            return

        # This cascades into systems->servers->voice_type->families...
        #
        self.initializingSpeech = True
        self._setupSpeechSystems(factories)
        self.initializingSpeech = False

    def _getSpeechDispatcherFactory(self):
        """Returns the Speech Dispatcher factory if available."""

        factories = cthulhu.cthulhuApp.settingsManager.getSpeechServerFactories()
        for factory in factories:
            try:
                if factory.SpeechServer.getFactoryName() == guilabels.SPEECH_DISPATCHER:
                    return factory
            except Exception:
                debug.printException(debug.LEVEL_FINEST)

        return None

    def _setEchoVoiceFamily(self, family):
        """Sets echo voice family from the selected server family object."""

        if not family:
            return

        name = family.get(speechserver.VoiceFamily.NAME)
        language = family.get(speechserver.VoiceFamily.LANG)
        dialect = family.get(speechserver.VoiceFamily.DIALECT)
        variant = family.get(speechserver.VoiceFamily.VARIANT)
        self.echoVoice[acss.ACSS.FAMILY] = {
            speechserver.VoiceFamily.NAME: name,
            speechserver.VoiceFamily.LANG: language,
            speechserver.VoiceFamily.DIALECT: dialect,
            speechserver.VoiceFamily.VARIANT: variant,
        }
        self.echoVoice['established'] = True

    def _resolveSpeechDispatcherServerForFamily(self, family):
        """Returns a concrete Speech Dispatcher server matching the voice family."""

        if not family or not self.speechServersChoices:
            return None

        name = family.get(speechserver.VoiceFamily.NAME)
        language = family.get(speechserver.VoiceFamily.LANG)
        dialect = family.get(speechserver.VoiceFamily.DIALECT)
        variant = family.get(speechserver.VoiceFamily.VARIANT)
        if not name:
            return None

        for server in self.speechServersChoices:
            info = server.getInfo()
            if not info or len(info) < 2 or info[1] == 'default':
                continue

            try:
                families = server.getVoiceFamilies()
            except Exception:
                debug.printException(debug.LEVEL_FINEST)
                continue

            for candidate in families:
                if candidate.get(speechserver.VoiceFamily.NAME) != name:
                    continue
                if candidate.get(speechserver.VoiceFamily.LANG) != language:
                    continue
                if candidate.get(speechserver.VoiceFamily.DIALECT) != dialect:
                    continue
                if candidate.get(speechserver.VoiceFamily.VARIANT) != variant:
                    continue

                tokens = [
                    "PREFERENCES DIALOG: Resolved voice family",
                    name,
                    "to speech server",
                    info,
                ]
                debug.printTokens(debug.LEVEL_INFO, tokens, True)
                return server

        tokens = [
            "PREFERENCES DIALOG: Could not resolve speech server for family",
            family,
        ]
        debug.printTokens(debug.LEVEL_INFO, tokens, True)
        return None

    def _getSpeechServerChoiceForSave(self):
        """Returns the server choice that should be persisted for speech settings."""

        server = self.speechServersChoice
        if not server or self.speechSystemsChoice != self._getSpeechDispatcherFactory():
            return server

        info = server.getInfo()
        if not info or len(info) < 2 or info[1] != 'default':
            return server

        defaultFamily = None
        if self.defaultVoice is not None:
            defaultFamily = self.defaultVoice.get(acss.ACSS.FAMILY)

        resolved = self._resolveSpeechDispatcherServerForFamily(defaultFamily)
        return resolved or server

    def _getEchoSpeechServerFamilyForSave(self):
        """Returns the most specific echo family to use for server resolution."""

        family = None
        if self.echoVoice is not None:
            family = self.echoVoice.get(acss.ACSS.FAMILY)

        if family:
            return family

        if self.defaultVoice is not None:
            return self.defaultVoice.get(acss.ACSS.FAMILY)

        return None

    def _getEchoSpeechServerChoiceForSave(self):
        """Returns the echo speech server choice that should be persisted."""

        server = self.echoSpeechServersChoice
        if server is None:
            return None

        info = server.getInfo()
        if not info or len(info) < 2 or info[1] != 'default':
            return server

        family = self._getEchoSpeechServerFamilyForSave()
        resolved = self._resolveSpeechDispatcherServerForFamily(family)
        if resolved is not None:
            return resolved

        speechServer = self._getSpeechServerChoiceForSave()
        if speechServer is not None:
            speechInfo = speechServer.getInfo()
            if speechInfo and len(speechInfo) >= 2 and speechInfo[1] != 'default':
                tokens = [
                    "PREFERENCES DIALOG: Falling back to main speech server for echo",
                    speechInfo,
                ]
                debug.printTokens(debug.LEVEL_INFO, tokens, True)
                return speechServer

        return server

    def _populateEchoSpeechFamilies(self, families):
        """Populate the echo family combobox from the provided families list."""

        combobox = self.get_widget("echoSpeechFamilies")
        combobox.set_model(None)
        self.echoSpeechFamiliesModel.clear()
        self.echoSpeechFamiliesChoices = list(families or [])
        self.echoSpeechFamiliesChoice = None

        selectedIndex = 0
        selectedMatchFound = False
        selectedFamily = self.echoVoice.get(acss.ACSS.FAMILY)
        selectedName = selectedLanguage = selectedDialect = selectedVariant = None
        if selectedFamily:
            selectedName = selectedFamily.get(speechserver.VoiceFamily.NAME)
            selectedLanguage = selectedFamily.get(speechserver.VoiceFamily.LANG)
            selectedDialect = selectedFamily.get(speechserver.VoiceFamily.DIALECT)
            selectedVariant = selectedFamily.get(speechserver.VoiceFamily.VARIANT)

        for i, family in enumerate(self.echoSpeechFamiliesChoices):
            name = family.get(speechserver.VoiceFamily.NAME) or ""
            language = family.get(speechserver.VoiceFamily.LANG) or ""
            dialect = family.get(speechserver.VoiceFamily.DIALECT) or ""
            variant = family.get(speechserver.VoiceFamily.VARIANT)
            display = name
            locale = language
            if dialect:
                locale = f"{language}-{dialect}" if language else dialect
            if locale:
                display = f"{name} ({locale})"
            self.echoSpeechFamiliesModel.append((i, display))
            if selectedName == name and selectedLanguage == language \
               and selectedDialect == dialect and selectedVariant == variant:
                selectedIndex = i
                selectedMatchFound = True

        combobox.set_model(self.echoSpeechFamiliesModel)
        if self.echoSpeechFamiliesChoices:
            self._updatingEchoSpeechFamilies = True
            try:
                combobox.set_active(selectedIndex)
            finally:
                self._updatingEchoSpeechFamilies = False
            self.echoSpeechFamiliesChoice = self.echoSpeechFamiliesChoices[selectedIndex]

    def _fetchEchoSpeechFamiliesWorker(self, serverInfo, requestToken):
        """Fetch module-specific voices in a subprocess and apply asynchronously."""

        queryScript = """
import json
import sys
from cthulhu import speechdispatcherfactory
from cthulhu import speechserver

serverInfo = json.loads(sys.argv[1])
if isinstance(serverInfo, list):
    serverInfo = tuple(serverInfo)

server = speechdispatcherfactory.SpeechServer.getSpeechServer(serverInfo)
families = server.getVoiceFamilies() if server else []

result = []
for family in families:
    result.append({
        "name": family.get(speechserver.VoiceFamily.NAME),
        "lang": family.get(speechserver.VoiceFamily.LANG),
        "dialect": family.get(speechserver.VoiceFamily.DIALECT),
        "variant": family.get(speechserver.VoiceFamily.VARIANT),
    })

print(json.dumps(result))
"""

        familyDefs = None
        try:
            completed = subprocess.run(
                [sys.executable, "-c", queryScript, json.dumps(serverInfo)],
                capture_output=True,
                text=True,
                timeout=4.0,
                check=False,
            )
            if completed.returncode == 0 and completed.stdout.strip():
                familyDefs = json.loads(completed.stdout.strip())
            else:
                tokens = [
                    "PREFERENCES DIALOG: Echo voice fetch failed for",
                    serverInfo,
                    "rc=",
                    completed.returncode,
                ]
                debug.printTokens(debug.LEVEL_WARNING, tokens, True)
        except subprocess.TimeoutExpired:
            tokens = ["PREFERENCES DIALOG: Echo voice fetch timed out for", serverInfo]
            debug.printTokens(debug.LEVEL_WARNING, tokens, True)
        except Exception:
            debug.printException(debug.LEVEL_WARNING)

        GLib.idle_add(self._applyFetchedEchoSpeechFamilies, requestToken, familyDefs)

    def _applyFetchedEchoSpeechFamilies(self, requestToken, familyDefs):
        """Apply fetched echo families if this is still the active request."""

        if requestToken != self._echoVoiceFetchToken:
            return False

        if not isinstance(familyDefs, list):
            return False

        families = []
        for familyDef in familyDefs:
            if not isinstance(familyDef, dict):
                continue
            families.append(
                speechserver.VoiceFamily({
                    speechserver.VoiceFamily.NAME: familyDef.get("name"),
                    speechserver.VoiceFamily.LANG: familyDef.get("lang"),
                    speechserver.VoiceFamily.DIALECT: familyDef.get("dialect"),
                    speechserver.VoiceFamily.VARIANT: familyDef.get("variant"),
                })
            )

        if not families:
            return False

        self._populateEchoSpeechFamilies(families)
        return False

    def _setupEchoSpeechFamilies(self):
        """Populate echo voice family list for the current echo server."""
        self._echoVoiceFetchToken += 1
        requestToken = self._echoVoiceFetchToken
        useCustomModule = self.get_widget("useCustomEchoSpeechServerCheckButton").get_active()
        fallbackFamilies = list(self.speechFamiliesChoices or [])
        self._populateEchoSpeechFamilies(fallbackFamilies)

        if useCustomModule and self.echoSpeechServersChoice:
            serverInfo = self.echoSpeechServersChoice.getInfo()
            thread = threading.Thread(
                target=self._fetchEchoSpeechFamiliesWorker,
                args=(serverInfo, requestToken),
                daemon=True,
            )
            thread.start()

    def _setEchoSpeechServersChoice(self, serverInfo):
        """Set the active echo module based on stored server info."""

        if not self.echoSpeechServersChoices:
            self.echoSpeechServersChoice = None
            self._setupEchoSpeechFamilies()
            return

        if serverInfo and len(serverInfo) >= 2 and serverInfo[1] == 'default':
            family = self._getEchoSpeechServerFamilyForSave()
            resolved = self._resolveSpeechDispatcherServerForFamily(family)
            if resolved is None:
                speechServer = self._getSpeechServerChoiceForSave()
                if speechServer is not None:
                    speechInfo = speechServer.getInfo()
                    if speechInfo and len(speechInfo) >= 2 and speechInfo[1] != 'default':
                        resolved = speechServer
                        tokens = [
                            "PREFERENCES DIALOG: Reusing main speech server for echo",
                            speechInfo,
                        ]
                        debug.printTokens(debug.LEVEL_INFO, tokens, True)
            if resolved is not None:
                serverInfo = resolved.getInfo()

        valueSet = False
        for i, server in enumerate(self.echoSpeechServersChoices):
            info = server.getInfo()
            if serverInfo and info == serverInfo:
                self.get_widget("echoSpeechServers").set_active(i)
                self.echoSpeechServersChoice = server
                valueSet = True
                break

        if not valueSet:
            self.get_widget("echoSpeechServers").set_active(0)
            self.echoSpeechServersChoice = self.echoSpeechServersChoices[0]

        self._setupEchoSpeechFamilies()

    def _setupEchoSpeechServers(self):
        """Populate available speech-dispatcher modules for echo."""

        combobox = self.get_widget("echoSpeechServers")
        combobox.set_model(None)
        self.echoSpeechServersModel.clear()
        self.echoSpeechServersChoices = []
        self.echoSpeechServersChoice = None

        if not self.prefsDict.get('enableSpeech', True):
            combobox.set_model(self.echoSpeechServersModel)
            self._setupEchoSpeechFamilies()
            return

        factory = self._getSpeechDispatcherFactory()
        if factory is None:
            combobox.set_model(self.echoSpeechServersModel)
            self._setupEchoSpeechFamilies()
            return

        try:
            self.echoSpeechServersChoices = factory.SpeechServer.getSpeechServers()
        except Exception:
            debug.printException(debug.LEVEL_WARNING)
            self.echoSpeechServersChoices = []

        for i, server in enumerate(self.echoSpeechServersChoices):
            name = server.getInfo()[0]
            self.echoSpeechServersModel.append((i, name))

        combobox.set_model(self.echoSpeechServersModel)
        self._setEchoSpeechServersChoice(self.prefsDict.get("echoSpeechServerInfo"))

    def _initEchoSpeechState(self):
        """Initialize echo voice controls and choices."""

        self.echoVoice = acss.ACSS(
            self.prefsDict.get("echoVoice", settings.echoVoice) or {})

        baseVoice = self.defaultVoice or acss.ACSS(
            self.prefsDict.get("voices", {}).get(settings.DEFAULT_VOICE, {}))

        rate = self.echoVoice.get(acss.ACSS.RATE, baseVoice.get(acss.ACSS.RATE, 50.0))
        pitch = self.echoVoice.get(acss.ACSS.AVERAGE_PITCH, baseVoice.get(acss.ACSS.AVERAGE_PITCH, 5.0))
        volume = self.echoVoice.get(acss.ACSS.GAIN, baseVoice.get(acss.ACSS.GAIN, 10.0))

        self.get_widget("echoRateScale").set_value(rate)
        self.get_widget("echoPitchScale").set_value(pitch)
        self.get_widget("echoVolumeScale").set_value(volume)

        self.initializingEchoSpeech = True
        self._setupEchoSpeechServers()
        self.initializingEchoSpeech = False

        self._setEchoVoiceItems()

    def _setSpokenTextAttributes(self, view, setAttributes,
                                 state, moveToTop=False):
        """Given a set of spoken text attributes, update the model used by the
        text attribute tree view.

        Arguments:
        - view: the text attribute tree view.
        - setAttributes: the list of spoken text attributes to update.
        - state: the state (True or False) that they all should be set to.
        - moveToTop: if True, move these attributes to the top of the list.
        """

        model = view.get_model()
        view.set_model(None)

        [attrList, attrDict] = \
           self.script.utilities.stringToKeysAndDict(setAttributes)
        [allAttrList, allAttrDict] = self.script.utilities.stringToKeysAndDict(
            cthulhu.cthulhuApp.settingsManager.getSetting('allTextAttributes'))

        for i in range(0, len(attrList)):
            for path in range(0, len(allAttrList)):
                localizedKey = text_attribute_names.getTextAttributeName(
                    attrList[i], self.script)
                localizedValue = text_attribute_names.getTextAttributeName(
                    attrDict[attrList[i]], self.script)
                if localizedKey == model[path][NAME]:
                    thisIter = model.get_iter(path)
                    model.set_value(thisIter, NAME, localizedKey)
                    model.set_value(thisIter, IS_SPOKEN, state)
                    model.set_value(thisIter, VALUE, localizedValue)
                    if moveToTop:
                        thisIter = model.get_iter(path)
                        otherIter = model.get_iter(i)
                        model.move_before(thisIter, otherIter)
                    break

        view.set_model(model)

    def _setBrailledTextAttributes(self, view, setAttributes, state):
        """Given a set of brailled text attributes, update the model used
        by the text attribute tree view.

        Arguments:
        - view: the text attribute tree view.
        - setAttributes: the list of brailled text attributes to update.
        - state: the state (True or False) that they all should be set to.
        """

        model = view.get_model()
        view.set_model(None)

        [attrList, attrDict] = \
            self.script.utilities.stringToKeysAndDict(setAttributes)
        [allAttrList, allAttrDict] = self.script.utilities.stringToKeysAndDict(
                cthulhu.cthulhuApp.settingsManager.getSetting('allTextAttributes'))

        for i in range(0, len(attrList)):
            for path in range(0, len(allAttrList)):
                localizedKey = text_attribute_names.getTextAttributeName(
                    attrList[i], self.script)
                if localizedKey == model[path][NAME]:
                    thisIter = model.get_iter(path)
                    model.set_value(thisIter, IS_BRAILLED, state)
                    break

        view.set_model(model)

    def _getAppNameForAttribute(self, attributeName):
        """Converts the given Atk attribute name into the application's
        equivalent. This is necessary because an application or toolkit
        (e.g. Gecko) might invent entirely new names for the same text
        attributes.

        Arguments:
        - attribName: The name of the text attribute

        Returns the application's equivalent name if found or attribName
        otherwise.
        """

        return self.script.utilities.getAppNameForAttribute(attributeName)

    def _updateTextDictEntry(self):
        """The user has updated the text attribute list in some way. Update
        the "enabledSpokenTextAttributes" and "enabledBrailledTextAttributes"
        preference strings to reflect the current state of the corresponding
        text attribute lists.
        """

        model = self.getTextAttributesView.get_model()
        spokenAttrStr = ""
        brailledAttrStr = ""
        noRows = model.iter_n_children(None)
        for path in range(0, noRows):
            localizedKey = model[path][NAME]
            key = text_attribute_names.getTextAttributeKey(localizedKey)

            # Convert the normalized, Atk attribute name back into what
            # the app/toolkit uses.
            #
            key = self._getAppNameForAttribute(key)

            localizedValue = model[path][VALUE]
            value = text_attribute_names.getTextAttributeKey(localizedValue)

            if model[path][IS_SPOKEN]:
                spokenAttrStr += key + ":" + value + "; "
            if model[path][IS_BRAILLED]:
                brailledAttrStr += key + ":" + value + "; "

        self.prefsDict["enabledSpokenTextAttributes"] = spokenAttrStr
        self.prefsDict["enabledBrailledTextAttributes"] = brailledAttrStr

    def contractedBrailleToggled(self, checkbox):
        grid = self.get_widget('contractionTableGrid')
        grid.set_sensitive(checkbox.get_active())
        self.prefsDict["enableContractedBraille"] = checkbox.get_active()

    def contractionTableComboChanged(self, combobox):
        model = combobox.get_model()
        myIter = combobox.get_active_iter()
        self.prefsDict["brailleContractionTable"] = model[myIter][1]

    def flashPersistenceToggled(self, checkbox):
        grid = self.get_widget('flashMessageDurationGrid')
        grid.set_sensitive(not checkbox.get_active())
        self.prefsDict["flashIsPersistent"] = checkbox.get_active()

    def textAttributeSpokenToggled(self, cell, path, model):
        """The user has toggled the state of one of the text attribute
        checkboxes to be spoken. Update our model to reflect this, then
        update the "enabledSpokenTextAttributes" preference string.

        Arguments:
        - cell: the cell that changed.
        - path: the path of that cell.
        - model: the model that the cell is part of.
        """

        thisIter = model.get_iter(path)
        model.set(thisIter, IS_SPOKEN, not model[path][IS_SPOKEN])
        self._updateTextDictEntry()

    def textAttributeBrailledToggled(self, cell, path, model):
        """The user has toggled the state of one of the text attribute
        checkboxes to be brailled. Update our model to reflect this,
        then update the "enabledBrailledTextAttributes" preference string.

        Arguments:
        - cell: the cell that changed.
        - path: the path of that cell.
        - model: the model that the cell is part of.
        """

        thisIter = model.get_iter(path)
        model.set(thisIter, IS_BRAILLED, not model[path][IS_BRAILLED])
        self._updateTextDictEntry()

    def textAttrValueEdited(self, cell, path, new_text, model):
        """The user has edited the value of one of the text attributes.
        Update our model to reflect this, then update the
        "enabledSpokenTextAttributes" and "enabledBrailledTextAttributes"
        preference strings.

        Arguments:
        - cell: the cell that changed.
        - path: the path of that cell.
        - new_text: the new text attribute value string.
        - model: the model that the cell is part of.
        """

        thisIter = model.get_iter(path)
        model.set(thisIter, VALUE, new_text)
        self._updateTextDictEntry()

    def textAttrCursorChanged(self, widget):
        """Set the search column in the text attribute tree view
        depending upon which column the user currently has the cursor in.
        """

        [path, focusColumn] = self.getTextAttributesView.get_cursor()
        if focusColumn:
            noColumns = len(self.getTextAttributesView.get_columns())
            for i in range(0, noColumns):
                col = self.getTextAttributesView.get_column(i)
                if focusColumn == col:
                    self.getTextAttributesView.set_search_column(i)
                    break

    def _createTextAttributesTreeView(self):
        """Create the text attributes tree view. The view is the
        textAttributesTreeView GtkTreeView widget. The view will consist
        of a list containing three columns:
          IS_SPOKEN - a checkbox whose state indicates whether this text
                      attribute will be spoken or not.
          NAME      - the text attribute name.
          VALUE     - if set, (and this attributes is enabled for speaking),
                      then this attribute will be spoken unless it equals
                      this value.
        """

        self.getTextAttributesView = self.get_widget("textAttributesTreeView")

        if self.getTextAttributesView.get_columns():
            for column in self.getTextAttributesView.get_columns():
                self.getTextAttributesView.remove_column(column)

        model = Gtk.ListStore(GObject.TYPE_STRING,
                              GObject.TYPE_BOOLEAN,
                              GObject.TYPE_BOOLEAN,
                              GObject.TYPE_STRING)

        # Initially setup the list store model based on the values of all
        # the known text attributes.
        #
        [allAttrList, allAttrDict] = self.script.utilities.stringToKeysAndDict(
            cthulhu.cthulhuApp.settingsManager.getSetting('allTextAttributes'))
        for i in range(0, len(allAttrList)):
            thisIter = model.append()
            localizedKey = text_attribute_names.getTextAttributeName(
                allAttrList[i], self.script)
            localizedValue = text_attribute_names.getTextAttributeName(
                allAttrDict[allAttrList[i]], self.script)
            model.set_value(thisIter, NAME, localizedKey)
            model.set_value(thisIter, IS_SPOKEN, False)
            model.set_value(thisIter, IS_BRAILLED, False)
            model.set_value(thisIter, VALUE, localizedValue)

        self.getTextAttributesView.set_model(model)

        # Attribute Name column (NAME).
        column = Gtk.TreeViewColumn(guilabels.TEXT_ATTRIBUTE_NAME)
        column.set_min_width(250)
        column.set_resizable(True)
        renderer = Gtk.CellRendererText()
        column.pack_end(renderer, True)
        column.add_attribute(renderer, 'text', NAME)
        self.getTextAttributesView.insert_column(column, 0)

        # Attribute Speak column (IS_SPOKEN).
        speakAttrColumnLabel = guilabels.PRESENTATION_SPEAK
        column = Gtk.TreeViewColumn(speakAttrColumnLabel)
        renderer = Gtk.CellRendererToggle()
        column.pack_start(renderer, False)
        column.add_attribute(renderer, 'active', IS_SPOKEN)
        renderer.connect("toggled",
                         self.textAttributeSpokenToggled,
                         model)
        self.getTextAttributesView.insert_column(column, 1)
        column.clicked()

        # Attribute Mark in Braille column (IS_BRAILLED).
        markAttrColumnLabel = guilabels.PRESENTATION_MARK_IN_BRAILLE
        column = Gtk.TreeViewColumn(markAttrColumnLabel)
        renderer = Gtk.CellRendererToggle()
        column.pack_start(renderer, False)
        column.add_attribute(renderer, 'active', IS_BRAILLED)
        renderer.connect("toggled",
                         self.textAttributeBrailledToggled,
                         model)
        self.getTextAttributesView.insert_column(column, 2)
        column.clicked()

        # Attribute Value column (VALUE)
        column = Gtk.TreeViewColumn(guilabels.PRESENTATION_PRESENT_UNLESS)
        renderer = Gtk.CellRendererText()
        renderer.set_property('editable', True)
        column.pack_end(renderer, True)
        column.add_attribute(renderer, 'text', VALUE)
        renderer.connect("edited", self.textAttrValueEdited, model)

        self.getTextAttributesView.insert_column(column, 4)

        # Check all the enabled (spoken) text attributes.
        #
        self._setSpokenTextAttributes(
            self.getTextAttributesView,
            cthulhu.cthulhuApp.settingsManager.getSetting('enabledSpokenTextAttributes'),
            True, True)

        # Check all the enabled (brailled) text attributes.
        #
        self._setBrailledTextAttributes(
            self.getTextAttributesView,
            cthulhu.cthulhuApp.settingsManager.getSetting('enabledBrailledTextAttributes'),
            True)

        # Connect a handler for when the user changes columns within the
        # view, so that we can adjust the search column for item lookups.
        #
        self.getTextAttributesView.connect("cursor_changed",
                                           self.textAttrCursorChanged)

    def pronActualValueEdited(self, cell, path, new_text, model):
        """The user has edited the value of one of the actual strings in
        the pronunciation dictionary. Update our model to reflect this.

        Arguments:
        - cell: the cell that changed.
        - path: the path of that cell.
        - new_text: the new pronunciation dictionary actual string.
        - model: the model that the cell is part of.
        """

        thisIter = model.get_iter(path)
        model.set(thisIter, ACTUAL, new_text)

    def pronReplacementValueEdited(self, cell, path, new_text, model):
        """The user has edited the value of one of the replacement strings
        in the pronunciation dictionary. Update our model to reflect this.

        Arguments:
        - cell: the cell that changed.
        - path: the path of that cell.
        - new_text: the new pronunciation dictionary replacement string.
        - model: the model that the cell is part of.
        """

        thisIter = model.get_iter(path)
        model.set(thisIter, REPLACEMENT, new_text)

    def pronunciationFocusChange(self, widget, event, isFocused):
        """Callback for the pronunciation tree's focus-{in,out}-event signal."""

        cthulhu.cthulhuApp.settingsManager.setSetting('usePronunciationDictionary', not isFocused)

    def pronunciationCursorChanged(self, widget):
        """Set the search column in the pronunciation dictionary tree view
        depending upon which column the user currently has the cursor in.
        """

        [path, focusColumn] = self.pronunciationView.get_cursor()
        if focusColumn:
            noColumns = len(self.pronunciationView.get_columns())
            for i in range(0, noColumns):
                col = self.pronunciationView.get_column(i)
                if focusColumn == col:
                    self.pronunciationView.set_search_column(i)
                    break

    def _createPronunciationTreeView(self):
        """Create the pronunciation dictionary tree view. The view is the
        pronunciationTreeView GtkTreeView widget. The view will consist
        of a list containing two columns:
          ACTUAL      - the actual text string (word).
          REPLACEMENT - the string that is used to pronounce that word.
        """

        self.pronunciationView = self.get_widget("pronunciationTreeView")

        if self.pronunciationView.get_columns():
            for column in self.pronunciationView.get_columns():
                self.pronunciationView.remove_column(column)

        model = Gtk.ListStore(GObject.TYPE_STRING,
                              GObject.TYPE_STRING)

        # Initially setup the list store model based on the values of all
        # existing entries in the pronunciation dictionary -- unless it's
        # the default script.
        #
        if not self.script.app:
            _profile = self.prefsDict.get('activeProfile')[1]
            pronDict = cthulhu.cthulhuApp.settingsManager.getPronunciations(_profile)
        else:
            pronDict = pronunciation_dict.pronunciation_dict
        for pronKey in sorted(pronDict.keys()):
            thisIter = model.append() 
            try:
                actual, replacement = pronDict[pronKey]
            except Exception:
                # Try to do something sensible for the previous format of
                # pronunciation dictionary entries. See bug #464754 for
                # more details.
                #
                actual = pronKey
                replacement = pronDict[pronKey]
            model.set(thisIter, 
                      ACTUAL, actual,
                      REPLACEMENT, replacement)

        self.pronunciationView.set_model(model)

        # Pronunciation Dictionary actual string (word) column (ACTUAL).
        column = Gtk.TreeViewColumn(guilabels.DICTIONARY_ACTUAL_STRING)
        column.set_min_width(250)
        column.set_resizable(True)
        renderer = Gtk.CellRendererText()
        renderer.set_property('editable', True)
        column.pack_end(renderer, True) 
        column.add_attribute(renderer, 'text', ACTUAL)
        renderer.connect("edited", self.pronActualValueEdited, model)
        self.pronunciationView.insert_column(column, 0)

        # Pronunciation Dictionary replacement string column (REPLACEMENT)
        column = Gtk.TreeViewColumn(guilabels.DICTIONARY_REPLACEMENT_STRING)
        renderer = Gtk.CellRendererText()
        renderer.set_property('editable', True)
        column.pack_end(renderer, True)
        column.add_attribute(renderer, 'text', REPLACEMENT)
        renderer.connect("edited", self.pronReplacementValueEdited, model)
        self.pronunciationView.insert_column(column, 1)

        self.pronunciationModel = model

        # Connect a handler for when the user changes columns within the
        # view, so that we can adjust the search column for item lookups.
        #
        self.pronunciationView.connect("cursor_changed",
                                       self.pronunciationCursorChanged)

        self.pronunciationView.connect(
            "focus_in_event", self.pronunciationFocusChange, True)
        self.pronunciationView.connect(
            "focus_out_event", self.pronunciationFocusChange, False)

    def _initGUIState(self):
        """Adjust the settings of the various components on the
        configuration GUI depending upon the users preferences.
        """

        prefs = self.prefsDict

        # Speech pane.
        #
        enable = prefs["enableSpeech"]
        self.get_widget("speechSupportCheckButton").set_active(enable)
        self.get_widget("speechOptionsGrid").set_sensitive(enable)

        enable = prefs["onlySpeakDisplayedText"]
        self.get_widget("onlySpeakDisplayedTextCheckButton").set_active(enable)
        self.get_widget("contextOptionsGrid").set_sensitive(not enable)

        if prefs["verbalizePunctuationStyle"] == \
                               settings.PUNCTUATION_STYLE_NONE:
            self.get_widget("noneButton").set_active(True)
        elif prefs["verbalizePunctuationStyle"] == \
                               settings.PUNCTUATION_STYLE_SOME:
            self.get_widget("someButton").set_active(True)
        elif prefs["verbalizePunctuationStyle"] == \
                               settings.PUNCTUATION_STYLE_MOST:
            self.get_widget("mostButton").set_active(True)
        else:
            self.get_widget("allButton").set_active(True)

        if prefs["speechVerbosityLevel"] == settings.VERBOSITY_LEVEL_BRIEF:
            self.get_widget("speechBriefButton").set_active(True)
        else:
            self.get_widget("speechVerboseButton").set_active(True)

        self.get_widget("onlySpeakDisplayedTextCheckButton").set_active(
            prefs["onlySpeakDisplayedText"])


        self.get_widget("speakBlankLinesCheckButton").set_active(\
            prefs["speakBlankLines"])
        self.get_widget("speakMultiCaseStringsAsWordsCheckButton").set_active(\
            prefs["speakMultiCaseStringsAsWords"])
        self.get_widget("speakNumbersAsDigitsCheckButton").set_active(
            prefs.get("speakNumbersAsDigits", settings.speakNumbersAsDigits))
        self.get_widget("enableTutorialMessagesCheckButton").set_active(\
            prefs["enableTutorialMessages"])
        self.get_widget("enablePauseBreaksCheckButton").set_active(\
            prefs["enablePauseBreaks"])
        self.get_widget("enablePositionSpeakingCheckButton").set_active(\
            prefs["enablePositionSpeaking"])
        self.get_widget("enableMnemonicSpeakingCheckButton").set_active(\
            prefs["enableMnemonicSpeaking"])
        self.get_widget("speakMisspelledIndicatorCheckButton").set_active(
            prefs.get("speakMisspelledIndicator", settings.speakMisspelledIndicator))
        self.get_widget("speakDescriptionCheckButton").set_active(
            prefs.get("speakDescription", settings.speakDescription))
        self.get_widget("speakContextBlockquoteCheckButton").set_active(
            prefs.get("speakContextBlockquote", settings.speakContextList))
        self.get_widget("speakContextLandmarkCheckButton").set_active(
            prefs.get("speakContextLandmark", settings.speakContextLandmark))
        self.get_widget("speakContextNonLandmarkFormCheckButton").set_active(
            prefs.get("speakContextNonLandmarkForm", settings.speakContextNonLandmarkForm))
        self.get_widget("speakContextListCheckButton").set_active(
            prefs.get("speakContextList", settings.speakContextList))
        self.get_widget("speakContextPanelCheckButton").set_active(
            prefs.get("speakContextPanel", settings.speakContextPanel))
        self.get_widget("speakContextTableCheckButton").set_active(
            prefs.get("speakContextTable", settings.speakContextTable))

        enable = prefs.get("messagesAreDetailed", settings.messagesAreDetailed)
        self.get_widget("messagesAreDetailedCheckButton").set_active(enable)

        enable = prefs.get("useColorNames", settings.useColorNames)
        self.get_widget("useColorNamesCheckButton").set_active(enable)

        enable = prefs.get("readFullRowInGUITable", settings.readFullRowInGUITable)
        self.get_widget("readFullRowInGUITableCheckButton").set_active(enable)

        enable = prefs.get("readFullRowInDocumentTable", settings.readFullRowInDocumentTable)
        self.get_widget("readFullRowInDocumentTableCheckButton").set_active(enable)

        enable = prefs.get("readFullRowInSpreadSheet", settings.readFullRowInSpreadSheet)
        self.get_widget("readFullRowInSpreadSheetCheckButton").set_active(enable)

        style = prefs.get("capitalizationStyle", settings.capitalizationStyle)
        combobox = self.get_widget("capitalizationStyle")
        options = [guilabels.CAPITALIZATION_STYLE_NONE,
                   guilabels.CAPITALIZATION_STYLE_ICON,
                   guilabels.CAPITALIZATION_STYLE_SPELL]
        self.populateComboBox(combobox, options)
        if style == settings.CAPITALIZATION_STYLE_ICON:
            value = guilabels.CAPITALIZATION_STYLE_ICON
        elif style == settings.CAPITALIZATION_STYLE_SPELL:
            value = guilabels.CAPITALIZATION_STYLE_SPELL
        else:
            value = guilabels.CAPITALIZATION_STYLE_NONE
        combobox.set_active(options.index(value))

        combobox2 = self.get_widget("dateFormatCombo")
        sdtime = time.strftime
        ltime = time.localtime
        self.populateComboBox(combobox2,
          [sdtime(messages.DATE_FORMAT_LOCALE, ltime()),
           sdtime(messages.DATE_FORMAT_NUMBERS_DM, ltime()),
           sdtime(messages.DATE_FORMAT_NUMBERS_MD, ltime()),
           sdtime(messages.DATE_FORMAT_NUMBERS_DMY, ltime()),
           sdtime(messages.DATE_FORMAT_NUMBERS_MDY, ltime()),
           sdtime(messages.DATE_FORMAT_NUMBERS_YMD, ltime()),
           sdtime(messages.DATE_FORMAT_FULL_DM, ltime()),
           sdtime(messages.DATE_FORMAT_FULL_MD, ltime()),
           sdtime(messages.DATE_FORMAT_FULL_DMY, ltime()),
           sdtime(messages.DATE_FORMAT_FULL_MDY, ltime()),
           sdtime(messages.DATE_FORMAT_FULL_YMD, ltime()),
           sdtime(messages.DATE_FORMAT_ABBREVIATED_DM, ltime()),
           sdtime(messages.DATE_FORMAT_ABBREVIATED_MD, ltime()),
           sdtime(messages.DATE_FORMAT_ABBREVIATED_DMY, ltime()),
           sdtime(messages.DATE_FORMAT_ABBREVIATED_MDY, ltime()),
           sdtime(messages.DATE_FORMAT_ABBREVIATED_YMD, ltime())
          ])

        indexdate = DATE_FORMAT_LOCALE
        dateFormat = self.prefsDict["presentDateFormat"]
        if dateFormat == messages.DATE_FORMAT_LOCALE:
            indexdate = DATE_FORMAT_LOCALE
        elif dateFormat == messages.DATE_FORMAT_NUMBERS_DM:
            indexdate = DATE_FORMAT_NUMBERS_DM
        elif dateFormat == messages.DATE_FORMAT_NUMBERS_MD:
            indexdate = DATE_FORMAT_NUMBERS_MD
        elif dateFormat == messages.DATE_FORMAT_NUMBERS_DMY:
            indexdate = DATE_FORMAT_NUMBERS_DMY
        elif dateFormat == messages.DATE_FORMAT_NUMBERS_MDY:
            indexdate = DATE_FORMAT_NUMBERS_MDY
        elif dateFormat == messages.DATE_FORMAT_NUMBERS_YMD:
            indexdate = DATE_FORMAT_NUMBERS_YMD
        elif dateFormat == messages.DATE_FORMAT_FULL_DM:
            indexdate = DATE_FORMAT_FULL_DM
        elif dateFormat == messages.DATE_FORMAT_FULL_MD:
            indexdate = DATE_FORMAT_FULL_MD
        elif dateFormat == messages.DATE_FORMAT_FULL_DMY:
            indexdate = DATE_FORMAT_FULL_DMY
        elif dateFormat == messages.DATE_FORMAT_FULL_MDY:
            indexdate = DATE_FORMAT_FULL_MDY
        elif dateFormat == messages.DATE_FORMAT_FULL_YMD:
            indexdate = DATE_FORMAT_FULL_YMD
        elif dateFormat == messages.DATE_FORMAT_ABBREVIATED_DM:
            indexdate = DATE_FORMAT_ABBREVIATED_DM
        elif dateFormat == messages.DATE_FORMAT_ABBREVIATED_MD:
            indexdate = DATE_FORMAT_ABBREVIATED_MD
        elif dateFormat == messages.DATE_FORMAT_ABBREVIATED_DMY:
            indexdate = DATE_FORMAT_ABBREVIATED_DMY
        elif dateFormat == messages.DATE_FORMAT_ABBREVIATED_MDY:
            indexdate = DATE_FORMAT_ABBREVIATED_MDY
        elif dateFormat == messages.DATE_FORMAT_ABBREVIATED_YMD:
            indexdate = DATE_FORMAT_ABBREVIATED_YMD
        combobox2.set_active (indexdate)

        combobox3 = self.get_widget("timeFormatCombo")
        self.populateComboBox(combobox3,
          [sdtime(messages.TIME_FORMAT_LOCALE, ltime()),
           sdtime(messages.TIME_FORMAT_12_HM, ltime()),
           sdtime(messages.TIME_FORMAT_12_HMS, ltime()),
           sdtime(messages.TIME_FORMAT_24_HMS, ltime()),
           sdtime(messages.TIME_FORMAT_24_HMS_WITH_WORDS, ltime()),
           sdtime(messages.TIME_FORMAT_24_HM, ltime()),
           sdtime(messages.TIME_FORMAT_24_HM_WITH_WORDS, ltime())])
        indextime = TIME_FORMAT_LOCALE
        timeFormat = self.prefsDict["presentTimeFormat"]
        if timeFormat == messages.TIME_FORMAT_LOCALE:
            indextime = TIME_FORMAT_LOCALE
        elif timeFormat == messages.TIME_FORMAT_12_HM:
            indextime = TIME_FORMAT_12_HM
        elif timeFormat == messages.TIME_FORMAT_12_HMS:
            indextime = TIME_FORMAT_12_HMS
        elif timeFormat == messages.TIME_FORMAT_24_HMS:
            indextime = TIME_FORMAT_24_HMS
        elif timeFormat == messages.TIME_FORMAT_24_HMS_WITH_WORDS:
            indextime = TIME_FORMAT_24_HMS_WITH_WORDS
        elif timeFormat == messages.TIME_FORMAT_24_HM:
            indextime = TIME_FORMAT_24_HM
        elif timeFormat == messages.TIME_FORMAT_24_HM_WITH_WORDS:
            indextime = TIME_FORMAT_24_HM_WITH_WORDS
        combobox3.set_active (indextime)

        self.get_widget("speakProgressBarUpdatesCheckButton").set_active(
            prefs.get("speakProgressBarUpdates", settings.speakProgressBarUpdates))
        self.get_widget("brailleProgressBarUpdatesCheckButton").set_active(
            prefs.get("brailleProgressBarUpdates", settings.brailleProgressBarUpdates))
        self.get_widget("beepProgressBarUpdatesCheckButton").set_active(
            prefs.get("beepProgressBarUpdates", settings.beepProgressBarUpdates))

        interval = prefs["progressBarUpdateInterval"]
        self.get_widget("progressBarUpdateIntervalSpinButton").set_value(interval)

        comboBox = self.get_widget("progressBarVerbosity")
        levels = [guilabels.PROGRESS_BAR_ALL,
                  guilabels.PROGRESS_BAR_APPLICATION,
                  guilabels.PROGRESS_BAR_WINDOW]
        self.populateComboBox(comboBox, levels)
        comboBox.set_active(prefs["progressBarVerbosity"])

        enable = prefs["enableMouseReview"]
        self.get_widget("enableMouseReviewCheckButton").set_active(enable)

        # Braille pane.
        #
        self.get_widget("enableBrailleCheckButton").set_active( \
                        prefs["enableBraille"])
        state = prefs["brailleRolenameStyle"] == \
                            settings.BRAILLE_ROLENAME_STYLE_SHORT
        self.get_widget("abbrevRolenames").set_active(state)

        self.get_widget("disableBrailleEOLCheckButton").set_active(
            prefs["disableBrailleEOL"])

        if louis is None:
            self.get_widget( \
                "contractedBrailleCheckButton").set_sensitive(False)
        else:
            self.get_widget("contractedBrailleCheckButton").set_active( \
                prefs["enableContractedBraille"])
            # Set up contraction table combo box and set it to the
            # currently used one.
            # 
            tablesCombo = self.get_widget("contractionTableCombo")
            tableDict = braille.listTables()
            selectedTableIter = None
            selectedTable = prefs["brailleContractionTable"] or \
                             braille.getDefaultTable()
            if tableDict:
                tablesModel = Gtk.ListStore(str, str)
                names = sorted(tableDict.keys())
                for name in names:
                    fname = tableDict[name]
                    it = tablesModel.append([name, fname])
                    if os.path.join(braille.tablesdir, fname) == \
                            selectedTable:
                        selectedTableIter = it
                cell = self.planeCellRendererText
                tablesCombo.clear()
                tablesCombo.pack_start(cell, True)
                tablesCombo.add_attribute(cell, 'text', 0)
                tablesCombo.set_model(tablesModel)
                if selectedTableIter:
                    tablesCombo.set_active_iter(selectedTableIter)
                else:
                    tablesCombo.set_active(0)
            else:
                tablesCombo.set_sensitive(False)
        if prefs["brailleVerbosityLevel"] == settings.VERBOSITY_LEVEL_BRIEF:
            self.get_widget("brailleBriefButton").set_active(True)
        else:
            self.get_widget("brailleVerboseButton").set_active(True)

        self.get_widget("enableBrailleWordWrapCheckButton").set_active(
            prefs.get("enableBrailleWordWrap", settings.enableBrailleWordWrap))

        selectionIndicator = prefs["brailleSelectorIndicator"]
        if selectionIndicator == settings.BRAILLE_UNDERLINE_7:
            self.get_widget("brailleSelection7Button").set_active(True)
        elif selectionIndicator == settings.BRAILLE_UNDERLINE_8:
            self.get_widget("brailleSelection8Button").set_active(True)
        elif selectionIndicator == settings.BRAILLE_UNDERLINE_BOTH:
            self.get_widget("brailleSelectionBothButton").set_active(True)
        else:
            self.get_widget("brailleSelectionNoneButton").set_active(True)

        linkIndicator = prefs["brailleLinkIndicator"]
        if linkIndicator == settings.BRAILLE_UNDERLINE_7:
            self.get_widget("brailleLink7Button").set_active(True)
        elif linkIndicator == settings.BRAILLE_UNDERLINE_8:
            self.get_widget("brailleLink8Button").set_active(True)
        elif linkIndicator == settings.BRAILLE_UNDERLINE_BOTH:
            self.get_widget("brailleLinkBothButton").set_active(True)
        else:
            self.get_widget("brailleLinkNoneButton").set_active(True)

        enable = prefs.get("enableFlashMessages", settings.enableFlashMessages)
        self.get_widget("enableFlashMessagesCheckButton").set_active(enable)

        enable = prefs.get("flashIsPersistent", settings.flashIsPersistent)
        self.get_widget("flashIsPersistentCheckButton").set_active(enable)

        enable = prefs.get("flashIsDetailed", settings.flashIsDetailed)
        self.get_widget("flashIsDetailedCheckButton").set_active(enable)

        duration = prefs["brailleFlashTime"]
        self.get_widget("brailleFlashTimeSpinButton").set_value(duration / 1000)

        # Key Echo pane.
        #
        self.get_widget("keyEchoCheckButton").set_active( \
                        prefs["enableKeyEcho"])
        self.get_widget("enableAlphabeticKeysCheckButton").set_active(
                        prefs.get("enableAlphabeticKeys", settings.enableAlphabeticKeys))
        self.get_widget("enableNumericKeysCheckButton").set_active(
                        prefs.get("enableNumericKeys", settings.enableNumericKeys))
        self.get_widget("enablePunctuationKeysCheckButton").set_active(
                        prefs.get("enablePunctuationKeys", settings.enablePunctuationKeys))
        self.get_widget("enableSpaceCheckButton").set_active(
                        prefs.get("enableSpace", settings.enableSpace))
        self.get_widget("enableModifierKeysCheckButton").set_active( \
                        prefs["enableModifierKeys"])
        self.get_widget("enableFunctionKeysCheckButton").set_active( \
                        prefs["enableFunctionKeys"])
        self.get_widget("enableActionKeysCheckButton").set_active( \
                        prefs["enableActionKeys"])
        self.get_widget("enableNavigationKeysCheckButton").set_active( \
                        prefs["enableNavigationKeys"])
        self.get_widget("enableDiacriticalKeysCheckButton").set_active( \
                        prefs["enableDiacriticalKeys"])
        self.get_widget("enableEchoByCharacterCheckButton").set_active( \
                        prefs["enableEchoByCharacter"])
        self.get_widget("enableEchoByWordCheckButton").set_active( \
                        prefs["enableEchoByWord"])
        self.get_widget("enableEchoBySentenceCheckButton").set_active( \
                        prefs["enableEchoBySentence"])
        self.get_widget("useCustomEchoVoiceCheckButton").set_active(
            prefs.get("useCustomEchoVoice", settings.useCustomEchoVoice))
        self.get_widget("useCustomEchoSpeechServerCheckButton").set_active(
            prefs.get("useCustomEchoSpeechServer", settings.useCustomEchoSpeechServer))
        self.get_widget("useCustomEchoForKeyCheckButton").set_active(
            prefs.get("useCustomEchoForKey", settings.useCustomEchoForKey))
        self.get_widget("useCustomEchoForCharacterCheckButton").set_active(
            prefs.get("useCustomEchoForCharacter", settings.useCustomEchoForCharacter))
        self.get_widget("useCustomEchoForWordCheckButton").set_active(
            prefs.get("useCustomEchoForWord", settings.useCustomEchoForWord))
        self.get_widget("useCustomEchoForSentenceCheckButton").set_active(
            prefs.get("useCustomEchoForSentence", settings.useCustomEchoForSentence))
        self._setEchoVoiceItems()

        # Text attributes pane.
        #
        self._createTextAttributesTreeView()

        brailleIndicator = prefs["textAttributesBrailleIndicator"]
        if brailleIndicator == settings.BRAILLE_UNDERLINE_7:
            self.get_widget("textBraille7Button").set_active(True)
        elif brailleIndicator == settings.BRAILLE_UNDERLINE_8:
            self.get_widget("textBraille8Button").set_active(True)
        elif brailleIndicator == settings.BRAILLE_UNDERLINE_BOTH:
            self.get_widget("textBrailleBothButton").set_active(True)
        else:
            self.get_widget("textBrailleNoneButton").set_active(True)

        # Pronunciation dictionary pane.
        #
        self._createPronunciationTreeView()

        # General pane.
        #
        self.get_widget("presentToolTipsCheckButton").set_active(
            prefs["presentToolTips"])

        if prefs["keyboardLayout"] == settings.GENERAL_KEYBOARD_LAYOUT_DESKTOP:
            self.get_widget("generalDesktopButton").set_active(True)
        else:
            self.get_widget("generalLaptopButton").set_active(True)

        combobox = self.get_widget("sayAllStyle")
        self.populateComboBox(combobox, [guilabels.SAY_ALL_STYLE_LINE,
                                         guilabels.SAY_ALL_STYLE_SENTENCE])
        combobox.set_active(prefs["sayAllStyle"])
        self.get_widget("rewindAndFastForwardInSayAllCheckButton").set_active(
            prefs.get("rewindAndFastForwardInSayAll", settings.rewindAndFastForwardInSayAll))
        self.get_widget("structNavInSayAllCheckButton").set_active(
            prefs.get("structNavInSayAll", settings.structNavInSayAll))
        self.get_widget("sayAllContextBlockquoteCheckButton").set_active(
            prefs.get("sayAllContextBlockquote", settings.sayAllContextBlockquote))
        self.get_widget("sayAllContextLandmarkCheckButton").set_active(
            prefs.get("sayAllContextLandmark", settings.sayAllContextLandmark))
        self.get_widget("sayAllContextNonLandmarkFormCheckButton").set_active(
            prefs.get("sayAllContextNonLandmarkForm", settings.sayAllContextNonLandmarkForm))
        self.get_widget("sayAllContextListCheckButton").set_active(
            prefs.get("sayAllContextList", settings.sayAllContextList))
        self.get_widget("sayAllContextPanelCheckButton").set_active(
            prefs.get("sayAllContextPanel", settings.sayAllContextPanel))
        self.get_widget("sayAllContextTableCheckButton").set_active(
            prefs.get("sayAllContextTable", settings.sayAllContextTable))

        # Cthulhu User Profiles
        #
        self.profilesCombo = self.get_widget('availableProfilesComboBox1')
        self.startingProfileCombo = self.get_widget('availableProfilesComboBox2')
        self.profilesComboModel = self.get_widget('model9')
        self.__initProfileCombo()
        if self.script.app:
            self.get_widget('profilesFrame').set_sensitive(False)
            
        active_plugins = self._get_active_plugins_for_tabs()
        self._update_plugin_tabs(active_plugins)

        # AI Assistant settings
        #
        if self._plugin_active(active_plugins, ["AIAssistant"]):
            self._initAIState()

        # Indentation settings
        #
        self._initIndentationState()
        
        # OCR Plugin settings
        #
        if self._plugin_active(active_plugins, ["OCR"]):
            self._initOCRState()

    def __initProfileCombo(self):
        """Adding available profiles and setting active as the active one"""

        availableProfiles = self.__getAvailableProfiles()
        self.profilesComboModel.clear()

        if not len(availableProfiles):
            self.profilesComboModel.append(self._defaultProfile)
        else:
            for profile in availableProfiles:
                self.profilesComboModel.append(profile)

        activeProfile = self.prefsDict.get('activeProfile') or self._defaultProfile
        startingProfile = self.prefsDict.get('startingProfile') or self._defaultProfile

        activeProfileIter = self.getComboBoxIndex(self.profilesCombo,
                                                  activeProfile[0])
        startingProfileIter = self.getComboBoxIndex(self.startingProfileCombo,
                                                  startingProfile[0])
        self.profilesCombo.set_active(activeProfileIter)
        self.startingProfileCombo.set_active(startingProfileIter)

    def __getAvailableProfiles(self):
        """Get available user profiles."""
        return cthulhu.cthulhuApp.settingsManager.availableProfiles()

    def _initAIState(self):
        """Initialize AI Assistant tab widgets with current settings."""
        prefs = self.prefsDict
        
        # Store widget references
        self.enableAICheckButton = self.get_widget("enableAICheckButton")
        self.aiProviderCombo = self.get_widget("aiProviderCombo")
        self.aiApiKeyEntry = self.get_widget("aiApiKeyEntry")
        self.aiOllamaModelEntry = self.get_widget("aiOllamaModelEntry")
        self.aiOllamaEndpointEntry = self.get_widget("aiOllamaEndpointEntry")
        self.aiConfirmationCheckButton = self.get_widget("aiConfirmationCheckButton")
        self.aiScreenshotQualityCombo = self.get_widget("aiScreenshotQualityCombo")
        
        # Set enable AI checkbox
        enabled = prefs.get("aiAssistantEnabled", settings.aiAssistantEnabled)
        self.enableAICheckButton.set_active(enabled)
        
        # Set provider combo
        provider = prefs.get("aiProvider", settings.aiProvider)
        providerIndex = 0  # Default to Claude Code
        if provider == settings.AI_PROVIDER_CODEX:
            providerIndex = 1
        elif provider == settings.AI_PROVIDER_GEMINI:
            providerIndex = 2
        elif provider == settings.AI_PROVIDER_OLLAMA:
            providerIndex = 3
        self.aiProviderCombo.set_active(providerIndex)
        
        # Set API key file
        apiKeyFile = prefs.get("aiApiKeyFile", settings.aiApiKeyFile)
        self.aiApiKeyEntry.set_text(apiKeyFile)
        
        # Set Ollama model
        ollamaModel = prefs.get("aiOllamaModel", settings.aiOllamaModel)
        self.aiOllamaModelEntry.set_text(ollamaModel)
        
        # Set Ollama endpoint
        ollamaEndpoint = prefs.get("aiOllamaEndpoint", settings.aiOllamaEndpoint)
        self.aiOllamaEndpointEntry.set_text(ollamaEndpoint)
        
        # Set confirmation checkbox
        confirmationRequired = prefs.get("aiConfirmationRequired", settings.aiConfirmationRequired)
        self.aiConfirmationCheckButton.set_active(confirmationRequired)
        
        # Set screenshot quality combo
        quality = prefs.get("aiScreenshotQuality", settings.aiScreenshotQuality)
        qualityIndex = 1  # Default to medium
        if quality == settings.AI_SCREENSHOT_QUALITY_LOW:
            qualityIndex = 0
        elif quality == settings.AI_SCREENSHOT_QUALITY_HIGH:
            qualityIndex = 2
        self.aiScreenshotQualityCombo.set_active(qualityIndex)
        
        # Enable/disable controls based on AI enabled state
        self._updateAIControlsState(enabled)
        
    def _updateAIControlsState(self, enabled):
        """Refresh AI controls while keeping configuration fields editable."""
        _ = enabled  # kept for signal/call compatibility
        # Keep settings editable even when AI assistant is disabled so users
        # can configure providers/keys before enabling it.
        self.aiProviderCombo.set_sensitive(True)
        self.aiConfirmationCheckButton.set_sensitive(True)
        self.aiScreenshotQualityCombo.set_sensitive(True)
        try:
            self.get_widget("aiGetClaudeKeyButton").set_sensitive(True)
        except:
            pass  # Button might not exist in older UI files

        current_provider = self.prefsDict.get("aiProvider", settings.aiProvider)
        self._updateProviderControls(current_provider)

    def _initIndentationState(self):
        """Initialize Indentation widgets with current settings."""
        prefs = self.prefsDict

        self.enableIndentationCheckButton = self.get_widget("enableIndentationCheckButton")
        self.indentationSpeechButton = self.get_widget("indentationSpeechButton")
        self.indentationBeepsButton = self.get_widget("indentationBeepsButton")
        self.indentationSpeechAndBeepsButton = self.get_widget("indentationSpeechAndBeepsButton")
        self.indentationModeGrid = self.get_widget("indentationModeGrid")

        enabled = prefs.get("enableIndentation", settings.enableIndentation)
        self.enableIndentationCheckButton.set_active(enabled)

        mode = prefs.get("indentationPresentationMode", settings.indentationPresentationMode)
        if mode == settings.INDENTATION_PRESENTATION_BEEPS:
            self.indentationBeepsButton.set_active(True)
        elif mode == settings.INDENTATION_PRESENTATION_SPEECH_AND_BEEPS:
            self.indentationSpeechAndBeepsButton.set_active(True)
        else:
            self.indentationSpeechButton.set_active(True)

        self._updateIndentationControlsState(enabled)

    def _updateIndentationControlsState(self, enabled):
        """Enable or disable indentation mode controls based on checkbox state."""
        self.indentationModeGrid.set_sensitive(enabled)
    
    def _updateProviderControls(self, provider):
        """Update visibility/sensitivity of provider-specific controls."""
        # API key controls (only needed for Gemini - not for Claude Code or Ollama)
        api_key_needed = provider in [settings.AI_PROVIDER_GEMINI]
        self.aiApiKeyEntry.set_sensitive(api_key_needed)
        
        # Get Claude API Key button (only for Claude Code)
        try:
            claude_button = self.get_widget("aiGetClaudeKeyButton")
            claude_button.set_visible(provider == settings.AI_PROVIDER_CLAUDE_CODE)
        except:
            pass  # Button might not exist
        
        # Ollama model and endpoint entries (only for Ollama)
        ollama_enabled = provider == settings.AI_PROVIDER_OLLAMA
        self.aiOllamaModelEntry.set_sensitive(ollama_enabled)
        self.aiOllamaEndpointEntry.set_sensitive(ollama_enabled)
        
        # Update labels based on provider
        if provider == settings.AI_PROVIDER_CLAUDE_CODE:
            self.aiApiKeyEntry.set_placeholder_text("No API key needed - uses Claude Code CLI")
        elif provider == settings.AI_PROVIDER_CODEX:
            self.aiApiKeyEntry.set_placeholder_text("No API key needed - uses Codex CLI")
        elif provider == settings.AI_PROVIDER_OLLAMA:
            self.aiApiKeyEntry.set_placeholder_text("No API key needed - uses local Ollama")
        else:
            self.aiApiKeyEntry.set_placeholder_text("Path to API key file")
    
    def _initOCRState(self):
        """Initialize OCR Plugin tab widgets with current settings."""
        prefs = self.prefsDict
        
        # Store widget references
        self.ocrLanguageEntry = self.get_widget("ocrLanguageEntry")
        self.ocrScaleSpinButton = self.get_widget("ocrScaleSpinButton")
        self.ocrGrayscaleCheckButton = self.get_widget("ocrGrayscaleCheckButton")
        self.ocrInvertCheckButton = self.get_widget("ocrInvertCheckButton")
        self.ocrBlackWhiteCheckButton = self.get_widget("ocrBlackWhiteCheckButton")
        self.ocrBlackWhiteValueSpinButton = self.get_widget("ocrBlackWhiteValueSpinButton")
        self.ocrColorCalculationCheckButton = self.get_widget("ocrColorCalculationCheckButton")
        self.ocrCopyToClipboardCheckButton = self.get_widget("ocrCopyToClipboardCheckButton")
        
        # Set language code
        languageCode = prefs.get("ocrLanguageCode", settings.ocrLanguageCode)
        self.ocrLanguageEntry.set_text(languageCode)
        
        # Set scale factor
        scaleFactor = prefs.get("ocrScaleFactor", settings.ocrScaleFactor)
        self.ocrScaleSpinButton.set_value(scaleFactor)
        
        # Set checkboxes
        grayscale = prefs.get("ocrGrayscaleImg", settings.ocrGrayscaleImg)
        self.ocrGrayscaleCheckButton.set_active(grayscale)
        
        invert = prefs.get("ocrInvertImg", settings.ocrInvertImg)
        self.ocrInvertCheckButton.set_active(invert)
        
        blackWhite = prefs.get("ocrBlackWhiteImg", settings.ocrBlackWhiteImg)
        self.ocrBlackWhiteCheckButton.set_active(blackWhite)
        
        blackWhiteValue = prefs.get("ocrBlackWhiteImgValue", settings.ocrBlackWhiteImgValue)
        self.ocrBlackWhiteValueSpinButton.set_value(blackWhiteValue)
        
        colorCalculation = prefs.get("ocrColorCalculation", settings.ocrColorCalculation)
        self.ocrColorCalculationCheckButton.set_active(colorCalculation)
        
        copyToClipboard = prefs.get("ocrCopyToClipboard", settings.ocrCopyToClipboard)
        self.ocrCopyToClipboardCheckButton.set_active(copyToClipboard)

    def _initSoundThemeState(self):
        """Initialize sound widgets with current settings."""
        prefs = self.prefsDict

        # Get widget references
        self.soundSinkCombo = self.get_widget("soundSinkCombo")
        self.soundThemeCombo = self.get_widget("soundThemeCombo")
        self.roleSoundPresentationCombo = self.get_widget("roleSoundPresentationCombo")
        self.soundVolumeScale = self.get_widget("soundVolumeScale")
        self.progressBarBeepIntervalSpinButton = self.get_widget("progressBarBeepIntervalSpinButton")
        self.soundSinkCombo.set_can_focus(False)
        self.soundThemeCombo.set_can_focus(False)
        self.roleSoundPresentationCombo.set_can_focus(False)
        self.get_widget("enableSoundCheckButton").set_active(
            prefs.get("enableSound", settings.enableSound)
        )
        self.soundVolumeScale.set_value(
            float(prefs.get("soundVolume", settings.soundVolume))
        )
        self.get_widget("playSoundForRoleCheckButton").set_active(
            prefs.get("playSoundForRole", settings.playSoundForRole)
        )
        self.get_widget("playSoundForStateCheckButton").set_active(
            prefs.get("playSoundForState", settings.playSoundForState)
        )
        self.get_widget("playSoundForPositionInSetCheckButton").set_active(
            prefs.get("playSoundForPositionInSet", settings.playSoundForPositionInSet)
        )
        self.get_widget("playSoundForValueCheckButton").set_active(
            prefs.get("playSoundForValue", settings.playSoundForValue)
        )
        self.get_widget("spatializeObjectSoundsCheckButton").set_active(
            prefs.get("spatializeObjectSounds", settings.spatializeObjectSounds)
        )
        self.get_widget("beepProgressBarUpdatesCheckButton").set_active(
            prefs.get("beepProgressBarUpdates", settings.beepProgressBarUpdates)
        )
        self.progressBarBeepIntervalSpinButton.set_value(
            int(prefs.get("progressBarBeepInterval", settings.progressBarBeepInterval))
        )

        self._soundSinkChoices = [
            (settings.SOUND_SINK_AUTO, guilabels.SOUND_BACKEND_AUTO),
            (settings.SOUND_SINK_PIPEWIRE, guilabels.SOUND_BACKEND_PIPEWIRE),
            (settings.SOUND_SINK_PULSE, guilabels.SOUND_BACKEND_PULSE),
            (settings.SOUND_SINK_ALSA, guilabels.SOUND_BACKEND_ALSA),
        ]
        self.soundSinkCombo.remove_all()
        for _, label in self._soundSinkChoices:
            self.soundSinkCombo.append_text(label)
        runtimeSoundSink = cthulhu.cthulhuApp.settingsManager.getSetting("soundSink")
        currentSink = sound_sink.normalize_sound_sink_choice(
            prefs.get("soundSink", runtimeSoundSink if runtimeSoundSink is not None else settings.soundSink)
        )
        sinkIndex = 0
        for index, (value, _) in enumerate(self._soundSinkChoices):
            if value == currentSink:
                sinkIndex = index
                break
        self.soundSinkCombo.set_active(sinkIndex)

        # 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)

        self._roleSoundPresentationChoices = [
            (settings.ROLE_SOUND_PRESENTATION_SOUND_AND_SPEECH, "Sound and speech"),
            (settings.ROLE_SOUND_PRESENTATION_SPEECH_ONLY, "Speech only"),
            (settings.ROLE_SOUND_PRESENTATION_SOUND_ONLY, "Sound only"),
        ]
        self.roleSoundPresentationCombo.remove_all()
        for _, label in self._roleSoundPresentationChoices:
            self.roleSoundPresentationCombo.append_text(label)
        currentPresentation = prefs.get(
            "roleSoundPresentation",
            settings.roleSoundPresentation
        )
        presentationIndex = 0
        for index, (value, _) in enumerate(self._roleSoundPresentationChoices):
            if value == currentPresentation:
                presentationIndex = index
                break
        self.roleSoundPresentationCombo.set_active(presentationIndex)

    def soundThemeComboChanged(self, widget):
        """Signal handler for the sound theme combo box."""
        activeText = widget.get_active_text()
        if activeText:
            self.prefsDict["soundTheme"] = activeText

    def soundSinkComboChanged(self, widget):
        """Signal handler for the sound backend combo box."""
        activeIndex = widget.get_active()
        if activeIndex < 0:
            return
        value = self._soundSinkChoices[activeIndex][0]
        self.prefsDict["soundSink"] = value

    def roleSoundPresentationComboChanged(self, widget):
        """Signal handler for the role sound presentation combo box."""
        activeIndex = widget.get_active()
        if activeIndex < 0:
            return
        value = self._roleSoundPresentationChoices[activeIndex][0]
        self.prefsDict["roleSoundPresentation"] = value

    def soundVolumeValueChanged(self, widget):
        """Signal handler for the sound volume scale."""
        self.prefsDict["soundVolume"] = widget.get_value()

    def progressBarBeepIntervalValueChanged(self, widget):
        """Signal handler for the progress bar beep interval spin button."""
        self.prefsDict["progressBarBeepInterval"] = widget.get_value_as_int()


    def _updateCthulhuModifier(self):
        combobox = self.get_widget("cthulhuModifierComboBox")
        keystring = ", ".join(self.prefsDict["cthulhuModifierKeys"])
        combobox.set_active(self.getComboBoxIndex(combobox, keystring))

    def populateComboBox(self, combobox, items):
        """Populates the combobox with the items provided.

        Arguments:
        - combobox: the GtkComboBox to populate
        - items: the list of strings with which to populate it
        """

        model = Gtk.ListStore(str)
        for item in items:
            model.append([item])
        combobox.set_model(model)

    def getComboBoxIndex(self, combobox, searchStr, col=0):
        """ For each of the entries in the given combo box, look for searchStr.
            Return the index of the entry if searchStr is found.

        Arguments:
        - combobox: the GtkComboBox to search.
        - searchStr: the string to search for.

        Returns the index of the first entry in combobox with searchStr, or
        0 if not found.
        """

        model = combobox.get_model()
        myiter = model.get_iter_first()
        for i in range(0, len(model)):
            name = model.get_value(myiter, col)
            if name == searchStr:
                return i
            myiter = model.iter_next(myiter)

        return 0

    def getComboBoxList(self, combobox):
        """Get the list of values from the active combox
        """
        active = combobox.get_active()
        model = combobox.get_model()
        activeIter = model.get_iter(active)
        activeLabel = model.get_value(activeIter, 0)
        activeName = model.get_value(activeIter, 1)
        return [activeLabel, activeName]

    def getKeyBindingsModelDict(self, model, modifiedOnly=True):
        modelDict = {}
        node = model.get_iter_first()
        while node:
            child = model.iter_children(node)
            while child:
                key, modified = model.get(child, HANDLER, MODIF)
                if modified or not modifiedOnly:
                    value = []
                    value.append(list(model.get(
                            child, KEY1, MOD_MASK1, MOD_USED1, CLICK_COUNT1)))
                    modelDict[key] = value
                child = model.iter_next(child)
            node = model.iter_next(node)

        return modelDict

    def getModelDict(self, model):
        """Get the list of values from a list[str,str] model
        """
        pronunciation_dict.pronunciation_dict = {}
        currentIter = model.get_iter_first()
        while currentIter is not None:
            key, value = model.get(currentIter, ACTUAL, REPLACEMENT)
            if key and value:
                pronunciation_dict.setPronunciation(key, value)
            currentIter = model.iter_next(currentIter)
        modelDict = pronunciation_dict.pronunciation_dict
        return modelDict

    def showGUI(self):
        """Show the Cthulhu configuration GUI window. This assumes that
        the GUI has already been created.
        """

        cthulhuSetupWindow = self.get_widget("cthulhuSetupWindow")

        accelGroup = Gtk.AccelGroup()
        cthulhuSetupWindow.add_accel_group(accelGroup)
        helpButton = self.get_widget("helpButton")
        (keyVal, modifierMask) = Gtk.accelerator_parse("F1")
        helpButton.add_accelerator("clicked",
                                   accelGroup,
                                   keyVal,
                                   modifierMask,
                                   0)

        try:
            ts = cthulhu_state.lastInputEvent.timestamp
            # Ensure timestamp fits in 32-bit range for GTK
            if ts > 4294967295:  # 2^32 - 1
                ts = ts % 4294967296  # Wrap to 32-bit range
        except Exception:
            ts = 0
        if ts == 0:
            ts = Gtk.get_current_event_time()
        cthulhuSetupWindow.present_with_time(int(ts))

        # We always want to re-order the text attributes page so that enabled
        # items are consistently at the top.
        #
        self._setSpokenTextAttributes(
                self.getTextAttributesView,
                cthulhu.cthulhuApp.settingsManager.getSetting('enabledSpokenTextAttributes'),
                True, True)

        if self.script.app:
            title = guilabels.PREFERENCES_APPLICATION_TITLE % AXObject.get_name(self.script.app)
            cthulhuSetupWindow.set_title(title)

        cthulhuSetupWindow.show()
        self._initialFocusSyncAttempts = 0
        GLib.idle_add(self._set_initial_window_state)
        GLib.idle_add(self._set_initial_gtk_focus)
        GLib.timeout_add(50, self._set_initial_window_state)

    def _set_initial_gtk_focus(self):
        """Give GTK focus to a real preferences control as soon as the dialog appears."""

        candidateIds = [
            "generalDesktopButton",
            "generalLaptopButton",
            "availableProfilesComboBox1",
            "speechSupportCheckButton",
            "notebook",
        ]
        cthulhuSetupWindow = self.get_widget("cthulhuSetupWindow")
        for widgetId in candidateIds:
            widget = self.get_widget(widgetId)
            if not widget.get_visible() or not widget.get_sensitive():
                continue
            if not widget.get_can_focus():
                continue

            debug.printMessage(
                debug.LEVEL_INFO,
                f"PREFERENCES DIALOG: Setting initial GTK focus to {widgetId}",
                True,
            )
            cthulhuSetupWindow.set_focus(widget)
            widget.grab_focus()
            return False

        debug.printMessage(
            debug.LEVEL_INFO,
            "PREFERENCES DIALOG: No focusable initial GTK widget found",
            True,
        )
        return False

    def _set_initial_window_state(self):
        """Sync Cthulhu's active window to preferences without forcing dialog focus."""

        self._initialFocusSyncAttempts += 1
        activeWindow = AXUtilities.find_active_window()
        if activeWindow is None:
            return self._initialFocusSyncAttempts < 5

        app = AXObject.get_application(activeWindow)
        appName = (AXObject.get_name(app) or "").lower()
        if appName != "cthulhu":
            return self._initialFocusSyncAttempts < 5

        debug.printTokens(
            debug.LEVEL_INFO,
            ["PREFERENCES DIALOG: Syncing active window to", activeWindow],
            True,
        )
        cthulhu.setActiveWindow(activeWindow, notifyScript=False)

        tokens = ["PREFERENCES DIALOG: Synced initial window state to", activeWindow]
        debug.printTokens(debug.LEVEL_INFO, tokens, True)
        return False

    def _initComboBox(self, combobox):
        """Initialize the given combo box to take a list of int/str pairs.

        Arguments:
        - combobox: the GtkComboBox to initialize.
        """

        cell = Gtk.CellRendererText()
        combobox.pack_start(cell, True)
        # We only want to display one column; not two.
        #
        try:
            columnToDisplay = combobox.get_cells()[0]
            combobox.add_attribute(columnToDisplay, 'text', 1)
        except Exception:
            combobox.add_attribute(cell, 'text', 1)
        model = Gtk.ListStore(int, str)
        combobox.set_model(model)

        # Force the display comboboxes to be left aligned.
        #
        if isinstance(combobox, Gtk.ComboBoxText):
            size = combobox.size_request()
            cell.set_fixed_size(size[0] - 29, -1)

        return model

    def _setKeyEchoItems(self):
        """[In]sensitize the checkboxes for the various types of key echo,
        depending upon whether the value of the key echo check button is set.
        """

        enable = self.get_widget("keyEchoCheckButton").get_active()
        self.get_widget("enableAlphabeticKeysCheckButton").set_sensitive(enable)
        self.get_widget("enableNumericKeysCheckButton").set_sensitive(enable)
        self.get_widget("enablePunctuationKeysCheckButton").set_sensitive(enable)
        self.get_widget("enableSpaceCheckButton").set_sensitive(enable)
        self.get_widget("enableModifierKeysCheckButton").set_sensitive(enable)
        self.get_widget("enableFunctionKeysCheckButton").set_sensitive(enable)
        self.get_widget("enableActionKeysCheckButton").set_sensitive(enable)
        self.get_widget("enableNavigationKeysCheckButton").set_sensitive(enable)
        self.get_widget("enableDiacriticalKeysCheckButton").set_sensitive( \
          enable)
        self._setEchoVoiceItems()

    def _setEchoVoiceItems(self):
        """[In]sensitize echo voice controls based on current state."""

        useCustomVoice = self.get_widget("useCustomEchoVoiceCheckButton").get_active()
        useCustomModule = self.get_widget("useCustomEchoSpeechServerCheckButton").get_active()
        keyEchoEnabled = self.get_widget("keyEchoCheckButton").get_active()
        charEchoEnabled = self.get_widget("enableEchoByCharacterCheckButton").get_active()
        wordEchoEnabled = self.get_widget("enableEchoByWordCheckButton").get_active()
        sentenceEchoEnabled = self.get_widget("enableEchoBySentenceCheckButton").get_active()
        speechEnabled = self.get_widget("speechSupportCheckButton").get_active()

        speechSystemIsDispatcher = False
        if self.speechSystemsChoice:
            try:
                speechSystemIsDispatcher = \
                    self.speechSystemsChoice.SpeechServer.getFactoryName() == guilabels.SPEECH_DISPATCHER
            except Exception:
                speechSystemIsDispatcher = False

        voiceControlsEnabled = useCustomVoice and speechEnabled
        moduleOverrideAvailable = voiceControlsEnabled and speechSystemIsDispatcher
        moduleControlsEnabled = moduleOverrideAvailable and useCustomModule

        self.get_widget("useCustomEchoSpeechServerCheckButton").set_sensitive(moduleOverrideAvailable)
        self.get_widget("echoSpeechServers").set_sensitive(moduleControlsEnabled)
        self.get_widget("echoSpeechFamilies").set_sensitive(voiceControlsEnabled)

        self.get_widget("echoRateScale").set_sensitive(useCustomVoice)
        self.get_widget("echoPitchScale").set_sensitive(useCustomVoice)
        self.get_widget("echoVolumeScale").set_sensitive(useCustomVoice)

        self.get_widget("useCustomEchoForKeyCheckButton").set_sensitive(useCustomVoice and keyEchoEnabled)
        self.get_widget("useCustomEchoForCharacterCheckButton").set_sensitive(
            useCustomVoice and charEchoEnabled)
        self.get_widget("useCustomEchoForWordCheckButton").set_sensitive(useCustomVoice and wordEchoEnabled)
        self.get_widget("useCustomEchoForSentenceCheckButton").set_sensitive(
            useCustomVoice and sentenceEchoEnabled)

    def _presentMessage(self, text, interrupt=False, voice=None):
        """If the text field is not None, presents the given text, optionally
        interrupting anything currently being spoken.

        Arguments:
        - text: the text to present
        - interrupt: if True, interrupt any speech currently being spoken
        - voice: the voice to use; if None, use the default system voice
        """

        self.script.speakMessage(text, voice=voice, interrupt=interrupt)
        try:
            self.script.displayBrailleMessage(text, flashTime=-1)
        except Exception:
            pass

    def _previewVoiceSelection(self, voiceType, name):
        """Present a short preview in the currently selected voice."""

        if not name:
            return

        voice = self._getACSSForVoiceType(voiceType)
        if not voice:
            return

        previewVoice = acss.ACSS(voice)
        if self.speechSystemsChoice \
           and self.speechSystemsChoice.SpeechServer.getFactoryName() == guilabels.SPEECH_DISPATCHER:
            previewMessage = messages.SPEECH_VOICE_VALUE % name
        else:
            previewMessage = messages.SPEECH_VOICE_VALUE_GENERIC % name

        # Speak through the currently-selected preferences speech server so
        # preview uses the chosen module/voice, not the globally-active one.
        server = self.speechServersChoice
        if server and hasattr(server, "speak"):
            try:
                server.speak(previewMessage, previewVoice, interrupt=True)
                return
            except Exception:
                debug.printException(debug.LEVEL_WARNING)

        self._presentMessage(previewMessage, interrupt=True, voice=previewVoice)

    def _createNode(self, appName):
        """Create a new root node in the TreeStore model with the name of the
            application.

        Arguments:
        - appName: the name of the TreeStore Node (the same of the application)
        """

        model = self.keyBindingsModel

        myiter = model.append(None)
        model.set_value(myiter, DESCRIP, appName)
        model.set_value(myiter, MODIF, False)

        return myiter

    def _getIterOf(self, appName):
        """Returns the Gtk.TreeIter of the TreeStore model
        that matches the application name passed as argument

        Arguments:
        - appName: a string with the name of the application of the node wanted
                    it's the same that the field DESCRIP of the model treeStore
        """

        model = self.keyBindingsModel

        for row in model:
            if ((model.iter_depth(row.iter) == 0) \
                and (row[DESCRIP] == appName)):
                return row.iter

        return None

    def _clickCountToString(self, clickCount):
        """Given a numeric clickCount, returns a string for inclusion
        in the list of keybindings.

        Argument:
        - clickCount: the number of clicks associated with the keybinding.
        """

        clickCountString = ""
        if clickCount == 2:
            clickCountString = f" ({guilabels.CLICK_COUNT_DOUBLE})"
        elif clickCount == 3:
            clickCountString = f" ({guilabels.CLICK_COUNT_TRIPLE})"

        return clickCountString

    def _insertRow(self, handl, kb, parent=None, modif=False):
        """Appends a new row with the new keybinding data to the treeview

        Arguments:
        - handl:  the name of the handler associated to the keyBinding
        - kb:     the new keybinding.
        - parent: the parent node of the treeview, where to append the kb
        - modif:  whether to check the modified field or not.

        Returns a Gtk.TreeIter pointing at the new row.
        """

        model = self.keyBindingsModel

        if parent is None:
            parent = self._getIterOf(guilabels.KB_GROUP_DEFAULT)

        if parent is not None:
            myiter = model.append(parent)
            if not kb.keysymstring:
                text = None
            else:
                clickCount = self._clickCountToString(kb.click_count)
                keysymstring = kb.keysymstring
                text = keybindings.getModifierNames(kb.modifiers) \
                       + keysymstring \
                       + clickCount

            model.set_value(myiter, HANDLER, handl)
            model.set_value(myiter, DESCRIP, kb.handler.description)
            model.set_value(myiter, MOD_MASK1, str(kb.modifier_mask))
            model.set_value(myiter, MOD_USED1, str(kb.modifiers))
            model.set_value(myiter, KEY1, kb.keysymstring)
            model.set_value(myiter, CLICK_COUNT1, str(kb.click_count))
            if text is not None:
                model.set_value(myiter, OLDTEXT1, text)
                model.set_value(myiter, TEXT1, text)
            model.set_value(myiter, MODIF, modif)
            model.set_value(myiter, EDITABLE, True)

            return myiter
        else:
            return None

    def _insertRowBraille(self, handl, com, inputEvHand, 
                          parent=None, modif=False):
        """Appends a new row with the new braille binding data to the treeview

        Arguments:
        - handl:       the name of the handler associated to the brailleBinding
        - com:         the BrlTTY command
        - inputEvHand: the inputEventHandler with the new brailleBinding
        - parent:      the parent node of the treeview, where to append the kb
        - modif:       whether to check the modified field or not.

        Returns a Gtk.TreeIter pointing at the new row.
        """

        model = self.keyBindingsModel

        if parent is None:
            parent = self._getIterOf(guilabels.KB_GROUP_BRAILLE)

        if parent is not None:
            myiter = model.append(parent)
            model.set_value(myiter, HANDLER, handl)
            model.set_value(myiter, DESCRIP, inputEvHand.description)
            model.set_value(myiter, KEY1, str(com))
            model.set_value(myiter, TEXT1, braille.command_name[com])
            model.set_value(myiter, MODIF, modif)
            model.set_value(myiter, EDITABLE, False)
            return myiter
        else:
            return None

    def _markModified(self):
        """ Mark as modified the user custom key bindings:
        """

        try:
            self.script.setupInputEventHandlers()
            keyBinds = keybindings.KeyBindings()
            keyBinds = cthulhu.cthulhuApp.settingsManager.overrideKeyBindings(self.script, keyBinds)
            keyBind = keybindings.KeyBinding(None, None, None, None)
            treeModel = self.keyBindingsModel

            myiter = treeModel.get_iter_first()
            while myiter is not None:
                iterChild = treeModel.iter_children(myiter)
                while iterChild is not None:
                    descrip = treeModel.get_value(iterChild, DESCRIP)
                    keyBind.handler = \
                        input_event.InputEventHandler(None, descrip)
                    if keyBinds.hasKeyBinding(keyBind,
                                              typeOfSearch="description"):
                        treeModel.set_value(iterChild, MODIF, True)
                    iterChild = treeModel.iter_next(iterChild)
                myiter = treeModel.iter_next(myiter)
        except Exception:
            debug.printException(debug.LEVEL_SEVERE)

    def _populateKeyBindings(self, clearModel=True):
        """Fills the TreeView with the list of Cthulhu keybindings

        Arguments:
        - clearModel: if True, initially clear out the key bindings model.
        """

        self.keyBindView.set_model(None)
        self.keyBindView.set_headers_visible(False)
        self.keyBindView.hide()
        if clearModel:
            self.keyBindingsModel.clear()
            self.kbindings = None

        appName = AXObject.get_name(self.script.app)
        iterApp = self._createNode(appName)
        iterCthulhu = self._createNode(guilabels.KB_GROUP_DEFAULT)
        iterUnbound = self._createNode(guilabels.KB_GROUP_UNBOUND)
        iterNotificationPresenter = self._createNode(guilabels.KB_GROUP_NOTIFICATIONS)
        iterFlatReviewPresenter = self._createNode(guilabels.KB_GROUP_FLAT_REVIEW)
        iterSpeechAndVerbosity = self._createNode(guilabels.KB_GROUP_SPEECH_VERBOSITY)
        iterDateAndTime = self._createNode(guilabels.KB_GROUP_DATE_AND_TIME)
        iterBookmarks = self._createNode(guilabels.KB_GROUP_BOOKMARKS)
        iterObjectNav = self._createNode(guilabels.KB_GROUP_OBJECT_NAVIGATION)
        iterWhereAmIPresenter = self._createNode(guilabels.KB_GROUP_WHERE_AM_I)
        iterLearnMode = self._createNode(guilabels.KB_GROUP_LEARN_MODE)
        iterMouseReviewer = self._createNode(guilabels.KB_GROUP_MOUSE_REVIEW)
        iterActionPresenter = self._createNode(guilabels.KB_GROUP_ACTIONS)

        if not self.kbindings:
            self.kbindings = keybindings.KeyBindings()
            self.script.setupInputEventHandlers()
            allKeyBindings = self.script.getKeyBindings()
            defKeyBindings = self.script.getDefaultKeyBindings()
            npKeyBindings = self.script.getNotificationPresenter().get_bindings()
            svKeyBindings = self.script.getSpeechAndVerbosityManager().get_bindings()
            dtKeyBindings = self.script.getDateAndTimePresenter().get_bindings()
            bmKeyBindings = self.script.getBookmarks().get_bindings()
            onKeyBindings = self.script.get_objectNavigator().get_bindings()
            lmKeyBindings = self.script.getLearnModePresenter().get_bindings()
            mrKeyBindings = self.script.getMouseReviewer().get_bindings()
            acKeyBindings = self.script.getActionPresenter().get_bindings()

            layout = cthulhu.cthulhuApp.settingsManager.getSetting('keyboardLayout')
            isDesktop = layout == settings.GENERAL_KEYBOARD_LAYOUT_DESKTOP
            frKeyBindings = self.script.getFlatReviewPresenter().get_bindings(isDesktop)
            waiKeyBindings = self.script.getWhereAmIPresenter().get_bindings(isDesktop)

            for kb in allKeyBindings.keyBindings:
                if not self.kbindings.hasKeyBinding(kb, "strict"):
                    handl = self.script.getInputEventHandlerKey(kb.handler)
                    if npKeyBindings.hasKeyBinding(kb, "description"):
                        self._insertRow(handl, kb, iterNotificationPresenter)
                    elif onKeyBindings.hasKeyBinding(kb, "description"):
                        self._insertRow(handl, kb, iterObjectNav)
                    elif frKeyBindings.hasKeyBinding(kb, "description"):
                        self._insertRow(handl, kb, iterFlatReviewPresenter)
                    elif waiKeyBindings.hasKeyBinding(kb, "description"):
                        self._insertRow(handl, kb, iterWhereAmIPresenter)
                    elif svKeyBindings.hasKeyBinding(kb, "description"):
                        self._insertRow(handl, kb, iterSpeechAndVerbosity)
                    elif dtKeyBindings.hasKeyBinding(kb, "description"):
                        self._insertRow(handl, kb, iterDateAndTime)
                    elif bmKeyBindings.hasKeyBinding(kb, "description"):
                        self._insertRow(handl, kb, iterBookmarks)
                    elif lmKeyBindings.hasKeyBinding(kb, "description"):
                        self._insertRow(handl, kb, iterLearnMode)
                    elif acKeyBindings.hasKeyBinding(kb, "description"):
                        self._insertRow(handl, kb, iterActionPresenter)
                    elif mrKeyBindings.hasKeyBinding(kb, "description"):
                        self._insertRow(handl, kb, iterMouseReviewer)
                    elif not defKeyBindings.hasKeyBinding(kb, "description"):
                        self._insertRow(handl, kb, iterApp)
                    elif kb.keysymstring:
                        self._insertRow(handl, kb, iterCthulhu)
                    else:
                        self._insertRow(handl, kb, iterUnbound)
                    self.kbindings.add(kb)

        if not self.keyBindingsModel.iter_has_child(iterApp):
            self.keyBindingsModel.remove(iterApp)

        if not self.keyBindingsModel.iter_has_child(iterUnbound):
            self.keyBindingsModel.remove(iterUnbound)

        self._updateCthulhuModifier()
        self._markModified()
        iterBB = self._createNode(guilabels.KB_GROUP_BRAILLE)
        self.bbindings = self.script.getBrailleBindings()
        for com, inputEvHand in self.bbindings.items():
            handl = self.script.getInputEventHandlerKey(inputEvHand)
            self._insertRowBraille(handl, com, inputEvHand, iterBB)

        self.keyBindView.set_model(self.keyBindingsModel)
        self.keyBindView.set_headers_visible(True)
        self.keyBindingsModel.set_sort_column_id(OLDTEXT1, Gtk.SortType.ASCENDING)
        self.keyBindView.show()

        # Keep track of new/unbound keybindings that have yet to be applied.
        #
        self.pendingKeyBindings = {}

    def _cleanupSpeechServers(self):
        """Remove unwanted factories and drivers for the current active
        factory, when the user dismisses the Cthulhu Preferences dialog."""

        for workingFactory in self.workingFactories:
            if not (workingFactory == self.speechSystemsChoice):
                workingFactory.SpeechServer.shutdownActiveServers()
            else:
                servers = workingFactory.SpeechServer.getSpeechServers()
                for server in servers:
                    if not (server == self.speechServersChoice):
                        server.shutdown()

    def speechSupportChecked(self, widget):
        """Signal handler for the "toggled" signal for the
           speechSupportCheckButton GtkCheckButton widget. The user has
           [un]checked the 'Enable Speech' checkbox. Set the 'enableSpeech'
           preference to the new value. Set the rest of the speech pane items
           [in]sensensitive depending upon whether this checkbox is checked.

        Arguments:
        - widget: the component that generated the signal.
        """

        enable = widget.get_active()
        self.prefsDict["enableSpeech"] = enable
        self.get_widget("speechOptionsGrid").set_sensitive(enable)
        self._setupEchoSpeechServers()
        self._setEchoVoiceItems()

    def onlySpeakDisplayedTextToggled(self, widget):
        """Signal handler for the "toggled" signal for the GtkCheckButton
        onlySpeakDisplayedText. In addition to updating the preferences,
        set the sensitivity of the contextOptionsGrid.

        Arguments:
        - widget: the component that generated the signal.
        """

        enable = widget.get_active()
        self.prefsDict["onlySpeakDisplayedText"] = enable
        self.get_widget("contextOptionsGrid").set_sensitive(not enable)

    def speechSystemsChanged(self, widget):
        """Signal handler for the "changed" signal for the speechSystems
           GtkComboBox widget. The user has selected a different speech
           system. Clear the existing list of speech servers, and setup
           a new list of speech servers based on the new choice. Setup a
           new list of voices for the first speech server in the list.

        Arguments:
        - widget: the component that generated the signal.
        """

        if self.initializingSpeech:
            return

        selectedIndex = widget.get_active()
        self.speechSystemsChoice = self.speechSystemsChoices[selectedIndex]
        self._setupSpeechServers()
        self._setupEchoSpeechServers()
        self._setEchoVoiceItems()

    def speechServersChanged(self, widget):
        """Signal handler for the "changed" signal for the speechServers
           GtkComboBox widget. The user has selected a different speech
           server. Clear the existing list of voices, and setup a new
           list of voices based on the new choice.

        Arguments:
        - widget: the component that generated the signal.
        """

        if self.initializingSpeech:
            return

        selectedIndex = widget.get_active()
        self.speechServersChoice = self.speechServersChoices[selectedIndex]

        # Whenever the speech servers change, we need to make sure we
        # clear whatever family was in use by the current voice types.
        # Otherwise, we can end up with family names from one server
        # bleeding over (e.g., "Paul" from Fonix ends up getting in
        # the "Default" voice type after we switch to eSpeak).
        #
        try:
            del self.defaultVoice[acss.ACSS.FAMILY]
            del self.uppercaseVoice[acss.ACSS.FAMILY]
            del self.hyperlinkVoice[acss.ACSS.FAMILY]
            del self.systemVoice[acss.ACSS.FAMILY]
        except Exception:
            pass

        self._setupVoices()
        self._setEchoVoiceItems()

    def speechLanguagesChanged(self, widget):
        """Signal handler for the "value_changed" signal for the languages
           GtkComboBox widget. The user has selected a different voice
           language. Save the new voice language name based on the new choice.

        Arguments:
        - widget: the component that generated the signal.
        """

        if self.initializingSpeech:
            return

        selectedIndex = widget.get_active()
        try:
            self.speechLanguagesChoice = self.speechLanguagesChoices[selectedIndex]
            if (self.speechServersChoice, self.speechLanguagesChoice) in \
                    self.selectedFamilyChoices:
                i = self.selectedFamilyChoices[self.speechServersChoice, \
                        self.speechLanguagesChoice]
                family = self.speechFamiliesChoices[i]
                name = family[speechserver.VoiceFamily.NAME]
                language = family[speechserver.VoiceFamily.LANG]
                dialect = family[speechserver.VoiceFamily.DIALECT]
                variant = family[speechserver.VoiceFamily.VARIANT]
                voiceType = self.get_widget("voiceTypesCombo").get_active()
                self._setFamilyNameForVoiceType(voiceType, name, language, dialect, variant)
        except Exception:
            debug.printException(debug.LEVEL_SEVERE)

        # Remember the last family manually selected by the user for the
        # current speech server.
        #
        if not selectedIndex == -1:
            self.selectedLanguageChoices[self.speechServersChoice] = selectedIndex

        self._setupFamilies()

    def speechFamiliesChanged(self, widget):
        """Signal handler for the "value_changed" signal for the families
           GtkComboBox widget. The user has selected a different voice
           family. Save the new voice family name based on the new choice.

        Arguments:
        - widget: the component that generated the signal.
        """

        if self.initializingSpeech:
            return

        selectedIndex = widget.get_active()
        try:
            family = self.speechFamiliesChoices[selectedIndex]
            name = family[speechserver.VoiceFamily.NAME]
            language = family[speechserver.VoiceFamily.LANG]
            dialect = family[speechserver.VoiceFamily.DIALECT]
            variant = family[speechserver.VoiceFamily.VARIANT]
            voiceType = self.get_widget("voiceTypesCombo").get_active()
            self._setFamilyNameForVoiceType(voiceType, name, language, dialect, variant)
            if not self._updatingSpeechFamilies:
                self._previewVoiceSelection(voiceType, name)
        except Exception:
            debug.printException(debug.LEVEL_SEVERE)

        # Remember the last family manually selected by the user for the
        # current speech server.
        #
        if not selectedIndex == -1:
            self.selectedFamilyChoices[self.speechServersChoice, \
                    self.speechLanguagesChoice] = selectedIndex

    def voiceTypesChanged(self, widget):
        """Signal handler for the "changed" signal for the voiceTypes
           GtkComboBox widget. The user has selected a different voice
           type. Setup the new family, rate, pitch and volume component
           values based on the new choice.

        Arguments:
        - widget: the component that generated the signal.
        """

        if self.initializingSpeech:
            return

        voiceType = widget.get_active()
        self._setVoiceSettingsForVoiceType(voiceType)

    def rateValueChanged(self, widget):
        """Signal handler for the "value_changed" signal for the rateScale
           GtkScale widget. The user has changed the current rate value.
           Save the new rate value based on the currently selected voice
           type.

        Arguments:
        - widget: the component that generated the signal.
        """

        rate = widget.get_value()
        voiceType = self.get_widget("voiceTypesCombo").get_active()
        self._setRateForVoiceType(voiceType, rate)
        voices = cthulhu.cthulhuApp.settingsManager.getSetting('voices')
        voices.get(settings.DEFAULT_VOICE, {})[acss.ACSS.RATE] = rate
        cthulhu.cthulhuApp.settingsManager.setSetting('voices', voices)

    def pitchValueChanged(self, widget):
        """Signal handler for the "value_changed" signal for the pitchScale
           GtkScale widget. The user has changed the current pitch value.
           Save the new pitch value based on the currently selected voice
           type.

        Arguments:
        - widget: the component that generated the signal.
        """

        pitch = widget.get_value()
        voiceType = self.get_widget("voiceTypesCombo").get_active()
        self._setPitchForVoiceType(voiceType, pitch)
        voices = cthulhu.cthulhuApp.settingsManager.getSetting('voices')
        voices.get(settings.DEFAULT_VOICE, {})[acss.ACSS.AVERAGE_PITCH] = pitch
        cthulhu.cthulhuApp.settingsManager.setSetting('voices', voices)

    def volumeValueChanged(self, widget):
        """Signal handler for the "value_changed" signal for the voiceScale
           GtkScale widget. The user has changed the current volume value.
           Save the new volume value based on the currently selected voice
           type.

        Arguments:
        - widget: the component that generated the signal.
        """

        volume = widget.get_value()
        voiceType = self.get_widget("voiceTypesCombo").get_active()
        self._setVolumeForVoiceType(voiceType, volume)
        voices = cthulhu.cthulhuApp.settingsManager.getSetting('voices')
        voices.get(settings.DEFAULT_VOICE, {})[acss.ACSS.GAIN] = volume
        cthulhu.cthulhuApp.settingsManager.setSetting('voices', voices)

    def useCustomEchoVoiceToggled(self, widget):
        """Signal handler for enabling/disabling custom echo voice settings."""

        self.prefsDict["useCustomEchoVoice"] = widget.get_active()
        self._setEchoVoiceItems()
        self._setupEchoSpeechFamilies()

    def useCustomEchoSpeechServerToggled(self, widget):
        """Signal handler for enabling/disabling custom echo module override."""

        self.prefsDict["useCustomEchoSpeechServer"] = widget.get_active()
        self._setEchoVoiceItems()
        self._setupEchoSpeechFamilies()

    def echoSpeechServersChanged(self, widget):
        """Signal handler for selecting the echo speech-dispatcher module."""

        if self.initializingEchoSpeech:
            return

        selectedIndex = widget.get_active()
        if selectedIndex < 0:
            self.echoSpeechServersChoice = None
            self.prefsDict["echoSpeechServerInfo"] = None
            self._setupEchoSpeechFamilies()
            return

        self.echoSpeechServersChoice = self.echoSpeechServersChoices[selectedIndex]
        self.prefsDict["echoSpeechServerInfo"] = self.echoSpeechServersChoice.getInfo()
        self._setupEchoSpeechFamilies()

    def echoSpeechFamiliesChanged(self, widget):
        """Signal handler for selecting the echo voice family."""

        if self.initializingEchoSpeech or self._updatingEchoSpeechFamilies:
            return

        selectedIndex = widget.get_active()
        if selectedIndex < 0:
            self.echoSpeechFamiliesChoice = None
            return

        self.echoSpeechFamiliesChoice = self.echoSpeechFamiliesChoices[selectedIndex]
        self._setEchoVoiceFamily(self.echoSpeechFamiliesChoice)

    def echoRateValueChanged(self, widget):
        """Signal handler for changing custom echo rate."""

        if self.echoVoice is None:
            self.echoVoice = acss.ACSS({})
        self.echoVoice[acss.ACSS.RATE] = widget.get_value()
        self.echoVoice['established'] = True

    def echoPitchValueChanged(self, widget):
        """Signal handler for changing custom echo pitch."""

        if self.echoVoice is None:
            self.echoVoice = acss.ACSS({})
        self.echoVoice[acss.ACSS.AVERAGE_PITCH] = widget.get_value()
        self.echoVoice['established'] = True

    def echoVolumeValueChanged(self, widget):
        """Signal handler for changing custom echo volume."""

        if self.echoVoice is None:
            self.echoVoice = acss.ACSS({})
        self.echoVoice[acss.ACSS.GAIN] = widget.get_value()
        self.echoVoice['established'] = True

    def checkButtonToggled(self, widget):
        """Signal handler for "toggled" signal for basic GtkCheckButton 
           widgets. The user has altered the state of the checkbox.
           Set the preference to the new value.

        Arguments:
        - widget: the component that generated the signal.
        """

        # To use this default handler please make sure:
        # The name of the setting that will be changed is: settingName
        # The id of the widget in the ui should be: settingNameCheckButton
        #
        settingName = Gtk.Buildable.get_name(widget)
        # strip "CheckButton" from the end.
        settingName = settingName[:-11] 
        self.prefsDict[settingName] = widget.get_active()
        if settingName in [
                "enableEchoByCharacter",
                "enableEchoByWord",
                "enableEchoBySentence",
                "useCustomEchoForKey",
                "useCustomEchoForCharacter",
                "useCustomEchoForWord",
                "useCustomEchoForSentence"]:
            self._setEchoVoiceItems()

    def keyEchoChecked(self, widget):
        """Signal handler for the "toggled" signal for the
           keyEchoCheckbutton GtkCheckButton widget. The user has
           [un]checked the 'Enable Key Echo' checkbox. Set the
           'enableKeyEcho' preference to the new value. [In]sensitize
           the checkboxes for the various types of key echo, depending
           upon whether this value is checked or unchecked.

        Arguments:
        - widget: the component that generated the signal.
        """

        self.prefsDict["enableKeyEcho"] = widget.get_active()
        self._setKeyEchoItems()

    def brailleSelectionChanged(self, widget):
        """Signal handler for the "toggled" signal for the
           brailleSelectionNoneButton, brailleSelection7Button,
           brailleSelection8Button or brailleSelectionBothButton
           GtkRadioButton widgets. The user has toggled the braille
           selection indicator value. If this signal was generated
           as the result of a radio button getting selected (as
           opposed to a radio button losing the selection), set the
           'brailleSelectorIndicator' preference to the new value.

        Arguments:
        - widget: the component that generated the signal.
        """

        if widget.get_active():
            if widget.get_label() == guilabels.BRAILLE_DOT_7:
                self.prefsDict["brailleSelectorIndicator"] = \
                    settings.BRAILLE_UNDERLINE_7
            elif widget.get_label() == guilabels.BRAILLE_DOT_8:
                self.prefsDict["brailleSelectorIndicator"] = \
                    settings.BRAILLE_UNDERLINE_8
            elif widget.get_label() == guilabels.BRAILLE_DOT_7_8:
                self.prefsDict["brailleSelectorIndicator"] = \
                    settings.BRAILLE_UNDERLINE_BOTH
            else:
                self.prefsDict["brailleSelectorIndicator"] = \
                    settings.BRAILLE_UNDERLINE_NONE

    def brailleLinkChanged(self, widget):
        """Signal handler for the "toggled" signal for the
           brailleLinkNoneButton, brailleLink7Button,
           brailleLink8Button or brailleLinkBothButton
           GtkRadioButton widgets. The user has toggled the braille
           link indicator value. If this signal was generated
           as the result of a radio button getting selected (as
           opposed to a radio button losing the selection), set the
           'brailleLinkIndicator' preference to the new value.

        Arguments:
        - widget: the component that generated the signal.
        """

        if widget.get_active():
            if widget.get_label() == guilabels.BRAILLE_DOT_7:
                self.prefsDict["brailleLinkIndicator"] = \
                    settings.BRAILLE_UNDERLINE_7
            elif widget.get_label() == guilabels.BRAILLE_DOT_8:
                self.prefsDict["brailleLinkIndicator"] = \
                    settings.BRAILLE_UNDERLINE_8
            elif widget.get_label() == guilabels.BRAILLE_DOT_7_8:
                self.prefsDict["brailleLinkIndicator"] = \
                    settings.BRAILLE_UNDERLINE_BOTH
            else:
                self.prefsDict["brailleLinkIndicator"] = \
                    settings.BRAILLE_UNDERLINE_NONE

    def brailleIndicatorChanged(self, widget):
        """Signal handler for the "toggled" signal for the
           textBrailleNoneButton, textBraille7Button, textBraille8Button
           or textBrailleBothButton GtkRadioButton widgets. The user has
           toggled the text attributes braille indicator value. If this signal
           was generated as the result of a radio button getting selected
           (as opposed to a radio button losing the selection), set the
           'textAttributesBrailleIndicator' preference to the new value.

        Arguments:
        - widget: the component that generated the signal.
        """

        if widget.get_active():
            if widget.get_label() == guilabels.BRAILLE_DOT_7:
                self.prefsDict["textAttributesBrailleIndicator"] = \
                    settings.BRAILLE_UNDERLINE_7
            elif widget.get_label() == guilabels.BRAILLE_DOT_8:
                self.prefsDict["textAttributesBrailleIndicator"] = \
                    settings.BRAILLE_UNDERLINE_8
            elif widget.get_label() == guilabels.BRAILLE_DOT_7_8:
                self.prefsDict["textAttributesBrailleIndicator"] = \
                    settings.BRAILLE_UNDERLINE_BOTH
            else:
                self.prefsDict["textAttributesBrailleIndicator"] = \
                    settings.BRAILLE_UNDERLINE_NONE

    def punctuationLevelChanged(self, widget):
        """Signal handler for the "toggled" signal for the noneButton,
           someButton or allButton GtkRadioButton widgets. The user has
           toggled the speech punctuation level value. If this signal
           was generated as the result of a radio button getting selected
           (as opposed to a radio button losing the selection), set the
           'verbalizePunctuationStyle' preference to the new value.

        Arguments:
        - widget: the component that generated the signal.
        """

        if widget.get_active():
            if widget.get_label() == guilabels.PUNCTUATION_STYLE_NONE:
                self.prefsDict["verbalizePunctuationStyle"] = \
                    settings.PUNCTUATION_STYLE_NONE
            elif widget.get_label() == guilabels.PUNCTUATION_STYLE_SOME:
                self.prefsDict["verbalizePunctuationStyle"] = \
                    settings.PUNCTUATION_STYLE_SOME
            elif widget.get_label() == guilabels.PUNCTUATION_STYLE_MOST:
                self.prefsDict["verbalizePunctuationStyle"] = \
                    settings.PUNCTUATION_STYLE_MOST
            else:
                self.prefsDict["verbalizePunctuationStyle"] = \
                    settings.PUNCTUATION_STYLE_ALL

    def cthulhuModifierChanged(self, widget):
        """Signal handler for the changed signal for the cthulhuModifierComboBox
           Set the 'cthulhuModifierKeys' preference to the new value.

        Arguments:
        - widget: the component that generated the signal.
        """

        model = widget.get_model()
        myIter = widget.get_active_iter()
        cthulhuModifier = model[myIter][0]
        self.prefsDict["cthulhuModifierKeys"] = cthulhuModifier.split(', ')

    def progressBarVerbosityChanged(self, widget):
        """Signal handler for the changed signal for the progressBarVerbosity
           GtkComboBox widget. Set the 'progressBarVerbosity' preference to
           the new value.

        Arguments:
        - widget: the component that generated the signal.
        """

        model = widget.get_model()
        myIter = widget.get_active_iter()
        progressBarVerbosity = model[myIter][0]
        if progressBarVerbosity == guilabels.PROGRESS_BAR_ALL:
            self.prefsDict["progressBarVerbosity"] = \
                settings.PROGRESS_BAR_ALL
        elif progressBarVerbosity == guilabels.PROGRESS_BAR_WINDOW:
            self.prefsDict["progressBarVerbosity"] = \
                settings.PROGRESS_BAR_WINDOW
        else:
            self.prefsDict["progressBarVerbosity"] = \
                settings.PROGRESS_BAR_APPLICATION

    def capitalizationStyleChanged(self, widget):
        model = widget.get_model()
        myIter = widget.get_active_iter()
        capitalizationStyle = model[myIter][0]
        if capitalizationStyle == guilabels.CAPITALIZATION_STYLE_ICON:
            self.prefsDict["capitalizationStyle"] = settings.CAPITALIZATION_STYLE_ICON
        elif capitalizationStyle == guilabels.CAPITALIZATION_STYLE_SPELL:
            self.prefsDict["capitalizationStyle"] = settings.CAPITALIZATION_STYLE_SPELL
        else:
            self.prefsDict["capitalizationStyle"] = settings.CAPITALIZATION_STYLE_NONE
        self.script.speechAndVerbosityManager.update_capitalization_style()

    def sayAllStyleChanged(self, widget):
        """Signal handler for the "changed" signal for the sayAllStyle
           GtkComboBox widget. Set the 'sayAllStyle' preference to the
           new value.

        Arguments:
        - widget: the component that generated the signal.
        """

        model = widget.get_model()
        myIter = widget.get_active_iter()
        sayAllStyle = model[myIter][0]
        if sayAllStyle == guilabels.SAY_ALL_STYLE_LINE:
            self.prefsDict["sayAllStyle"] = settings.SAYALL_STYLE_LINE
        elif sayAllStyle == guilabels.SAY_ALL_STYLE_SENTENCE:
            self.prefsDict["sayAllStyle"] = settings.SAYALL_STYLE_SENTENCE

    def dateFormatChanged(self, widget):
        """Signal handler for the "changed" signal for the dateFormat
           GtkComboBox widget. Set the 'dateFormat' preference to the
           new value.

        Arguments:
        - widget: the component that generated the signal.
        """

        dateFormatCombo = widget.get_active()
        if dateFormatCombo == DATE_FORMAT_LOCALE:
            newFormat = messages.DATE_FORMAT_LOCALE
        elif dateFormatCombo == DATE_FORMAT_NUMBERS_DM:
            newFormat = messages.DATE_FORMAT_NUMBERS_DM
        elif dateFormatCombo == DATE_FORMAT_NUMBERS_MD:
            newFormat = messages.DATE_FORMAT_NUMBERS_MD
        elif dateFormatCombo == DATE_FORMAT_NUMBERS_DMY:
            newFormat = messages.DATE_FORMAT_NUMBERS_DMY
        elif dateFormatCombo == DATE_FORMAT_NUMBERS_MDY:
            newFormat = messages.DATE_FORMAT_NUMBERS_MDY
        elif dateFormatCombo == DATE_FORMAT_NUMBERS_YMD:
            newFormat = messages.DATE_FORMAT_NUMBERS_YMD
        elif dateFormatCombo == DATE_FORMAT_FULL_DM:
            newFormat = messages.DATE_FORMAT_FULL_DM
        elif dateFormatCombo == DATE_FORMAT_FULL_MD:
            newFormat = messages.DATE_FORMAT_FULL_MD
        elif dateFormatCombo == DATE_FORMAT_FULL_DMY:
            newFormat = messages.DATE_FORMAT_FULL_DMY
        elif dateFormatCombo == DATE_FORMAT_FULL_MDY:
            newFormat = messages.DATE_FORMAT_FULL_MDY
        elif dateFormatCombo == DATE_FORMAT_FULL_YMD:
            newFormat = messages.DATE_FORMAT_FULL_YMD
        elif dateFormatCombo == DATE_FORMAT_ABBREVIATED_DM:
            newFormat = messages.DATE_FORMAT_ABBREVIATED_DM
        elif dateFormatCombo == DATE_FORMAT_ABBREVIATED_MD:
            newFormat = messages.DATE_FORMAT_ABBREVIATED_MD
        elif dateFormatCombo == DATE_FORMAT_ABBREVIATED_DMY:
            newFormat = messages.DATE_FORMAT_ABBREVIATED_DMY
        elif dateFormatCombo == DATE_FORMAT_ABBREVIATED_MDY:
            newFormat = messages.DATE_FORMAT_ABBREVIATED_MDY
        elif dateFormatCombo == DATE_FORMAT_ABBREVIATED_YMD:
            newFormat = messages.DATE_FORMAT_ABBREVIATED_YMD
        self.prefsDict["presentDateFormat"] = newFormat

    def timeFormatChanged(self, widget):
        """Signal handler for the "changed" signal for the timeFormat
           GtkComboBox widget. Set the 'timeFormat' preference to the
           new value.

        Arguments:
        - widget: the component that generated the signal.
        """

        timeFormatCombo = widget.get_active()
        if timeFormatCombo == TIME_FORMAT_LOCALE:
            newFormat = messages.TIME_FORMAT_LOCALE
        elif timeFormatCombo == TIME_FORMAT_12_HM:
            newFormat = messages.TIME_FORMAT_12_HM
        elif timeFormatCombo == TIME_FORMAT_12_HMS:
            newFormat = messages.TIME_FORMAT_12_HMS
        elif timeFormatCombo == TIME_FORMAT_24_HMS:
            newFormat = messages.TIME_FORMAT_24_HMS
        elif timeFormatCombo == TIME_FORMAT_24_HMS_WITH_WORDS:
            newFormat = messages.TIME_FORMAT_24_HMS_WITH_WORDS
        elif timeFormatCombo == TIME_FORMAT_24_HM:
            newFormat = messages.TIME_FORMAT_24_HM
        elif timeFormatCombo == TIME_FORMAT_24_HM_WITH_WORDS:
            newFormat  = messages.TIME_FORMAT_24_HM_WITH_WORDS
        self.prefsDict["presentTimeFormat"] =  newFormat

    def speechVerbosityChanged(self, widget):
        """Signal handler for the "toggled" signal for the speechBriefButton,
           or speechVerboseButton GtkRadioButton widgets. The user has
           toggled the speech verbosity level value. If this signal was
           generated as the result of a radio button getting selected
           (as opposed to a radio button losing the selection), set the
           'speechVerbosityLevel' preference to the new value.

        Arguments:
        - widget: the component that generated the signal.
        """

        if widget.get_active():
            if widget.get_label() == guilabels.VERBOSITY_LEVEL_BRIEF:
                self.prefsDict["speechVerbosityLevel"] = \
                    settings.VERBOSITY_LEVEL_BRIEF
            else:
                self.prefsDict["speechVerbosityLevel"] = \
                    settings.VERBOSITY_LEVEL_VERBOSE

    def progressBarUpdateIntervalValueChanged(self, widget):
        """Signal handler for the "value_changed" signal for the
           progressBarUpdateIntervalSpinButton GtkSpinButton widget.

        Arguments:
        - widget: the component that generated the signal.
        """

        self.prefsDict["progressBarUpdateInterval"] = widget.get_value_as_int()

    def brailleFlashTimeValueChanged(self, widget):
        self.prefsDict["brailleFlashTime"] = widget.get_value_as_int() * 1000

    def abbrevRolenamesChecked(self, widget):
        """Signal handler for the "toggled" signal for the abbrevRolenames
           GtkCheckButton widget. The user has [un]checked the 'Abbreviated
           Rolenames' checkbox. Set the 'brailleRolenameStyle' preference
           to the new value.

        Arguments:
        - widget: the component that generated the signal.
        """

        if widget.get_active():
            self.prefsDict["brailleRolenameStyle"] = \
                settings.BRAILLE_ROLENAME_STYLE_SHORT
        else:
            self.prefsDict["brailleRolenameStyle"] = \
                settings.BRAILLE_ROLENAME_STYLE_LONG

    def brailleVerbosityChanged(self, widget):
        """Signal handler for the "toggled" signal for the brailleBriefButton,
           or brailleVerboseButton GtkRadioButton widgets. The user has
           toggled the braille verbosity level value. If this signal was
           generated as the result of a radio button getting selected
           (as opposed to a radio button losing the selection), set the
           'brailleVerbosityLevel' preference to the new value.

        Arguments:
        - widget: the component that generated the signal.
        """

        if widget.get_active():
            if widget.get_label() == guilabels.VERBOSITY_LEVEL_BRIEF:
                self.prefsDict["brailleVerbosityLevel"] = \
                    settings.VERBOSITY_LEVEL_BRIEF
            else:
                self.prefsDict["brailleVerbosityLevel"] = \
                    settings.VERBOSITY_LEVEL_VERBOSE

    def keyModifiedToggle(self, cell, path, model, col):
        """When the user changes a checkbox field (boolean field)"""

        model[path][col] = not model[path][col]
        return

    def editingKey(self, cell, editable, path, treeModel):
        """Starts user input of a Key for a selected key binding"""

        self._presentMessage(messages.KB_ENTER_NEW_KEY)
        cthulhu_state.capturingKeys = True
        try:
            script_manager.get_manager().get_active_script().removeKeyGrabs()
        except Exception:
            pass
        try:
            input_event_manager.get_manager().unmap_all_modifiers()
        except Exception:
            pass
        self._unmapCthulhuModifiersForCapture()
        editable.connect('key-press-event', self.kbKeyPressed)
        return

    def _unmapCthulhuModifiersForCapture(self):
        """Unmap Cthulhu modifier keys so they can be captured as bindings."""
        device = cthulhu_state.device
        if device is None:
            return

        for modifierKey in settings.cthulhuModifierKeys:
            keycode = keybindings.getKeycode(modifierKey)
            if keycode == 0 and modifierKey == "Shift_Lock":
                keycode = keybindings.getKeycode("Caps_Lock")
            if keycode:
                try:
                    device.unmap_modifier(keycode)
                except Exception:
                    pass

    def editingCanceledKey(self, editable):
        """Stops user input of a Key for a selected key binding"""

        cthulhu_state.capturingKeys = False
        self._capturedKey = []
        try:
            script_manager.get_manager().get_active_script().refreshKeyGrabs()
        except Exception:
            pass
        return

    def _processKeyCaptured(self, keyPressedEvent):
        """Called when a new key event arrives and we are capturing keys.
        (used for key bindings redefinition)
        """

        # We want the keyname rather than the printable character.
        # If it's not on the keypad, get the name of the unshifted
        # character. (i.e. "1" instead of "!")
        #
        keycode = keyPressedEvent.hardware_keycode
        keymap = Gdk.Keymap.get_default()
        entries_for_keycode = keymap.get_entries_for_keycode(keycode)
        entries = entries_for_keycode[-1]
        eventString = Gdk.keyval_name(entries[0])
        eventState = keyPressedEvent.state
        eventKeyvalName = Gdk.keyval_name(keyPressedEvent.keyval)

        cthulhuMods = settings.cthulhuModifierKeys
        if eventKeyvalName in cthulhuMods:
            eventString = eventKeyvalName
            self._capturedKey = ['', keybindings.CTHULHU_MODIFIER_MASK, 0]
            return False
        if eventKeyvalName == "KP_0" \
           and "KP_Insert" in cthulhuMods \
           and eventState & Gdk.ModifierType.SHIFT_MASK:
            eventString = "KP_Insert"
            self._capturedKey = ['', keybindings.CTHULHU_MODIFIER_MASK, 0]
            return False

        modifierKeys =  ['Alt_L', 'Alt_R', 'Control_L', 'Control_R',
                         'Shift_L', 'Shift_R', 'Meta_L', 'Meta_R',
                         'Num_Lock', 'Caps_Lock', 'Shift_Lock']
        if eventString in modifierKeys:
            return False

        eventState = eventState & Gtk.accelerator_get_default_mod_mask()
        if not self._capturedKey \
           or eventString in ['Return', 'Escape']:
            self._capturedKey = [eventString, eventState, 1]
            return True

        string, modifiers, clickCount = self._capturedKey
        isCthulhuModifier = modifiers & keybindings.CTHULHU_MODIFIER_MASK
        if isCthulhuModifier:
            eventState |= keybindings.CTHULHU_MODIFIER_MASK
            self._capturedKey = [eventString, eventState, clickCount + 1]

        return True

    def kbKeyPressed(self, editable, event):
        """Special handler for the key_pressed events when editing the
        keybindings.  This lets us control what gets inserted into the
        entry.
        """

        keyProcessed = self._processKeyCaptured(event)
        if not keyProcessed:
            return True

        if not self._capturedKey:
            return False

        keyName, modifiers, clickCount = self._capturedKey
        if not keyName or keyName in ["Return", "Escape"]:
            return False

        isCthulhuModifier = modifiers & keybindings.CTHULHU_MODIFIER_MASK
        if keyName in ["Delete", "BackSpace"] and not isCthulhuModifier:
            editable.set_text("")
            self._presentMessage(messages.KB_DELETED)
            self._capturedKey = []
            self.newBinding = None
            return True

        self.newBinding = keybindings.KeyBinding(keyName,
                                                 keybindings.defaultModifierMask,
                                                 modifiers,
                                                 None,
                                                 clickCount)
        modifierNames = keybindings.getModifierNames(modifiers)
        clickCountString = self._clickCountToString(clickCount)
        newString = modifierNames + keyName + clickCountString
        description = self.pendingKeyBindings.get(newString)

        if description is None:

            def match(x):
                return x.keysymstring == keyName and x.modifiers == modifiers \
                    and x.click_count == clickCount and x.handler

            matches = list(filter(match, self.kbindings.keyBindings))
            if matches:
                description = matches[0].handler.description

        if description:
            msg = messages.KB_ALREADY_BOUND % description
            delay = int(1000 * settings.doubleClickTimeout)
            GLib.timeout_add(delay, self._presentMessage, msg)
        else:
            msg = messages.KB_CAPTURED % newString
            editable.set_text(newString)
            self._presentMessage(msg)

        return True

    def editedKey(self, cell, path, new_text, treeModel,
                  modMask, modUsed, key, click_count, text):
        """The user changed the key for a Keybinding: update the model of
        the treeview.
        """

        cthulhu_state.capturingKeys = False
        self._capturedKey = []
        try:
            script_manager.get_manager().get_active_script().refreshKeyGrabs()
        except Exception:
            pass
        myiter = treeModel.get_iter_from_string(path)
        try:
            originalBinding = treeModel.get_value(myiter, text)
        except Exception:
            originalBinding = ''
        modified = (originalBinding != new_text)

        try:
            string = self.newBinding.keysymstring
            mods = self.newBinding.modifiers
            clickCount = self.newBinding.click_count
        except Exception:
            string = ''
            mods = 0
            clickCount = 1

        mods = mods & Gdk.ModifierType.MODIFIER_MASK
        if mods & (1 << Atspi.ModifierType.SHIFTLOCK) \
           and mods & keybindings.CTHULHU_MODIFIER_MASK:
            mods ^= (1 << Atspi.ModifierType.SHIFTLOCK)

        treeModel.set(myiter,
                      modMask, str(keybindings.defaultModifierMask),
                      modUsed, str(int(mods)),
                      key, string,
                      text, new_text,
                      click_count, str(clickCount),
                      MODIF, modified)
        speech.stop()
        if new_text:
            message = messages.KB_CAPTURED_CONFIRMATION % new_text
            description = treeModel.get_value(myiter, DESCRIP)
            self.pendingKeyBindings[new_text] = description
        else:
            message = messages.KB_DELETED_CONFIRMATION

        if modified:
            self._presentMessage(message)
            self.pendingKeyBindings[originalBinding] = ""

        return

    def presentToolTipsChecked(self, widget):
        """Signal handler for the "toggled" signal for the
           presentToolTipsCheckButton GtkCheckButton widget.
           The user has [un]checked the 'Present ToolTips'
           checkbox. Set the 'presentToolTips'
           preference to the new value if the user can present tooltips.

        Arguments:
        - widget: the component that generated the signal.
        """

        self.prefsDict["presentToolTips"] = widget.get_active()

    def keyboardLayoutChanged(self, widget):
        """Signal handler for the "toggled" signal for the generalDesktopButton,
           or generalLaptopButton GtkRadioButton widgets. The user has
           toggled the keyboard layout value. If this signal was
           generated as the result of a radio button getting selected
           (as opposed to a radio button losing the selection), set the
           'keyboardLayout' preference to the new value. Also set the
           matching list of Cthulhu modifier keys

        Arguments:
        - widget: the component that generated the signal.
        """

        if widget.get_active():
            if widget.get_label() == guilabels.KEYBOARD_LAYOUT_DESKTOP:
                self.prefsDict["keyboardLayout"] = \
                    settings.GENERAL_KEYBOARD_LAYOUT_DESKTOP
                self.prefsDict["cthulhuModifierKeys"] = \
                    settings.DESKTOP_MODIFIER_KEYS
            else:
                self.prefsDict["keyboardLayout"] = \
                    settings.GENERAL_KEYBOARD_LAYOUT_LAPTOP
                self.prefsDict["cthulhuModifierKeys"] = \
                    settings.LAPTOP_MODIFIER_KEYS

    def pronunciationAddButtonClicked(self, widget):
        """Signal handler for the "clicked" signal for the
        pronunciationAddButton GtkButton widget. The user has clicked
        the Add button on the Pronunciation pane. A new row will be 
        added to the end of the pronunciation dictionary list. Both the
        actual and replacement strings will initially be set to an empty
        string. Focus will be moved to that row.

        Arguments:
        - widget: the component that generated the signal.
        """

        model = self.pronunciationView.get_model()
        thisIter = model.append()
        model.set(thisIter, ACTUAL, "", REPLACEMENT, "")        
        path = model.get_path(thisIter)
        col = self.pronunciationView.get_column(0)
        self.pronunciationView.grab_focus()
        self.pronunciationView.set_cursor(path, col, True) 

    def pronunciationDeleteButtonClicked(self, widget):
        """Signal handler for the "clicked" signal for the
        pronunciationDeleteButton GtkButton widget. The user has clicked
        the Delete button on the Pronunciation pane. The row in the 
        pronunciation dictionary list with focus will be deleted.

        Arguments:
        - widget: the component that generated the signal.
        """

        model, oldIter = self.pronunciationView.get_selection().get_selected()
        model.remove(oldIter)

    def textSelectAllButtonClicked(self, widget):
        """Signal handler for the "clicked" signal for the
        textSelectAllButton GtkButton widget. The user has clicked
        the Speak all button.  Check all the text attributes and
        then update the "enabledSpokenTextAttributes" and
        "enabledBrailledTextAttributes" preference strings.

        Arguments:
        - widget: the component that generated the signal.
        """

        attributes = cthulhu.cthulhuApp.settingsManager.getSetting('allTextAttributes')
        self._setSpokenTextAttributes(
            self.getTextAttributesView, attributes, True)
        self._setBrailledTextAttributes(
            self.getTextAttributesView, attributes, True)
        self._updateTextDictEntry()

    def textUnselectAllButtonClicked(self, widget):
        """Signal handler for the "clicked" signal for the
        textUnselectAllButton GtkButton widget. The user has clicked
        the Speak none button. Uncheck all the text attributes and
        then update the "enabledSpokenTextAttributes" and
        "enabledBrailledTextAttributes" preference strings.

        Arguments:
        - widget: the component that generated the signal.
        """

        attributes = cthulhu.cthulhuApp.settingsManager.getSetting('allTextAttributes')
        self._setSpokenTextAttributes(
            self.getTextAttributesView, attributes, False)
        self._setBrailledTextAttributes(
            self.getTextAttributesView, attributes, False)
        self._updateTextDictEntry()

    def textResetButtonClicked(self, widget):
        """Signal handler for the "clicked" signal for the
        textResetButton GtkButton widget. The user has clicked
        the Reset button. Reset all the text attributes to their
        initial state and then update the "enabledSpokenTextAttributes"
        and "enabledBrailledTextAttributes" preference strings.

        Arguments:
        - widget: the component that generated the signal.
        """

        attributes = cthulhu.cthulhuApp.settingsManager.getSetting('allTextAttributes')
        self._setSpokenTextAttributes(
            self.getTextAttributesView, attributes, False)
        self._setBrailledTextAttributes(
            self.getTextAttributesView, attributes, False)

        attributes = cthulhu.cthulhuApp.settingsManager.getSetting('enabledSpokenTextAttributes')
        self._setSpokenTextAttributes(
            self.getTextAttributesView, attributes, True)

        attributes = \
            cthulhu.cthulhuApp.settingsManager.getSetting('enabledBrailledTextAttributes')
        self._setBrailledTextAttributes(
            self.getTextAttributesView, attributes, True)

        self._updateTextDictEntry()

    def textMoveToTopButtonClicked(self, widget):
        """Signal handler for the "clicked" signal for the
        textMoveToTopButton GtkButton widget. The user has clicked
        the Move to top button. Move the selected rows in the text
        attribute view to the very top of the list and then update
        the "enabledSpokenTextAttributes" and "enabledBrailledTextAttributes"
        preference strings.

        Arguments:
        - widget: the component that generated the signal.
        """

        textSelection = self.getTextAttributesView.get_selection()
        [model, paths] = textSelection.get_selected_rows()
        for path in paths:
            thisIter = model.get_iter(path)
            model.move_after(thisIter, None)
        self._updateTextDictEntry()

    def textMoveUpOneButtonClicked(self, widget):
        """Signal handler for the "clicked" signal for the
        textMoveUpOneButton GtkButton widget. The user has clicked
        the Move up one button. Move the selected rows in the text
        attribute view up one row in the list and then update the
        "enabledSpokenTextAttributes" and "enabledBrailledTextAttributes"
        preference strings.

        Arguments:
        - widget: the component that generated the signal.
        """

        textSelection = self.getTextAttributesView.get_selection()
        [model, paths] = textSelection.get_selected_rows()
        for path in paths:
            thisIter = model.get_iter(path)
            indices = path.get_indices()
            if indices[0]:
                otherIter = model.iter_nth_child(None, indices[0]-1)
                model.swap(thisIter, otherIter)
        self._updateTextDictEntry()

    def textMoveDownOneButtonClicked(self, widget):
        """Signal handler for the "clicked" signal for the
        textMoveDownOneButton GtkButton widget. The user has clicked
        the Move down one button. Move the selected rows in the text
        attribute view down one row in the list and then update the
        "enabledSpokenTextAttributes" and "enabledBrailledTextAttributes"
        preference strings.

        Arguments:
        - widget: the component that generated the signal.
        """

        textSelection = self.getTextAttributesView.get_selection()
        [model, paths] = textSelection.get_selected_rows()
        noRows = model.iter_n_children(None)
        for path in paths:
            thisIter = model.get_iter(path)
            indices = path.get_indices()
            if indices[0] < noRows-1:
                otherIter = model.iter_next(thisIter)
                model.swap(thisIter, otherIter)
        self._updateTextDictEntry()

    def textMoveToBottomButtonClicked(self, widget):
        """Signal handler for the "clicked" signal for the
        textMoveToBottomButton GtkButton widget. The user has clicked
        the Move to bottom button. Move the selected rows in the text
        attribute view to the bottom of the list and then update the
        "enabledSpokenTextAttributes" and "enabledBrailledTextAttributes"
        preference strings.

        Arguments:
        - widget: the component that generated the signal.
        """

        textSelection = self.getTextAttributesView.get_selection()
        [model, paths] = textSelection.get_selected_rows()
        for path in paths:
            thisIter = model.get_iter(path)
            model.move_before(thisIter, None)
        self._updateTextDictEntry()

    def helpButtonClicked(self, widget):
        """Signal handler for the "clicked" signal for the helpButton
           GtkButton widget. The user has clicked the Help button.

        Arguments:
        - widget: the component that generated the signal.
        """

        self.script.learnModePresenter.show_help(page="preferences")

    def restoreSettings(self):
        """Restore the settings we saved away when opening the preferences
           dialog."""
        # Restore the default rate/pitch/gain,
        # in case the user played with the sliders.
        #
        voices = cthulhu.cthulhuApp.settingsManager.getSetting('voices')
        defaultVoice = voices.get(settings.DEFAULT_VOICE)
        if defaultVoice is not None:
            defaultVoice[acss.ACSS.GAIN] = self.savedGain
            defaultVoice[acss.ACSS.AVERAGE_PITCH] = self.savedPitch
            defaultVoice[acss.ACSS.RATE] = self.savedRate

    def saveBasicSettings(self):
        if not self._isInitialSetup:
            self.restoreSettings()

        if self.soundSinkCombo is not None:
            activeIndex = self.soundSinkCombo.get_active()
            if activeIndex >= 0:
                self.prefsDict["soundSink"] = self._soundSinkChoices[activeIndex][0]

        enable = self.get_widget("speechSupportCheckButton").get_active()
        self.prefsDict["enableSpeech"] = enable

        if self.speechSystemsChoice:
            self.prefsDict["speechServerFactory"] = \
                self.speechSystemsChoice.__name__

        speechServerChoice = self._getSpeechServerChoiceForSave()
        if speechServerChoice:
            self.prefsDict["speechServerInfo"] = \
                speechServerChoice.getInfo()
        else:
            activeSpeechInfo = speech.getInfo()
            if activeSpeechInfo:
                self.prefsDict["speechServerInfo"] = activeSpeechInfo

            runtimeSpeechServerInfo = cthulhu.cthulhuApp.settingsManager.getSetting(
                "speechServerInfo"
            )
            if runtimeSpeechServerInfo and not self.prefsDict.get("speechServerInfo"):
                self.prefsDict["speechServerInfo"] = runtimeSpeechServerInfo

            existingSpeechServerInfo = self.prefsDict.get("speechServerInfo")
            if existingSpeechServerInfo:
                self.prefsDict["speechServerInfo"] = existingSpeechServerInfo

        if self.defaultVoice is not None:
            self.prefsDict["voices"] = {
                settings.DEFAULT_VOICE: acss.ACSS(self.defaultVoice),
                settings.UPPERCASE_VOICE: acss.ACSS(self.uppercaseVoice),
                settings.HYPERLINK_VOICE: acss.ACSS(self.hyperlinkVoice),
                settings.SYSTEM_VOICE: acss.ACSS(self.systemVoice),
            }

        self.prefsDict["useCustomEchoVoice"] = \
            self.get_widget("useCustomEchoVoiceCheckButton").get_active()
        self.prefsDict["useCustomEchoSpeechServer"] = \
            self.get_widget("useCustomEchoSpeechServerCheckButton").get_active()
        self.prefsDict["useCustomEchoForKey"] = \
            self.get_widget("useCustomEchoForKeyCheckButton").get_active()
        self.prefsDict["useCustomEchoForCharacter"] = \
            self.get_widget("useCustomEchoForCharacterCheckButton").get_active()
        self.prefsDict["useCustomEchoForWord"] = \
            self.get_widget("useCustomEchoForWordCheckButton").get_active()
        self.prefsDict["useCustomEchoForSentence"] = \
            self.get_widget("useCustomEchoForSentenceCheckButton").get_active()

        if self.echoVoice is None:
            self.echoVoice = acss.ACSS({})

        # Persist slider values directly so saving does not depend on
        # value-changed signal timing.
        self.echoVoice[acss.ACSS.RATE] = self.get_widget("echoRateScale").get_value()
        self.echoVoice[acss.ACSS.AVERAGE_PITCH] = self.get_widget("echoPitchScale").get_value()
        self.echoVoice[acss.ACSS.GAIN] = self.get_widget("echoVolumeScale").get_value()
        self.echoVoice['established'] = True
        self.prefsDict["echoVoice"] = acss.ACSS(self.echoVoice)

        echoSpeechServerChoice = self._getEchoSpeechServerChoiceForSave()
        if echoSpeechServerChoice:
            self.prefsDict["echoSpeechServerInfo"] = echoSpeechServerChoice.getInfo()
        else:
            self.prefsDict["echoSpeechServerInfo"] = None

    def applyButtonClicked(self, widget):
        """Signal handler for the "clicked" signal for the applyButton
           GtkButton widget. The user has clicked the Apply button.
           Write out the users preferences. Shut down any active speech servers
           that were started. Reload the users preferences to get the new
           speech, braille and key echo value to take effect. Do not dismiss
           the configuration window.

        Arguments:
        - widget: the component that generated the signal.
        """

        msg = "PREFERENCES DIALOG: Apply button clicked"
        debug.printMessage(debug.LEVEL_ALL, msg, True)

        self.saveBasicSettings()
        activeProfile = self.getComboBoxList(self.profilesCombo)
        startingProfile = self.getComboBoxList(self.startingProfileCombo)

        self.prefsDict['profile'] = activeProfile
        self.prefsDict['activeProfile'] = activeProfile
        self.prefsDict['startingProfile'] = startingProfile
        cthulhu.cthulhuApp.settingsManager.setStartingProfile(startingProfile)

        self._apply_plugin_changes()
        self.writeUserPreferences()
        cthulhu.loadUserSettings(self.script)
        self._initSoundThemeState()
        self._refresh_dynamic_plugin_tabs()
        braille.checkBrailleSetting()
        self._initSpeechState()
        self._initEchoSpeechState()
        self._populateKeyBindings()
        self.__initProfileCombo()

        msg = "PREFERENCES DIALOG: Handling Apply button click complete"
        debug.printMessage(debug.LEVEL_ALL, msg, True)

    def cancelButtonClicked(self, widget):
        """Signal handler for the "clicked" signal for the cancelButton
           GtkButton widget. The user has clicked the Cancel button.
           Don't write out the preferences. Destroy the configuration window.

        Arguments:
        - widget: the component that generated the signal.
        """

        msg = "PREFERENCES DIALOG: Cancel button clicked"
        debug.printMessage(debug.LEVEL_ALL, msg, True)

        self.windowClosed(widget)
        self.get_widget("cthulhuSetupWindow").destroy()

        msg = "PREFERENCES DIALOG: Handling Cancel button click complete"
        debug.printMessage(debug.LEVEL_ALL, msg, True)

    def okButtonClicked(self, widget=None):
        """Signal handler for the "clicked" signal for the okButton
           GtkButton widget. The user has clicked the OK button.
           Write out the users preferences. Shut down any active speech servers
           that were started. Reload the users preferences to get the new
           speech, braille and key echo value to take effect. Hide the
           configuration window.

        Arguments:
        - widget: the component that generated the signal.
        """

        msg = "PREFERENCES DIALOG: OK button clicked"
        debug.printMessage(debug.LEVEL_ALL, msg, True)

        self.applyButtonClicked(widget)
        self._cleanupSpeechServers()
        self.get_widget("cthulhuSetupWindow").destroy()

        msg = "PREFERENCES DIALOG: Handling OK button click complete"
        debug.printMessage(debug.LEVEL_ALL, msg, True)

    def windowClosed(self, widget):
        """Signal handler for the "closed" signal for the cthulhuSetupWindow
           GtkWindow widget. This is effectively the same as pressing the
           cancel button, except the window is destroyed for us.

        Arguments:
        - widget: the component that generated the signal.
        """

        msg = "PREFERENCES DIALOG: Window is being closed"
        debug.printMessage(debug.LEVEL_ALL, msg, True)

        self.suspendEvents()

        factory = cthulhu.cthulhuApp.settingsManager.getSetting('speechServerFactory')
        if factory:
            self._setSpeechSystemsChoice(factory)

        server = cthulhu.cthulhuApp.settingsManager.getSetting('speechServerInfo')
        if server:
            self._setSpeechServersChoice(server)

        self._cleanupSpeechServers()
        self.restoreSettings()

        GObject.timeout_add(1000, self.resumeEvents)

        msg = "PREFERENCES DIALOG: Window closure complete"
        debug.printMessage(debug.LEVEL_ALL, msg, True)

    def windowDestroyed(self, widget):
        """Signal handler for the "destroyed" signal for the cthulhuSetupWindow
           GtkWindow widget. Reset cthulhu_state.cthulhuOS to None, so that the 
           GUI can be rebuilt from the GtkBuilder file the next time the user
           wants to display the configuration GUI.

        Arguments:
        - widget: the component that generated the signal.
        """

        msg = "PREFERENCES DIALOG: Window is being destroyed"
        debug.printMessage(debug.LEVEL_ALL, msg, True)

        self.keyBindView.set_model(None)
        self.getTextAttributesView.set_model(None)
        self.pronunciationView.set_model(None)
        self.keyBindView.set_headers_visible(False)
        self.getTextAttributesView.set_headers_visible(False)
        self.pronunciationView.set_headers_visible(False)
        self.keyBindView.hide()
        self.getTextAttributesView.hide()
        self.pronunciationView.hide()
        cthulhu_state.cthulhuOS = None

        msg = "PREFERENCES DIALOG: Window destruction complete"
        debug.printMessage(debug.LEVEL_ALL, msg, True)

    def resumeEvents(self):
        msg = "PREFERENCES DIALOG: Re-registering floody events."
        debug.printMessage(debug.LEVEL_ALL, msg, True)

        manager = event_manager.getManager()
        manager.registerListener("object:state-changed:showing")
        manager.registerListener("object:children-changed:remove")
        manager.registerListener("object:selection-changed")
        manager.registerListener("object:property-change:accessible-name")

    def suspendEvents(self):
        msg = "PREFERENCES DIALOG: Deregistering floody events."
        debug.printMessage(debug.LEVEL_ALL, msg, True)

        manager = event_manager.getManager()
        manager.deregisterListener("object:state-changed:showing")
        manager.deregisterListener("object:children-changed:remove")
        manager.deregisterListener("object:selection-changed")
        manager.deregisterListener("object:property-change:accessible-name")

    def showProfileGUI(self, widget):
        """Show profile Dialog to add a new one"""

        cthulhu_gui_profile.showProfileUI(self)

    def saveProfile(self, profileToSaveLabel):
        """Creates a new profile based on the name profileToSaveLabel and
        updates the Preferences dialog combo boxes accordingly."""

        if not profileToSaveLabel:
            return
        profileToSave = profileToSaveLabel.replace(' ', '_').lower()
        profile = [profileToSaveLabel, profileToSave]

        def saveActiveProfile(newProfile = True):
            if newProfile:
                activeProfileIter = self.profilesComboModel.append(profile)
                self.profilesCombo.set_active_iter(activeProfileIter)

            self.prefsDict['profile'] = profile
            self.prefsDict['activeProfile'] = profile
            self.saveBasicSettings()
            self.writeUserPreferences()

        availableProfiles = [p[1] for p in self.__getAvailableProfiles()]
        if isinstance(profileToSave, str) \
                and profileToSave != '' \
                and profileToSave not in availableProfiles \
                and profileToSave != self._defaultProfile[1]:
            saveActiveProfile()
        else:
            if profileToSave is not None:
                message = guilabels.PROFILE_CONFLICT_MESSAGE % \
                    f"<b>{GLib.markup_escape_text(profileToSaveLabel)}</b>"
                dialog = Gtk.MessageDialog(None,
                        Gtk.DialogFlags.MODAL,
                        type=Gtk.MessageType.INFO,
                        buttons=Gtk.ButtonsType.YES_NO)
                dialog.set_markup(f"<b>{guilabels.PROFILE_CONFLICT_LABEL}</b>")
                dialog.format_secondary_markup(message)
                dialog.set_title(guilabels.PROFILE_CONFLICT_TITLE)
                response = dialog.run()
                if response == Gtk.ResponseType.YES:
                    dialog.destroy()
                    saveActiveProfile(False)
                else:
                    dialog.destroy()


    def removeProfileButtonClicked(self, widget):
        """Remove profile button clicked handler

        If we removed the last profile, a default one will automatically get
        added back by the settings manager.
        """

        oldProfile = self.getComboBoxList(self.profilesCombo)

        message = guilabels.PROFILE_REMOVE_MESSAGE % \
            f"<b>{GLib.markup_escape_text(oldProfile[0])}</b>"
        dialog = Gtk.MessageDialog(self.window, Gtk.DialogFlags.MODAL,
                                   type=Gtk.MessageType.INFO,
                                   buttons=Gtk.ButtonsType.YES_NO)
        dialog.set_markup(f"<b>{guilabels.PROFILE_REMOVE_LABEL}</b>")
        dialog.format_secondary_markup(message)
        if dialog.run() == Gtk.ResponseType.YES:
            # If we remove the currently used starting profile, fallback on
            # the first listed profile, or the default one if there's
            # nothing better
            newStartingProfile = self.prefsDict.get('startingProfile')
            if not newStartingProfile or newStartingProfile == oldProfile:
                newStartingProfile = self._defaultProfile
                for row in self.profilesComboModel:
                    rowProfile = row[:]
                    if rowProfile != oldProfile:
                        newStartingProfile = rowProfile
                        break
            # Update the current profile to the active profile unless we're
            # removing that one, in which case we use the new starting
            # profile
            newProfile = self.prefsDict.get('activeProfile')
            if not newProfile or newProfile == oldProfile:
                newProfile = newStartingProfile

            cthulhu.cthulhuApp.settingsManager.removeProfile(oldProfile[1])
            self.loadProfile(newProfile)

            # Make sure nothing is referencing the removed profile anymore
            startingProfile = self.prefsDict.get('startingProfile')
            if not startingProfile or startingProfile == oldProfile:
                self.prefsDict['startingProfile'] = newStartingProfile
                cthulhu.cthulhuApp.settingsManager.setStartingProfile(newStartingProfile)
                self.writeUserPreferences()

        dialog.destroy()

    def loadProfileButtonClicked(self, widget):
        """Load profile button clicked handler"""

        if self._isInitialSetup:
            return

        dialog = Gtk.MessageDialog(None,
                Gtk.DialogFlags.MODAL,
                type=Gtk.MessageType.INFO,
                buttons=Gtk.ButtonsType.YES_NO)

        dialog.set_markup(f"<b>{guilabels.PROFILE_LOAD_LABEL}</b>")
        dialog.format_secondary_markup(guilabels.PROFILE_LOAD_MESSAGE)
        response = dialog.run()
        if response == Gtk.ResponseType.YES:
            dialog.destroy()
            self.loadSelectedProfile()
        else:
            dialog.destroy()

    def loadSelectedProfile(self):
        """Load selected profile"""

        activeProfile = self.getComboBoxList(self.profilesCombo)
        self.loadProfile(activeProfile)

    def loadProfile(self, profile):
        """Load profile"""

        self.saveBasicSettings()

        self.prefsDict['activeProfile'] = profile
        cthulhu.cthulhuApp.settingsManager.setProfile(profile[1])
        self.prefsDict = cthulhu.cthulhuApp.settingsManager.getGeneralSettings(profile[1])

        cthulhu.loadUserSettings(skipReloadMessage=True)

        self._initGUIState()
        self._initSoundThemeState()

        braille.checkBrailleSetting()

        self._initSpeechState()
        self._initEchoSpeechState()

        self._populateKeyBindings()

        self.__initProfileCombo()
        
    # Indentation signal handlers

    def enableIndentationToggled(self, widget):
        """Enable indentation checkbox toggled handler."""
        enabled = widget.get_active()
        self.prefsDict["enableIndentation"] = enabled
        self._updateIndentationControlsState(enabled)

    def indentationModeToggled(self, widget):
        """Indentation mode radio toggled handler."""
        if not widget.get_active():
            return

        mapping = {
            "indentationSpeechButton": settings.INDENTATION_PRESENTATION_SPEECH,
            "indentationBeepsButton": settings.INDENTATION_PRESENTATION_BEEPS,
            "indentationSpeechAndBeepsButton": settings.INDENTATION_PRESENTATION_SPEECH_AND_BEEPS,
        }
        widgetName = Gtk.Buildable.get_name(widget)
        mode = mapping.get(widgetName)
        if mode is not None:
            self.prefsDict["indentationPresentationMode"] = mode

    # AI Assistant signal handlers
    
    def enableAIToggled(self, widget):
        """Enable AI Assistant checkbox toggled handler"""
        enabled = widget.get_active()
        self.prefsDict["aiAssistantEnabled"] = enabled
        self._updateAIControlsState(enabled)
    
    def aiProviderChanged(self, widget):
        """AI Provider combo box changed handler"""
        providers = [
            settings.AI_PROVIDER_CLAUDE_CODE,
            settings.AI_PROVIDER_CODEX,
            settings.AI_PROVIDER_GEMINI,
            settings.AI_PROVIDER_OLLAMA
        ]
        activeIndex = widget.get_active()
        if 0 <= activeIndex < len(providers):
            provider = providers[activeIndex]
            self.prefsDict["aiProvider"] = provider
            self._updateProviderControls(provider)
    
    def aiApiKeyChanged(self, widget):
        """AI API key file entry changed handler"""
        self.prefsDict["aiApiKeyFile"] = widget.get_text()
    
    def aiOllamaModelChanged(self, widget):
        """AI Ollama model entry changed handler"""
        self.prefsDict["aiOllamaModel"] = widget.get_text()
    
    def aiOllamaEndpointChanged(self, widget):
        """AI Ollama endpoint entry changed handler"""
        self.prefsDict["aiOllamaEndpoint"] = widget.get_text()
    
    def aiGetClaudeKeyClicked(self, widget):
        """Get Claude API Key button clicked handler"""
        import subprocess
        import os
        
        try:
            # Open browser to Claude API key page
            subprocess.run(["xdg-open", "https://console.anthropic.com/"], check=True)
            
            # Show dialog with instructions
            dialog = Gtk.MessageDialog(
                parent=self,
                flags=Gtk.DialogFlags.MODAL,
                type=Gtk.MessageType.INFO,
                buttons=Gtk.ButtonsType.OK,
                message_format="Claude API Key Setup"
            )
            dialog.format_secondary_text(
                "Browser opened to get your Claude API key.\n\n"
                "💡 TIP: Claude offers $5 free credit, then ~$20/month for Pro.\n"
                "Ollama is also available as a free alternative.\n\n"
                "Steps:\n"
                "1. Sign up or log in to your Anthropic account\n"
                "2. Go to 'API Keys' in Account Settings\n"
                "3. Click 'Create Key' and copy the API key\n"
                "4. Click OK below when you have your key ready\n"
                "5. Paste the API key when prompted"
            )
            dialog.run()
            dialog.destroy()
            
            # Show API key input dialog
            key_dialog = Gtk.MessageDialog(
                parent=self,
                flags=Gtk.DialogFlags.MODAL,
                type=Gtk.MessageType.QUESTION,
                buttons=Gtk.ButtonsType.OK_CANCEL,
                message_format="Enter Claude API Key"
            )
            key_dialog.format_secondary_text("Paste your Claude API key (starts with 'sk-ant-'):")
            
            # Add text entry to dialog
            entry = Gtk.Entry()
            entry.set_placeholder_text("sk-ant-your-api-key-here...")
            entry.set_visibility(False)  # Hide key for security
            entry.set_width_chars(50)
            key_dialog.get_content_area().pack_start(entry, False, False, 0)
            entry.show()
            
            response = key_dialog.run()
            api_key = entry.get_text().strip()
            key_dialog.destroy()
            
            if response == Gtk.ResponseType.OK and api_key:
                # Save API key to file
                config_dir = os.path.expanduser("~/.local/share/cthulhu")
                os.makedirs(config_dir, exist_ok=True)
                api_key_file = os.path.join(config_dir, "claude-api-key")
                
                with open(api_key_file, 'w') as f:
                    f.write(api_key)
                os.chmod(api_key_file, 0o600)  # Secure file permissions
                
                # Update GUI
                self.get_widget("aiApiKeyEntry").set_text(api_key_file)
                self.prefsDict["aiApiKeyFile"] = api_key_file
                
                # Success message
                success_dialog = Gtk.MessageDialog(
                    parent=self,
                    flags=Gtk.DialogFlags.MODAL,
                    type=Gtk.MessageType.INFO,
                    buttons=Gtk.ButtonsType.OK,
                    message_format="API Key Saved Successfully"
                )
                success_dialog.format_secondary_text(f"Claude API key saved to:\n{api_key_file}")
                success_dialog.run()
                success_dialog.destroy()
                
        except Exception as e:
            # Error dialog
            error_dialog = Gtk.MessageDialog(
                parent=self,
                flags=Gtk.DialogFlags.MODAL,
                type=Gtk.MessageType.ERROR,
                buttons=Gtk.ButtonsType.OK,
                message_format="Error Setting Up API Key"
            )
            error_dialog.format_secondary_text(f"Failed to open browser or save API key:\n{str(e)}")
            error_dialog.run()
            error_dialog.destroy()
    
    def aiApiKeyBrowseClicked(self, widget):
        """AI API key browse button clicked handler"""
        dialog = Gtk.FileChooserDialog(
            title="Select API Key File",
            parent=self,
            action=Gtk.FileChooserAction.OPEN
        )
        dialog.add_buttons(
            Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
            Gtk.STOCK_OPEN, Gtk.ResponseType.OK
        )
        
        response = dialog.run()
        if response == Gtk.ResponseType.OK:
            filename = dialog.get_filename()
            self.aiApiKeyEntry.set_text(filename)
            self.prefsDict["aiApiKeyFile"] = filename
        
        dialog.destroy()
    
    def aiConfirmationToggled(self, widget):
        """AI confirmation required checkbox toggled handler"""
        self.prefsDict["aiConfirmationRequired"] = widget.get_active()
    
    def aiScreenshotQualityChanged(self, widget):
        """AI screenshot quality combo box changed handler"""
        qualities = [settings.AI_SCREENSHOT_QUALITY_LOW, 
                    settings.AI_SCREENSHOT_QUALITY_MEDIUM,
                    settings.AI_SCREENSHOT_QUALITY_HIGH]
        activeIndex = widget.get_active()
        if 0 <= activeIndex < len(qualities):
            self.prefsDict["aiScreenshotQuality"] = qualities[activeIndex]
    
    # OCR Plugin Settings Handlers
    def ocrLanguageChanged(self, widget):
        """OCR language code entry changed handler"""
        self.prefsDict["ocrLanguageCode"] = widget.get_text()
    
    def ocrScaleChanged(self, widget):
        """OCR scale factor spin button changed handler"""
        self.prefsDict["ocrScaleFactor"] = int(widget.get_value())
    
    def ocrGrayscaleToggled(self, widget):
        """OCR grayscale image checkbox toggled handler"""
        self.prefsDict["ocrGrayscaleImg"] = widget.get_active()
    
    def ocrInvertToggled(self, widget):
        """OCR invert image checkbox toggled handler"""
        self.prefsDict["ocrInvertImg"] = widget.get_active()
    
    def ocrBlackWhiteToggled(self, widget):
        """OCR black and white image checkbox toggled handler"""
        self.prefsDict["ocrBlackWhiteImg"] = widget.get_active()
    
    def ocrBlackWhiteValueChanged(self, widget):
        """OCR black/white threshold spin button changed handler"""
        self.prefsDict["ocrBlackWhiteImgValue"] = int(widget.get_value())
    
    def ocrColorCalculationToggled(self, widget):
        """OCR color calculation checkbox toggled handler"""
        self.prefsDict["ocrColorCalculation"] = widget.get_active()
    
    def ocrCopyToClipboardToggled(self, widget):
        """OCR copy to clipboard checkbox toggled handler"""
        self.prefsDict["ocrCopyToClipboard"] = widget.get_active()
    
