Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 51ef3de672 | |||
| c6049ef5f3 | |||
| 78ef51d01f | |||
| 30a40f6974 |
@@ -1,7 +1,7 @@
|
||||
# Maintainer: Storm Dragon <storm_dragon@stormux.org>
|
||||
|
||||
pkgname=cthulhu
|
||||
pkgver=2026.01.19
|
||||
pkgver=2026.01.26
|
||||
pkgrel=1
|
||||
pkgdesc="Desktop-agnostic screen reader with plugin system, forked from Orca"
|
||||
url="https://git.stormux.org/storm/cthulhu"
|
||||
@@ -27,7 +27,6 @@ depends=(
|
||||
|
||||
# Plugin system and D-Bus remote control
|
||||
python-pluggy
|
||||
python-tomlkit
|
||||
python-dasbus
|
||||
|
||||
# AI Assistant dependencies (for screenshots, HTTP requests, and actions)
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
# Cthulhu Logging Guidelines
|
||||
|
||||
This document defines the preferred format for debug logging in Cthulhu.
|
||||
The goal is to keep logs consistent, searchable, and easy to scan.
|
||||
|
||||
## Helpers
|
||||
|
||||
Use the helpers in `cthulhu.debug` for new logs:
|
||||
|
||||
- `debug.print_log(level, prefix, message, reason=None, timestamp=False, stack=False)`
|
||||
- `debug.print_log_tokens(level, prefix, tokens, reason=None, timestamp=False, stack=False)`
|
||||
|
||||
These helpers ensure the prefix and optional reason tag are formatted consistently.
|
||||
Timestamps are appended at the end of the message when enabled.
|
||||
|
||||
## Prefixes
|
||||
|
||||
Use short, uppercase prefixes that identify the subsystem:
|
||||
|
||||
- `EVENT MANAGER`
|
||||
- `FOCUS MANAGER`
|
||||
- `INPUT EVENT`
|
||||
- `SCRIPT MANAGER`
|
||||
- `WEB`
|
||||
|
||||
## Messages
|
||||
|
||||
- Keep messages short and action-focused.
|
||||
- Do not include the prefix in the message.
|
||||
- Prefer consistent verbs (e.g., "Not using …", "Setting …", "Ignoring …").
|
||||
|
||||
## Reason Tags
|
||||
|
||||
Use reason tags to explain decisions or early exits.
|
||||
|
||||
- Lowercase with hyphens (e.g., `focus-mode`, `no-active-script`).
|
||||
- Use a short phrase rather than a full sentence.
|
||||
- If a human-readable reason is already available, it can be used directly.
|
||||
|
||||
## Examples
|
||||
|
||||
```text
|
||||
WEB: Not using caret navigation (reason=disabled)
|
||||
FOCUS MANAGER: Setting locus of focus to existing locus of focus (reason=no-change)
|
||||
SCRIPT MANAGER: Setting active script to [script] (reason=focus: active-window)
|
||||
```
|
||||
+1
-1
@@ -290,7 +290,7 @@ toggle the reading of tables, either by single cell or whole row.
|
||||
.B Cthulhu
|
||||
user preferences directory
|
||||
.TP
|
||||
.BI ~/.local/share/cthulhu/user-settings.toml
|
||||
.BI ~/.local/share/cthulhu/user-settings.conf
|
||||
.B Cthulhu
|
||||
user preferences configuration file.
|
||||
.TP
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
project('cthulhu',
|
||||
version: '2026.01.19-testing',
|
||||
version: '2026.01.26-master',
|
||||
meson_version: '>= 1.0.0',
|
||||
)
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ license = { text = "LGPL-2.1-or-later" }
|
||||
dependencies = [
|
||||
"pygobject>=3.18",
|
||||
"pluggy",
|
||||
"tomlkit",
|
||||
"brlapi; extra == 'braille'",
|
||||
"python-speechd; extra == 'speech'",
|
||||
"piper-tts; extra == 'piper'",
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
# Forked from Orca screen reader.
|
||||
# Cthulhu project: https://git.stormux.org/storm/cthulhu
|
||||
|
||||
"""TOML backend for Cthulhu settings"""
|
||||
"""JSON backend for Cthulhu settings"""
|
||||
|
||||
__id__ = "$Id$"
|
||||
__version__ = "$Revision$"
|
||||
@@ -31,22 +31,20 @@ __date__ = "$Date$"
|
||||
__copyright__ = "Copyright (c) 2010-2011 Consorcio Fernando de los Rios."
|
||||
__license__ = "LGPL"
|
||||
|
||||
from json import load, dump
|
||||
import os
|
||||
|
||||
from tomlkit import parse, dumps, document
|
||||
|
||||
from cthulhu import settings, acss
|
||||
|
||||
class Backend:
|
||||
|
||||
def __init__(self, prefsDir):
|
||||
""" Initialize the TOML Backend.
|
||||
"""
|
||||
""" Initialize the JSON Backend.
|
||||
"""
|
||||
self.general = {}
|
||||
self.pronunciations = {}
|
||||
self.keybindings = {}
|
||||
self.profiles = {}
|
||||
self.settingsFile = os.path.join(prefsDir, "user-settings.toml")
|
||||
self.settingsFile = os.path.join(prefsDir, "user-settings.conf")
|
||||
self.appPrefsDir = os.path.join(prefsDir, "app-settings")
|
||||
|
||||
self._defaultProfiles = {'default': { 'profile': settings.profile,
|
||||
@@ -55,137 +53,75 @@ class Backend:
|
||||
}
|
||||
}
|
||||
|
||||
def _stripNone(self, value):
|
||||
if isinstance(value, dict):
|
||||
cleaned = {}
|
||||
for key, item in value.items():
|
||||
if item is None:
|
||||
continue
|
||||
cleanedItem = self._stripNone(item)
|
||||
if cleanedItem is None:
|
||||
continue
|
||||
cleaned[key] = cleanedItem
|
||||
return cleaned
|
||||
if isinstance(value, list):
|
||||
cleanedList = []
|
||||
for item in value:
|
||||
if item is None:
|
||||
continue
|
||||
cleanedList.append(self._stripNone(item))
|
||||
return cleanedList
|
||||
return value
|
||||
|
||||
def _readDocument(self, fileName):
|
||||
if os.path.exists(fileName):
|
||||
with open(fileName, 'r', encoding='utf-8') as settingsFile:
|
||||
return parse(settingsFile.read())
|
||||
return document()
|
||||
|
||||
def _writeDocument(self, fileName, prefsDoc):
|
||||
with open(fileName, 'w', encoding='utf-8') as settingsFile:
|
||||
settingsFile.write(dumps(prefsDoc))
|
||||
|
||||
def _updateTable(self, targetTable, newValues):
|
||||
if not isinstance(newValues, dict):
|
||||
return
|
||||
|
||||
for key in list(targetTable.keys()):
|
||||
if key not in newValues:
|
||||
del targetTable[key]
|
||||
continue
|
||||
newValue = newValues[key]
|
||||
existingValue = targetTable.get(key)
|
||||
if isinstance(newValue, dict) and isinstance(existingValue, dict):
|
||||
self._updateTable(existingValue, newValue)
|
||||
continue
|
||||
if existingValue != newValue:
|
||||
targetTable[key] = newValue
|
||||
|
||||
for key, newValue in newValues.items():
|
||||
if key not in targetTable:
|
||||
targetTable[key] = newValue
|
||||
|
||||
def saveDefaultSettings(self, general, pronunciations, keybindings):
|
||||
""" Save default settings for all the properties from
|
||||
cthulhu.settings. """
|
||||
prefs = {'general': self._stripNone(general),
|
||||
'profiles': self._stripNone(self._defaultProfiles),
|
||||
'pronunciations': self._stripNone(pronunciations),
|
||||
'keybindings': self._stripNone(keybindings)}
|
||||
prefs = {'general': general,
|
||||
'profiles': self._defaultProfiles,
|
||||
'pronunciations': pronunciations,
|
||||
'keybindings': keybindings}
|
||||
|
||||
self.general = general
|
||||
self.profiles = self._defaultProfiles
|
||||
self.pronunciations = pronunciations
|
||||
self.keybindings = keybindings
|
||||
|
||||
prefsDoc = document()
|
||||
prefsDoc['general'] = prefs['general']
|
||||
prefsDoc['profiles'] = prefs['profiles']
|
||||
prefsDoc['pronunciations'] = prefs['pronunciations']
|
||||
prefsDoc['keybindings'] = prefs['keybindings']
|
||||
|
||||
self._writeDocument(self.settingsFile, prefsDoc)
|
||||
settingsFile = open(self.settingsFile, 'w')
|
||||
dump(prefs, settingsFile, indent=4)
|
||||
settingsFile.close()
|
||||
|
||||
def getAppSettings(self, appName):
|
||||
fileName = os.path.join(self.appPrefsDir, f"{appName}.toml")
|
||||
return self._readDocument(fileName)
|
||||
fileName = os.path.join(self.appPrefsDir, f"{appName}.conf")
|
||||
if os.path.exists(fileName):
|
||||
settingsFile = open(fileName, 'r')
|
||||
prefs = load(settingsFile)
|
||||
settingsFile.close()
|
||||
else:
|
||||
prefs = {}
|
||||
|
||||
return prefs
|
||||
|
||||
def saveAppSettings(self, appName, profile, general, pronunciations, keybindings):
|
||||
prefsDoc = self.getAppSettings(appName)
|
||||
profiles = prefsDoc.get('profiles')
|
||||
if profiles is None or not isinstance(profiles, dict):
|
||||
prefsDoc['profiles'] = {}
|
||||
profiles = prefsDoc['profiles']
|
||||
prefs = self.getAppSettings(appName)
|
||||
profiles = prefs.get('profiles', {})
|
||||
profiles[profile] = {'general': general,
|
||||
'pronunciations': pronunciations,
|
||||
'keybindings': keybindings}
|
||||
prefs['profiles'] = profiles
|
||||
|
||||
profileTable = profiles.get(profile)
|
||||
if profileTable is None or not isinstance(profileTable, dict):
|
||||
profiles[profile] = {}
|
||||
profileTable = profiles[profile]
|
||||
|
||||
self._updateTable(profileTable, {
|
||||
'general': self._stripNone(general),
|
||||
'pronunciations': self._stripNone(pronunciations),
|
||||
'keybindings': self._stripNone(keybindings),
|
||||
})
|
||||
|
||||
fileName = os.path.join(self.appPrefsDir, f"{appName}.toml")
|
||||
self._writeDocument(fileName, prefsDoc)
|
||||
fileName = os.path.join(self.appPrefsDir, f"{appName}.conf")
|
||||
settingsFile = open(fileName, 'w')
|
||||
dump(prefs, settingsFile, indent=4)
|
||||
settingsFile.close()
|
||||
|
||||
def saveProfileSettings(self, profile, general,
|
||||
pronunciations, keybindings):
|
||||
""" Save minimal subset defined in the profile against current
|
||||
""" Save minimal subset defined in the profile against current
|
||||
defaults. """
|
||||
if profile is None:
|
||||
profile = 'default'
|
||||
|
||||
general['pronunciations'] = pronunciations
|
||||
general['keybindings'] = keybindings
|
||||
general = self._stripNone(general)
|
||||
|
||||
prefsDoc = self._readDocument(self.settingsFile)
|
||||
profiles = prefsDoc.get('profiles')
|
||||
if profiles is None or not isinstance(profiles, dict):
|
||||
prefsDoc['profiles'] = {}
|
||||
profiles = prefsDoc['profiles']
|
||||
|
||||
profileTable = profiles.get(profile)
|
||||
if profileTable is None or not isinstance(profileTable, dict):
|
||||
profiles[profile] = {}
|
||||
profileTable = profiles[profile]
|
||||
|
||||
self._updateTable(profileTable, general)
|
||||
self._writeDocument(self.settingsFile, prefsDoc)
|
||||
with open(self.settingsFile, 'r+') as settingsFile:
|
||||
prefs = load(settingsFile)
|
||||
prefs['profiles'][profile] = general
|
||||
settingsFile.seek(0)
|
||||
settingsFile.truncate()
|
||||
dump(prefs, settingsFile, indent=4)
|
||||
|
||||
def _getSettings(self):
|
||||
""" Load from config file all settings """
|
||||
prefsDoc = self._readDocument(self.settingsFile)
|
||||
settingsFile = open(self.settingsFile)
|
||||
try:
|
||||
self.general = dict(prefsDoc.get('general', {}))
|
||||
self.pronunciations = dict(prefsDoc.get('pronunciations', {}))
|
||||
self.keybindings = dict(prefsDoc.get('keybindings', {}))
|
||||
self.profiles = dict(prefsDoc.get('profiles', {}))
|
||||
except Exception:
|
||||
prefs = load(settingsFile)
|
||||
except ValueError:
|
||||
return
|
||||
self.general = prefs['general'].copy()
|
||||
self.pronunciations = prefs['pronunciations']
|
||||
self.keybindings = prefs['keybindings']
|
||||
self.profiles = prefs['profiles'].copy()
|
||||
|
||||
def _migrateSettings(self, settingsDict):
|
||||
"""Migrate old setting names to new ones."""
|
||||
@@ -259,24 +195,18 @@ class Backend:
|
||||
|
||||
def isFirstStart(self):
|
||||
""" Check if we're in first start. """
|
||||
|
||||
|
||||
return not os.path.exists(self.settingsFile)
|
||||
|
||||
def _setProfileKey(self, key, value):
|
||||
self.general[key] = value
|
||||
|
||||
prefsDoc = self._readDocument(self.settingsFile)
|
||||
general = prefsDoc.get('general')
|
||||
if general is None or not isinstance(general, dict):
|
||||
prefsDoc['general'] = {}
|
||||
general = prefsDoc['general']
|
||||
|
||||
if value is None:
|
||||
if key in general:
|
||||
del general[key]
|
||||
else:
|
||||
general[key] = value
|
||||
self._writeDocument(self.settingsFile, prefsDoc)
|
||||
with open(self.settingsFile, 'r+') as settingsFile:
|
||||
prefs = load(settingsFile)
|
||||
prefs['general'][key] = value
|
||||
settingsFile.seek(0)
|
||||
settingsFile.truncate()
|
||||
dump(prefs, settingsFile, indent=4)
|
||||
|
||||
def setFirstStart(self, value=False):
|
||||
"""Set firstStart. This user-configurable setting is primarily
|
||||
@@ -308,8 +238,10 @@ class Backend:
|
||||
if profile in self.profiles:
|
||||
removeProfileFrom(self.profiles)
|
||||
|
||||
prefsDoc = self._readDocument(self.settingsFile)
|
||||
profiles = prefsDoc.get('profiles')
|
||||
if isinstance(profiles, dict) and profile in profiles:
|
||||
removeProfileFrom(profiles)
|
||||
self._writeDocument(self.settingsFile, prefsDoc)
|
||||
with open(self.settingsFile, 'r+') as settingsFile:
|
||||
prefs = load(settingsFile)
|
||||
if profile in prefs['profiles']:
|
||||
removeProfileFrom(prefs['profiles'])
|
||||
settingsFile.seek(0)
|
||||
settingsFile.truncate()
|
||||
dump(prefs, settingsFile, indent=4)
|
||||
@@ -1,9 +1,9 @@
|
||||
backends_python_sources = files([
|
||||
'__init__.py',
|
||||
'toml_backend.py',
|
||||
'json_backend.py',
|
||||
])
|
||||
|
||||
python3.install_sources(
|
||||
backends_python_sources,
|
||||
subdir: 'cthulhu/backends'
|
||||
)
|
||||
)
|
||||
@@ -23,5 +23,5 @@
|
||||
# Forked from Orca screen reader.
|
||||
# Cthulhu project: https://git.stormux.org/storm/cthulhu
|
||||
|
||||
version = "2026.01.19"
|
||||
codeName = "testing"
|
||||
version = "2026.01.26"
|
||||
codeName = "master"
|
||||
|
||||
@@ -180,8 +180,6 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
|
||||
self.savedGain = None
|
||||
self.savedPitch = None
|
||||
self.savedRate = None
|
||||
self.soundThemeCombo = None
|
||||
self.roleSoundPresentationCombo = None
|
||||
self._isInitialSetup = False
|
||||
self.selectedFamilyChoices = {}
|
||||
self.selectedLanguageChoices = {}
|
||||
@@ -2673,8 +2671,6 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
|
||||
# Get widget references
|
||||
self.soundThemeCombo = self.get_widget("soundThemeCombo")
|
||||
self.roleSoundPresentationCombo = self.get_widget("roleSoundPresentationCombo")
|
||||
self.soundThemeCombo.set_can_focus(False)
|
||||
self.roleSoundPresentationCombo.set_can_focus(False)
|
||||
|
||||
# Populate sound theme combo box
|
||||
themeManager = sound_theme_manager.getManager()
|
||||
@@ -2732,7 +2728,6 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
|
||||
value = self._roleSoundPresentationChoices[activeIndex][0]
|
||||
self.prefsDict["roleSoundPresentation"] = value
|
||||
|
||||
|
||||
def _updateCthulhuModifier(self):
|
||||
combobox = self.get_widget("cthulhuModifierComboBox")
|
||||
keystring = ", ".join(self.prefsDict["cthulhuModifierKeys"])
|
||||
|
||||
@@ -1139,7 +1139,7 @@ class RemoteControllerEvent(InputEvent):
|
||||
|
||||
class InputEventHandler:
|
||||
|
||||
def __init__(self, function, description, learnModeEnabled=True):
|
||||
def __init__(self, function, description, learnModeEnabled=True, enabled=True):
|
||||
"""Creates a new InputEventHandler instance. All bindings
|
||||
(e.g., key bindings and braille bindings) will be handled
|
||||
by an instance of an InputEventHandler.
|
||||
@@ -1159,6 +1159,15 @@ class InputEventHandler:
|
||||
self.function = function
|
||||
self.description = description
|
||||
self.learnModeEnabled = learnModeEnabled
|
||||
self._enabled = enabled
|
||||
|
||||
def is_enabled(self):
|
||||
"""Returns True if this handler is enabled."""
|
||||
return self._enabled
|
||||
|
||||
def set_enabled(self, enabled):
|
||||
"""Sets enabled state of this handler."""
|
||||
self._enabled = enabled
|
||||
|
||||
def __eq__(self, other):
|
||||
"""Compares one input handler to another."""
|
||||
|
||||
@@ -74,7 +74,10 @@ class InputEventManager:
|
||||
|
||||
msg = "INPUT EVENT MANAGER: Starting key watcher."
|
||||
debug.print_message(debug.LEVEL_INFO, msg, True)
|
||||
self._device = Atspi.Device.new_full("org.stormux.Cthulhu")
|
||||
if Atspi.get_version() >= (2, 55, 90):
|
||||
self._device = Atspi.Device.new_full("org.stormux.Cthulhu")
|
||||
else:
|
||||
self._device = Atspi.Device.new()
|
||||
self._device.add_key_watcher(self.process_keyboard_event)
|
||||
|
||||
def stop_key_watcher(self) -> None:
|
||||
@@ -100,43 +103,30 @@ class InputEventManager:
|
||||
msg = f"INPUT EVENT MANAGER: {grab_id} for: {binding}"
|
||||
debug.print_message(debug.LEVEL_INFO, msg, True)
|
||||
|
||||
def _get_key_definitions(self, binding: keybindings.KeyBinding) -> List[Atspi.KeyDefinition]:
|
||||
if hasattr(binding, "key_definitions"):
|
||||
return list(binding.key_definitions())
|
||||
if hasattr(binding, "keyDefs"):
|
||||
return list(binding.keyDefs())
|
||||
return []
|
||||
|
||||
def add_grabs_for_keybinding(self, binding: keybindings.KeyBinding) -> List[int]:
|
||||
"""Adds grabs for binding if it is enabled, returns grab IDs."""
|
||||
|
||||
if not (binding.is_enabled() and binding.is_bound()):
|
||||
return []
|
||||
|
||||
if binding.has_grabs():
|
||||
tokens = ["INPUT EVENT MANAGER:", binding, "already has grabs."]
|
||||
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
|
||||
return []
|
||||
|
||||
if self._device is None:
|
||||
tokens = ["INPUT EVENT MANAGER: No device to add grab for", binding]
|
||||
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
|
||||
return []
|
||||
|
||||
grab_ids = []
|
||||
key_definitions = self._get_key_definitions(binding)
|
||||
if not key_definitions:
|
||||
tokens = ["INPUT EVENT MANAGER: No key definitions for", binding]
|
||||
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
|
||||
return []
|
||||
|
||||
for kd in key_definitions:
|
||||
for kd in binding.key_definitions():
|
||||
grab_id = self._device.add_key_grab(kd, None)
|
||||
# When we have double/triple-click bindings, the single-click binding will be
|
||||
# registered first, and subsequent attempts to register what is externally the
|
||||
# same grab will fail. If we only have a double/triple-click, it succeeds.
|
||||
# A grab id of 0 indicates failure.
|
||||
if grab_id == 0:
|
||||
continue
|
||||
grab_ids.append(grab_id)
|
||||
self._grabbed_bindings[grab_id] = binding
|
||||
|
||||
if grab_ids:
|
||||
tokens = ["INPUT EVENT MANAGER: Added grabs", grab_ids, "for", binding]
|
||||
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
|
||||
|
||||
return grab_ids
|
||||
|
||||
def remove_grabs_for_keybinding(self, binding: keybindings.KeyBinding) -> None:
|
||||
@@ -147,30 +137,18 @@ class InputEventManager:
|
||||
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
|
||||
return
|
||||
|
||||
grab_ids = None
|
||||
if hasattr(binding, "get_grab_ids"):
|
||||
grab_ids = binding.get_grab_ids()
|
||||
elif hasattr(binding, "_grab_ids"):
|
||||
grab_ids = list(binding._grab_ids)
|
||||
|
||||
grab_ids = binding.get_grab_ids()
|
||||
if not grab_ids:
|
||||
tokens = ["INPUT EVENT MANAGER:", binding, "doesn't have grabs to remove."]
|
||||
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
|
||||
return
|
||||
|
||||
tokens = ["INPUT EVENT MANAGER: Removing grabs", grab_ids, "for", binding]
|
||||
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
|
||||
|
||||
for grab_id in grab_ids:
|
||||
self._device.remove_key_grab(grab_id)
|
||||
removed = self._grabbed_bindings.pop(grab_id, None)
|
||||
if removed is None:
|
||||
msg = f"INPUT EVENT MANAGER: No key binding for grab id {grab_id}"
|
||||
debug.print_message(debug.LEVEL_INFO, msg, True)
|
||||
if hasattr(binding, "_grab_ids") and grab_id in binding._grab_ids:
|
||||
binding._grab_ids.remove(grab_id)
|
||||
if hasattr(binding, "_grab_ids") and not binding._grab_ids:
|
||||
delattr(binding, "_grab_ids")
|
||||
|
||||
def remove_grab_by_id(self, grab_id: int) -> None:
|
||||
"""Removes a grab by id."""
|
||||
@@ -182,10 +160,8 @@ class InputEventManager:
|
||||
|
||||
self._device.remove_key_grab(grab_id)
|
||||
binding = self._grabbed_bindings.pop(grab_id, None)
|
||||
if binding and hasattr(binding, "_grab_ids") and grab_id in binding._grab_ids:
|
||||
if binding and grab_id in binding._grab_ids:
|
||||
binding._grab_ids.remove(grab_id)
|
||||
if not binding._grab_ids:
|
||||
delattr(binding, "_grab_ids")
|
||||
if binding:
|
||||
tokens = ["INPUT EVENT MANAGER: Removed grab", grab_id, "for", binding]
|
||||
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
|
||||
|
||||
+162
-153
@@ -26,6 +26,8 @@
|
||||
"""Provides support for defining keybindings and matching them to input
|
||||
events."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
__id__ = "$Id$"
|
||||
__version__ = "$Revision$"
|
||||
__date__ = "$Date$"
|
||||
@@ -39,6 +41,7 @@ from gi.repository import Gdk
|
||||
from gi.repository import Atspi
|
||||
|
||||
import functools
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from . import debug
|
||||
from . import settings
|
||||
@@ -46,6 +49,9 @@ from . import cthulhu_state
|
||||
|
||||
from .cthulhu_i18n import _
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .input_event import KeyboardEvent, InputEventHandler
|
||||
|
||||
_keysymsCache = {}
|
||||
_keycodeCache = {}
|
||||
|
||||
@@ -80,63 +86,35 @@ NON_LOCKING_MODIFIER_MASK = (1 << Atspi.ModifierType.SHIFT |
|
||||
1 << MODIFIER_CTHULHU)
|
||||
defaultModifierMask = NON_LOCKING_MODIFIER_MASK
|
||||
|
||||
def getKeycode(keysym):
|
||||
"""Converts an XKeysym string (e.g., 'KP_Enter') to a keycode that
|
||||
should match the event.hw_code for key events.
|
||||
|
||||
This whole situation is caused by the fact that Solaris chooses
|
||||
to give us different keycodes for the same key, and the keypad
|
||||
is the primary place where this happens: if NumLock is not on,
|
||||
there is no telling the difference between keypad keys and the
|
||||
other navigation keys (e.g., arrows, page up/down, etc.). One,
|
||||
for example, would expect to get KP_End for the '1' key on the
|
||||
keypad if NumLock were not on. Instead, we get 'End' and the
|
||||
keycode for it matches the keycode for the other 'End' key. Odd.
|
||||
If NumLock is on, we at least get KP_* keys.
|
||||
|
||||
So...when setting up keybindings, we say we're interested in
|
||||
KeySyms, but those keysyms are carefully chosen so as to result
|
||||
in a keycode that matches the actual key on the keyboard. This
|
||||
is why we use KP_1 instead of KP_End and so on in our keybindings.
|
||||
|
||||
Arguments:
|
||||
- keysym: a string that is a valid representation of an XKeysym.
|
||||
|
||||
Returns an integer representing a key code that should match the
|
||||
event.hw_code for key events.
|
||||
"""
|
||||
CAN_USE_KEYSYMS = Atspi.get_version() >= (2, 55, 0)
|
||||
|
||||
def get_keycodes(keysym):
|
||||
"""Converts an XKeysym string to a (keyval, keycode) tuple."""
|
||||
if not keysym:
|
||||
return 0
|
||||
return (0, 0)
|
||||
|
||||
if keysym not in _keycodeCache:
|
||||
keymap = Gdk.Keymap.get_default()
|
||||
|
||||
# Find the numerical value of the keysym
|
||||
#
|
||||
keyval = Gdk.keyval_from_name(keysym)
|
||||
if keyval == 0:
|
||||
return 0
|
||||
return (0, 0)
|
||||
|
||||
# Now find the keycodes for the keysym. Since a keysym can
|
||||
# be associated with more than one key, we'll shoot for the
|
||||
# keysym that's in group 0, regardless of shift level (each
|
||||
# entry is of the form [keycode, group, level]).
|
||||
#
|
||||
_keycodeCache[keysym] = 0
|
||||
success, entries = keymap.get_entries_for_keyval(keyval)
|
||||
_keycodeCache[keysym] = (keyval, 0)
|
||||
_success, entries = keymap.get_entries_for_keyval(keyval)
|
||||
|
||||
for entry in entries:
|
||||
if entry.group == 0:
|
||||
_keycodeCache[keysym] = entry.keycode
|
||||
_keycodeCache[keysym] = (keyval, entry.keycode)
|
||||
break
|
||||
if _keycodeCache[keysym] == 0:
|
||||
_keycodeCache[keysym] = entries[0].keycode
|
||||
|
||||
#print keysym, keyval, entries, _keycodeCache[keysym]
|
||||
if _keycodeCache[keysym] == (keyval, 0):
|
||||
_keycodeCache[keysym] = (keyval, entries[0].keycode)
|
||||
|
||||
return _keycodeCache[keysym]
|
||||
|
||||
def getKeycode(keysym):
|
||||
"""Converts an XKeysym string to a keycode. Legacy wrapper."""
|
||||
return get_keycodes(keysym)[1]
|
||||
|
||||
def getModifierNames(mods):
|
||||
"""Gets the modifier names of a numeric modifier mask as a human
|
||||
consumable string.
|
||||
@@ -211,13 +189,43 @@ def get_click_countString(count):
|
||||
return _("triple click")
|
||||
return ""
|
||||
|
||||
def create_key_definitions(keycode, keyval, modifiers):
|
||||
"""Returns a list of Atspi key definitions for the given keycode, keyval, and modifiers."""
|
||||
ret = []
|
||||
if modifiers & CTHULHU_MODIFIER_MASK:
|
||||
modifier_list = []
|
||||
other_modifiers = modifiers & ~CTHULHU_MODIFIER_MASK
|
||||
from . import input_event_manager
|
||||
manager = input_event_manager.get_manager()
|
||||
for key in settings.cthulhuModifierKeys:
|
||||
mod_keyval, mod_keycode = get_keycodes(key)
|
||||
if mod_keycode == 0 and key == "Shift_Lock":
|
||||
mod_keyval, mod_keycode = get_keycodes("Caps_Lock")
|
||||
if CAN_USE_KEYSYMS:
|
||||
mod = manager.map_keysym_to_modifier(mod_keyval)
|
||||
else:
|
||||
mod = manager.map_keycode_to_modifier(mod_keycode)
|
||||
if mod:
|
||||
modifier_list.append(mod | other_modifiers)
|
||||
else:
|
||||
modifier_list = [modifiers]
|
||||
for mod in modifier_list:
|
||||
kd = Atspi.KeyDefinition()
|
||||
if CAN_USE_KEYSYMS:
|
||||
kd.keysym = keyval
|
||||
else:
|
||||
kd.keycode = keycode
|
||||
kd.modifiers = mod
|
||||
ret.append(kd)
|
||||
return ret
|
||||
|
||||
class KeyBinding:
|
||||
"""A single key binding, consisting of a keycode, a modifier mask,
|
||||
and the InputEventHandler.
|
||||
"""
|
||||
|
||||
def __init__(self, keysymstring, modifier_mask, modifiers, handler,
|
||||
click_count = 1):
|
||||
click_count = 1, enabled=True):
|
||||
"""Creates a new key binding.
|
||||
|
||||
Arguments:
|
||||
@@ -237,42 +245,22 @@ class KeyBinding:
|
||||
self.modifiers = modifiers
|
||||
self.handler = handler
|
||||
self.click_count = click_count
|
||||
self.keycode = None
|
||||
self.keycode = 0
|
||||
self.keyval = 0
|
||||
self._enabled = enabled
|
||||
self._grab_ids = []
|
||||
|
||||
def matches(self, keycode, modifiers):
|
||||
"""Returns true if this key binding matches the given keycode and
|
||||
modifier state.
|
||||
"""
|
||||
def matches(self, keyval, keycode, modifiers):
|
||||
"""Returns true if this key binding matches the given keyval/keycode and modifier state."""
|
||||
|
||||
# We lazily bind the keycode. The primary reason for doing this
|
||||
# is so that atspi does not have to be initialized before setting
|
||||
# keybindings in the user's preferences file.
|
||||
#
|
||||
if not self.keycode:
|
||||
self.keycode = getKeycode(self.keysymstring)
|
||||
self.keyval, self.keycode = get_keycodes(self.keysymstring)
|
||||
|
||||
# Debug logging for DisplayVersion plugin specifically
|
||||
if self.keysymstring == 'v' and self.modifiers == 257:
|
||||
with open('/tmp/displayversion_matches.log', 'a') as f:
|
||||
f.write(f"=== DisplayVersion matches() debug ===\n")
|
||||
f.write(f"Self keycode: {self.keycode}\n")
|
||||
f.write(f"Self keysymstring: {self.keysymstring}\n")
|
||||
f.write(f"Self modifiers: {self.modifiers}\n")
|
||||
f.write(f"Self modifier_mask: {self.modifier_mask}\n")
|
||||
f.write(f"Input keycode: {keycode}\n")
|
||||
f.write(f"Input modifiers: {modifiers}\n")
|
||||
f.write(f"Keycode match: {self.keycode == keycode}\n")
|
||||
if self.keycode == keycode:
|
||||
result = modifiers & self.modifier_mask
|
||||
f.write(f"Modifier calculation: {modifiers} & {self.modifier_mask} = {result}\n")
|
||||
f.write(f"Modifier match: {result == self.modifiers}\n")
|
||||
f.write(f"Overall match: {self.keycode == keycode and (modifiers & self.modifier_mask) == self.modifiers}\n")
|
||||
|
||||
if self.keycode == keycode:
|
||||
if self.keycode == keycode or self.keyval == keyval:
|
||||
result = modifiers & self.modifier_mask
|
||||
return result == self.modifiers
|
||||
else:
|
||||
return False
|
||||
|
||||
return False
|
||||
|
||||
def description(self):
|
||||
"""Returns the description of this binding's functionality."""
|
||||
@@ -292,42 +280,53 @@ class KeyBinding:
|
||||
|
||||
return string.strip()
|
||||
|
||||
def keyDefs(self):
|
||||
""" return a list of Atspi key definitions for the given binding.
|
||||
This may return more than one binding if the Cthulhu modifier is bound
|
||||
to more than one key.
|
||||
If AT-SPI is older than 2.40, then this function will not work and
|
||||
will return an empty set.
|
||||
"""
|
||||
def is_bound(self):
|
||||
"""Returns True if this KeyBinding is bound to a key."""
|
||||
return bool(self.keysymstring)
|
||||
|
||||
def is_enabled(self):
|
||||
"""Returns True if this KeyBinding is enabled."""
|
||||
return self._enabled
|
||||
|
||||
def set_enabled(self, enabled):
|
||||
"""Set this KeyBinding's enabled state."""
|
||||
self._enabled = enabled
|
||||
|
||||
def get_grab_ids(self):
|
||||
"""Returns the grab IDs for this KeyBinding."""
|
||||
return self._grab_ids
|
||||
|
||||
def has_grabs(self):
|
||||
"""Returns True if there are existing grabs associated with this KeyBinding."""
|
||||
return bool(self._grab_ids)
|
||||
|
||||
def add_grabs(self):
|
||||
"""Adds key grabs for this KeyBinding."""
|
||||
from . import input_event_manager
|
||||
self._grab_ids = input_event_manager.get_manager().add_grabs_for_keybinding(self)
|
||||
|
||||
def remove_grabs(self):
|
||||
"""Removes key grabs for this KeyBinding."""
|
||||
from . import input_event_manager
|
||||
input_event_manager.get_manager().remove_grabs_for_keybinding(self)
|
||||
self._grab_ids = []
|
||||
|
||||
def key_definitions(self):
|
||||
"""Return a list of Atspi key definitions for the given binding."""
|
||||
ret = []
|
||||
if not self.keycode:
|
||||
self.keycode = getKeycode(self.keysymstring)
|
||||
|
||||
if self.modifiers & CTHULHU_MODIFIER_MASK:
|
||||
device = cthulhu_state.device
|
||||
if device is None:
|
||||
return ret
|
||||
modList = []
|
||||
otherMods = self.modifiers & ~CTHULHU_MODIFIER_MASK
|
||||
numLockMod = device.get_modifier(getKeycode("Num_Lock"))
|
||||
lockedMods = device.get_locked_modifiers()
|
||||
numLockOn = lockedMods & numLockMod
|
||||
for key in settings.cthulhuModifierKeys:
|
||||
keycode = getKeycode(key)
|
||||
if keycode == 0 and key == "Shift_Lock":
|
||||
keycode = getKeycode("Caps_Lock")
|
||||
mod = device.map_modifier(keycode)
|
||||
if key != "KP_Insert" or not numLockOn:
|
||||
modList.append(mod | otherMods)
|
||||
else:
|
||||
modList = [self.modifiers]
|
||||
for mod in modList:
|
||||
kd = Atspi.KeyDefinition()
|
||||
kd.keycode = self.keycode
|
||||
kd.modifiers = mod
|
||||
ret.append(kd)
|
||||
self.keyval, self.keycode = get_keycodes(self.keysymstring)
|
||||
ret.extend(create_key_definitions(self.keycode, self.keyval, self.modifiers))
|
||||
if CAN_USE_KEYSYMS and self.modifiers & SHIFT_MODIFIER_MASK:
|
||||
upper_keyval = Gdk.keyval_to_upper(self.keyval)
|
||||
if upper_keyval != self.keyval:
|
||||
ret.extend(create_key_definitions(self.keycode, upper_keyval, self.modifiers))
|
||||
return ret
|
||||
|
||||
def keyDefs(self):
|
||||
"""Legacy wrapper. Use key_definitions() instead."""
|
||||
return self.key_definitions()
|
||||
|
||||
class KeyBindings:
|
||||
"""Structure that maintains a set of KeyBinding instances.
|
||||
"""
|
||||
@@ -347,7 +346,7 @@ class KeyBindings:
|
||||
result += "]"
|
||||
return result
|
||||
|
||||
def add(self, keyBinding):
|
||||
def add(self, keyBinding, include_grabs=False):
|
||||
"""Adds the given KeyBinding instance to this set of keybindings.
|
||||
"""
|
||||
|
||||
@@ -359,17 +358,29 @@ class KeyBindings:
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
|
||||
self.keyBindings.append(keyBinding)
|
||||
if include_grabs:
|
||||
keyBinding.add_grabs()
|
||||
|
||||
def remove(self, keyBinding):
|
||||
def remove(self, keyBinding, include_grabs=False):
|
||||
"""Removes the given KeyBinding instance from this set of keybindings.
|
||||
"""
|
||||
|
||||
if keyBinding not in self.keyBindings:
|
||||
candidates = self.getBindingsForHandler(keyBinding.handler)
|
||||
if not candidates:
|
||||
return
|
||||
for candidate in self.getBindingsForHandler(keyBinding.handler):
|
||||
self.remove(candidate, include_grabs)
|
||||
return
|
||||
|
||||
if keyBinding.has_grabs():
|
||||
if include_grabs:
|
||||
keyBinding.remove_grabs()
|
||||
|
||||
try:
|
||||
i = self.keyBindings.index(keyBinding)
|
||||
except Exception:
|
||||
self.keyBindings.remove(keyBinding)
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
del self.keyBindings[i]
|
||||
|
||||
def removeByHandler(self, handler):
|
||||
"""Removes the given KeyBinding instance from this set of keybindings.
|
||||
@@ -380,6 +391,38 @@ class KeyBindings:
|
||||
del self.keyBindings[i - 1]
|
||||
i = i - 1
|
||||
|
||||
def add_key_grabs(self, reason=""):
|
||||
"""Adds grabs for all enabled bindings in this set of keybindings."""
|
||||
msg = "KEYBINDINGS: Adding key grabs"
|
||||
if reason:
|
||||
msg += f": {reason}"
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
|
||||
count = 0
|
||||
for binding in self.keyBindings:
|
||||
if binding.is_enabled() and not binding.has_grabs():
|
||||
count += 1
|
||||
binding.add_grabs()
|
||||
|
||||
msg = f"KEYBINDINGS: {count} key grabs added (total bindings: {len(self.keyBindings)})."
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
|
||||
def remove_key_grabs(self, reason=""):
|
||||
"""Removes all grabs for this set of keybindings."""
|
||||
msg = "KEYBINDINGS: Removing key grabs"
|
||||
if reason:
|
||||
msg += f": {reason}"
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
|
||||
count = 0
|
||||
for binding in self.keyBindings:
|
||||
if binding.has_grabs():
|
||||
count += 1
|
||||
binding.remove_grabs()
|
||||
|
||||
msg = f"KEYBINDINGS: {count} key grabs removed (total bindings: {len(self.keyBindings)})."
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
|
||||
def hasKeyBinding (self, newKeyBinding, typeOfSearch="strict"):
|
||||
"""Return True if keyBinding is already in self.keyBindings.
|
||||
|
||||
@@ -461,6 +504,13 @@ class KeyBindings:
|
||||
|
||||
return [kb for kb in self.keyBindings if kb.handler == handler]
|
||||
|
||||
def has_enabled_handler(self, handler):
|
||||
"""Returns True if the handler is found in this set of keybindings and is enabled."""
|
||||
for binding in self.keyBindings:
|
||||
if binding.handler == handler and binding.is_enabled():
|
||||
return True
|
||||
return False
|
||||
|
||||
def _checkMatchingBindings(self, keyboardEvent, result):
|
||||
if debug.debugLevel > debug.LEVEL_INFO:
|
||||
return
|
||||
@@ -487,39 +537,11 @@ class KeyBindings:
|
||||
given keycode and modifiers, or None if no match exists.
|
||||
"""
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Check if this might be the DisplayVersion key combination
|
||||
event_str = keyboardEvent.event_string if hasattr(keyboardEvent, 'event_string') else 'unknown'
|
||||
if event_str.lower() == 'v':
|
||||
logger.info(f"=== KeyBindings.getInputHandler: Looking for handler ===")
|
||||
logger.info(f"Event string: {event_str}")
|
||||
logger.info(f"Hardware code: {keyboardEvent.hw_code}")
|
||||
logger.info(f"Modifiers: {keyboardEvent.modifiers}")
|
||||
logger.info(f"Total keybindings to check: {len(self.keyBindings)}")
|
||||
|
||||
with open('/tmp/keybinding_lookup.log', 'a') as f:
|
||||
f.write(f"=== Looking for 'v' key handler ===\n")
|
||||
f.write(f"Event string: {event_str}\n")
|
||||
f.write(f"Hardware code: {keyboardEvent.hw_code}\n")
|
||||
f.write(f"Modifiers: {keyboardEvent.modifiers}\n")
|
||||
f.write(f"Total keybindings: {len(self.keyBindings)}\n")
|
||||
|
||||
# Log all keybindings for comparison
|
||||
for i, kb in enumerate(self.keyBindings):
|
||||
if 'v' in kb.keysymstring.lower() or 'version' in kb.handler.description.lower():
|
||||
logger.info(f"Binding {i}: keysym={kb.keysymstring}, modifiers={kb.modifiers}, mask={kb.modifier_mask}, desc={kb.handler.description}")
|
||||
with open('/tmp/keybinding_lookup.log', 'a') as f:
|
||||
f.write(f"Found V-related binding {i}: keysym={kb.keysymstring}, modifiers={kb.modifiers}, mask={kb.modifier_mask}, desc={kb.handler.description}\n")
|
||||
|
||||
matches = []
|
||||
candidates = []
|
||||
clickCount = keyboardEvent.get_click_count()
|
||||
for keyBinding in self.keyBindings:
|
||||
if keyBinding.matches(keyboardEvent.hw_code, keyboardEvent.modifiers):
|
||||
if event_str.lower() == 'v':
|
||||
logger.info(f"MATCH found! keysym={keyBinding.keysymstring}, desc={keyBinding.handler.description}")
|
||||
if keyBinding.matches(keyboardEvent.id, keyboardEvent.hw_code, keyboardEvent.modifiers):
|
||||
if (keyboardEvent.modifiers & keyBinding.modifier_mask) == keyBinding.modifiers and \
|
||||
keyBinding.click_count == clickCount:
|
||||
matches.append(keyBinding)
|
||||
@@ -529,17 +551,8 @@ class KeyBindings:
|
||||
if keyBinding.keysymstring:
|
||||
candidates.append(keyBinding)
|
||||
|
||||
if event_str.lower() == 'v':
|
||||
logger.info(f"Exact matches: {len(matches)}")
|
||||
logger.info(f"Candidates: {len(candidates)}")
|
||||
with open('/tmp/keybinding_lookup.log', 'a') as f:
|
||||
f.write(f"Exact matches: {len(matches)}\n")
|
||||
f.write(f"Candidates: {len(candidates)}\n")
|
||||
|
||||
self._checkMatchingBindings(keyboardEvent, matches)
|
||||
if matches:
|
||||
if event_str.lower() == 'v':
|
||||
logger.info(f"Returning exact match handler: {matches[0].handler.description}")
|
||||
return matches[0].handler
|
||||
|
||||
if keyboardEvent.isKeyPadKeyWithNumlockOn():
|
||||
@@ -553,12 +566,8 @@ class KeyBindings:
|
||||
self._checkMatchingBindings(keyboardEvent, candidates)
|
||||
for candidate in candidates:
|
||||
if candidate.click_count <= clickCount:
|
||||
if event_str.lower() == 'v':
|
||||
logger.info(f"Returning candidate handler: {candidate.handler.description}")
|
||||
return candidate.handler
|
||||
|
||||
if event_str.lower() == 'v':
|
||||
logger.info("No handler found!")
|
||||
return None
|
||||
|
||||
def load(self, keymap, handlers):
|
||||
|
||||
@@ -140,19 +140,21 @@ Access comprehensive OCR settings through Cthulhu Preferences:
|
||||
|
||||
### Configuration File
|
||||
Settings are automatically stored in Cthulhu's configuration system:
|
||||
- **Global Settings**: `~/.local/share/cthulhu/user-settings.toml`
|
||||
- **Application Settings**: `~/.local/share/cthulhu/app-settings/<app>.toml`
|
||||
- **Global Settings**: `~/.local/share/cthulhu/user-settings.conf`
|
||||
- **Profile Settings**: `~/.local/share/cthulhu/app-settings/[profile]/`
|
||||
|
||||
### Example Configuration Values
|
||||
```toml
|
||||
ocrLanguageCode = "eng"
|
||||
ocrScaleFactor = 3
|
||||
ocrGrayscaleImg = false
|
||||
ocrInvertImg = false
|
||||
ocrBlackWhiteImg = false
|
||||
ocrBlackWhiteImgValue = 200
|
||||
ocrColorCalculation = false
|
||||
ocrCopyToClipboard = true
|
||||
```json
|
||||
{
|
||||
"ocrLanguageCode": "eng",
|
||||
"ocrScaleFactor": 3,
|
||||
"ocrGrayscaleImg": false,
|
||||
"ocrInvertImg": false,
|
||||
"ocrBlackWhiteImg": false,
|
||||
"ocrBlackWhiteImgValue": 200,
|
||||
"ocrColorCalculation": false,
|
||||
"ocrCopyToClipboard": true
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@@ -4455,7 +4455,7 @@ class Utilities:
|
||||
tokens = ["SCRIPT UTILITIES: ", obj, f"has {nRows} rows"]
|
||||
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||
|
||||
x, y, width, height = boundingbox
|
||||
x, y, width, height = boundingbox.x, boundingbox.y, boundingbox.width, boundingbox.height
|
||||
cell = self.descendantAtPoint(obj, x, y + 1)
|
||||
row, col = self.coordinatesForCell(cell)
|
||||
startIndex = max(0, row)
|
||||
|
||||
@@ -36,11 +36,11 @@ __license__ = "LGPL"
|
||||
|
||||
import copy
|
||||
import importlib
|
||||
import json
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
|
||||
|
||||
from gi.repository import Gio, GLib
|
||||
from tomlkit import parse
|
||||
|
||||
from . import debug
|
||||
from . import cthulhu_i18n
|
||||
@@ -62,7 +62,7 @@ class SettingsManager(object):
|
||||
"""Settings backend manager. This class manages cthulhu user's settings
|
||||
using different backends"""
|
||||
|
||||
def __init__(self, app: Cthulhu, backend: str = 'toml') -> None: # Modified signature
|
||||
def __init__(self, app: Cthulhu, backend: str = 'json') -> None: # Modified signature
|
||||
|
||||
debug.printMessage(debug.LEVEL_INFO, 'SETTINGS MANAGER: Initializing', True)
|
||||
self.app: Cthulhu = app # Store app instance
|
||||
@@ -259,13 +259,13 @@ class SettingsManager(object):
|
||||
if not self._prefsDir:
|
||||
return {}
|
||||
|
||||
settings_path = os.path.join(self._prefsDir, "user-settings.toml")
|
||||
settings_path = os.path.join(self._prefsDir, "user-settings.conf")
|
||||
if not os.path.exists(settings_path):
|
||||
return {}
|
||||
|
||||
try:
|
||||
with open(settings_path, "r", encoding="utf-8") as settings_file:
|
||||
prefs = parse(settings_file.read())
|
||||
prefs = json.load(settings_file)
|
||||
except Exception as error:
|
||||
msg = f"SETTINGS MANAGER: Unable to read default settings from {settings_path}: {error}"
|
||||
debug.printMessage(debug.LEVEL_WARNING, msg, True)
|
||||
@@ -572,10 +572,9 @@ class SettingsManager(object):
|
||||
"""Return the current general settings.
|
||||
Those settings comes from updating the default settings
|
||||
with the profiles' ones"""
|
||||
general = self.defaultGeneral.copy()
|
||||
if self._backend:
|
||||
general.update(self._backend.getGeneral(profile))
|
||||
return general
|
||||
return self._backend.getGeneral(profile)
|
||||
return {}
|
||||
|
||||
def getPronunciations(self, profile: str = 'default') -> Dict[str, Any]:
|
||||
"""Return the current pronunciations settings.
|
||||
@@ -822,3 +821,4 @@ def getManager() -> Optional[SettingsManager]:
|
||||
# During import phase, cthulhuApp may not exist yet
|
||||
pass
|
||||
return _managerInstance
|
||||
|
||||
|
||||
@@ -215,37 +215,16 @@ class SoundThemeManager:
|
||||
def getSoundPath(self, themeName, soundName):
|
||||
"""Get path to a specific sound file.
|
||||
|
||||
Checks for common audio file extensions: .wav, .ogg, .mp3, .flac.
|
||||
User themes are checked first, then system themes as fallback so
|
||||
partial user themes can inherit missing sounds.
|
||||
Checks for common audio file extensions: .wav, .ogg, .mp3, .flac
|
||||
"""
|
||||
themePaths = []
|
||||
|
||||
userPath = os.path.join(self.getUserSoundsDir(), themeName)
|
||||
if os.path.isdir(userPath):
|
||||
themePaths.append(userPath)
|
||||
|
||||
systemPath = os.path.join(self.getSystemSoundsDir(), themeName)
|
||||
if os.path.isdir(systemPath):
|
||||
themePaths.append(systemPath)
|
||||
|
||||
fallbackDirs = [
|
||||
'/usr/share/cthulhu/sounds',
|
||||
'/usr/local/share/cthulhu/sounds',
|
||||
]
|
||||
for fallbackDir in fallbackDirs:
|
||||
fallbackPath = os.path.join(fallbackDir, themeName)
|
||||
if os.path.isdir(fallbackPath) and fallbackPath not in themePaths:
|
||||
themePaths.append(fallbackPath)
|
||||
|
||||
if not themePaths:
|
||||
themePath = self.getThemePath(themeName)
|
||||
if not themePath:
|
||||
return None
|
||||
|
||||
for themePath in themePaths:
|
||||
for ext in ['.wav', '.ogg', '.mp3', '.flac']:
|
||||
soundPath = os.path.join(themePath, soundName + ext)
|
||||
if os.path.isfile(soundPath):
|
||||
return soundPath
|
||||
for ext in ['.wav', '.ogg', '.mp3', '.flac']:
|
||||
soundPath = os.path.join(themePath, soundName + ext)
|
||||
if os.path.isfile(soundPath):
|
||||
return soundPath
|
||||
|
||||
return None
|
||||
|
||||
@@ -416,3 +395,4 @@ def getManager():
|
||||
from . import cthulhu
|
||||
_manager = SoundThemeManager(cthulhu.cthulhuApp)
|
||||
return _manager
|
||||
|
||||
|
||||
Reference in New Issue
Block a user