Merge branch 'testing'
This commit is contained in:
@@ -17,7 +17,6 @@ This package requires the following packages, all available from SlackBuilds.org
|
||||
- at-spi2-core
|
||||
- brltty
|
||||
- gobject-introspection
|
||||
- gsettings-desktop-schemas
|
||||
- gstreamer
|
||||
- gst-plugins-base
|
||||
- gst-plugins-good
|
||||
|
||||
@@ -5,6 +5,6 @@ DOWNLOAD="https://git.stormux.org/storm/cthulhu.git"
|
||||
MD5SUM="SKIP"
|
||||
DOWNLOAD_x86_64=""
|
||||
MD5SUM_x86_64=""
|
||||
REQUIRES="at-spi2-core brltty gobject-introspection gsettings-desktop-schemas gstreamer gst-plugins-base gst-plugins-good gtk3 liblouis libwnck3 python3-atspi python3-cairo python3-gobject python3-setproctitle speech-dispatcher"
|
||||
REQUIRES="at-spi2-core brltty gobject-introspection gstreamer gst-plugins-base gst-plugins-good gtk3 liblouis libwnck3 python3-atspi python3-cairo python3-gobject python3-setproctitle speech-dispatcher"
|
||||
MAINTAINER="Storm Dragon"
|
||||
EMAIL="storm_dragon@stormux.org"
|
||||
|
||||
+9
-11
@@ -322,17 +322,15 @@ originated as a community effort led by the Sun Microsystems Inc.
|
||||
Accessibility Program Office and with contributions from many community members.
|
||||
.SH SEE ALSO
|
||||
For more information please visit
|
||||
.B cthulhu
|
||||
wiki at
|
||||
.UR http://live.gnome.org/Cthulhu
|
||||
<http://live.gnome.org/Cthulhu>
|
||||
.B Cthulhu
|
||||
at
|
||||
.UR https://git.stormux.org/storm/cthulhu
|
||||
<https://git.stormux.org/storm/cthulhu>
|
||||
.UE
|
||||
.P
|
||||
The
|
||||
.B cthulhu
|
||||
mailing list
|
||||
.UR http://mail.gnome.org/mailman/listinfo/cthulhu-list
|
||||
<http://mail.gnome.org/mailman/listinfo/cthulhu-list>
|
||||
To post a message to all
|
||||
.B cthulhu
|
||||
list, send a email to https://groups.io/g/stormux
|
||||
.B Stormux
|
||||
community list is available at
|
||||
.UR https://groups.io/g/stormux
|
||||
<https://groups.io/g/stormux>
|
||||
.UE
|
||||
|
||||
@@ -109,6 +109,111 @@ class Backend:
|
||||
if key not in targetTable:
|
||||
targetTable[key] = newValue
|
||||
|
||||
def _legacyProfileValue(self, profileTable, sectionName, legacyKey):
|
||||
section = profileTable.get(sectionName)
|
||||
if not isinstance(section, dict):
|
||||
return None
|
||||
return section.get(legacyKey)
|
||||
|
||||
def _normalizeLegacyValue(self, currentKey, legacyValue):
|
||||
if currentKey == 'keyboardLayout' and isinstance(legacyValue, str):
|
||||
layout = legacyValue.strip().lower()
|
||||
if layout == 'desktop':
|
||||
return settings.GENERAL_KEYBOARD_LAYOUT_DESKTOP
|
||||
if layout == 'laptop':
|
||||
return settings.GENERAL_KEYBOARD_LAYOUT_LAPTOP
|
||||
return legacyValue
|
||||
|
||||
def _normalizeLegacyProfile(self, profileName, profileTable):
|
||||
if not isinstance(profileTable, dict):
|
||||
return profileTable
|
||||
|
||||
profileSettings = {}
|
||||
legacySections = {
|
||||
'metadata',
|
||||
'plugins',
|
||||
'ai-assistant',
|
||||
'ocr',
|
||||
}
|
||||
|
||||
for key, value in profileTable.items():
|
||||
if key in legacySections:
|
||||
continue
|
||||
if key == 'keybindings':
|
||||
if isinstance(value, dict):
|
||||
keybindings = dict(value)
|
||||
keybindings.pop('keyboard-layout', None)
|
||||
keybindings.pop('desktop-modifier-keys', None)
|
||||
keybindings.pop('laptop-modifier-keys', None)
|
||||
if keybindings:
|
||||
profileSettings[key] = keybindings
|
||||
continue
|
||||
profileSettings[key] = value
|
||||
|
||||
displayName = self._legacyProfileValue(profileTable, 'metadata', 'display-name')
|
||||
internalName = self._legacyProfileValue(profileTable, 'metadata', 'internal-name')
|
||||
if 'profile' not in profileSettings and (displayName or internalName):
|
||||
profileSettings['profile'] = [
|
||||
displayName or str(profileName).title(),
|
||||
internalName or profileName,
|
||||
]
|
||||
|
||||
legacyKeyMap = {
|
||||
('keybindings', 'keyboard-layout'): 'keyboardLayout',
|
||||
('keybindings', 'desktop-modifier-keys'): 'cthulhuModifierKeys',
|
||||
('keybindings', 'laptop-modifier-keys'): 'cthulhuModifierKeys',
|
||||
('plugins', 'active-plugins'): 'activePlugins',
|
||||
('plugins', 'plugin-sources'): 'pluginSources',
|
||||
('ai-assistant', 'enabled'): 'aiAssistantEnabled',
|
||||
('ai-assistant', 'provider'): 'aiProvider',
|
||||
('ai-assistant', 'api-key-file'): 'aiApiKeyFile',
|
||||
('ai-assistant', 'ollama-model'): 'aiOllamaModel',
|
||||
('ai-assistant', 'ollama-endpoint'): 'aiOllamaEndpoint',
|
||||
('ai-assistant', 'confirmation-required'): 'aiConfirmationRequired',
|
||||
('ai-assistant', 'action-timeout'): 'aiActionTimeout',
|
||||
('ai-assistant', 'screenshot-quality'): 'aiScreenshotQuality',
|
||||
('ai-assistant', 'max-context-length'): 'aiMaxContextLength',
|
||||
('ocr', 'language-code'): 'ocrLanguageCode',
|
||||
('ocr', 'scale-factor'): 'ocrScaleFactor',
|
||||
('ocr', 'grayscale-image'): 'ocrGrayscaleImg',
|
||||
('ocr', 'invert-image'): 'ocrInvertImg',
|
||||
('ocr', 'black-white-image'): 'ocrBlackWhiteImg',
|
||||
('ocr', 'black-white-threshold'): 'ocrBlackWhiteImgValue',
|
||||
('ocr', 'color-calculation'): 'ocrColorCalculation',
|
||||
('ocr', 'color-calculation-max'): 'ocrColorCalculationMax',
|
||||
('ocr', 'copy-to-clipboard'): 'ocrCopyToClipboard',
|
||||
}
|
||||
|
||||
for (sectionName, legacyKey), currentKey in legacyKeyMap.items():
|
||||
if currentKey in profileSettings:
|
||||
continue
|
||||
legacyValue = self._legacyProfileValue(profileTable, sectionName, legacyKey)
|
||||
if legacyValue is not None:
|
||||
profileSettings[currentKey] = self._normalizeLegacyValue(currentKey, legacyValue)
|
||||
|
||||
return profileSettings
|
||||
|
||||
def _normalizeProfiles(self, profiles):
|
||||
if not isinstance(profiles, dict):
|
||||
return {}
|
||||
return {
|
||||
profileName: self._normalizeLegacyProfile(profileName, profileTable)
|
||||
for profileName, profileTable in profiles.items()
|
||||
}
|
||||
|
||||
def _normalizeProfilesDocument(self, prefsDoc):
|
||||
profiles = prefsDoc.get('profiles')
|
||||
if not isinstance(profiles, dict):
|
||||
return
|
||||
|
||||
for profileName in list(profiles.keys()):
|
||||
profileTable = profiles[profileName]
|
||||
normalizedProfile = self._normalizeLegacyProfile(profileName, profileTable)
|
||||
profiles[profileName] = self._stripNone(normalizedProfile)
|
||||
|
||||
if 'format-version' in prefsDoc:
|
||||
del prefsDoc['format-version']
|
||||
|
||||
def saveDefaultSettings(self, general, pronunciations, keybindings):
|
||||
""" Save default settings for all the properties from
|
||||
cthulhu.settings. """
|
||||
@@ -167,6 +272,7 @@ class Backend:
|
||||
general = self._stripNone(general)
|
||||
|
||||
prefsDoc = self._readDocument(self.settingsFile)
|
||||
self._normalizeProfilesDocument(prefsDoc)
|
||||
profiles = prefsDoc.get('profiles')
|
||||
if profiles is None or not isinstance(profiles, dict):
|
||||
prefsDoc['profiles'] = {}
|
||||
@@ -192,7 +298,7 @@ class Backend:
|
||||
self.general = dict(prefsDoc.get('general', {}))
|
||||
self.pronunciations = dict(prefsDoc.get('pronunciations', {}))
|
||||
self.keybindings = dict(prefsDoc.get('keybindings', {}))
|
||||
self.profiles = dict(prefsDoc.get('profiles', {}))
|
||||
self.profiles = self._normalizeProfiles(dict(prefsDoc.get('profiles', {})))
|
||||
except Exception:
|
||||
return
|
||||
|
||||
|
||||
@@ -178,7 +178,7 @@ class BrailleGenerator(generator.Generator):
|
||||
Atspi.Role.EXTENDED,
|
||||
Atspi.Role.LINK]
|
||||
|
||||
# egg-list-box, e.g. privacy panel in gnome-control-center
|
||||
# egg-list-box-style containers can expose selected panels as list items.
|
||||
if AXUtilities.is_list_box(AXObject.get_parent(obj)):
|
||||
doNotPresent.append(AXObject.get_role(obj))
|
||||
|
||||
|
||||
@@ -42,7 +42,6 @@ from . import dbus_service
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from types import FrameType
|
||||
from gi.repository.Gio import Settings as GSettings
|
||||
from gi.repository import Gtk
|
||||
|
||||
from .settings_manager import SettingsManager
|
||||
@@ -250,12 +249,6 @@ from gi.repository import Atspi
|
||||
from gi.repository import Gdk
|
||||
from gi.repository import GObject
|
||||
|
||||
try:
|
||||
from gi.repository.Gio import Settings
|
||||
a11yAppSettings: Optional[GSettings] = Settings(schema_id='org.gnome.desktop.a11y.applications')
|
||||
except Exception:
|
||||
a11yAppSettings = None
|
||||
|
||||
from . import braille
|
||||
from . import debug
|
||||
from . import event_manager
|
||||
@@ -299,15 +292,6 @@ from . import resource_manager
|
||||
|
||||
# Old global variables removed - now using cthulhuApp.* instead
|
||||
|
||||
def onEnabledChanged(gsetting: GSettings, key: str) -> None:
|
||||
try:
|
||||
enabled: bool = gsetting.get_boolean(key)
|
||||
except Exception:
|
||||
return
|
||||
|
||||
if key == 'screen-reader-enabled' and not enabled:
|
||||
shutdown()
|
||||
|
||||
EXIT_CODE_HANG: int = 50
|
||||
|
||||
# The user-settings module (see loadUserSettings).
|
||||
@@ -651,12 +635,6 @@ def init() -> bool:
|
||||
signal.alarm(0)
|
||||
|
||||
_initialized = True
|
||||
# In theory, we can do this through dbus. In practice, it fails to
|
||||
# work sometimes. Until we know why, we need to leave this as-is
|
||||
# so that we respond when gnome-control-center is used to stop Cthulhu.
|
||||
if a11yAppSettings:
|
||||
a11yAppSettings.connect('changed', onEnabledChanged)
|
||||
|
||||
debug.printMessage(debug.LEVEL_INFO, 'CTHULHU: Initialized', True)
|
||||
|
||||
return True
|
||||
|
||||
@@ -4984,12 +4984,10 @@ print(json.dumps(result))
|
||||
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. If GNOME accessibility hadn't
|
||||
previously been enabled, warn the user that they will need to
|
||||
log out. 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.
|
||||
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.
|
||||
@@ -5042,11 +5040,10 @@ print(json.dumps(result))
|
||||
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. If GNOME accessibility hadn't
|
||||
previously been enabled, warn the user that they will need to
|
||||
log out. 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.
|
||||
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.
|
||||
|
||||
@@ -1525,6 +1525,106 @@ class Script(script.Script):
|
||||
for character in itemString:
|
||||
self.speakCharacter(character)
|
||||
|
||||
def _diagnostic_object_summary(self, obj):
|
||||
if obj is None:
|
||||
return "None"
|
||||
|
||||
try:
|
||||
if AXObject.is_dead(obj):
|
||||
return "dead"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
name = AXObject.get_name(obj) or ""
|
||||
except Exception:
|
||||
name = "<error>"
|
||||
|
||||
try:
|
||||
role = AXObject.get_role_name(obj) or ""
|
||||
except Exception:
|
||||
role = "<error>"
|
||||
|
||||
try:
|
||||
pid = AXObject.get_process_id(obj)
|
||||
except Exception:
|
||||
pid = "<error>"
|
||||
|
||||
return f"name={name!r}, role={role!r}, pid={pid}"
|
||||
|
||||
def _diagnostic_callable_value(self, obj, methodName):
|
||||
method = getattr(obj, methodName, None)
|
||||
if not callable(method):
|
||||
return "<unavailable>"
|
||||
try:
|
||||
return method()
|
||||
except Exception as error:
|
||||
return f"<error: {error}>"
|
||||
|
||||
@dbus_service.command
|
||||
def getDiagnosticState(self, script=None, event=None, notify_user=True) -> str:
|
||||
"""Dumps runtime state useful for diagnosing sluggish web-app behavior."""
|
||||
|
||||
app = cthulhu.cthulhuApp
|
||||
activeScript = cthulhu_state.activeScript
|
||||
eventManager = getattr(app, "eventManager", None)
|
||||
compositor = getattr(app, "compositorStateAdapter", None)
|
||||
inputManager = getattr(eventManager, "_inputEventManager", None)
|
||||
|
||||
lines = [
|
||||
f"timestamp={time.strftime('%Y-%m-%d %H:%M:%S')}",
|
||||
f"default-script={self.__class__.__module__}.{self.__class__.__name__}",
|
||||
f"active-script={activeScript.__class__.__module__}.{activeScript.__class__.__name__}" if activeScript else "active-script=None",
|
||||
f"active-window={self._diagnostic_object_summary(cthulhu_state.activeWindow)}",
|
||||
f"locus-of-focus={self._diagnostic_object_summary(cthulhu_state.locusOfFocus)}",
|
||||
f"pending-self-hosted-focus={self._diagnostic_object_summary(getattr(cthulhu_state, 'pendingSelfHostedFocus', None))}",
|
||||
]
|
||||
|
||||
if eventManager is not None:
|
||||
eventQueue = getattr(eventManager, "_eventQueue", None)
|
||||
try:
|
||||
queueSize = eventQueue.qsize() if eventQueue is not None else "<unavailable>"
|
||||
except Exception as error:
|
||||
queueSize = f"<error: {error}>"
|
||||
prioritizedEvent = getattr(eventManager, "_prioritizedEvent", None)
|
||||
lines.extend([
|
||||
f"event-manager-active={getattr(eventManager, '_active', '<unavailable>')}",
|
||||
f"event-queue-size={queueSize}",
|
||||
f"events-suspended={getattr(eventManager, '_eventsSuspended', '<unavailable>')}",
|
||||
f"churn-suppressed={getattr(eventManager, '_churnSuppressed', '<unavailable>')}",
|
||||
f"state-pause-atspi-churn={cthulhu_state.pauseAtspiChurn}",
|
||||
f"prioritized-context-token={getattr(eventManager, '_prioritizedContextToken', None)}",
|
||||
f"state-prioritized-context-token={cthulhu_state.prioritizedDesktopContextToken}",
|
||||
f"prioritized-event-type={getattr(prioritizedEvent, 'type', None)}",
|
||||
f"gidle-id={getattr(eventManager, '_gidleId', '<unavailable>')}",
|
||||
f"prioritized-idle-id={getattr(eventManager, '_prioritizedIdleId', '<unavailable>')}",
|
||||
])
|
||||
|
||||
if inputManager is not None:
|
||||
lines.extend([
|
||||
f"key-handling-active={getattr(eventManager, '_keyHandlingActive', '<unavailable>')}",
|
||||
f"input-manager-device={getattr(inputManager, '_device', None)}",
|
||||
f"input-manager-watcher={getattr(inputManager, '_keyWatcher', None)}",
|
||||
])
|
||||
|
||||
if compositor is not None:
|
||||
snapshot = self._diagnostic_callable_value(compositor, "get_snapshot")
|
||||
lines.append(f"compositor-snapshot={snapshot}")
|
||||
|
||||
if activeScript is not None:
|
||||
lines.extend([
|
||||
f"active-script-focus-mode={self._diagnostic_callable_value(activeScript, 'inFocusMode')}",
|
||||
f"active-script-focus-sticky={self._diagnostic_callable_value(activeScript, 'focusModeIsSticky')}",
|
||||
f"active-script-browse-sticky={self._diagnostic_callable_value(activeScript, 'browseModeIsSticky')}",
|
||||
f"active-script-structural-navigation={getattr(getattr(activeScript, 'structuralNavigation', None), 'enabled', '<unavailable>')}",
|
||||
])
|
||||
|
||||
report = "\n".join(lines)
|
||||
debug.printMessage(debug.LEVEL_INFO, "CTHULHU DIAGNOSTIC STATE:\n" + report, True)
|
||||
if notify_user and script is not None:
|
||||
script.presentMessage("Cthulhu diagnostic state written to debug log.")
|
||||
return report
|
||||
|
||||
@dbus_service.command
|
||||
def sayAll(self, inputEvent, obj=None, offset=None, notify_user=True):
|
||||
"""Speaks the entire document or text, starting from the current position."""
|
||||
|
||||
@@ -462,8 +462,7 @@ class SettingsManager(object):
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
|
||||
def _enableAccessibility(self) -> bool:
|
||||
"""Enables the GNOME accessibility flag. Users need to log out and
|
||||
then back in for this to take effect.
|
||||
"""Enables the desktop accessibility bus flag when available.
|
||||
|
||||
Returns True if an action was taken (i.e., accessibility was not
|
||||
set prior to this call).
|
||||
|
||||
@@ -1,532 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright (c) 2025 Stormux
|
||||
# Copyright (c) 2025 Igalia, S.L.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Enhanced speech settings management for D-Bus remote controller."""
|
||||
|
||||
__id__ = "$Id$"
|
||||
__version__ = "$Revision$"
|
||||
__date__ = "$Date$"
|
||||
__copyright__ = "Copyright (c) 2025 Stormux"
|
||||
__license__ = "LGPL"
|
||||
|
||||
from . import cthulhu_state
|
||||
from . import debug
|
||||
from . import dbus_service
|
||||
from . import messages
|
||||
from . import settings
|
||||
from . import settings_manager
|
||||
|
||||
class SpeechDBusManager:
|
||||
"""Enhanced speech settings for D-Bus remote control."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the speech D-Bus manager."""
|
||||
self._settings_manager = settings_manager.getManager()
|
||||
|
||||
@dbus_service.getter
|
||||
def get_verbosity_level(self) -> str:
|
||||
"""Returns the current speech verbosity level."""
|
||||
|
||||
level = self._settings_manager.getSetting("speechVerbosityLevel")
|
||||
if level == settings.VERBOSITY_LEVEL_BRIEF:
|
||||
return "brief"
|
||||
else:
|
||||
return "verbose"
|
||||
|
||||
@dbus_service.setter
|
||||
def set_verbosity_level(self, value: str) -> bool:
|
||||
"""Sets the speech verbosity level."""
|
||||
|
||||
if value.lower() == "brief":
|
||||
setting_value = settings.VERBOSITY_LEVEL_BRIEF
|
||||
elif value.lower() == "verbose":
|
||||
setting_value = settings.VERBOSITY_LEVEL_VERBOSE
|
||||
else:
|
||||
msg = f"SPEECH DBUS MANAGER: Invalid verbosity level: {value}"
|
||||
debug.printMessage(debug.LEVEL_WARNING, msg, True)
|
||||
return False
|
||||
|
||||
msg = f"SPEECH DBUS MANAGER: Setting verbosity level to {value}."
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
self._settings_manager.setSetting("speechVerbosityLevel", setting_value)
|
||||
return True
|
||||
|
||||
@dbus_service.getter
|
||||
def get_capitalization_style(self) -> str:
|
||||
"""Returns the current capitalization style."""
|
||||
|
||||
style = self._settings_manager.getSetting("capitalizationStyle")
|
||||
if style == settings.CAPITALIZATION_STYLE_NONE:
|
||||
return "none"
|
||||
elif style == settings.CAPITALIZATION_STYLE_SPELL:
|
||||
return "spell"
|
||||
elif style == settings.CAPITALIZATION_STYLE_ICON:
|
||||
return "icon"
|
||||
else:
|
||||
return "none"
|
||||
|
||||
@dbus_service.setter
|
||||
def set_capitalization_style(self, value: str) -> bool:
|
||||
"""Sets the capitalization style."""
|
||||
|
||||
value_lower = value.lower()
|
||||
if value_lower == "none":
|
||||
setting_value = settings.CAPITALIZATION_STYLE_NONE
|
||||
elif value_lower == "spell":
|
||||
setting_value = settings.CAPITALIZATION_STYLE_SPELL
|
||||
elif value_lower == "icon":
|
||||
setting_value = settings.CAPITALIZATION_STYLE_ICON
|
||||
else:
|
||||
msg = f"SPEECH DBUS MANAGER: Invalid capitalization style: {value}"
|
||||
debug.printMessage(debug.LEVEL_WARNING, msg, True)
|
||||
return False
|
||||
|
||||
msg = f"SPEECH DBUS MANAGER: Setting capitalization style to {value}."
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
self._settings_manager.setSetting("capitalizationStyle", setting_value)
|
||||
return True
|
||||
|
||||
@dbus_service.getter
|
||||
def get_punctuation_level(self) -> str:
|
||||
"""Returns the current punctuation level."""
|
||||
|
||||
level = self._settings_manager.getSetting("verbalizePunctuationStyle")
|
||||
if level == settings.PUNCTUATION_STYLE_NONE:
|
||||
return "none"
|
||||
elif level == settings.PUNCTUATION_STYLE_SOME:
|
||||
return "some"
|
||||
elif level == settings.PUNCTUATION_STYLE_MOST:
|
||||
return "most"
|
||||
elif level == settings.PUNCTUATION_STYLE_ALL:
|
||||
return "all"
|
||||
else:
|
||||
return "some"
|
||||
|
||||
@dbus_service.setter
|
||||
def set_punctuation_level(self, value: str) -> bool:
|
||||
"""Sets the punctuation level."""
|
||||
|
||||
value_lower = value.lower()
|
||||
if value_lower == "none":
|
||||
setting_value = settings.PUNCTUATION_STYLE_NONE
|
||||
elif value_lower == "some":
|
||||
setting_value = settings.PUNCTUATION_STYLE_SOME
|
||||
elif value_lower == "most":
|
||||
setting_value = settings.PUNCTUATION_STYLE_MOST
|
||||
elif value_lower == "all":
|
||||
setting_value = settings.PUNCTUATION_STYLE_ALL
|
||||
else:
|
||||
msg = f"SPEECH DBUS MANAGER: Invalid punctuation level: {value}"
|
||||
debug.printMessage(debug.LEVEL_WARNING, msg, True)
|
||||
return False
|
||||
|
||||
msg = f"SPEECH DBUS MANAGER: Setting punctuation level to {value}."
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
self._settings_manager.setSetting("verbalizePunctuationStyle", setting_value)
|
||||
return True
|
||||
|
||||
@dbus_service.getter
|
||||
def get_speak_numbers_as_digits(self) -> bool:
|
||||
"""Returns whether numbers are spoken as digits."""
|
||||
|
||||
return self._settings_manager.getSetting("speakNumbersAsDigits")
|
||||
|
||||
@dbus_service.setter
|
||||
def set_speak_numbers_as_digits(self, value: bool) -> bool:
|
||||
"""Sets whether numbers are spoken as digits."""
|
||||
|
||||
msg = f"SPEECH DBUS MANAGER: Setting speak numbers as digits to {value}."
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
self._settings_manager.setSetting("speakNumbersAsDigits", value)
|
||||
return True
|
||||
|
||||
@dbus_service.getter
|
||||
def get_speech_is_muted(self) -> bool:
|
||||
"""Returns whether speech output is temporarily muted."""
|
||||
|
||||
return self._settings_manager.getSetting("silenceSpeech")
|
||||
|
||||
@dbus_service.setter
|
||||
def set_speech_is_muted(self, value: bool) -> bool:
|
||||
"""Sets whether speech output is temporarily muted."""
|
||||
|
||||
msg = f"SPEECH DBUS MANAGER: Setting speech muted to {value}."
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
self._settings_manager.setSetting("silenceSpeech", value)
|
||||
return True
|
||||
|
||||
@dbus_service.getter
|
||||
def get_only_speak_displayed_text(self) -> bool:
|
||||
"""Returns whether only displayed text should be spoken."""
|
||||
|
||||
return self._settings_manager.getSetting("onlySpeakDisplayedText")
|
||||
|
||||
@dbus_service.setter
|
||||
def set_only_speak_displayed_text(self, value: bool) -> bool:
|
||||
"""Sets whether only displayed text should be spoken."""
|
||||
|
||||
msg = f"SPEECH DBUS MANAGER: Setting only speak displayed text to {value}."
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
self._settings_manager.setSetting("onlySpeakDisplayedText", value)
|
||||
return True
|
||||
|
||||
@dbus_service.getter
|
||||
def get_speak_indentation_and_justification(self) -> bool:
|
||||
"""Returns whether speaking of indentation and justification is enabled."""
|
||||
|
||||
return self._settings_manager.getSetting("enableSpeechIndentation")
|
||||
|
||||
def _sync_indentation_presentation_mode(self, enable_speech):
|
||||
mode = self._settings_manager.getSetting("indentationPresentationMode") \
|
||||
or settings.indentationPresentationMode
|
||||
if enable_speech:
|
||||
if mode == settings.INDENTATION_PRESENTATION_OFF:
|
||||
mode = settings.INDENTATION_PRESENTATION_SPEECH
|
||||
elif mode == settings.INDENTATION_PRESENTATION_BEEPS:
|
||||
mode = settings.INDENTATION_PRESENTATION_SPEECH_AND_BEEPS
|
||||
else:
|
||||
if mode == settings.INDENTATION_PRESENTATION_SPEECH:
|
||||
mode = settings.INDENTATION_PRESENTATION_OFF
|
||||
elif mode == settings.INDENTATION_PRESENTATION_SPEECH_AND_BEEPS:
|
||||
mode = settings.INDENTATION_PRESENTATION_BEEPS
|
||||
|
||||
self._settings_manager.setSetting("indentationPresentationMode", mode)
|
||||
|
||||
@dbus_service.setter
|
||||
def set_speak_indentation_and_justification(self, value: bool) -> bool:
|
||||
"""Sets whether speaking of indentation and justification is enabled."""
|
||||
|
||||
msg = f"SPEECH DBUS MANAGER: Setting speak indentation and justification to {value}."
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
self._settings_manager.setSetting("enableSpeechIndentation", value)
|
||||
self._sync_indentation_presentation_mode(value)
|
||||
return True
|
||||
|
||||
@dbus_service.command
|
||||
def toggle_speech(self, script=None, event=None):
|
||||
"""Toggles speech on and off."""
|
||||
|
||||
tokens = ["SPEECH DBUS MANAGER: toggle_speech. Script:", script, "Event:", event]
|
||||
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||
|
||||
if script is not None:
|
||||
script.presentationInterrupt()
|
||||
|
||||
if self.get_speech_is_muted():
|
||||
self.set_speech_is_muted(False)
|
||||
if script is not None:
|
||||
script.presentMessage(messages.SPEECH_ENABLED)
|
||||
elif not self._settings_manager.getSetting("enableSpeech"):
|
||||
self._settings_manager.setSetting("enableSpeech", True)
|
||||
if script is not None:
|
||||
script.presentMessage(messages.SPEECH_ENABLED)
|
||||
else:
|
||||
if script is not None:
|
||||
script.presentMessage(messages.SPEECH_DISABLED)
|
||||
self.set_speech_is_muted(True)
|
||||
|
||||
@dbus_service.command
|
||||
def toggle_verbosity(self, script=None, event=None):
|
||||
"""Toggles speech verbosity level between verbose and brief."""
|
||||
|
||||
tokens = ["SPEECH DBUS MANAGER: toggle_verbosity. Script:", script, "Event:", event]
|
||||
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||
|
||||
current_level = self._settings_manager.getSetting("speechVerbosityLevel")
|
||||
if current_level == settings.VERBOSITY_LEVEL_BRIEF:
|
||||
if script is not None:
|
||||
script.presentMessage(messages.SPEECH_VERBOSITY_VERBOSE)
|
||||
self._settings_manager.setSetting("speechVerbosityLevel", settings.VERBOSITY_LEVEL_VERBOSE)
|
||||
else:
|
||||
if script is not None:
|
||||
script.presentMessage(messages.SPEECH_VERBOSITY_BRIEF)
|
||||
self._settings_manager.setSetting("speechVerbosityLevel", settings.VERBOSITY_LEVEL_BRIEF)
|
||||
|
||||
@dbus_service.command
|
||||
def change_number_style(self, script=None, event=None):
|
||||
"""Changes spoken number style between digits and words."""
|
||||
|
||||
tokens = ["SPEECH DBUS MANAGER: change_number_style. Script:", script, "Event:", event]
|
||||
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||
|
||||
speak_digits = self.get_speak_numbers_as_digits()
|
||||
if speak_digits:
|
||||
brief = messages.NUMBER_STYLE_WORDS_BRIEF
|
||||
full = messages.NUMBER_STYLE_WORDS_FULL
|
||||
else:
|
||||
brief = messages.NUMBER_STYLE_DIGITS_BRIEF
|
||||
full = messages.NUMBER_STYLE_DIGITS_FULL
|
||||
|
||||
self.set_speak_numbers_as_digits(not speak_digits)
|
||||
if script is not None:
|
||||
script.presentMessage(full, brief)
|
||||
|
||||
@dbus_service.command
|
||||
def say_all(self, script=None, event=None):
|
||||
"""Speaks the entire document or text, starting from the current position."""
|
||||
|
||||
tokens = ["SPEECH DBUS MANAGER: say_all. Script:", script, "Event:", event]
|
||||
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||
|
||||
# Use the current active script if not provided
|
||||
if script is None:
|
||||
script = cthulhu_state.activeScript
|
||||
|
||||
if script is None:
|
||||
msg = "SPEECH DBUS MANAGER: No active script available for Say All"
|
||||
debug.printMessage(debug.LEVEL_WARNING, msg, True)
|
||||
return False
|
||||
|
||||
# Call the script's Say All method
|
||||
try:
|
||||
script.sayAll(event, notify_user=False)
|
||||
return True
|
||||
except Exception as e:
|
||||
msg = f"SPEECH DBUS MANAGER: Error during Say All: {e}"
|
||||
debug.printMessage(debug.LEVEL_SEVERE, msg, True)
|
||||
return False
|
||||
|
||||
# Key Echo Controls
|
||||
@dbus_service.getter
|
||||
def get_key_echo_enabled(self) -> bool:
|
||||
"""Returns whether echo of key presses is enabled."""
|
||||
|
||||
return self._settings_manager.getSetting("enableKeyEcho")
|
||||
|
||||
@dbus_service.setter
|
||||
def set_key_echo_enabled(self, value: bool) -> bool:
|
||||
"""Sets whether echo of key presses is enabled."""
|
||||
|
||||
msg = f"SPEECH DBUS MANAGER: Setting enable key echo to {value}."
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
self._settings_manager.setSetting("enableKeyEcho", value)
|
||||
return True
|
||||
|
||||
@dbus_service.getter
|
||||
def get_character_echo_enabled(self) -> bool:
|
||||
"""Returns whether echo of inserted characters is enabled."""
|
||||
|
||||
return self._settings_manager.getSetting("enableEchoByCharacter")
|
||||
|
||||
@dbus_service.setter
|
||||
def set_character_echo_enabled(self, value: bool) -> bool:
|
||||
"""Sets whether echo of inserted characters is enabled."""
|
||||
|
||||
msg = f"SPEECH DBUS MANAGER: Setting enable character echo to {value}."
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
self._settings_manager.setSetting("enableEchoByCharacter", value)
|
||||
return True
|
||||
|
||||
@dbus_service.getter
|
||||
def get_word_echo_enabled(self) -> bool:
|
||||
"""Returns whether word echo is enabled."""
|
||||
|
||||
return self._settings_manager.getSetting("enableEchoByWord")
|
||||
|
||||
@dbus_service.setter
|
||||
def set_word_echo_enabled(self, value: bool) -> bool:
|
||||
"""Sets whether word echo is enabled."""
|
||||
|
||||
msg = f"SPEECH DBUS MANAGER: Setting enable word echo to {value}."
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
self._settings_manager.setSetting("enableEchoByWord", value)
|
||||
return True
|
||||
|
||||
@dbus_service.getter
|
||||
def get_sentence_echo_enabled(self) -> bool:
|
||||
"""Returns whether sentence echo is enabled."""
|
||||
|
||||
return self._settings_manager.getSetting("enableEchoBySentence")
|
||||
|
||||
@dbus_service.setter
|
||||
def set_sentence_echo_enabled(self, value: bool) -> bool:
|
||||
"""Sets whether sentence echo is enabled."""
|
||||
|
||||
msg = f"SPEECH DBUS MANAGER: Setting enable sentence echo to {value}."
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
self._settings_manager.setSetting("enableEchoBySentence", value)
|
||||
return True
|
||||
|
||||
@dbus_service.getter
|
||||
def get_alphabetic_keys_enabled(self) -> bool:
|
||||
"""Returns whether alphabetic keys will be echoed when key echo is enabled."""
|
||||
|
||||
return self._settings_manager.getSetting("enableAlphabeticKeys")
|
||||
|
||||
@dbus_service.setter
|
||||
def set_alphabetic_keys_enabled(self, value: bool) -> bool:
|
||||
"""Sets whether alphabetic keys will be echoed when key echo is enabled."""
|
||||
|
||||
msg = f"SPEECH DBUS MANAGER: Setting enable alphabetic keys to {value}."
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
self._settings_manager.setSetting("enableAlphabeticKeys", value)
|
||||
return True
|
||||
|
||||
@dbus_service.getter
|
||||
def get_numeric_keys_enabled(self) -> bool:
|
||||
"""Returns whether numeric keys will be echoed when key echo is enabled."""
|
||||
|
||||
return self._settings_manager.getSetting("enableNumericKeys")
|
||||
|
||||
@dbus_service.setter
|
||||
def set_numeric_keys_enabled(self, value: bool) -> bool:
|
||||
"""Sets whether numeric keys will be echoed when key echo is enabled."""
|
||||
|
||||
msg = f"SPEECH DBUS MANAGER: Setting enable numeric keys to {value}."
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
self._settings_manager.setSetting("enableNumericKeys", value)
|
||||
return True
|
||||
|
||||
@dbus_service.getter
|
||||
def get_punctuation_keys_enabled(self) -> bool:
|
||||
"""Returns whether punctuation keys will be echoed when key echo is enabled."""
|
||||
|
||||
return self._settings_manager.getSetting("enablePunctuationKeys")
|
||||
|
||||
@dbus_service.setter
|
||||
def set_punctuation_keys_enabled(self, value: bool) -> bool:
|
||||
"""Sets whether punctuation keys will be echoed when key echo is enabled."""
|
||||
|
||||
msg = f"SPEECH DBUS MANAGER: Setting enable punctuation keys to {value}."
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
self._settings_manager.setSetting("enablePunctuationKeys", value)
|
||||
return True
|
||||
|
||||
@dbus_service.getter
|
||||
def get_space_enabled(self) -> bool:
|
||||
"""Returns whether space key will be echoed when key echo is enabled."""
|
||||
|
||||
return self._settings_manager.getSetting("enableSpace")
|
||||
|
||||
@dbus_service.setter
|
||||
def set_space_enabled(self, value: bool) -> bool:
|
||||
"""Sets whether space key will be echoed when key echo is enabled."""
|
||||
|
||||
msg = f"SPEECH DBUS MANAGER: Setting enable space to {value}."
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
self._settings_manager.setSetting("enableSpace", value)
|
||||
return True
|
||||
|
||||
@dbus_service.getter
|
||||
def get_modifier_keys_enabled(self) -> bool:
|
||||
"""Returns whether modifier keys will be echoed when key echo is enabled."""
|
||||
|
||||
return self._settings_manager.getSetting("enableModifierKeys")
|
||||
|
||||
@dbus_service.setter
|
||||
def set_modifier_keys_enabled(self, value: bool) -> bool:
|
||||
"""Sets whether modifier keys will be echoed when key echo is enabled."""
|
||||
|
||||
msg = f"SPEECH DBUS MANAGER: Setting enable modifier keys to {value}."
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
self._settings_manager.setSetting("enableModifierKeys", value)
|
||||
return True
|
||||
|
||||
@dbus_service.getter
|
||||
def get_function_keys_enabled(self) -> bool:
|
||||
"""Returns whether function keys will be echoed when key echo is enabled."""
|
||||
|
||||
return self._settings_manager.getSetting("enableFunctionKeys")
|
||||
|
||||
@dbus_service.setter
|
||||
def set_function_keys_enabled(self, value: bool) -> bool:
|
||||
"""Sets whether function keys will be echoed when key echo is enabled."""
|
||||
|
||||
msg = f"SPEECH DBUS MANAGER: Setting enable function keys to {value}."
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
self._settings_manager.setSetting("enableFunctionKeys", value)
|
||||
return True
|
||||
|
||||
@dbus_service.getter
|
||||
def get_action_keys_enabled(self) -> bool:
|
||||
"""Returns whether action keys will be echoed when key echo is enabled."""
|
||||
|
||||
return self._settings_manager.getSetting("enableActionKeys")
|
||||
|
||||
@dbus_service.setter
|
||||
def set_action_keys_enabled(self, value: bool) -> bool:
|
||||
"""Sets whether action keys will be echoed when key echo is enabled."""
|
||||
|
||||
msg = f"SPEECH DBUS MANAGER: Setting enable action keys to {value}."
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
self._settings_manager.setSetting("enableActionKeys", value)
|
||||
return True
|
||||
|
||||
@dbus_service.getter
|
||||
def get_navigation_keys_enabled(self) -> bool:
|
||||
"""Returns whether navigation keys will be echoed when key echo is enabled."""
|
||||
|
||||
return self._settings_manager.getSetting("enableNavigationKeys")
|
||||
|
||||
@dbus_service.setter
|
||||
def set_navigation_keys_enabled(self, value: bool) -> bool:
|
||||
"""Sets whether navigation keys will be echoed when key echo is enabled."""
|
||||
|
||||
msg = f"SPEECH DBUS MANAGER: Setting enable navigation keys to {value}."
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
self._settings_manager.setSetting("enableNavigationKeys", value)
|
||||
return True
|
||||
|
||||
@dbus_service.command
|
||||
def cycle_key_echo(self, script=None, event=None):
|
||||
"""Cycle through the key echo levels."""
|
||||
|
||||
tokens = ["SPEECH DBUS MANAGER: cycle_key_echo. Script:", script, "Event:", event]
|
||||
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||
|
||||
# Get current settings
|
||||
key = self._settings_manager.getSetting("enableKeyEcho")
|
||||
word = self._settings_manager.getSetting("enableEchoByWord")
|
||||
sentence = self._settings_manager.getSetting("enableEchoBySentence")
|
||||
|
||||
# Cycle through the combinations: none -> key -> word -> sentence -> all -> none
|
||||
if not key and not word and not sentence:
|
||||
# None -> Key only
|
||||
new_key, new_word, new_sentence = True, False, False
|
||||
brief = messages.KEY_ECHO_KEY_BRIEF
|
||||
full = messages.KEY_ECHO_KEY_FULL
|
||||
elif key and not word and not sentence:
|
||||
# Key -> Word
|
||||
new_key, new_word, new_sentence = False, True, False
|
||||
brief = messages.KEY_ECHO_WORD_BRIEF
|
||||
full = messages.KEY_ECHO_WORD_FULL
|
||||
elif not key and word and not sentence:
|
||||
# Word -> Sentence
|
||||
new_key, new_word, new_sentence = False, False, True
|
||||
brief = messages.KEY_ECHO_SENTENCE_BRIEF
|
||||
full = messages.KEY_ECHO_SENTENCE_FULL
|
||||
elif not key and not word and sentence:
|
||||
# Sentence -> All
|
||||
new_key, new_word, new_sentence = True, True, True
|
||||
brief = messages.KEY_ECHO_KEY_AND_WORD_BRIEF
|
||||
full = messages.KEY_ECHO_KEY_AND_WORD_FULL
|
||||
else:
|
||||
# All -> None
|
||||
new_key, new_word, new_sentence = False, False, False
|
||||
brief = messages.KEY_ECHO_NONE_BRIEF
|
||||
full = messages.KEY_ECHO_NONE_FULL
|
||||
|
||||
# Apply new settings
|
||||
self._settings_manager.setSetting("enableKeyEcho", new_key)
|
||||
self._settings_manager.setSetting("enableEchoByWord", new_word)
|
||||
self._settings_manager.setSetting("enableEchoBySentence", new_sentence)
|
||||
|
||||
if script is not None:
|
||||
script.presentMessage(full, brief)
|
||||
@@ -717,7 +717,7 @@ class SpeechGenerator(generator.Generator):
|
||||
and AXUtilities.is_selected(obj):
|
||||
return []
|
||||
|
||||
# egg-list-box, e.g. privacy panel in gnome-control-center
|
||||
# egg-list-box-style containers can expose selected panels as list items.
|
||||
if AXUtilities.is_list_box(parent):
|
||||
doNotPresent.append(AXObject.get_role(obj))
|
||||
|
||||
|
||||
@@ -86,6 +86,25 @@ class LegacyTomlSchemaMigrationTests(unittest.TestCase):
|
||||
self.assertNotIn("format-version = 2", savedSettings)
|
||||
self.assertNotIn("[profiles.default.metadata]", savedSettings)
|
||||
|
||||
def test_legacy_profile_keybindings_are_preserved(self):
|
||||
legacySettings = LEGACY_SETTINGS.replace(
|
||||
'desktop-modifier-keys = ["Insert", "KP_Insert"]',
|
||||
'desktop-modifier-keys = ["Insert", "KP_Insert"]\ncustom-binding = "kb:cthulhu+x"',
|
||||
)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tempDir:
|
||||
Path(tempDir, "user-settings.toml").write_text(
|
||||
legacySettings,
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
backend = Backend(tempDir)
|
||||
|
||||
self.assertEqual(
|
||||
backend.getKeybindings("default"),
|
||||
{"custom-binding": "kb:cthulhu+x"},
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user