First round of bug fixes and plugin capability extensions. Creating a preferences tab should no longer require editing Cthulhu itself.
This commit is contained in:
@@ -29,6 +29,7 @@ toolkit, OpenOffice/LibreOffice, Gecko, WebKitGtk, and KDE Qt toolkit.
|
||||
- **Extensible architecture**: Plugin system using pluggy framework
|
||||
- **Hot-reloadable plugins**: Add functionality without restarting
|
||||
- **Community plugins**: User and system plugin directories
|
||||
- **Plugin preferences**: Plugins can provide preferences pages that appear only when the plugin is active
|
||||
|
||||
### Remote Control
|
||||
- **D-Bus interface**: External control via D-Bus service
|
||||
@@ -83,6 +84,13 @@ The `PluginSystemManager` module provides **session-only** plugin control (no pr
|
||||
- `SetPluginActive` (parameterized)
|
||||
- `RescanPlugins`
|
||||
|
||||
### Plugin Preferences Pages
|
||||
|
||||
Plugins can add their own Preferences tab without modifying Cthulhu core code.
|
||||
Implement `getPreferencesGUI()` to return a Gtk widget (or `(widget, label)`),
|
||||
and `getPreferencesFromGUI()` to return a settings dict. The tab appears only
|
||||
when the plugin is active.
|
||||
|
||||
### More Documentation
|
||||
|
||||
See `README-REMOTE-CONTROLLER.md` and `REMOTE-CONTROLLER-COMMANDS.md` for the full D-Bus API
|
||||
|
||||
+12
-1
@@ -470,7 +470,18 @@ def loadUserSettings(script=None, inputEvent=None, skipReloadMessage=False):
|
||||
cthulhuApp.getSignalManager().emitSignal('load-setting-begin')
|
||||
|
||||
# NOW load plugins after script system is ready
|
||||
activePlugins = list(_settingsManager.getSetting('activePlugins'))
|
||||
activePluginsSetting = _settingsManager.getSetting('activePlugins') or []
|
||||
debug.printMessage(
|
||||
debug.LEVEL_INFO,
|
||||
f"CTHULHU: Active plugins defaults: {settings.activePlugins}",
|
||||
True,
|
||||
)
|
||||
debug.printMessage(
|
||||
debug.LEVEL_INFO,
|
||||
f"CTHULHU: Active plugins from settings manager: {activePluginsSetting}",
|
||||
True,
|
||||
)
|
||||
activePlugins = list(activePluginsSetting)
|
||||
debug.printMessage(debug.LEVEL_INFO, f'CTHULHU: Loading active plugins: {activePlugins}', True)
|
||||
cthulhuApp.getPluginSystemManager().setActivePlugins(activePlugins)
|
||||
|
||||
|
||||
@@ -155,6 +155,9 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
|
||||
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
|
||||
@@ -396,6 +399,9 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
|
||||
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()
|
||||
|
||||
@@ -799,6 +805,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
|
||||
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 [])
|
||||
@@ -835,6 +842,147 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
|
||||
|
||||
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.
|
||||
@@ -864,11 +1012,31 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
|
||||
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())
|
||||
_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.
|
||||
@@ -2283,9 +2451,13 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
|
||||
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
|
||||
#
|
||||
self._initAIState()
|
||||
if self._plugin_active(active_plugins, ["AIAssistant"]):
|
||||
self._initAIState()
|
||||
|
||||
# Indentation settings
|
||||
#
|
||||
@@ -2293,7 +2465,8 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
|
||||
|
||||
# OCR Plugin settings
|
||||
#
|
||||
self._initOCRState()
|
||||
if self._plugin_active(active_plugins, ["OCR"]):
|
||||
self._initOCRState()
|
||||
|
||||
def __initProfileCombo(self):
|
||||
"""Adding available profiles and setting active as the active one"""
|
||||
@@ -4149,6 +4322,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
|
||||
self._apply_plugin_changes()
|
||||
self.writeUserPreferences()
|
||||
cthulhu.loadUserSettings(self.script)
|
||||
self._refresh_dynamic_plugin_tabs()
|
||||
braille.checkBrailleSetting()
|
||||
self._initSpeechState()
|
||||
self._populateKeyBindings()
|
||||
|
||||
@@ -87,6 +87,14 @@ class Plugin:
|
||||
"""Get keybindings for this plugin. Override in subclasses."""
|
||||
return self._bindings
|
||||
|
||||
def getPreferencesGUI(self):
|
||||
"""Return a Gtk widget for plugin preferences, or None if not provided."""
|
||||
return None
|
||||
|
||||
def getPreferencesFromGUI(self):
|
||||
"""Return a dict of plugin preferences from the GUI."""
|
||||
return {}
|
||||
|
||||
def registerGestureByString(self, function, name, gestureString, learnModeEnabled=True):
|
||||
"""Register a gesture by string."""
|
||||
if self.app:
|
||||
|
||||
@@ -3,5 +3,5 @@ Name = AI Assistant
|
||||
Module = AIAssistant
|
||||
Description = AI-powered accessibility assistant for analyzing screens and taking actions
|
||||
Authors = Stormux <storm_dragon@stormux.org>
|
||||
Version = 1.0.0
|
||||
Category = Accessibility
|
||||
Version = 2.0.0
|
||||
Category = Accessibility
|
||||
|
||||
@@ -69,6 +69,8 @@ class AIAssistant(Plugin):
|
||||
|
||||
# Pre-captured screen data (to avoid capturing dialog itself)
|
||||
self._current_screen_data = None
|
||||
self._prefs_grid = None
|
||||
self._prefs_widgets = {}
|
||||
|
||||
@cthulhu_hookimpl
|
||||
def activate(self, plugin=None):
|
||||
@@ -128,6 +130,215 @@ class AIAssistant(Plugin):
|
||||
self._unregister_keybindings()
|
||||
|
||||
self._enabled = False
|
||||
|
||||
def getPreferencesGUI(self):
|
||||
if not self._prefs_grid:
|
||||
self._prefs_grid = self._build_preferences_gui()
|
||||
self._load_preferences_into_widgets()
|
||||
return self._prefs_grid, "AI Assistant"
|
||||
|
||||
def getPreferencesFromGUI(self):
|
||||
if not self._prefs_widgets:
|
||||
return {}
|
||||
|
||||
provider_values = self._prefs_widgets.get("provider_values", [])
|
||||
quality_values = self._prefs_widgets.get("quality_values", [])
|
||||
provider_index = self._prefs_widgets["provider_combo"].get_active()
|
||||
quality_index = self._prefs_widgets["quality_combo"].get_active()
|
||||
|
||||
if provider_index < 0 or provider_index >= len(provider_values):
|
||||
provider = settings.aiProvider
|
||||
else:
|
||||
provider = provider_values[provider_index]
|
||||
|
||||
if quality_index < 0 or quality_index >= len(quality_values):
|
||||
screenshot_quality = settings.aiScreenshotQuality
|
||||
else:
|
||||
screenshot_quality = quality_values[quality_index]
|
||||
|
||||
return {
|
||||
"aiAssistantEnabled": self._prefs_widgets["enable_check"].get_active(),
|
||||
"aiProvider": provider,
|
||||
"aiApiKeyFile": self._prefs_widgets["api_key_entry"].get_text().strip(),
|
||||
"aiOllamaModel": self._prefs_widgets["ollama_model_entry"].get_text().strip(),
|
||||
"aiOllamaEndpoint": self._prefs_widgets["ollama_endpoint_entry"].get_text().strip(),
|
||||
"aiConfirmationRequired": self._prefs_widgets["confirmation_check"].get_active(),
|
||||
"aiScreenshotQuality": screenshot_quality,
|
||||
}
|
||||
|
||||
def _build_preferences_gui(self):
|
||||
grid = Gtk.Grid(
|
||||
row_spacing=6,
|
||||
column_spacing=12,
|
||||
margin_left=12,
|
||||
margin_right=12,
|
||||
margin_top=12,
|
||||
margin_bottom=12,
|
||||
)
|
||||
|
||||
row = 0
|
||||
|
||||
enable_check = Gtk.CheckButton(label="Enable AI Assistant")
|
||||
enable_check.connect("toggled", self._on_ai_enabled_toggled)
|
||||
grid.attach(enable_check, 0, row, 2, 1)
|
||||
row += 1
|
||||
|
||||
provider_label = Gtk.Label(label="_Provider:")
|
||||
provider_label.set_use_underline(True)
|
||||
provider_label.set_halign(Gtk.Align.START)
|
||||
|
||||
provider_combo = Gtk.ComboBoxText()
|
||||
provider_combo.set_hexpand(True)
|
||||
provider_values = [
|
||||
settings.AI_PROVIDER_CLAUDE_CODE,
|
||||
settings.AI_PROVIDER_CODEX,
|
||||
settings.AI_PROVIDER_GEMINI,
|
||||
settings.AI_PROVIDER_OLLAMA,
|
||||
]
|
||||
provider_labels = ["Claude Code", "Codex", "Gemini", "Ollama"]
|
||||
for label in provider_labels:
|
||||
provider_combo.append_text(label)
|
||||
provider_combo.connect("changed", self._on_provider_changed)
|
||||
provider_label.set_mnemonic_widget(provider_combo)
|
||||
|
||||
grid.attach(provider_label, 0, row, 1, 1)
|
||||
grid.attach(provider_combo, 1, row, 1, 1)
|
||||
row += 1
|
||||
|
||||
api_key_label = Gtk.Label(label="API _Key File:")
|
||||
api_key_label.set_use_underline(True)
|
||||
api_key_label.set_halign(Gtk.Align.START)
|
||||
api_key_entry = Gtk.Entry()
|
||||
api_key_entry.set_hexpand(True)
|
||||
api_key_label.set_mnemonic_widget(api_key_entry)
|
||||
|
||||
grid.attach(api_key_label, 0, row, 1, 1)
|
||||
grid.attach(api_key_entry, 1, row, 1, 1)
|
||||
row += 1
|
||||
|
||||
ollama_model_label = Gtk.Label(label="_Ollama Model:")
|
||||
ollama_model_label.set_use_underline(True)
|
||||
ollama_model_label.set_halign(Gtk.Align.START)
|
||||
ollama_model_entry = Gtk.Entry()
|
||||
ollama_model_entry.set_hexpand(True)
|
||||
ollama_model_label.set_mnemonic_widget(ollama_model_entry)
|
||||
|
||||
grid.attach(ollama_model_label, 0, row, 1, 1)
|
||||
grid.attach(ollama_model_entry, 1, row, 1, 1)
|
||||
row += 1
|
||||
|
||||
ollama_endpoint_label = Gtk.Label(label="Ollama _Endpoint:")
|
||||
ollama_endpoint_label.set_use_underline(True)
|
||||
ollama_endpoint_label.set_halign(Gtk.Align.START)
|
||||
ollama_endpoint_entry = Gtk.Entry()
|
||||
ollama_endpoint_entry.set_hexpand(True)
|
||||
ollama_endpoint_label.set_mnemonic_widget(ollama_endpoint_entry)
|
||||
|
||||
grid.attach(ollama_endpoint_label, 0, row, 1, 1)
|
||||
grid.attach(ollama_endpoint_entry, 1, row, 1, 1)
|
||||
row += 1
|
||||
|
||||
confirmation_check = Gtk.CheckButton(label="Require confirmation before actions")
|
||||
grid.attach(confirmation_check, 0, row, 2, 1)
|
||||
row += 1
|
||||
|
||||
quality_label = Gtk.Label(label="Screenshot _Quality:")
|
||||
quality_label.set_use_underline(True)
|
||||
quality_label.set_halign(Gtk.Align.START)
|
||||
quality_combo = Gtk.ComboBoxText()
|
||||
quality_combo.set_hexpand(True)
|
||||
quality_values = [
|
||||
settings.AI_SCREENSHOT_QUALITY_LOW,
|
||||
settings.AI_SCREENSHOT_QUALITY_MEDIUM,
|
||||
settings.AI_SCREENSHOT_QUALITY_HIGH,
|
||||
]
|
||||
quality_labels = ["Low", "Medium", "High"]
|
||||
for label in quality_labels:
|
||||
quality_combo.append_text(label)
|
||||
quality_label.set_mnemonic_widget(quality_combo)
|
||||
|
||||
grid.attach(quality_label, 0, row, 1, 1)
|
||||
grid.attach(quality_combo, 1, row, 1, 1)
|
||||
|
||||
self._prefs_widgets = {
|
||||
"enable_check": enable_check,
|
||||
"provider_combo": provider_combo,
|
||||
"provider_values": provider_values,
|
||||
"api_key_entry": api_key_entry,
|
||||
"ollama_model_entry": ollama_model_entry,
|
||||
"ollama_endpoint_entry": ollama_endpoint_entry,
|
||||
"confirmation_check": confirmation_check,
|
||||
"quality_combo": quality_combo,
|
||||
"quality_values": quality_values,
|
||||
}
|
||||
|
||||
return grid
|
||||
|
||||
def _load_preferences_into_widgets(self):
|
||||
if not self._prefs_widgets:
|
||||
return
|
||||
|
||||
enabled = self._settings_manager.getSetting("aiAssistantEnabled")
|
||||
if enabled is None:
|
||||
enabled = settings.aiAssistantEnabled
|
||||
self._prefs_widgets["enable_check"].set_active(enabled)
|
||||
|
||||
provider = self._settings_manager.getSetting("aiProvider") or settings.aiProvider
|
||||
provider_values = self._prefs_widgets.get("provider_values", [])
|
||||
try:
|
||||
provider_index = provider_values.index(provider)
|
||||
except ValueError:
|
||||
provider_index = 0
|
||||
self._prefs_widgets["provider_combo"].set_active(provider_index)
|
||||
|
||||
api_key_file = self._settings_manager.getSetting("aiApiKeyFile") or settings.aiApiKeyFile
|
||||
self._prefs_widgets["api_key_entry"].set_text(api_key_file or "")
|
||||
|
||||
ollama_model = self._settings_manager.getSetting("aiOllamaModel") or settings.aiOllamaModel
|
||||
self._prefs_widgets["ollama_model_entry"].set_text(ollama_model or "")
|
||||
|
||||
ollama_endpoint = self._settings_manager.getSetting("aiOllamaEndpoint") or settings.aiOllamaEndpoint
|
||||
self._prefs_widgets["ollama_endpoint_entry"].set_text(ollama_endpoint or "")
|
||||
|
||||
confirmation_required = self._settings_manager.getSetting("aiConfirmationRequired")
|
||||
if confirmation_required is None:
|
||||
confirmation_required = settings.aiConfirmationRequired
|
||||
self._prefs_widgets["confirmation_check"].set_active(confirmation_required)
|
||||
|
||||
screenshot_quality = self._settings_manager.getSetting("aiScreenshotQuality") or settings.aiScreenshotQuality
|
||||
quality_values = self._prefs_widgets.get("quality_values", [])
|
||||
try:
|
||||
quality_index = quality_values.index(screenshot_quality)
|
||||
except ValueError:
|
||||
quality_index = 0
|
||||
self._prefs_widgets["quality_combo"].set_active(quality_index)
|
||||
|
||||
self._update_preferences_sensitivity()
|
||||
|
||||
def _on_ai_enabled_toggled(self, widget):
|
||||
self._update_preferences_sensitivity()
|
||||
|
||||
def _on_provider_changed(self, widget):
|
||||
self._update_preferences_sensitivity()
|
||||
|
||||
def _update_preferences_sensitivity(self):
|
||||
if not self._prefs_widgets:
|
||||
return
|
||||
|
||||
enabled = self._prefs_widgets["enable_check"].get_active()
|
||||
provider_values = self._prefs_widgets.get("provider_values", [])
|
||||
provider_index = self._prefs_widgets["provider_combo"].get_active()
|
||||
provider = provider_values[provider_index] if 0 <= provider_index < len(provider_values) else settings.aiProvider
|
||||
|
||||
is_gemini = provider == settings.AI_PROVIDER_GEMINI
|
||||
is_ollama = provider == settings.AI_PROVIDER_OLLAMA
|
||||
|
||||
self._prefs_widgets["provider_combo"].set_sensitive(enabled)
|
||||
self._prefs_widgets["api_key_entry"].set_sensitive(enabled and is_gemini)
|
||||
self._prefs_widgets["ollama_model_entry"].set_sensitive(enabled and is_ollama)
|
||||
self._prefs_widgets["ollama_endpoint_entry"].set_sensitive(enabled and is_ollama)
|
||||
self._prefs_widgets["confirmation_check"].set_sensitive(enabled)
|
||||
self._prefs_widgets["quality_combo"].set_sensitive(enabled)
|
||||
|
||||
def refresh_settings(self):
|
||||
"""Refresh plugin settings and reinitialize provider. Called when settings change."""
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
name = IndentationAudio
|
||||
version = 1.0.0
|
||||
version = 2.0.0
|
||||
description = Provides audio feedback for indentation level changes when navigating code or text
|
||||
authors = Stormux <storm_dragon@stormux.org>
|
||||
website = https://git.stormux.org/storm/cthulhu
|
||||
copyright = Copyright 2025
|
||||
builtin = false
|
||||
hidden = false
|
||||
hidden = false
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
import logging
|
||||
import math
|
||||
import re
|
||||
from gi.repository import GLib
|
||||
from gi.repository import GLib, Gtk
|
||||
|
||||
from cthulhu.plugin import Plugin, cthulhu_hookimpl
|
||||
from cthulhu import debug
|
||||
@@ -56,6 +56,8 @@ class IndentationAudio(Plugin):
|
||||
self._saved_speech_indentation = None
|
||||
|
||||
self._activated = False
|
||||
self._prefs_grid = None
|
||||
self._prefs_widgets = {}
|
||||
debug.printMessage(debug.LEVEL_INFO, "IndentationAudio: Plugin initialized", True)
|
||||
|
||||
@cthulhu_hookimpl
|
||||
@@ -86,6 +88,7 @@ class IndentationAudio(Plugin):
|
||||
# Connect to text caret movement events
|
||||
self._connect_to_events()
|
||||
|
||||
self._enabled = True
|
||||
self._activated = True
|
||||
debug.printMessage(debug.LEVEL_INFO, "IndentationAudio: Plugin activated successfully", True)
|
||||
return True
|
||||
@@ -109,6 +112,7 @@ class IndentationAudio(Plugin):
|
||||
# Clear tracking data
|
||||
self._last_indentation_data.clear()
|
||||
|
||||
self._enabled = False
|
||||
self._activated = False
|
||||
debug.printMessage(debug.LEVEL_INFO, "IndentationAudio: Plugin deactivated successfully", True)
|
||||
return True
|
||||
@@ -116,6 +120,164 @@ class IndentationAudio(Plugin):
|
||||
except Exception as e:
|
||||
debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: ERROR deactivating plugin: {e}", True)
|
||||
return False
|
||||
|
||||
def getPreferencesGUI(self):
|
||||
if not self._prefs_grid:
|
||||
self._prefs_grid = self._build_preferences_gui()
|
||||
self._load_preferences_into_widgets()
|
||||
return self._prefs_grid, "Indentation Audio"
|
||||
|
||||
def getPreferencesFromGUI(self):
|
||||
if not self._prefs_widgets:
|
||||
return {}
|
||||
|
||||
audio_unit = settings.INDENTATION_UNIT_COLUMNS
|
||||
if self._prefs_widgets["audio_unit_levels"].get_active():
|
||||
audio_unit = settings.INDENTATION_UNIT_LEVELS
|
||||
|
||||
return {
|
||||
"indentationAudioUnit": audio_unit,
|
||||
"indentationAudioBaseFrequency": self._prefs_widgets["base_frequency_spin"].get_value_as_int(),
|
||||
"indentationAudioStepFrequency": self._prefs_widgets["step_frequency_spin"].get_value_as_int(),
|
||||
"indentationAudioMaxFrequency": self._prefs_widgets["max_frequency_spin"].get_value_as_int(),
|
||||
"indentationAudioDuration": float(self._prefs_widgets["duration_spin"].get_value()),
|
||||
"indentationAudioVolume": float(self._prefs_widgets["volume_spin"].get_value()),
|
||||
}
|
||||
|
||||
def _build_preferences_gui(self):
|
||||
grid = Gtk.Grid(
|
||||
row_spacing=6,
|
||||
column_spacing=12,
|
||||
margin_left=12,
|
||||
margin_right=12,
|
||||
margin_top=12,
|
||||
margin_bottom=12,
|
||||
)
|
||||
|
||||
row = 0
|
||||
|
||||
description = Gtk.Label(label="Configure audio feedback for indentation changes.")
|
||||
description.set_line_wrap(True)
|
||||
description.set_halign(Gtk.Align.START)
|
||||
grid.attach(description, 0, row, 2, 1)
|
||||
row += 1
|
||||
|
||||
audio_unit_label = Gtk.Label(label="Audio _Unit:")
|
||||
audio_unit_label.set_use_underline(True)
|
||||
audio_unit_label.set_halign(Gtk.Align.START)
|
||||
|
||||
audio_unit_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4)
|
||||
audio_unit_levels = Gtk.RadioButton(label="Levels")
|
||||
audio_unit_columns = Gtk.RadioButton.new_with_label_from_widget(audio_unit_levels, "Columns")
|
||||
audio_unit_box.pack_start(audio_unit_levels, False, False, 0)
|
||||
audio_unit_box.pack_start(audio_unit_columns, False, False, 0)
|
||||
|
||||
audio_unit_label.set_mnemonic_widget(audio_unit_levels)
|
||||
grid.attach(audio_unit_label, 0, row, 1, 1)
|
||||
grid.attach(audio_unit_box, 1, row, 1, 1)
|
||||
row += 1
|
||||
|
||||
base_adjust = Gtk.Adjustment(200, 50, 2000, 10, 50, 0)
|
||||
step_adjust = Gtk.Adjustment(80, 1, 500, 5, 20, 0)
|
||||
max_adjust = Gtk.Adjustment(1200, 200, 5000, 50, 100, 0)
|
||||
duration_adjust = Gtk.Adjustment(0.15, 0.01, 2, 0.01, 0.1, 0)
|
||||
volume_adjust = Gtk.Adjustment(0.7, 0, 1, 0.05, 0.1, 0)
|
||||
|
||||
base_frequency_spin, row = self._add_spin_row(
|
||||
grid,
|
||||
row,
|
||||
"Base _Frequency (Hz):",
|
||||
base_adjust,
|
||||
0,
|
||||
)
|
||||
step_frequency_spin, row = self._add_spin_row(
|
||||
grid,
|
||||
row,
|
||||
"_Step per Unit (Hz):",
|
||||
step_adjust,
|
||||
0,
|
||||
)
|
||||
max_frequency_spin, row = self._add_spin_row(
|
||||
grid,
|
||||
row,
|
||||
"_Maximum Frequency (Hz):",
|
||||
max_adjust,
|
||||
0,
|
||||
)
|
||||
duration_spin, row = self._add_spin_row(
|
||||
grid,
|
||||
row,
|
||||
"Tone _Duration (sec):",
|
||||
duration_adjust,
|
||||
2,
|
||||
)
|
||||
volume_spin, row = self._add_spin_row(
|
||||
grid,
|
||||
row,
|
||||
"_Volume Multiplier:",
|
||||
volume_adjust,
|
||||
2,
|
||||
)
|
||||
|
||||
self._prefs_widgets = {
|
||||
"audio_unit_levels": audio_unit_levels,
|
||||
"audio_unit_columns": audio_unit_columns,
|
||||
"base_frequency_spin": base_frequency_spin,
|
||||
"step_frequency_spin": step_frequency_spin,
|
||||
"max_frequency_spin": max_frequency_spin,
|
||||
"duration_spin": duration_spin,
|
||||
"volume_spin": volume_spin,
|
||||
}
|
||||
|
||||
return grid
|
||||
|
||||
def _add_spin_row(self, grid, row, label_text, adjustment, digits):
|
||||
label = Gtk.Label(label=label_text)
|
||||
label.set_use_underline(True)
|
||||
label.set_halign(Gtk.Align.START)
|
||||
spin = Gtk.SpinButton(adjustment=adjustment, climb_rate=1, numeric=True)
|
||||
if digits:
|
||||
spin.set_digits(digits)
|
||||
label.set_mnemonic_widget(spin)
|
||||
|
||||
grid.attach(label, 0, row, 1, 1)
|
||||
grid.attach(spin, 1, row, 1, 1)
|
||||
return spin, row + 1
|
||||
|
||||
def _load_preferences_into_widgets(self):
|
||||
if not self._prefs_widgets:
|
||||
return
|
||||
|
||||
audio_unit = _settingsManager.getSetting("indentationAudioUnit") or settings.indentationAudioUnit
|
||||
if audio_unit == settings.INDENTATION_UNIT_LEVELS:
|
||||
self._prefs_widgets["audio_unit_levels"].set_active(True)
|
||||
else:
|
||||
self._prefs_widgets["audio_unit_columns"].set_active(True)
|
||||
|
||||
base_frequency = _settingsManager.getSetting("indentationAudioBaseFrequency")
|
||||
if base_frequency is None:
|
||||
base_frequency = settings.indentationAudioBaseFrequency
|
||||
self._prefs_widgets["base_frequency_spin"].set_value(base_frequency)
|
||||
|
||||
step_frequency = _settingsManager.getSetting("indentationAudioStepFrequency")
|
||||
if step_frequency is None:
|
||||
step_frequency = settings.indentationAudioStepFrequency
|
||||
self._prefs_widgets["step_frequency_spin"].set_value(step_frequency)
|
||||
|
||||
max_frequency = _settingsManager.getSetting("indentationAudioMaxFrequency")
|
||||
if max_frequency is None:
|
||||
max_frequency = settings.indentationAudioMaxFrequency
|
||||
self._prefs_widgets["max_frequency_spin"].set_value(max_frequency)
|
||||
|
||||
duration_value = _settingsManager.getSetting("indentationAudioDuration")
|
||||
if duration_value is None:
|
||||
duration_value = settings.indentationAudioDuration
|
||||
self._prefs_widgets["duration_spin"].set_value(duration_value)
|
||||
|
||||
volume_value = _settingsManager.getSetting("indentationAudioVolume")
|
||||
if volume_value is None:
|
||||
volume_value = settings.indentationAudioVolume
|
||||
self._prefs_widgets["volume_spin"].set_value(volume_value)
|
||||
|
||||
def _register_keybinding(self):
|
||||
"""Register the Cthulhu+I keybinding for toggling the plugin."""
|
||||
|
||||
@@ -84,7 +84,7 @@ To add support for other languages, install additional Tesseract language packs:
|
||||
|
||||
1. **Enable the Plugin**: The OCR plugin is enabled by default in Cthulhu. If disabled, you can enable it through:
|
||||
- Cthulhu Preferences → Plugins → Check "OCR"
|
||||
- Or ensure `'OCR'` is in the `activePlugins` list in settings.py
|
||||
- Or add `OCR` to your `activePlugins` preference
|
||||
|
||||
2. **Basic OCR Workflow**:
|
||||
- Navigate to content you want to OCR
|
||||
@@ -115,7 +115,7 @@ To add support for other languages, install additional Tesseract language packs:
|
||||
Access comprehensive OCR settings through Cthulhu Preferences:
|
||||
|
||||
1. **Open Cthulhu Preferences**: `~/.local/bin/cthulhu -s`
|
||||
2. **Navigate to OCR Tab**: Use keyboard navigation to find the OCR settings tab
|
||||
2. **Navigate to OCR Tab**: Use keyboard navigation to find the OCR settings tab (only shown when the plugin is active)
|
||||
3. **Configure Settings**: Adjust all OCR parameters through the accessible interface
|
||||
|
||||
### Available Settings
|
||||
@@ -318,4 +318,4 @@ For issues, questions, or contributions:
|
||||
|
||||
---
|
||||
|
||||
*Part of the Cthulhu Screen Reader project - Making the desktop accessible for everyone.*
|
||||
*Part of the Cthulhu Screen Reader project - Making the desktop accessible for everyone.*
|
||||
|
||||
@@ -494,6 +494,15 @@ class PluginManager(Plugin):
|
||||
|
||||
debug.printMessage(debug.LEVEL_INFO, f"PluginManager: Updated active plugins: {active_plugins}", True)
|
||||
|
||||
try:
|
||||
from cthulhu import plugin_system_manager
|
||||
manager = plugin_system_manager.getManager()
|
||||
if manager:
|
||||
manager.setActivePlugins(active_plugins)
|
||||
debug.printMessage(debug.LEVEL_INFO, "PluginManager: Applied active plugin changes", True)
|
||||
except Exception as apply_error:
|
||||
debug.printMessage(debug.LEVEL_INFO, f"PluginManager: Failed to apply plugin changes: {apply_error}", True)
|
||||
|
||||
except Exception as error:
|
||||
logger.error(f"PluginManager: Error toggling plugin {plugin_name}: {error}")
|
||||
debug.printMessage(debug.LEVEL_INFO, f"PluginManager: Error toggling {plugin_name}: {error}", True)
|
||||
|
||||
@@ -30,4 +30,13 @@ pip install pluggy
|
||||
|
||||
## Usage
|
||||
|
||||
The plugin will be automatically loaded when Cthulhu starts if it's listed in the activePlugins setting.
|
||||
Enable the plugin in Cthulhu Preferences → Plugins to load it at startup. You can also add it to your
|
||||
`activePlugins` preference.
|
||||
|
||||
## Preferences Pages (Optional)
|
||||
|
||||
Plugins can add their own Preferences tab by implementing:
|
||||
- `getPreferencesGUI()` → returns a Gtk widget (or `(widget, label)`).
|
||||
- `getPreferencesFromGUI()` → returns a dict of settings to save.
|
||||
|
||||
Tabs are only shown when the plugin is active.
|
||||
|
||||
@@ -494,12 +494,12 @@ presentChatRoomLast = False
|
||||
presentLiveRegionFromInactiveTab = False
|
||||
|
||||
# Plugins
|
||||
activePlugins = ['AIAssistant', 'DisplayVersion', 'OCR', 'PluginManager', 'HelloCthulhu', 'ByeCthulhu', 'IndentationAudio', 'WindowTitleReader']
|
||||
activePlugins = ['DisplayVersion', 'OCR', 'PluginManager', 'HelloCthulhu', 'ByeCthulhu', 'WindowTitleReader']
|
||||
pluginSources = []
|
||||
|
||||
# AI Assistant settings (disabled by default for opt-in behavior)
|
||||
aiAssistantEnabled = True
|
||||
aiProvider = AI_PROVIDER_CLAUDE_CODE
|
||||
aiAssistantEnabled = False
|
||||
aiProvider = AI_PROVIDER_OLLAMA
|
||||
aiApiKeyFile = ""
|
||||
aiOllamaModel = "llama3.2-vision"
|
||||
aiOllamaEndpoint = "http://localhost:11434"
|
||||
|
||||
Reference in New Issue
Block a user