Improvements to the plugins tab.

This commit is contained in:
Storm Dragon
2026-01-04 15:09:28 -05:00
parent 31cf0dbf5a
commit 1f6a2a06e5
12 changed files with 511 additions and 117 deletions
+152 -49
View File
@@ -106,6 +106,10 @@ if louis and not tablesdir:
DATE_FORMAT_ABBREVIATED_MDY, DATE_FORMAT_ABBREVIATED_YMD) = list(range(16)) DATE_FORMAT_ABBREVIATED_MDY, DATE_FORMAT_ABBREVIATED_YMD) = list(range(16))
class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper): class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
PLUGIN_COL_ENABLED = 0
PLUGIN_COL_DISPLAY = 1
PLUGIN_COL_CAN_TOGGLE = 2
PLUGIN_COL_NAME = 3
def __init__(self, fileName, windowName, prefsDict): def __init__(self, fileName, windowName, prefsDict):
"""Initialize the Cthulhu configuration GUI. """Initialize the Cthulhu configuration GUI.
@@ -135,8 +139,11 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
self.planeCellRendererText = None self.planeCellRendererText = None
self.pronunciationModel = None self.pronunciationModel = None
self.pronunciationView = None self.pronunciationView = None
self._plugin_checkboxes = {} self._plugin_treeview = None
self._plugin_listbox = None self._plugin_model = None
self._plugin_iters = {}
self._plugin_enabled_iter = None
self._plugin_disabled_iter = None
self._plugin_sources = [] self._plugin_sources = []
self._plugin_sources_entry = None self._plugin_sources_entry = None
self._plugin_sources_listbox = None self._plugin_sources_listbox = None
@@ -420,9 +427,41 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
pluginsScrolled = Gtk.ScrolledWindow() pluginsScrolled = Gtk.ScrolledWindow()
pluginsScrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) pluginsScrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
pluginsScrolled.set_size_request(-1, 200) pluginsScrolled.set_size_request(-1, 200)
self._plugin_listbox = Gtk.ListBox() self._plugin_model = Gtk.TreeStore(
self._plugin_listbox.set_selection_mode(Gtk.SelectionMode.NONE) GObject.TYPE_BOOLEAN, # enabled
pluginsScrolled.add(self._plugin_listbox) GObject.TYPE_STRING, # display text
GObject.TYPE_BOOLEAN, # can toggle
GObject.TYPE_STRING, # plugin name
)
self._plugin_treeview = Gtk.TreeView(model=self._plugin_model)
self._plugin_treeview.set_headers_visible(True)
self._plugin_treeview.set_enable_search(False)
self._plugin_treeview.connect("key-press-event", self._on_plugin_tree_key_press)
self._plugin_treeview.connect("row-activated", self._on_plugin_tree_row_activated)
toggle_renderer = Gtk.CellRendererToggle()
toggle_renderer.set_activatable(True)
toggle_renderer.connect("toggled", self._on_plugin_tree_toggled)
toggle_column = Gtk.TreeViewColumn(
"Enabled",
toggle_renderer,
active=self.PLUGIN_COL_ENABLED,
activatable=self.PLUGIN_COL_CAN_TOGGLE
)
toggle_column.add_attribute(toggle_renderer, "visible", self.PLUGIN_COL_CAN_TOGGLE)
self._plugin_treeview.append_column(toggle_column)
text_renderer = Gtk.CellRendererText()
text_renderer.set_property("ellipsize", Pango.EllipsizeMode.END)
text_column = Gtk.TreeViewColumn(
"Plugin",
text_renderer,
text=self.PLUGIN_COL_DISPLAY
)
text_column.set_expand(True)
self._plugin_treeview.append_column(text_column)
pluginsScrolled.add(self._plugin_treeview)
pluginsFrameBox.pack_start(pluginsScrolled, True, True, 0) pluginsFrameBox.pack_start(pluginsScrolled, True, True, 0)
pluginsPage.pack_start(pluginsFrame, True, True, 0) pluginsPage.pack_start(pluginsFrame, True, True, 0)
@@ -496,12 +535,12 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
listbox.remove(child) listbox.remove(child)
def _populate_plugin_list(self): def _populate_plugin_list(self):
if not self._plugin_listbox: if not self._plugin_treeview:
return return
try: try:
self._clear_listbox(self._plugin_listbox) self._plugin_model.clear()
self._plugin_checkboxes.clear() self._plugin_iters = {}
self._available_plugins = set() self._available_plugins = set()
self._plugin_canonical_map = {} self._plugin_canonical_map = {}
self._plugin_group_map = {} self._plugin_group_map = {}
@@ -526,60 +565,62 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
canonical_counts[canonical] = canonical_counts.get(canonical, 0) + 1 canonical_counts[canonical] = canonical_counts.get(canonical, 0) + 1
if info.builtin: if info.builtin:
canonical_builtins[canonical] = True canonical_builtins[canonical] = True
self._plugin_enabled_iter = self._plugin_model.append(
None,
[False, "Enabled plugins", False, ""]
)
self._plugin_disabled_iter = self._plugin_model.append(
None,
[False, "Disabled plugins", False, ""]
)
for plugin_info in sorted(plugin_infos, key=lambda item: (item.get_name() or item.get_module_name()).lower()): for plugin_info in sorted(plugin_infos, key=lambda item: (item.get_name() or item.get_module_name()).lower()):
plugin_name = plugin_info.get_module_name() plugin_name = plugin_info.get_module_name()
canonical_name = plugin_info.get_canonical_name() canonical_name = plugin_info.get_canonical_name()
if plugin_info.hidden or canonical_name == "PluginManager": if plugin_info.hidden or canonical_name == "PluginManager":
continue continue
row = Gtk.ListBoxRow() self._plugin_canonical_map[plugin_name] = canonical_name
row.set_activatable(False)
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
hbox.set_border_width(5)
checkbox = Gtk.CheckButton()
is_active = ( is_active = (
plugin_name in active_plugins plugin_name in active_plugins
or plugin_name.lower() in active_plugins_lower or plugin_name.lower() in active_plugins_lower
or (plugin_info.preferred_alias and (canonical_name in active_plugins or canonical_name.lower() in active_plugins_lower)) or (plugin_info.preferred_alias and (canonical_name in active_plugins or canonical_name.lower() in active_plugins_lower))
or plugin_info.builtin or plugin_info.builtin
) )
can_toggle = True
if canonical_builtins.get(canonical_name) and not plugin_info.builtin: if canonical_builtins.get(canonical_name) and not plugin_info.builtin:
is_active = False is_active = False
checkbox.set_active(is_active) can_toggle = False
if plugin_info.builtin:
checkbox.set_sensitive(False)
elif canonical_builtins.get(canonical_name):
checkbox.set_sensitive(False)
checkbox.connect("toggled", self._on_plugin_checkbox_toggled, plugin_name)
display_name = GLib.markup_escape_text(plugin_info.get_name() or plugin_name) if plugin_info.builtin:
info_text = f"<b>{display_name}</b>" can_toggle = False
display_name = plugin_info.get_name() or plugin_name
info_text = display_name
description = plugin_info.get_description() description = plugin_info.get_description()
if description: if description:
info_text += f"\n{GLib.markup_escape_text(description)}" info_text += f" - {description}"
version = plugin_info.get_version() version = plugin_info.get_version()
if version: if version:
info_text += f" (v{GLib.markup_escape_text(version)})" info_text += f" (v{version})"
if canonical_counts.get(canonical_name, 0) > 1: if canonical_counts.get(canonical_name, 0) > 1:
info_text += f"\nSource: {GLib.markup_escape_text(plugin_info.get_source_label())}" info_text += f" - Source: {plugin_info.get_source_label()}"
if canonical_builtins.get(canonical_name) and not plugin_info.builtin: if canonical_builtins.get(canonical_name) and not plugin_info.builtin:
info_text += "\nDisabled because a builtin plugin uses this name." info_text += " - Disabled because a builtin plugin uses this name."
label = Gtk.Label() parent_iter = self._plugin_enabled_iter if is_active else self._plugin_disabled_iter
label.set_markup(info_text) tree_iter = self._plugin_model.append(
label.set_halign(Gtk.Align.START) parent_iter,
label.set_line_wrap(True) [is_active, info_text, can_toggle, plugin_name]
)
hbox.pack_start(checkbox, False, False, 0) self._plugin_iters[plugin_name] = tree_iter
hbox.pack_start(label, True, True, 0)
row.add(hbox)
self._plugin_listbox.add(row)
self._plugin_checkboxes[plugin_name] = checkbox
self._plugin_canonical_map[plugin_name] = canonical_name
self._plugin_group_map.setdefault(canonical_name, []).append(plugin_name) self._plugin_group_map.setdefault(canonical_name, []).append(plugin_name)
self._plugin_listbox.show_all() self._plugin_treeview.collapse_all()
self._plugin_treeview.show_all()
except Exception as e: except Exception as e:
debug.printMessage(debug.LEVEL_WARNING, f"PREFERENCES DIALOG: Plugin list build failed: {e}", True) debug.printMessage(debug.LEVEL_WARNING, f"PREFERENCES DIALOG: Plugin list build failed: {e}", True)
@@ -685,30 +726,92 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
self._set_plugin_update_status(message) self._set_plugin_update_status(message)
self._populate_plugin_list() self._populate_plugin_list()
def _on_plugin_checkbox_toggled(self, checkbox, plugin_name): def _on_plugin_tree_toggled(self, renderer, path):
if not checkbox.get_active(): if not self._plugin_model:
return return
canonical_name = self._plugin_canonical_map.get(plugin_name)
if not canonical_name: tree_iter = self._plugin_model.get_iter(path)
if not tree_iter:
return return
for other_name in self._plugin_group_map.get(canonical_name, []):
if other_name == plugin_name: can_toggle = self._plugin_model.get_value(tree_iter, self.PLUGIN_COL_CAN_TOGGLE)
continue if not can_toggle:
other_checkbox = self._plugin_checkboxes.get(other_name) return
if other_checkbox and other_checkbox.get_active():
other_checkbox.set_active(False) plugin_name = self._plugin_model.get_value(tree_iter, self.PLUGIN_COL_NAME)
if not plugin_name:
return
current_active = self._plugin_model.get_value(tree_iter, self.PLUGIN_COL_ENABLED)
new_active = not current_active
if new_active:
canonical_name = self._plugin_canonical_map.get(plugin_name)
for other_name in self._plugin_group_map.get(canonical_name, []):
if other_name == plugin_name:
continue
self._set_plugin_row_active(other_name, False)
self._set_plugin_row_active(plugin_name, new_active)
def _on_plugin_tree_key_press(self, widget, event):
if event.keyval != Gdk.KEY_space:
return False
selection = self._plugin_treeview.get_selection()
model, tree_iter = selection.get_selected()
if not tree_iter:
return False
path = model.get_path(tree_iter)
self._on_plugin_tree_toggled(None, path)
return True
def _on_plugin_tree_row_activated(self, treeview, path, column):
self._on_plugin_tree_toggled(None, path)
def _set_plugin_row_active(self, plugin_name, is_active):
tree_iter = self._plugin_iters.get(plugin_name)
if not tree_iter:
return
current_active = self._plugin_model.get_value(tree_iter, self.PLUGIN_COL_ENABLED)
if current_active == is_active:
return
can_toggle = self._plugin_model.get_value(tree_iter, self.PLUGIN_COL_CAN_TOGGLE)
display_text = self._plugin_model.get_value(tree_iter, self.PLUGIN_COL_DISPLAY)
selection = self._plugin_treeview.get_selection()
model, selected_iter = selection.get_selected()
was_selected = selected_iter == tree_iter
self._plugin_model.remove(tree_iter)
parent_iter = self._plugin_enabled_iter if is_active else self._plugin_disabled_iter
new_iter = self._plugin_model.append(
parent_iter,
[is_active, display_text, can_toggle, plugin_name]
)
self._plugin_iters[plugin_name] = new_iter
if was_selected:
path = self._plugin_model.get_path(new_iter)
self._plugin_treeview.expand_to_path(path)
selection.select_path(path)
def _get_active_plugins_from_ui(self): def _get_active_plugins_from_ui(self):
existing_plugins = list(self.prefsDict.get("activePlugins", settings.activePlugins) or []) existing_plugins = list(self.prefsDict.get("activePlugins", settings.activePlugins) or [])
preserved_plugins = [ preserved_plugins = [
name for name in existing_plugins name for name in existing_plugins
if name not in self._plugin_checkboxes and name in self._available_plugins if name not in self._plugin_iters and name in self._available_plugins
] ]
selected_plugins = [] selected_plugins = []
for canonical_name, plugin_names in self._plugin_group_map.items(): for canonical_name, plugin_names in self._plugin_group_map.items():
active_in_group = [ active_in_group = [
name for name in plugin_names name for name in plugin_names
if self._plugin_checkboxes.get(name) and self._plugin_checkboxes[name].get_active() if self._plugin_iters.get(name)
and self._plugin_model.get_value(self._plugin_iters[name], self.PLUGIN_COL_ENABLED)
] ]
if active_in_group: if active_in_group:
selected_plugins.append(active_in_group[-1]) selected_plugins.append(active_in_group[-1])
+17 -1
View File
@@ -876,9 +876,25 @@ class KeyboardEvent(InputEvent):
return method.__func__ == self._handler.function return method.__func__ == self._handler.function
def _should_interrupt_presentation_on_press(self):
if not settings.gameMode:
return True
if cthulhu_state.bypassNextCommand and not self.is_modifier_key():
return False
if self._handler or self._consumer:
return True
if self.isCthulhuModifier():
return True
return False
def _present(self, inputEvent=None): def _present(self, inputEvent=None):
if self.is_pressed_key(): if self.is_pressed_key():
self._script.presentationInterrupt() if self._should_interrupt_presentation_on_press():
self._script.presentationInterrupt()
if self._script.learnModePresenter.is_active(): if self._script.learnModePresenter.is_active():
return False return False
+6
View File
@@ -2333,6 +2333,12 @@ STOP_CTHULHU = _("Cthulhu lurks beneath the waves.")
SLEEP_MODE_ENABLED_FOR = _("Sleep mode enabled for %s") SLEEP_MODE_ENABLED_FOR = _("Sleep mode enabled for %s")
SLEEP_MODE_DISABLED_FOR = _("Sleep mode disabled for %s") SLEEP_MODE_DISABLED_FOR = _("Sleep mode disabled for %s")
# Translators: This message is presented to the user when game mode is enabled.
GAME_MODE_ENABLED = _("Game mode enabled")
# Translators: This message is presented to the user when game mode is disabled.
GAME_MODE_DISABLED = _("Game mode disabled")
# Translators: This message means speech synthesis is not installed or working. # Translators: This message means speech synthesis is not installed or working.
SPEECH_UNAVAILABLE = _("Speech is unavailable.") SPEECH_UNAVAILABLE = _("Speech is unavailable.")
+39
View File
@@ -34,6 +34,14 @@ logger = logging.getLogger(__name__)
if PLUGIN_DEBUG: if PLUGIN_DEBUG:
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
LEGACY_PLUGIN_NAME_ALIASES = {
"ocrdesktop": "OCR",
}
LEGACY_PLUGIN_DIR_ALIASES = {
"OCRDesktop": "OCR",
}
_manager = None _manager = None
def getManager(): def getManager():
@@ -450,6 +458,18 @@ class PluginSystemManager:
return False return False
canonical_name = os.path.basename(plugin_dir) canonical_name = os.path.basename(plugin_dir)
canonical_override = LEGACY_PLUGIN_DIR_ALIASES.get(canonical_name)
if canonical_override:
if canonical_override in self._plugin_name_index:
logger.info(
f"Skipping deprecated plugin directory {canonical_name} because "
f"{canonical_override} is already registered."
)
return True
logger.info(
f"Registering deprecated plugin directory {canonical_name} as {canonical_override}."
)
canonical_name = canonical_override
origin, source_id = self._get_origin_info(plugin_dir) origin, source_id = self._get_origin_info(plugin_dir)
module_name = self._make_unique_plugin_id(canonical_name, source_id) module_name = self._make_unique_plugin_id(canonical_name, source_id)
if canonical_name != module_name: if canonical_name != module_name:
@@ -623,6 +643,25 @@ class PluginSystemManager:
logger.info(f"PLUGIN SYSTEM: setActivePlugins called with: {activePlugins}") logger.info(f"PLUGIN SYSTEM: setActivePlugins called with: {activePlugins}")
logger.info(f"Setting active plugins: {activePlugins}") logger.info(f"Setting active plugins: {activePlugins}")
original_active = list(activePlugins or [])
normalized_requested = []
for name in original_active:
if not name:
continue
name_lower = name.lower()
alias = LEGACY_PLUGIN_NAME_ALIASES.get(name_lower)
if alias:
logger.info(f"Mapping legacy plugin name {name} to {alias}")
name = alias
normalized_requested.append(name)
activePlugins = normalized_requested
if activePlugins != original_active:
try:
from . import settings as settings_module
settings_module.activePlugins = list(activePlugins)
except Exception:
logger.debug("Unable to normalize settings.activePlugins for legacy plugin aliases.")
# Make sure we have scanned for plugins first # Make sure we have scanned for plugins first
if not self._plugins: if not self._plugins:
logger.info("No plugins found, rescanning...") logger.info("No plugins found, rescanning...")
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env python3
#
# Copyright (c) 2025 Stormux
#
# 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.
"""GameMode plugin package."""
from .plugin import GameMode
__all__ = ['GameMode']
+14
View File
@@ -0,0 +1,14 @@
gamemode_python_sources = files([
'__init__.py',
'plugin.py'
])
python3.install_sources(
gamemode_python_sources,
subdir: 'cthulhu/plugins/GameMode'
)
install_data(
'plugin.info',
install_dir: python3.get_install_dir() / 'cthulhu' / 'plugins' / 'GameMode'
)
+8
View File
@@ -0,0 +1,8 @@
name = GameMode
version = 1.0.0
description = Reduces speech interruptions while holding keys
authors = Stormux <storm_dragon@stormux.org>
website = https://git.stormux.org/storm/cthulhu
copyright = Copyright 2025
builtin = false
hidden = false
+97
View File
@@ -0,0 +1,97 @@
#!/usr/bin/env python3
#
# Copyright (c) 2025 Stormux
#
# 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.
"""GameMode plugin for Cthulhu - reduce speech interruption while gaming."""
from cthulhu.plugin import Plugin, cthulhu_hookimpl
from cthulhu import debug
from cthulhu import messages
from cthulhu import settings_manager
settingsManager = settings_manager.getManager()
class GameMode(Plugin):
"""Plugin that reduces speech interruption while holding keys."""
def __init__(self, *args, **kwargs):
"""Initialize the GameMode plugin."""
super().__init__(*args, **kwargs)
self._activated = False
self._kbBinding = None
debug.printMessage(debug.LEVEL_INFO, "GameMode: Plugin initialized", True)
@cthulhu_hookimpl
def activate(self, plugin=None):
"""Activate the GameMode plugin."""
if plugin is not None and plugin is not self:
return
if self._activated:
debug.printMessage(debug.LEVEL_INFO, "GameMode: Already activated, skipping", True)
return
try:
debug.printMessage(debug.LEVEL_INFO, "GameMode: Plugin activation starting", True)
self._register_keybinding()
self._activated = True
debug.printMessage(debug.LEVEL_INFO, "GameMode: Plugin activated successfully", True)
return True
except Exception as error:
debug.printMessage(debug.LEVEL_INFO, f"GameMode: ERROR activating plugin: {error}", True)
return False
@cthulhu_hookimpl
def deactivate(self, plugin=None):
"""Deactivate the GameMode plugin."""
if plugin is not None and plugin is not self:
return
try:
debug.printMessage(debug.LEVEL_INFO, "GameMode: Plugin deactivation starting", True)
settingsManager.setSetting('gameMode', False)
self._activated = False
debug.printMessage(debug.LEVEL_INFO, "GameMode: Plugin deactivated successfully", True)
return True
except Exception as error:
debug.printMessage(debug.LEVEL_INFO, f"GameMode: ERROR deactivating plugin: {error}", True)
return False
def _register_keybinding(self):
"""Register the Cthulhu+Control+G keybinding for toggling game mode."""
try:
if not self.app:
debug.printMessage(debug.LEVEL_INFO, "GameMode: No app reference for keybinding", True)
return
gestureString = "kb:cthulhu+control+g"
description = "Toggle game mode"
self._kbBinding = self.registerGestureByString(
self._toggle_game_mode,
description,
gestureString
)
if self._kbBinding:
debug.printMessage(debug.LEVEL_INFO, f"GameMode: Registered keybinding {gestureString}", True)
else:
debug.printMessage(debug.LEVEL_INFO, f"GameMode: Failed to register keybinding {gestureString}", True)
except Exception as error:
debug.printMessage(debug.LEVEL_INFO, f"GameMode: Error registering keybinding: {error}", True)
def _toggle_game_mode(self, script, inputEvent=None):
"""Toggle game mode on or off."""
currentValue = bool(settingsManager.getSetting('gameMode'))
newValue = not currentValue
settingsManager.setSetting('gameMode', newValue)
message = messages.GAME_MODE_ENABLED if newValue else messages.GAME_MODE_DISABLED
if script:
script.presentMessage(message)
return True
+147 -54
View File
@@ -16,7 +16,7 @@ from pathlib import Path
import gi import gi
gi.require_version('Gtk', '3.0') gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GLib from gi.repository import Gtk, Gdk, Pango
from cthulhu.plugin import Plugin, cthulhu_hookimpl from cthulhu.plugin import Plugin, cthulhu_hookimpl
from cthulhu import debug from cthulhu import debug
@@ -29,12 +29,18 @@ _settingsManager = settings_manager.getManager()
class PluginManager(Plugin): class PluginManager(Plugin):
"""Plugin that provides a GUI interface for managing other plugins.""" """Plugin that provides a GUI interface for managing other plugins."""
PLUGIN_COL_ENABLED = 0
PLUGIN_COL_DISPLAY = 1
PLUGIN_COL_CAN_TOGGLE = 2
PLUGIN_COL_NAME = 3
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
"""Initialize the PluginManager plugin.""" """Initialize the PluginManager plugin."""
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self._kb_binding = None self._kb_binding = None
self._dialog = None self._dialog = None
self._plugin_checkboxes = {} self._plugin_treeview = None
self._plugin_model = None
self._activated = False self._activated = False
debug.printMessage(debug.LEVEL_INFO, "PluginManager: Plugin initialized", True) debug.printMessage(debug.LEVEL_INFO, "PluginManager: Plugin initialized", True)
@@ -164,10 +170,36 @@ class PluginManager(Plugin):
scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
scrolled.set_size_request(-1, 200) scrolled.set_size_request(-1, 200)
# Create list box for plugins self._plugin_model = Gtk.TreeStore(bool, str, bool, str)
self._plugin_listbox = Gtk.ListBox() self._plugin_treeview = Gtk.TreeView(model=self._plugin_model)
self._plugin_listbox.set_selection_mode(Gtk.SelectionMode.NONE) self._plugin_treeview.set_headers_visible(True)
scrolled.add(self._plugin_listbox) self._plugin_treeview.set_enable_search(False)
self._plugin_treeview.connect("key-press-event", self._on_plugin_list_key_press)
self._plugin_treeview.connect("row-activated", self._on_plugin_row_activated)
toggle_renderer = Gtk.CellRendererToggle()
toggle_renderer.set_activatable(True)
toggle_renderer.connect("toggled", self._on_plugin_toggled)
toggle_column = Gtk.TreeViewColumn(
"Enabled",
toggle_renderer,
active=self.PLUGIN_COL_ENABLED,
activatable=self.PLUGIN_COL_CAN_TOGGLE
)
toggle_column.add_attribute(toggle_renderer, "visible", self.PLUGIN_COL_CAN_TOGGLE)
self._plugin_treeview.append_column(toggle_column)
text_renderer = Gtk.CellRendererText()
text_renderer.set_property("ellipsize", Pango.EllipsizeMode.END)
text_column = Gtk.TreeViewColumn(
"Plugin",
text_renderer,
text=self.PLUGIN_COL_DISPLAY
)
text_column.set_expand(True)
self._plugin_treeview.append_column(text_column)
scrolled.add(self._plugin_treeview)
content_area.pack_start(scrolled, True, True, 0) content_area.pack_start(scrolled, True, True, 0)
@@ -197,49 +229,41 @@ class PluginManager(Plugin):
debug.printMessage(debug.LEVEL_INFO, f"PluginManager: Found {len(available_plugins)} plugins", True) debug.printMessage(debug.LEVEL_INFO, f"PluginManager: Found {len(available_plugins)} plugins", True)
# Clear existing checkboxes # Clear existing model rows
self._plugin_checkboxes.clear() self._plugin_model.clear()
# Add each plugin as a checkbox (except PluginManager itself) # Add each plugin as a checkbox (except PluginManager itself)
enabled_iter = self._plugin_model.append(
None,
[False, "Enabled plugins", False, ""]
)
disabled_iter = self._plugin_model.append(
None,
[False, "Disabled plugins", False, ""]
)
for plugin_name, plugin_info in sorted(available_plugins.items()): for plugin_name, plugin_info in sorted(available_plugins.items()):
# Skip PluginManager to prevent users from disabling plugin management # Skip PluginManager to prevent users from disabling plugin management
if plugin_name == "PluginManager": if plugin_name == "PluginManager":
continue continue
# Create row container display_name = plugin_info.get('name', plugin_name)
row = Gtk.ListBoxRow() display_text = display_name
row.set_activatable(False) description = plugin_info.get('description')
if description:
display_text += f" - {description}"
version = plugin_info.get('version')
if version:
display_text += f" (v{version})"
# Create horizontal box for checkbox and info is_active = plugin_name in active_plugins
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) parent_iter = enabled_iter if is_active else disabled_iter
hbox.set_border_width(5) self._plugin_model.append(
parent_iter,
[is_active, display_text, True, plugin_name]
)
# Create checkbox self._plugin_treeview.collapse_all()
checkbox = Gtk.CheckButton()
checkbox.set_active(plugin_name in active_plugins)
checkbox.connect("toggled", self._on_plugin_toggled, plugin_name)
# Create plugin info label
info_text = f"<b>{plugin_info.get('name', plugin_name)}</b>"
if plugin_info.get('description'):
info_text += f"\n{plugin_info['description']}"
if plugin_info.get('version'):
info_text += f" (v{plugin_info['version']})"
label = Gtk.Label()
label.set_markup(info_text)
label.set_halign(Gtk.Align.START)
label.set_line_wrap(True)
# Pack widgets
hbox.pack_start(checkbox, False, False, 0)
hbox.pack_start(label, True, True, 0)
row.add(hbox)
self._plugin_listbox.add(row)
# Store checkbox reference
self._plugin_checkboxes[plugin_name] = checkbox
except Exception as e: except Exception as e:
logger.error(f"PluginManager: Error populating plugin list: {e}") logger.error(f"PluginManager: Error populating plugin list: {e}")
@@ -344,32 +368,101 @@ class PluginManager(Plugin):
except Exception as e: except Exception as e:
logger.error(f"PluginManager: Error scanning directory {directory}: {e}") logger.error(f"PluginManager: Error scanning directory {directory}: {e}")
def _on_plugin_toggled(self, checkbox, plugin_name): def _on_plugin_toggled(self, renderer, path):
"""Handle plugin checkbox toggle.""" """Handle plugin toggle via TreeView."""
try:
tree_iter = self._plugin_model.get_iter(path)
if not tree_iter:
return
can_toggle = self._plugin_model.get_value(tree_iter, self.PLUGIN_COL_CAN_TOGGLE)
if not can_toggle:
return
is_active = self._plugin_model.get_value(tree_iter, self.PLUGIN_COL_ENABLED)
plugin_name = self._plugin_model.get_value(tree_iter, self.PLUGIN_COL_NAME)
new_active = not is_active
self._plugin_model.set_value(tree_iter, self.PLUGIN_COL_ENABLED, new_active)
self._set_plugin_active(plugin_name, new_active)
self._rebuild_plugin_groups()
except Exception as error:
logger.error(f"PluginManager: Error toggling plugin at path {path}: {error}")
debug.printMessage(debug.LEVEL_INFO, f"PluginManager: Error toggling plugin at path {path}: {error}", True)
def _on_plugin_list_key_press(self, widget, event):
"""Toggle plugin on Space while keeping Tab navigation outside the list."""
if event.keyval != Gdk.KEY_space:
return False
selection = self._plugin_treeview.get_selection()
model, tree_iter = selection.get_selected()
if not tree_iter:
return False
path = model.get_path(tree_iter)
self._on_plugin_toggled(None, path)
return True
def _on_plugin_row_activated(self, treeview, path, column):
"""Toggle plugin when activating a row."""
self._on_plugin_toggled(None, path)
def _rebuild_plugin_groups(self):
"""Rebuild the tree so enabled/disabled groups stay accurate."""
if not self._plugin_model:
return
selection = self._plugin_treeview.get_selection()
model, tree_iter = selection.get_selected()
selected_name = None
if tree_iter:
selected_name = model.get_value(tree_iter, self.PLUGIN_COL_NAME)
self._populate_plugin_list()
if selected_name:
self._select_plugin_row(selected_name)
def _select_plugin_row(self, plugin_name):
"""Select the row for the given plugin name."""
def _walk(model, tree_iter):
while tree_iter:
name = model.get_value(tree_iter, self.PLUGIN_COL_NAME)
if name == plugin_name:
path = model.get_path(tree_iter)
self._plugin_treeview.expand_to_path(path)
self._plugin_treeview.get_selection().select_path(path)
return True
if model.iter_has_child(tree_iter):
child = model.iter_children(tree_iter)
if _walk(model, child):
return True
tree_iter = model.iter_next(tree_iter)
return False
root = self._plugin_model.get_iter_first()
_walk(self._plugin_model, root)
def _set_plugin_active(self, plugin_name, is_active):
"""Update settings to enable or disable a plugin."""
try: try:
is_active = checkbox.get_active()
debug.printMessage(debug.LEVEL_INFO, f"PluginManager: Plugin {plugin_name} toggled to {'active' if is_active else 'inactive'}", True) debug.printMessage(debug.LEVEL_INFO, f"PluginManager: Plugin {plugin_name} toggled to {'active' if is_active else 'inactive'}", True)
# Get current active plugins # Get current active plugins
active_plugins = _settingsManager.getSetting('activePlugins') or [] active_plugins = _settingsManager.getSetting('activePlugins') or []
active_plugins = list(active_plugins) # Make a copy active_plugins = list(active_plugins)
# Update the list
if is_active and plugin_name not in active_plugins: if is_active and plugin_name not in active_plugins:
active_plugins.append(plugin_name) active_plugins.append(plugin_name)
elif not is_active and plugin_name in active_plugins: elif not is_active and plugin_name in active_plugins:
active_plugins.remove(plugin_name) active_plugins.remove(plugin_name)
# Save updated settings
_settingsManager.setSetting('activePlugins', active_plugins) _settingsManager.setSetting('activePlugins', active_plugins)
# Save to disk using the backend directly
try: try:
# Get current general settings
current_general = _settingsManager.getGeneralSettings() current_general = _settingsManager.getGeneralSettings()
current_general['activePlugins'] = active_plugins current_general['activePlugins'] = active_plugins
# Save using the backend
backend = _settingsManager._backend backend = _settingsManager._backend
if backend: if backend:
backend.saveDefaultSettings( backend.saveDefaultSettings(
@@ -381,14 +474,14 @@ class PluginManager(Plugin):
else: else:
debug.printMessage(debug.LEVEL_INFO, "PluginManager: No backend available for saving", True) debug.printMessage(debug.LEVEL_INFO, "PluginManager: No backend available for saving", True)
except Exception as save_e: except Exception as save_error:
debug.printMessage(debug.LEVEL_INFO, f"PluginManager: Error saving via backend: {save_e}", True) debug.printMessage(debug.LEVEL_INFO, f"PluginManager: Error saving via backend: {save_error}", True)
debug.printMessage(debug.LEVEL_INFO, f"PluginManager: Updated active plugins: {active_plugins}", True) debug.printMessage(debug.LEVEL_INFO, f"PluginManager: Updated active plugins: {active_plugins}", True)
except Exception as e: except Exception as error:
logger.error(f"PluginManager: Error toggling plugin {plugin_name}: {e}") logger.error(f"PluginManager: Error toggling plugin {plugin_name}: {error}")
debug.printMessage(debug.LEVEL_INFO, f"PluginManager: Error toggling {plugin_name}: {e}", True) debug.printMessage(debug.LEVEL_INFO, f"PluginManager: Error toggling {plugin_name}: {error}", True)
def _on_dialog_response(self, dialog, response_id): def _on_dialog_response(self, dialog, response_id):
+1
View File
@@ -4,6 +4,7 @@ subdir('ByeCthulhu')
subdir('Clipboard') subdir('Clipboard')
subdir('DisplayVersion') subdir('DisplayVersion')
subdir('HelloCthulhu') subdir('HelloCthulhu')
subdir('GameMode')
subdir('IndentationAudio') subdir('IndentationAudio')
subdir('OCR') subdir('OCR')
subdir('PluginManager') subdir('PluginManager')
+1
View File
@@ -824,6 +824,7 @@ class Script(script.Script):
sleepModeManager.toggleSleepMode(self) sleepModeManager.toggleSleepMode(self)
return True return True
def bypassNextCommand(self, inputEvent=None): def bypassNextCommand(self, inputEvent=None):
"""Causes the next keyboard command to be ignored by Cthulhu """Causes the next keyboard command to be ignored by Cthulhu
and passed along to the current application. and passed along to the current application.
+2
View File
@@ -64,6 +64,7 @@ userCustomizableSettings = [
"enableEchoByWord", "enableEchoByWord",
"enableEchoBySentence", "enableEchoBySentence",
"enableKeyEcho", "enableKeyEcho",
"gameMode",
"enableAlphabeticKeys", "enableAlphabeticKeys",
"enableNumericKeys", "enableNumericKeys",
"enablePunctuationKeys", "enablePunctuationKeys",
@@ -317,6 +318,7 @@ verbalizePunctuationStyle = PUNCTUATION_STYLE_MOST
speechVerbosityLevel = VERBOSITY_LEVEL_VERBOSE speechVerbosityLevel = VERBOSITY_LEVEL_VERBOSE
messagesAreDetailed = True messagesAreDetailed = True
enablePauseBreaks = True enablePauseBreaks = True
gameMode = False
speakDescription = True speakDescription = True
speakContextBlockquote = True speakContextBlockquote = True
speakContextPanel = True speakContextPanel = True