Compare commits
40 Commits
2024.12.18
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
d36b664319 | ||
|
02be96aa69 | ||
|
48575ab6cd | ||
|
2c28021ed4 | ||
|
8a79725df8 | ||
|
1b4c4916e3 | ||
|
35a83327ac | ||
|
c712bea421 | ||
|
815d39fc3f | ||
|
231d74efa0 | ||
|
7876a18c12 | ||
|
0b7cf681c3 | ||
|
4b8ebcb599 | ||
|
d6a373c726 | ||
|
dfe20fca30 | ||
|
3f7d60763d | ||
|
084d4fe85f | ||
|
6bbe6e47fc | ||
|
312476bbed | ||
|
0005d5ec71 | ||
|
88ad9833d2 | ||
|
654f1acc21 | ||
|
dfb53fff89 | ||
|
9cdb9f74e5 | ||
|
41dae26d90 | ||
|
edc1cbf7af | ||
|
d3d268004b | ||
|
a8e16fcf01 | ||
|
6bbf3d0e67 | ||
|
cbe3424e29 | ||
|
1cf566c37c | ||
|
936153f307 | ||
|
29f8697a9f | ||
|
ba735c554b | ||
|
1c9ca14272 | ||
|
d6af91bf42 | ||
|
5dd872535d | ||
|
8e9ea3af5a | ||
|
a40087cd20 | ||
|
35acddeb8f |
15
configure.ac
15
configure.ac
@ -31,7 +31,7 @@ PKG_CHECK_MODULES([PYGOBJECT], [pygobject-3.0 >= pygobject_required_version])
|
|||||||
PKG_CHECK_MODULES([ATSPI2], [atspi-2 >= atspi_required_version])
|
PKG_CHECK_MODULES([ATSPI2], [atspi-2 >= atspi_required_version])
|
||||||
PKG_CHECK_MODULES([ATKBRIDGE], [atk-bridge-2.0 >= atkbridge_required_version])
|
PKG_CHECK_MODULES([ATKBRIDGE], [atk-bridge-2.0 >= atkbridge_required_version])
|
||||||
PKG_CHECK_MODULES([GSTREAMER], [gstreamer-1.0], [gstreamer="yes"], [gstreamer="no"])
|
PKG_CHECK_MODULES([GSTREAMER], [gstreamer-1.0], [gstreamer="yes"], [gstreamer="no"])
|
||||||
PKG_CHECK_MODULES([LIBPEAS], [libpeas-1.0], [libpeas="yes"], [libpeas="no"])
|
PKG_CHECK_MODULES([LIBPEAS], [libpeas-1.0])
|
||||||
|
|
||||||
dnl Needed programs
|
dnl Needed programs
|
||||||
AC_PROG_INSTALL
|
AC_PROG_INSTALL
|
||||||
@ -46,6 +46,7 @@ AM_PATH_PYTHON(3.3)
|
|||||||
|
|
||||||
AM_CHECK_PYMOD(gi,,,[AC_MSG_ERROR(Could not find python module: gi)])
|
AM_CHECK_PYMOD(gi,,,[AC_MSG_ERROR(Could not find python module: gi)])
|
||||||
AM_CHECK_PYMOD(json,,,[AC_MSG_ERROR(Could not find python module: json)])
|
AM_CHECK_PYMOD(json,,,[AC_MSG_ERROR(Could not find python module: json)])
|
||||||
|
AM_CHECK_PYMOD(pluggy,,[pluggy_available="yes"],[pluggy_available="no"])
|
||||||
AM_CHECK_PYMOD(brlapi,,[brlapi_available="yes"],[brlapi_available="no"])
|
AM_CHECK_PYMOD(brlapi,,[brlapi_available="yes"],[brlapi_available="no"])
|
||||||
AM_CHECK_PYMOD(speechd,,[speechd_available="yes"],[speechd_available="no"])
|
AM_CHECK_PYMOD(speechd,,[speechd_available="yes"],[speechd_available="no"])
|
||||||
AC_ARG_WITH([liblouis],
|
AC_ARG_WITH([liblouis],
|
||||||
@ -110,6 +111,7 @@ src/cthulhu/scripts/apps/SeaMonkey/Makefile
|
|||||||
src/cthulhu/scripts/apps/smuxi-frontend-gnome/Makefile
|
src/cthulhu/scripts/apps/smuxi-frontend-gnome/Makefile
|
||||||
src/cthulhu/scripts/apps/Thunderbird/Makefile
|
src/cthulhu/scripts/apps/Thunderbird/Makefile
|
||||||
src/cthulhu/scripts/apps/xfwm4/Makefile
|
src/cthulhu/scripts/apps/xfwm4/Makefile
|
||||||
|
src/cthulhu/scripts/sleepmode/Makefile
|
||||||
src/cthulhu/scripts/switcher/Makefile
|
src/cthulhu/scripts/switcher/Makefile
|
||||||
src/cthulhu/scripts/terminal/Makefile
|
src/cthulhu/scripts/terminal/Makefile
|
||||||
src/cthulhu/scripts/web/Makefile
|
src/cthulhu/scripts/web/Makefile
|
||||||
@ -125,14 +127,10 @@ src/cthulhu/scripts/toolkits/gtk/Makefile
|
|||||||
src/cthulhu/plugins/Makefile
|
src/cthulhu/plugins/Makefile
|
||||||
src/cthulhu/plugins/ByeCthulhu/Makefile
|
src/cthulhu/plugins/ByeCthulhu/Makefile
|
||||||
src/cthulhu/plugins/HelloCthulhu/Makefile
|
src/cthulhu/plugins/HelloCthulhu/Makefile
|
||||||
src/cthulhu/plugins/PluginManager/Makefile
|
|
||||||
src/cthulhu/plugins/Clipboard/Makefile
|
src/cthulhu/plugins/Clipboard/Makefile
|
||||||
src/cthulhu/plugins/HelloWorld/Makefile
|
src/cthulhu/plugins/DisplayVersion/Makefile
|
||||||
src/cthulhu/plugins/CapsLockHack/Makefile
|
src/cthulhu/plugins/hello_world/Makefile
|
||||||
src/cthulhu/plugins/SelfVoice/Makefile
|
src/cthulhu/plugins/self_voice/Makefile
|
||||||
src/cthulhu/plugins/Date/Makefile
|
|
||||||
src/cthulhu/plugins/Time/Makefile
|
|
||||||
src/cthulhu/plugins/MouseReview/Makefile
|
|
||||||
src/cthulhu/plugins/SimplePluginSystem/Makefile
|
src/cthulhu/plugins/SimplePluginSystem/Makefile
|
||||||
src/cthulhu/backends/Makefile
|
src/cthulhu/backends/Makefile
|
||||||
src/cthulhu/cthulhu_bin.py
|
src/cthulhu/cthulhu_bin.py
|
||||||
@ -166,6 +164,7 @@ if test "$have_libpeas" = "no"; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo
|
echo
|
||||||
|
echo Use pluggy: $pluggy_available
|
||||||
echo Use speech-dispatcher: $speechd_available
|
echo Use speech-dispatcher: $speechd_available
|
||||||
echo Use brltty: $brlapi_available
|
echo Use brltty: $brlapi_available
|
||||||
echo Use liblouis: $louis_available
|
echo Use liblouis: $louis_available
|
||||||
|
20
contrib/toggle.screenreader.gschema.xml
Normal file
20
contrib/toggle.screenreader.gschema.xml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<schemalist gettext-domain="slint">
|
||||||
|
<schema id="toggle.screenreader" path="/toggle/screenreader/">
|
||||||
|
<key name="binding" type="s">
|
||||||
|
<default>'<Shift><Alt>v'</default>
|
||||||
|
<summary>Keybinding</summary>
|
||||||
|
<description>Keybinding associated to toggle screen reader.</description>
|
||||||
|
</key>
|
||||||
|
<key name="action" type="s">
|
||||||
|
<default>'/opt/I38/scripts/toggle_screenreader.sh 1>/dev/null'</default>
|
||||||
|
<summary>Command</summary>
|
||||||
|
<description>Command to toggle the screen reader between orca and cthulhu.</description>
|
||||||
|
</key>
|
||||||
|
<key name="name" type="s">
|
||||||
|
<default>'Toggle screenreader'</default>
|
||||||
|
<summary>Name</summary>
|
||||||
|
<description>Description associated to toggle screen reader.</description>
|
||||||
|
</key>
|
||||||
|
</schema>
|
||||||
|
</schemalist>
|
@ -460,6 +460,9 @@ TOGGLE_SPEECH = _("Toggle the silencing of speech")
|
|||||||
# This string describes that command.
|
# This string describes that command.
|
||||||
TOGGLE_SPEECH_VERBOSITY = _("Toggle speech verbosity level")
|
TOGGLE_SPEECH_VERBOSITY = _("Toggle speech verbosity level")
|
||||||
|
|
||||||
|
# Translators: this string is associated with the keyboard shortcut to toggle sleepmode
|
||||||
|
TOGGLE_SLEEP_MODE = _("Toggle sleep mode")
|
||||||
|
|
||||||
# Translators: this string is associated with the keyboard shortcut to quit
|
# Translators: this string is associated with the keyboard shortcut to quit
|
||||||
# Cthulhu.
|
# Cthulhu.
|
||||||
QUIT_CTHULHU = _("Quit the screen reader")
|
QUIT_CTHULHU = _("Quit the screen reader")
|
||||||
|
@ -57,6 +57,9 @@ keymap = (
|
|||||||
("BackSpace", defaultModifierMask, CTHULHU_MODIFIER_MASK,
|
("BackSpace", defaultModifierMask, CTHULHU_MODIFIER_MASK,
|
||||||
"bypassNextCommandHandler"),
|
"bypassNextCommandHandler"),
|
||||||
|
|
||||||
|
("q", defaultModifierMask, CTHULHU_CTRL_ALT_MODIFIER_MASK, CTHULHU_SHIFT_MODIFIER_MASK,
|
||||||
|
"toggleSleepModeHandler"),
|
||||||
|
|
||||||
("q", defaultModifierMask, CTHULHU_MODIFIER_MASK,
|
("q", defaultModifierMask, CTHULHU_MODIFIER_MASK,
|
||||||
"shutdownHandler"),
|
"shutdownHandler"),
|
||||||
|
|
||||||
|
@ -34,6 +34,108 @@ __copyright__ = "Copyright (c) 2004-2009 Sun Microsystems Inc." \
|
|||||||
__license__ = "LGPL"
|
__license__ = "LGPL"
|
||||||
|
|
||||||
import faulthandler
|
import faulthandler
|
||||||
|
|
||||||
|
class APIHelper:
|
||||||
|
"""Helper class for plugin API interactions, including keybindings."""
|
||||||
|
|
||||||
|
def __init__(self, app):
|
||||||
|
"""Initialize the APIHelper.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
- app: the Cthulhu application
|
||||||
|
"""
|
||||||
|
self.app = app
|
||||||
|
self._gestureBindings = {}
|
||||||
|
|
||||||
|
def registerGestureByString(self, function, name, gestureString,
|
||||||
|
inputEventType='default', normalizer='cthulhu',
|
||||||
|
learnModeEnabled=True, contextName=None):
|
||||||
|
"""Register a gesture by string.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
- function: the function to call when the gesture is performed
|
||||||
|
- name: a human-readable name for this gesture
|
||||||
|
- gestureString: string representation of the gesture (e.g., 'kb:cthulhu+z')
|
||||||
|
- inputEventType: the type of input event
|
||||||
|
- normalizer: the normalizer to use
|
||||||
|
- learnModeEnabled: whether this should be available in learn mode
|
||||||
|
- contextName: the context for this gesture (e.g., plugin name)
|
||||||
|
|
||||||
|
Returns the binding ID or None if registration failed
|
||||||
|
"""
|
||||||
|
if not gestureString.startswith("kb:"):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Extract the key portion from the gesture string
|
||||||
|
key = gestureString.split(":", 1)[1]
|
||||||
|
|
||||||
|
# Handle Cthulhu modifier specially
|
||||||
|
if "cthulhu+" in key.lower():
|
||||||
|
from . import keybindings
|
||||||
|
key_parts = key.lower().split("+")
|
||||||
|
|
||||||
|
# Determine appropriate modifier mask
|
||||||
|
modifiers = keybindings.CTHULHU_MODIFIER_MASK
|
||||||
|
|
||||||
|
# Extract the final key (without modifiers)
|
||||||
|
final_key = key_parts[-1]
|
||||||
|
|
||||||
|
# Check for additional modifiers
|
||||||
|
if "shift" in key_parts:
|
||||||
|
modifiers = keybindings.CTHULHU_SHIFT_MODIFIER_MASK
|
||||||
|
elif "ctrl" in key_parts or "control" in key_parts:
|
||||||
|
modifiers = keybindings.CTHULHU_CTRL_MODIFIER_MASK
|
||||||
|
elif "alt" in key_parts:
|
||||||
|
modifiers = keybindings.CTHULHU_ALT_MODIFIER_MASK
|
||||||
|
|
||||||
|
# Create a keybinding handler
|
||||||
|
class GestureHandler:
|
||||||
|
def __init__(self, function, description):
|
||||||
|
self.function = function
|
||||||
|
self.description = description
|
||||||
|
|
||||||
|
def __call__(self, script, inputEvent):
|
||||||
|
return self.function(script, inputEvent)
|
||||||
|
|
||||||
|
handler = GestureHandler(function, name)
|
||||||
|
|
||||||
|
# Register the binding with the active script
|
||||||
|
from . import cthulhu_state
|
||||||
|
if cthulhu_state.activeScript:
|
||||||
|
bindings = cthulhu_state.activeScript.getKeyBindings()
|
||||||
|
binding = keybindings.KeyBinding(
|
||||||
|
final_key,
|
||||||
|
keybindings.defaultModifierMask,
|
||||||
|
modifiers,
|
||||||
|
handler)
|
||||||
|
bindings.add(binding)
|
||||||
|
|
||||||
|
# Store binding for later reference
|
||||||
|
if contextName not in self._gestureBindings:
|
||||||
|
self._gestureBindings[contextName] = []
|
||||||
|
self._gestureBindings[contextName].append(binding)
|
||||||
|
|
||||||
|
return binding
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def unregisterShortcut(self, binding, contextName=None):
|
||||||
|
"""Unregister a previously registered shortcut.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
- binding: the binding to unregister
|
||||||
|
- contextName: the context for this gesture
|
||||||
|
"""
|
||||||
|
# Remove from script's keybindings
|
||||||
|
from . import cthulhu_state
|
||||||
|
if cthulhu_state.activeScript:
|
||||||
|
bindings = cthulhu_state.activeScript.getKeyBindings()
|
||||||
|
bindings.remove(binding)
|
||||||
|
|
||||||
|
# Remove from our tracking
|
||||||
|
if contextName in self._gestureBindings:
|
||||||
|
if binding in self._gestureBindings[contextName]:
|
||||||
|
self._gestureBindings[contextName].remove(binding)
|
||||||
import gi
|
import gi
|
||||||
import importlib
|
import importlib
|
||||||
import os
|
import os
|
||||||
@ -74,7 +176,7 @@ from .ax_object import AXObject
|
|||||||
from .ax_utilities import AXUtilities
|
from .ax_utilities import AXUtilities
|
||||||
from .input_event import BrailleEvent
|
from .input_event import BrailleEvent
|
||||||
from . import cmdnames
|
from . import cmdnames
|
||||||
from . import plugin_system_manager
|
from . import plugin_system_manager # This will now be your pluggy-based implementation
|
||||||
from . import guilabels
|
from . import guilabels
|
||||||
from . import acss
|
from . import acss
|
||||||
from . import text_attribute_names
|
from . import text_attribute_names
|
||||||
@ -920,7 +1022,6 @@ class Cthulhu(GObject.Object):
|
|||||||
GObject.Object.__init__(self)
|
GObject.Object.__init__(self)
|
||||||
# add members
|
# add members
|
||||||
self.resourceManager = resource_manager.ResourceManager(self)
|
self.resourceManager = resource_manager.ResourceManager(self)
|
||||||
self.APIHelper = plugin_system_manager.APIHelper(self)
|
|
||||||
self.eventManager = _eventManager
|
self.eventManager = _eventManager
|
||||||
self.settingsManager = _settingsManager
|
self.settingsManager = _settingsManager
|
||||||
self.scriptManager = _scriptManager
|
self.scriptManager = _scriptManager
|
||||||
@ -928,8 +1029,11 @@ class Cthulhu(GObject.Object):
|
|||||||
self.dynamicApiManager = dynamic_api_manager.DynamicApiManager(self)
|
self.dynamicApiManager = dynamic_api_manager.DynamicApiManager(self)
|
||||||
self.translationManager = translation_manager.TranslationManager(self)
|
self.translationManager = translation_manager.TranslationManager(self)
|
||||||
self.debugManager = debug
|
self.debugManager = debug
|
||||||
|
self.APIHelper = APIHelper(self)
|
||||||
self.createCompatAPI()
|
self.createCompatAPI()
|
||||||
self.pluginSystemManager = plugin_system_manager.PluginSystemManager(self)
|
self.pluginSystemManager = plugin_system_manager.PluginSystemManager(self)
|
||||||
|
# Scan for available plugins at startup
|
||||||
|
self.pluginSystemManager.rescanPlugins()
|
||||||
def getAPIHelper(self):
|
def getAPIHelper(self):
|
||||||
return self.APIHelper
|
return self.APIHelper
|
||||||
def getPluginSystemManager(self):
|
def getPluginSystemManager(self):
|
||||||
@ -987,6 +1091,7 @@ class Cthulhu(GObject.Object):
|
|||||||
# cthulhu lets say, special compat handling....
|
# cthulhu lets say, special compat handling....
|
||||||
self.getDynamicApiManager().registerAPI('EmitRegionChanged', emitRegionChanged)
|
self.getDynamicApiManager().registerAPI('EmitRegionChanged', emitRegionChanged)
|
||||||
self.getDynamicApiManager().registerAPI('LoadUserSettings', loadUserSettings)
|
self.getDynamicApiManager().registerAPI('LoadUserSettings', loadUserSettings)
|
||||||
|
self.getDynamicApiManager().registerAPI('APIHelper', self.APIHelper)
|
||||||
|
|
||||||
cthulhuApp = Cthulhu()
|
cthulhuApp = Cthulhu()
|
||||||
|
|
||||||
|
@ -23,5 +23,5 @@
|
|||||||
# Fork of Orca Screen Reader (GNOME)
|
# Fork of Orca Screen Reader (GNOME)
|
||||||
# Original source: https://gitlab.gnome.org/GNOME/orca
|
# Original source: https://gitlab.gnome.org/GNOME/orca
|
||||||
|
|
||||||
version = "2024.12.18"
|
version = "2025.04.04"
|
||||||
codeName = "stable"
|
codeName = "testing"
|
||||||
|
@ -193,7 +193,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
|
|||||||
# ***** Key Bindings treeview initialization *****
|
# ***** Key Bindings treeview initialization *****
|
||||||
|
|
||||||
self.keyBindView = self.get_widget("keyBindingsTreeview")
|
self.keyBindView = self.get_widget("keyBindingsTreeview")
|
||||||
|
|
||||||
if self.keyBindView.get_columns():
|
if self.keyBindView.get_columns():
|
||||||
for column in self.keyBindView.get_columns():
|
for column in self.keyBindView.get_columns():
|
||||||
self.keyBindView.remove_column(column)
|
self.keyBindView.remove_column(column)
|
||||||
@ -337,7 +337,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
|
|||||||
column.set_resizable(True)
|
column.set_resizable(True)
|
||||||
column.set_sort_column_id(EDITABLE)
|
column.set_sort_column_id(EDITABLE)
|
||||||
self.keyBindView.append_column(column)
|
self.keyBindView.append_column(column)
|
||||||
|
|
||||||
# Populates the treeview with all the keybindings:
|
# Populates the treeview with all the keybindings:
|
||||||
#
|
#
|
||||||
self._populateKeyBindings()
|
self._populateKeyBindings()
|
||||||
@ -582,7 +582,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
|
|||||||
self.get_widget("rateScale").set_value(rate)
|
self.get_widget("rateScale").set_value(rate)
|
||||||
else:
|
else:
|
||||||
self.get_widget("rateScale").set_value(50.0)
|
self.get_widget("rateScale").set_value(50.0)
|
||||||
|
|
||||||
pitch = self._getPitchForVoiceType(voiceType)
|
pitch = self._getPitchForVoiceType(voiceType)
|
||||||
if pitch is not None:
|
if pitch is not None:
|
||||||
self.get_widget("pitchScale").set_value(pitch)
|
self.get_widget("pitchScale").set_value(pitch)
|
||||||
@ -1150,7 +1150,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
|
|||||||
grid = self.get_widget('flashMessageDurationGrid')
|
grid = self.get_widget('flashMessageDurationGrid')
|
||||||
grid.set_sensitive(not checkbox.get_active())
|
grid.set_sensitive(not checkbox.get_active())
|
||||||
self.prefsDict["flashIsPersistent"] = checkbox.get_active()
|
self.prefsDict["flashIsPersistent"] = checkbox.get_active()
|
||||||
|
|
||||||
def textAttributeSpokenToggled(self, cell, path, model):
|
def textAttributeSpokenToggled(self, cell, path, model):
|
||||||
"""The user has toggled the state of one of the text attribute
|
"""The user has toggled the state of one of the text attribute
|
||||||
checkboxes to be spoken. Update our model to reflect this, then
|
checkboxes to be spoken. Update our model to reflect this, then
|
||||||
@ -1596,7 +1596,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
|
|||||||
elif dateFormat == messages.DATE_FORMAT_ABBREVIATED_YMD:
|
elif dateFormat == messages.DATE_FORMAT_ABBREVIATED_YMD:
|
||||||
indexdate = DATE_FORMAT_ABBREVIATED_YMD
|
indexdate = DATE_FORMAT_ABBREVIATED_YMD
|
||||||
combobox2.set_active (indexdate)
|
combobox2.set_active (indexdate)
|
||||||
|
|
||||||
combobox3 = self.get_widget("timeFormatCombo")
|
combobox3 = self.get_widget("timeFormatCombo")
|
||||||
self.populateComboBox(combobox3,
|
self.populateComboBox(combobox3,
|
||||||
[sdtime(messages.TIME_FORMAT_LOCALE, ltime()),
|
[sdtime(messages.TIME_FORMAT_LOCALE, ltime()),
|
||||||
@ -1757,7 +1757,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
|
|||||||
prefs["enableEchoByWord"])
|
prefs["enableEchoByWord"])
|
||||||
self.get_widget("enableEchoBySentenceCheckButton").set_active( \
|
self.get_widget("enableEchoBySentenceCheckButton").set_active( \
|
||||||
prefs["enableEchoBySentence"])
|
prefs["enableEchoBySentence"])
|
||||||
|
|
||||||
# Text attributes pane.
|
# Text attributes pane.
|
||||||
#
|
#
|
||||||
self._createTextAttributesTreeView()
|
self._createTextAttributesTreeView()
|
||||||
@ -1785,7 +1785,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
|
|||||||
self.get_widget("generalDesktopButton").set_active(True)
|
self.get_widget("generalDesktopButton").set_active(True)
|
||||||
else:
|
else:
|
||||||
self.get_widget("generalLaptopButton").set_active(True)
|
self.get_widget("generalLaptopButton").set_active(True)
|
||||||
|
|
||||||
combobox = self.get_widget("sayAllStyle")
|
combobox = self.get_widget("sayAllStyle")
|
||||||
self.populateComboBox(combobox, [guilabels.SAY_ALL_STYLE_LINE,
|
self.populateComboBox(combobox, [guilabels.SAY_ALL_STYLE_LINE,
|
||||||
guilabels.SAY_ALL_STYLE_SENTENCE])
|
guilabels.SAY_ALL_STYLE_SENTENCE])
|
||||||
@ -2748,7 +2748,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
|
|||||||
elif dateFormatCombo == DATE_FORMAT_ABBREVIATED_YMD:
|
elif dateFormatCombo == DATE_FORMAT_ABBREVIATED_YMD:
|
||||||
newFormat = messages.DATE_FORMAT_ABBREVIATED_YMD
|
newFormat = messages.DATE_FORMAT_ABBREVIATED_YMD
|
||||||
self.prefsDict["presentDateFormat"] = newFormat
|
self.prefsDict["presentDateFormat"] = newFormat
|
||||||
|
|
||||||
def timeFormatChanged(self, widget):
|
def timeFormatChanged(self, widget):
|
||||||
"""Signal handler for the "changed" signal for the timeFormat
|
"""Signal handler for the "changed" signal for the timeFormat
|
||||||
GtkComboBox widget. Set the 'timeFormat' preference to the
|
GtkComboBox widget. Set the 'timeFormat' preference to the
|
||||||
|
@ -66,7 +66,7 @@ class DynamicApiManager():
|
|||||||
def getAPI(self, key, application = '', fallback = True):
|
def getAPI(self, key, application = '', fallback = True):
|
||||||
# get dynamic API
|
# get dynamic API
|
||||||
api = None
|
api = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
api = self.api[application][key]
|
api = self.api[application][key]
|
||||||
return api
|
return api
|
||||||
@ -83,5 +83,5 @@ class DynamicApiManager():
|
|||||||
api = self.api[application]['']
|
api = self.api[application]['']
|
||||||
except:
|
except:
|
||||||
print('API Key: "{}/{}" not found,'.format(application, key))
|
print('API Key: "{}/{}" not found,'.format(application, key))
|
||||||
|
|
||||||
return api
|
return api
|
||||||
|
@ -44,7 +44,7 @@ CTHULHU_SHIFT_MODIFIER_MASK = keybindings.CTHULHU_SHIFT_MODIFIER_MASK
|
|||||||
CTHULHU_CTRL_MODIFIER_MASK = keybindings.CTHULHU_CTRL_MODIFIER_MASK
|
CTHULHU_CTRL_MODIFIER_MASK = keybindings.CTHULHU_CTRL_MODIFIER_MASK
|
||||||
|
|
||||||
keymap = (
|
keymap = (
|
||||||
|
|
||||||
("9", defaultModifierMask, CTHULHU_MODIFIER_MASK,
|
("9", defaultModifierMask, CTHULHU_MODIFIER_MASK,
|
||||||
"routePointerToItemHandler"),
|
"routePointerToItemHandler"),
|
||||||
|
|
||||||
|
@ -2299,7 +2299,12 @@ SPOKEN_ELLIPSIS = _(" dot dot dot")
|
|||||||
START_CTHULHU = _("Cthulhu welcomes you.")
|
START_CTHULHU = _("Cthulhu welcomes you.")
|
||||||
|
|
||||||
# Translators: This message is presented to the user when Cthulhu is quit.
|
# Translators: This message is presented to the user when Cthulhu is quit.
|
||||||
STOP_CTHULHU = _("Cthulhu lerks beneath the waves.")
|
STOP_CTHULHU = _("Cthulhu lurks beneath the waves.")
|
||||||
|
|
||||||
|
# Sleep Mode
|
||||||
|
# Translators: This message is presented to the user when Cthulhu enters or leaves sleep mode.
|
||||||
|
SLEEP_MODE_ENABLED_FOR = _("Sleep mode enabled for %s")
|
||||||
|
SLEEP_MODE_DISABLED_FOR = _("Sleep mode disabled for %s")
|
||||||
|
|
||||||
# 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.")
|
||||||
|
@ -1,185 +1,92 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
#
|
|
||||||
# Copyright (c) 2024 Stormux
|
# Copyright (c) 2024 Stormux
|
||||||
# Copyright (c) 2010-2012 The Orca Team
|
|
||||||
# Copyright (c) 2012 Igalia, S.L.
|
|
||||||
# Copyright (c) 2005-2010 Sun Microsystems Inc.
|
|
||||||
#
|
#
|
||||||
# This library is free software; you can redistribute it and/or
|
# This library is free software; you can redistribute it and/or
|
||||||
# modify it under the terms of the GNU Lesser General Public
|
# modify it under the terms of the GNU Lesser General Public
|
||||||
# License as published by the Free Software Foundation; either
|
# License as published by the Free Software Foundation; either
|
||||||
# version 2.1 of the License, or (at your option) any later version.
|
# version 2.1 of the License, or (at your option) any later version.
|
||||||
#
|
|
||||||
# This library is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
# Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Lesser General Public
|
|
||||||
# License along with this library; if not, write to the
|
|
||||||
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
|
|
||||||
# Boston MA 02110-1301 USA.
|
|
||||||
#
|
|
||||||
# Fork of Orca Screen Reader (GNOME)
|
|
||||||
# Original source: https://gitlab.gnome.org/GNOME/orca
|
|
||||||
|
|
||||||
import os, inspect
|
"""Base class for Cthulhu plugins using pluggy."""
|
||||||
import gi
|
|
||||||
from gi.repository import GObject
|
|
||||||
|
|
||||||
class Plugin():
|
import os
|
||||||
#__gtype_name__ = 'BasePlugin'
|
import logging
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
# Import pluggy for hook specifications
|
||||||
self.API = None
|
try:
|
||||||
self.pluginInfo = None
|
import pluggy
|
||||||
self.moduleDir = ''
|
cthulhu_hookimpl = pluggy.HookimplMarker("cthulhu")
|
||||||
self.hidden = False
|
PLUGGY_AVAILABLE = True
|
||||||
self.moduleName = ''
|
logging.getLogger(__name__).info("Successfully imported pluggy")
|
||||||
|
except ImportError:
|
||||||
|
# Fallback if pluggy is not available
|
||||||
|
def cthulhu_hookimpl(func=None, **kwargs):
|
||||||
|
"""Fallback decorator when pluggy is not available.
|
||||||
|
|
||||||
|
This is a no-op decorator that returns the original function.
|
||||||
|
It allows the code to continue working without pluggy, though
|
||||||
|
plugins will be disabled.
|
||||||
|
"""
|
||||||
|
if func is None:
|
||||||
|
return lambda f: f
|
||||||
|
return func
|
||||||
|
PLUGGY_AVAILABLE = False
|
||||||
|
logging.getLogger(__name__).warning("Pluggy not available, plugins will be disabled")
|
||||||
|
import traceback
|
||||||
|
logging.getLogger(__name__).debug(traceback.format_exc())
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class Plugin:
|
||||||
|
"""Base class for Cthulhu plugins."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize the plugin with default attributes."""
|
||||||
|
self.app = None
|
||||||
|
self.plugin_info = None
|
||||||
|
self.module_name = ''
|
||||||
self.name = ''
|
self.name = ''
|
||||||
self.version = ''
|
self.version = ''
|
||||||
self.website = ''
|
|
||||||
self.authors = []
|
|
||||||
self.buildIn = False
|
|
||||||
self.description = ''
|
self.description = ''
|
||||||
self.iconName = ''
|
|
||||||
self.copyright = ''
|
def set_app(self, app):
|
||||||
self.dependencies = False
|
"""Set the application reference."""
|
||||||
self.helpUri = ''
|
|
||||||
self.dataDir = ''
|
|
||||||
self.translationContext = None
|
|
||||||
def setApp(self, app):
|
|
||||||
self.app = app
|
self.app = app
|
||||||
self.dynamicApiManager = app.getDynamicApiManager()
|
|
||||||
self.signalManager = app.getSignalManager()
|
|
||||||
|
|
||||||
def getApp(self):
|
def set_plugin_info(self, plugin_info):
|
||||||
return self.app
|
"""Set plugin information and extract relevant attributes."""
|
||||||
def setPluginInfo(self, pluginInfo):
|
self.plugin_info = plugin_info
|
||||||
self.pluginInfo = pluginInfo
|
if plugin_info:
|
||||||
self.updatePluginInfoAttributes()
|
self.module_name = getattr(plugin_info, 'module_name', '')
|
||||||
def getPluginInfo(self):
|
self.name = getattr(plugin_info, 'name', '')
|
||||||
return self.pluginInfo
|
self.version = getattr(plugin_info, 'version', '')
|
||||||
def updatePluginInfoAttributes(self):
|
self.description = getattr(plugin_info, 'description', '')
|
||||||
self.moduleDir = ''
|
|
||||||
self.hidden = False
|
@cthulhu_hookimpl
|
||||||
self.moduleName = ''
|
def activate(self, plugin=None):
|
||||||
self.name = ''
|
"""Activate the plugin. Override this in subclasses."""
|
||||||
self.version = ''
|
if plugin is not None and plugin is not self:
|
||||||
self.website = ''
|
|
||||||
self.authors = []
|
|
||||||
self.buildIn = False
|
|
||||||
self.description = ''
|
|
||||||
self.iconName = ''
|
|
||||||
self.copyright = ''
|
|
||||||
self.dependencies = False
|
|
||||||
self.helpUri = ''
|
|
||||||
self.dataDir = ''
|
|
||||||
pluginInfo = self.getPluginInfo()
|
|
||||||
if pluginInfo == None:
|
|
||||||
return
|
return
|
||||||
self.moduleName = self.getApp().getPluginSystemManager().getPluginModuleName(pluginInfo)
|
logger.info(f"Activating plugin: {self.name}")
|
||||||
self.name = self.getApp().getPluginSystemManager().getPluginName(pluginInfo)
|
|
||||||
self.version = self.getApp().getPluginSystemManager().getPluginVersion(pluginInfo)
|
|
||||||
self.moduleDir = self.getApp().getPluginSystemManager().getPluginModuleDir(pluginInfo)
|
|
||||||
self.buildIn = self.getApp().getPluginSystemManager().isPluginBuildIn(pluginInfo)
|
|
||||||
self.description = self.getApp().getPluginSystemManager().getPluginDescription(pluginInfo)
|
|
||||||
self.hidden = self.getApp().getPluginSystemManager().isPluginHidden(pluginInfo)
|
|
||||||
self.website = self.getApp().getPluginSystemManager().getPluginWebsite(pluginInfo)
|
|
||||||
self.authors = self.getApp().getPluginSystemManager().getPluginAuthors(pluginInfo)
|
|
||||||
self.iconName = self.getApp().getPluginSystemManager().getPluginIconName(pluginInfo)
|
|
||||||
self.copyright = self.getApp().getPluginSystemManager().getPluginCopyright(pluginInfo)
|
|
||||||
self.dependencies = self.getApp().getPluginSystemManager().getPluginDependencies(pluginInfo)
|
|
||||||
|
|
||||||
#settings = self.getApp().getPluginSystemManager().getPluginSettings(pluginInfo)
|
@cthulhu_hookimpl
|
||||||
#hasDependencies = self.getApp().getPluginSystemManager().hasPluginDependency(pluginInfo)
|
def deactivate(self, plugin=None):
|
||||||
|
"""Deactivate the plugin. Override this in subclasses."""
|
||||||
|
if plugin is not None and plugin is not self:
|
||||||
|
return
|
||||||
|
logger.info(f"Deactivating plugin: {self.name}")
|
||||||
|
|
||||||
#externalData = self.getApp().getPluginSystemManager().getPluginExternalData(pluginInfo)
|
def registerGestureByString(self, function, name, gestureString, learnModeEnabled=True):
|
||||||
self.helpUri = self.getApp().getPluginSystemManager().getPlugingetHelpUri(pluginInfo)
|
"""Register a gesture by string."""
|
||||||
self.dataDir = self.getApp().getPluginSystemManager().getPluginDataDir(pluginInfo)
|
if self.app:
|
||||||
self.updateTranslationContext()
|
api_helper = self.app.getAPIHelper()
|
||||||
|
if api_helper:
|
||||||
def updateTranslationContext(self, domain = None, localeDir = None, language = None, fallbackToCthulhuTranslation = True):
|
return api_helper.registerGestureByString(
|
||||||
self.translationContext = None
|
function,
|
||||||
useLocaleDir = '{}/locale/'.format(self.getModuleDir())
|
name,
|
||||||
if localeDir:
|
gestureString,
|
||||||
if os.path.isdir(localeDir):
|
'default',
|
||||||
useLocaleDir = localeDir
|
'cthulhu',
|
||||||
useName = self.getModuleName()
|
learnModeEnabled,
|
||||||
useDomain = useName
|
contextName=self.module_name
|
||||||
if domain:
|
)
|
||||||
useDomain = domain
|
|
||||||
useLanguage = None
|
|
||||||
if language:
|
|
||||||
useLanguage = language
|
|
||||||
self.translationContext = self.getApp().getTranslationManager().initTranslation(useName, domain=useDomain, localeDir=useLocaleDir, language=useLanguage, fallbackToCthulhuTranslation=fallbackToCthulhuTranslation)
|
|
||||||
# Point _ to the translation object in the globals namespace of the caller frame
|
|
||||||
try:
|
|
||||||
callerFrame = inspect.currentframe().f_back
|
|
||||||
# Install our gettext and ngettext function to the upper frame
|
|
||||||
callerFrame.f_globals['_'] = self.translationContext.gettext
|
|
||||||
callerFrame.f_globals['ngettext'] = self.translationContext.ngettext
|
|
||||||
finally:
|
|
||||||
del callerFrame # Avoid reference problems with frames (per python docs)
|
|
||||||
def getTranslationContext(self):
|
|
||||||
return self.translationContext
|
|
||||||
def isPluginBuildIn(self):
|
|
||||||
return self.buildIn
|
|
||||||
def isPluginHidden(self):
|
|
||||||
return self.hidden
|
|
||||||
def getAuthors(self):
|
|
||||||
return self.authors
|
|
||||||
def getCopyright(self):
|
|
||||||
return self.copyright
|
|
||||||
def getDataDir(self):
|
|
||||||
return self.dataDir
|
|
||||||
def getDependencies(self):
|
|
||||||
return self.dependencies
|
|
||||||
def getDescription(self):
|
|
||||||
return self.description
|
|
||||||
def getgetHelpUri(self):
|
|
||||||
return self.helpUri
|
|
||||||
def getIconName(self):
|
|
||||||
return self.iconName
|
|
||||||
def getModuleDir(self):
|
|
||||||
return self.moduleDir
|
|
||||||
def getModuleName(self):
|
|
||||||
return self.moduleName
|
|
||||||
def getName(self):
|
|
||||||
return self.name
|
|
||||||
def getVersion(self):
|
|
||||||
return self.version
|
|
||||||
def getWebsite(self):
|
|
||||||
return self.website
|
|
||||||
def getSetting(key):
|
|
||||||
#self.getModuleName())
|
|
||||||
return None
|
return None
|
||||||
def setSetting(key, value):
|
|
||||||
#self.getModuleName())
|
|
||||||
pass
|
|
||||||
def registerGestureByString(self, function, name, gestureString, learnModeEnabled = True):
|
|
||||||
keybinding = self.getApp().getAPIHelper().registerGestureByString(function, name, gestureString, 'default', 'cthulhu', learnModeEnabled, contextName = self.getModuleName())
|
|
||||||
return keybinding
|
|
||||||
def unregisterShortcut(self, function, name, gestureString, learnModeEnabled = True):
|
|
||||||
ok = self.getApp().getAPIHelper().unregisterShortcut(keybinding, contextName = self.getModuleName())
|
|
||||||
return ok
|
|
||||||
def registerSignal(self, signalName, signalFlag = GObject.SignalFlags.RUN_LAST, closure = GObject.TYPE_NONE, accumulator=()):
|
|
||||||
ok = self.signalManager.registerSignal(signalName, signalFlag, closure, accumulator, contextName = self.getModuleName())
|
|
||||||
return ok
|
|
||||||
def unregisterSignal(self, signalName):
|
|
||||||
# how to unregister?
|
|
||||||
pass
|
|
||||||
|
|
||||||
def connectSignal(self, signalName, function, param = None):
|
|
||||||
signalID = self.signalManager.connectSignal(signalName, function, param, contextName = self.getModuleName())
|
|
||||||
return signalID
|
|
||||||
def disconnectSignalByFunction(self, function):
|
|
||||||
# need get mapped function
|
|
||||||
mappedFunction = function
|
|
||||||
self.signalManager.disconnectSignalByFunction(mappedFunction, contextName = self.getModuleName())
|
|
||||||
|
|
||||||
def registerAPI(self, key, value, application = ''):
|
|
||||||
ok = self.dynamicApiManager.registerAPI(key, value, application = application, contextName = self.getModuleName())
|
|
||||||
return ok
|
|
||||||
def unregisterAPI(self, key, application = ''):
|
|
||||||
self.dynamicApiManager.unregisterAPI(key, application = application, contextName = self.getModuleName())
|
|
||||||
|
@ -1,544 +1,469 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
#
|
|
||||||
# Copyright (c) 2024 Stormux
|
# Copyright (c) 2024 Stormux
|
||||||
# Copyright (c) 2010-2012 The Orca Team
|
|
||||||
# Copyright (c) 2012 Igalia, S.L.
|
|
||||||
# Copyright (c) 2005-2010 Sun Microsystems Inc.
|
|
||||||
#
|
#
|
||||||
# This library is free software; you can redistribute it and/or
|
# This library is free software; you can redistribute it and/or
|
||||||
# modify it under the terms of the GNU Lesser General Public
|
# modify it under the terms of the GNU Lesser General Public
|
||||||
# License as published by the Free Software Foundation; either
|
# License as published by the Free Software Foundation; either
|
||||||
# version 2.1 of the License, or (at your option) any later version.
|
# version 2.1 of the License, or (at your option) any later version.
|
||||||
#
|
|
||||||
# This library is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
# Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Lesser General Public
|
|
||||||
# License along with this library; if not, write to the
|
|
||||||
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
|
|
||||||
# Boston MA 02110-1301 USA.
|
|
||||||
#
|
|
||||||
# Fork of Orca Screen Reader (GNOME)
|
|
||||||
# Original source: https://gitlab.gnome.org/GNOME/orca
|
|
||||||
|
|
||||||
"""PluginManager for loading cthulhu plugins."""
|
"""Plugin System Manager for Cthulhu using pluggy."""
|
||||||
import os, inspect, sys, tarfile, shutil
|
|
||||||
|
|
||||||
|
import os
|
||||||
|
import inspect
|
||||||
|
import importlib.util
|
||||||
|
import logging
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
|
|
||||||
version = sys.version[:3] # we only need major.minor version.
|
# Import pluggy if available
|
||||||
if version in ["3.3","3.4"]:
|
try:
|
||||||
from importlib.machinery import SourceFileLoader
|
import pluggy
|
||||||
else: # Python 3.5+, no support for python < 3.3.
|
PLUGGY_AVAILABLE = True
|
||||||
import importlib.util
|
except ImportError:
|
||||||
|
PLUGGY_AVAILABLE = False
|
||||||
|
logging.getLogger(__name__).info("Pluggy not available, plugins will be disabled")
|
||||||
|
|
||||||
import gi
|
# Set to True for more detailed plugin loading debug info
|
||||||
gi.require_version('Peas', '1.0')
|
PLUGIN_DEBUG = True
|
||||||
from gi.repository import GObject
|
|
||||||
from gi.repository import Peas
|
|
||||||
|
|
||||||
gi.require_version('Atspi', '2.0')
|
logger = logging.getLogger(__name__)
|
||||||
from gi.repository import Atspi
|
if PLUGIN_DEBUG:
|
||||||
|
logger.setLevel(logging.DEBUG)
|
||||||
from cthulhu import resource_manager
|
|
||||||
|
|
||||||
class API(GObject.GObject):
|
|
||||||
"""Interface that gives access to all the objects of Cthulhu."""
|
|
||||||
def __init__(self, app):
|
|
||||||
GObject.GObject.__init__(self)
|
|
||||||
self.app = app
|
|
||||||
|
|
||||||
class PluginType(IntEnum):
|
class PluginType(IntEnum):
|
||||||
"""Types of plugins we support, depending on their directory location."""
|
"""Types of plugins we support."""
|
||||||
# pylint: disable=comparison-with-callable,inconsistent-return-statements,no-else-return
|
|
||||||
# SYSTEM: provides system wide plugins
|
|
||||||
SYSTEM = 1
|
SYSTEM = 1
|
||||||
# USER: provides per user plugin
|
|
||||||
USER = 2
|
USER = 2
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
if self.value == PluginType.SYSTEM:
|
|
||||||
return _("System plugin")
|
|
||||||
elif self.value == PluginType.USER:
|
|
||||||
return _("User plugin")
|
|
||||||
|
|
||||||
def get_root_dir(self):
|
def get_root_dir(self):
|
||||||
"""Returns the directory where this type of plugins can be found."""
|
"""Returns the directory where this type of plugins can be found."""
|
||||||
if self.value == PluginType.SYSTEM:
|
if self.value == PluginType.SYSTEM:
|
||||||
return os.path.dirname(os.path.realpath(os.path.abspath(inspect.getfile(inspect.currentframe())))) + '/plugins'
|
current_file = inspect.getfile(inspect.currentframe())
|
||||||
|
current_dir = os.path.dirname(os.path.realpath(os.path.abspath(current_file)))
|
||||||
|
return os.path.join(current_dir, 'plugins')
|
||||||
elif self.value == PluginType.USER:
|
elif self.value == PluginType.USER:
|
||||||
return os.path.expanduser('~') + '/.local/share/cthulhu/plugins'
|
return os.path.expanduser('~/.local/share/cthulhu/plugins')
|
||||||
|
|
||||||
|
|
||||||
class PluginSystemManager():
|
class PluginInfo:
|
||||||
"""Cthulhu Plugin Manager to handle a set of plugins.
|
"""Information about a plugin."""
|
||||||
Attributes:
|
|
||||||
DEFAULT_LOADERS (tuple): Default loaders used by the plugin manager. For
|
def __init__(self, name, module_name, module_dir, metadata=None):
|
||||||
possible values see
|
self.name = name
|
||||||
https://developer.gnome.org/libpeas/stable/PeasEngine.html#peas-engine-enable-loader
|
self.module_name = module_name
|
||||||
"""
|
self.module_dir = module_dir
|
||||||
DEFAULT_LOADERS = ("python3", )
|
self.metadata = metadata or {}
|
||||||
|
self.builtin = False
|
||||||
|
self.hidden = False
|
||||||
|
self.module = None
|
||||||
|
self.instance = None
|
||||||
|
self.loaded = False
|
||||||
|
|
||||||
|
def get_module_name(self):
|
||||||
|
return self.module_name
|
||||||
|
|
||||||
|
def get_name(self):
|
||||||
|
return self.metadata.get('name', self.name)
|
||||||
|
|
||||||
|
def get_version(self):
|
||||||
|
return self.metadata.get('version', '0.0.0')
|
||||||
|
|
||||||
|
def get_description(self):
|
||||||
|
return self.metadata.get('description', '')
|
||||||
|
|
||||||
|
def get_module_dir(self):
|
||||||
|
return self.module_dir
|
||||||
|
|
||||||
|
|
||||||
|
class PluginSystemManager:
|
||||||
|
"""Cthulhu Plugin Manager using pluggy."""
|
||||||
|
|
||||||
def __init__(self, app):
|
def __init__(self, app):
|
||||||
self.app = app
|
self.app = app
|
||||||
self.engine = Peas.Engine.get_default()
|
logger.info("Initializing PluginSystemManager")
|
||||||
|
|
||||||
for loader in self.DEFAULT_LOADERS:
|
# Initialize plugin manager
|
||||||
self.engine.enable_loader(loader)
|
if PLUGGY_AVAILABLE:
|
||||||
|
logger.info("Pluggy is available, setting up plugin manager")
|
||||||
|
self.plugin_manager = pluggy.PluginManager("cthulhu")
|
||||||
|
|
||||||
self._setupPluginsDir()
|
# Define hook specifications
|
||||||
self._setupExtensionSet()
|
hook_spec = pluggy.HookspecMarker("cthulhu")
|
||||||
|
|
||||||
|
class CthulhuHookSpecs:
|
||||||
|
@hook_spec
|
||||||
|
def activate(self, plugin=None):
|
||||||
|
"""Called when the plugin is activated."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@hook_spec
|
||||||
|
def deactivate(self, plugin=None):
|
||||||
|
"""Called when the plugin is deactivated."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
logger.info("Adding hook specifications to plugin manager")
|
||||||
|
self.plugin_manager.add_hookspecs(CthulhuHookSpecs)
|
||||||
|
else:
|
||||||
|
logger.warning("Pluggy is not available, plugins will be disabled")
|
||||||
|
self.plugin_manager = None
|
||||||
|
|
||||||
|
# Plugin storage
|
||||||
|
self._plugins = {} # module_name -> PluginInfo
|
||||||
|
self._active_plugins = []
|
||||||
|
|
||||||
|
# Create plugin directories
|
||||||
|
self._setup_plugin_dirs()
|
||||||
|
|
||||||
if self.app:
|
# Log available plugins directory paths
|
||||||
self.gsettingsManager = self.app.getSettingsManager()
|
logger.info(f"System plugins directory: {PluginType.SYSTEM.get_root_dir()}")
|
||||||
# settings else:
|
logger.info(f"User plugins directory: {PluginType.USER.get_root_dir()}")
|
||||||
# settings self.gsettingsManager = gsettings_manager.getSettingsManager(self.app)
|
|
||||||
self._activePlugins = []
|
def _setup_plugin_dirs(self):
|
||||||
self._ignorePluginModulePath = []
|
"""Ensure plugin directories exist."""
|
||||||
|
os.makedirs(PluginType.SYSTEM.get_root_dir(), exist_ok=True)
|
||||||
|
os.makedirs(PluginType.USER.get_root_dir(), exist_ok=True)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def plugins(self):
|
def plugins(self):
|
||||||
"""Gets the engine's plugin list."""
|
"""Get all available plugins."""
|
||||||
return self.engine.get_plugin_list()
|
return list(self._plugins.values())
|
||||||
@classmethod
|
|
||||||
def getPluginType(cls, pluginInfo):
|
|
||||||
"""Gets the PluginType for the specified Peas.PluginInfo."""
|
|
||||||
paths = [pluginInfo.get_data_dir(), PluginType.SYSTEM.get_root_dir()]
|
|
||||||
if os.path.commonprefix(paths) == PluginType.SYSTEM.get_root_dir():
|
|
||||||
return PluginType.SYSTEM
|
|
||||||
return PluginType.USER
|
|
||||||
|
|
||||||
def getExtension(self, pluginInfo):
|
|
||||||
if not pluginInfo:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return self.extension_set.get_extension(pluginInfo)
|
|
||||||
def rescanPlugins(self):
|
|
||||||
self.engine.garbage_collect()
|
|
||||||
self.engine.rescan_plugins()
|
|
||||||
def getApp(self):
|
def getApp(self):
|
||||||
return self.app
|
return self.app
|
||||||
def getPluginInfoByName(self, pluginName, pluginType=PluginType.USER):
|
|
||||||
"""Gets the plugin info for the specified plugin name.
|
def rescanPlugins(self):
|
||||||
Args:
|
"""Scan for plugins in the plugin directories."""
|
||||||
pluginName (str): The name from the .plugin file of the module.
|
old_plugins = self._plugins.copy()
|
||||||
Returns:
|
self._plugins = {}
|
||||||
Peas.PluginInfo: The plugin info if it exists. Otherwise, `None`.
|
|
||||||
"""
|
# Scan system and user plugins
|
||||||
for pluginInfo in self.plugins:
|
self._scan_plugins_in_directory(PluginType.SYSTEM.get_root_dir())
|
||||||
if pluginInfo.get_module_name() == pluginName and PluginSystemManager.getPluginType(pluginInfo) == pluginType:
|
self._scan_plugins_in_directory(PluginType.USER.get_root_dir())
|
||||||
return pluginInfo
|
|
||||||
return None
|
# Preserve state for already loaded plugins
|
||||||
|
for name, old_info in old_plugins.items():
|
||||||
|
if name in self._plugins and old_info.loaded:
|
||||||
|
self._plugins[name].loaded = True
|
||||||
|
self._plugins[name].instance = old_info.instance
|
||||||
|
self._plugins[name].module = old_info.module
|
||||||
|
|
||||||
|
def _scan_plugins_in_directory(self, directory):
|
||||||
|
"""Scan for plugins in a directory."""
|
||||||
|
if not os.path.exists(directory) or not os.path.isdir(directory):
|
||||||
|
logger.warning(f"Plugin directory not found or not a directory: {directory}")
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info(f"Scanning for plugins in directory: {directory}")
|
||||||
|
for item in os.listdir(directory):
|
||||||
|
plugin_dir = os.path.join(directory, item)
|
||||||
|
if not os.path.isdir(plugin_dir):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check for the traditional structure first (plugin.py & plugin.info)
|
||||||
|
plugin_file = os.path.join(plugin_dir, "plugin.py")
|
||||||
|
metadata_file = os.path.join(plugin_dir, "plugin.info")
|
||||||
|
|
||||||
|
# Fall back to [PluginName].py if plugin.py doesn't exist
|
||||||
|
if not os.path.isfile(plugin_file):
|
||||||
|
alternative_plugin_file = os.path.join(plugin_dir, f"{item}.py")
|
||||||
|
if os.path.isfile(alternative_plugin_file):
|
||||||
|
plugin_file = alternative_plugin_file
|
||||||
|
logger.info(f"Using alternative plugin file: {alternative_plugin_file}")
|
||||||
|
|
||||||
|
# Check if we have any valid plugin file
|
||||||
|
if os.path.isfile(plugin_file):
|
||||||
|
# Extract plugin info
|
||||||
|
module_name = os.path.basename(plugin_dir)
|
||||||
|
logger.info(f"Found plugin: {module_name} in {plugin_dir}")
|
||||||
|
metadata = self._load_plugin_metadata(metadata_file)
|
||||||
|
|
||||||
|
plugin_info = PluginInfo(
|
||||||
|
metadata.get('name', module_name),
|
||||||
|
module_name,
|
||||||
|
plugin_dir,
|
||||||
|
metadata
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if it's a built-in or hidden plugin
|
||||||
|
plugin_info.builtin = metadata.get('builtin', 'false').lower() == 'true'
|
||||||
|
plugin_info.hidden = metadata.get('hidden', 'false').lower() == 'true'
|
||||||
|
|
||||||
|
logger.info(f"Adding plugin to registry: {module_name}")
|
||||||
|
self._plugins[module_name] = plugin_info
|
||||||
|
else:
|
||||||
|
logger.warning(f"No plugin file found in directory: {plugin_dir}")
|
||||||
|
|
||||||
|
def _load_plugin_metadata(self, metadata_file):
|
||||||
|
"""Load plugin metadata from a file."""
|
||||||
|
metadata = {}
|
||||||
|
|
||||||
|
if os.path.isfile(metadata_file):
|
||||||
|
try:
|
||||||
|
with open(metadata_file, 'r') as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
if not line or line.startswith('#'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if '=' in line:
|
||||||
|
key, value = line.split('=', 1)
|
||||||
|
metadata[key.strip()] = value.strip()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error loading plugin metadata: {e}")
|
||||||
|
|
||||||
|
return metadata
|
||||||
|
|
||||||
def getActivePlugins(self):
|
def getActivePlugins(self):
|
||||||
return self._activePlugins
|
"""Get the list of active plugin names."""
|
||||||
|
return self._active_plugins
|
||||||
|
|
||||||
def setActivePlugins(self, activePlugins):
|
def setActivePlugins(self, activePlugins):
|
||||||
self._activePlugins = activePlugins
|
"""Set active plugins and sync their state."""
|
||||||
|
logger.info(f"Setting active plugins: {activePlugins}")
|
||||||
|
|
||||||
|
# Make sure we have scanned for plugins first
|
||||||
|
if not self._plugins:
|
||||||
|
logger.info("No plugins found, rescanning...")
|
||||||
|
self.rescanPlugins()
|
||||||
|
|
||||||
|
self._active_plugins = activePlugins
|
||||||
|
|
||||||
|
# Log active vs available plugins
|
||||||
|
available_plugins = [p.get_module_name() for p in self.plugins]
|
||||||
|
logger.info(f"Available plugins: {available_plugins}")
|
||||||
|
logger.info(f"Active plugins: {self._active_plugins}")
|
||||||
|
|
||||||
|
# Find missing plugins
|
||||||
|
missing_plugins = [p for p in self._active_plugins if p not in available_plugins]
|
||||||
|
if missing_plugins:
|
||||||
|
logger.warning(f"Active plugins not found: {missing_plugins}")
|
||||||
|
|
||||||
self.syncAllPluginsActive()
|
self.syncAllPluginsActive()
|
||||||
def isPluginBuildIn(self, pluginInfo):
|
|
||||||
return pluginInfo.is_builtin()
|
|
||||||
def isPluginHidden(self, pluginInfo):
|
|
||||||
return pluginInfo.is_hidden()
|
|
||||||
def getPluginAuthors(self, pluginInfo):
|
|
||||||
return pluginInfo.get_authors()
|
|
||||||
def getPluginCopyright(self, pluginInfo):
|
|
||||||
return pluginInfo.get_copyright()
|
|
||||||
def getPluginDataDir(self, pluginInfo):
|
|
||||||
return pluginInfo.get_data_dir()
|
|
||||||
def getPluginDependencies(self, pluginInfo):
|
|
||||||
return pluginInfo.get_dependencies()
|
|
||||||
def getPluginDescription(self, pluginInfo):
|
|
||||||
return pluginInfo.get_description()
|
|
||||||
def getPlugingetHelpUri(self, pluginInfo):
|
|
||||||
return pluginInfo.get_help_uri()
|
|
||||||
def getPluginIconName(self, pluginInfo):
|
|
||||||
return pluginInfo.get_icon_name()
|
|
||||||
def getPluginModuleDir(self, pluginInfo):
|
|
||||||
return pluginInfo.get_module_dir()
|
|
||||||
def getPluginModuleName(self, pluginInfo):
|
|
||||||
return pluginInfo.get_module_name()
|
|
||||||
def getPluginName(self, pluginInfo):
|
|
||||||
return pluginInfo.get_name()
|
|
||||||
def getPluginSettings(self, pluginInfo):
|
|
||||||
return pluginInfo.get_settings()
|
|
||||||
def getPluginVersion(self, pluginInfo):
|
|
||||||
return pluginInfo.get_version()
|
|
||||||
def getPluginWebsite(self, pluginInfo):
|
|
||||||
return pluginInfo.get_website()
|
|
||||||
# has_dependency and get_external_data seems broken-> takes exactly 2 arguments (1 given) but documentation doesnt say any parameter
|
|
||||||
#def hasPluginDependency(self, pluginInfo):
|
|
||||||
# return pluginInfo.has_dependency()
|
|
||||||
#def getPluginExternalData(self, pluginInfo):
|
|
||||||
# return pluginInfo.get_external_data()
|
|
||||||
def isPluginAvailable(self, pluginInfo):
|
|
||||||
try:
|
|
||||||
return pluginInfo.is_available()
|
|
||||||
except:
|
|
||||||
return False
|
|
||||||
def isPluginLoaded(self, pluginInfo):
|
|
||||||
try:
|
|
||||||
return pluginInfo.is_loaded()
|
|
||||||
except:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def getIgnoredPlugins(self):
|
|
||||||
return self._ignorePluginModulePath
|
|
||||||
def setIgnoredPlugins(self, pluginModulePath, ignored):
|
|
||||||
if pluginModulePath.endswith('/'):
|
|
||||||
pluginModulePath = pluginModulePath[:-1]
|
|
||||||
if ignored:
|
|
||||||
if not pluginModulePath in self.getIgnoredPlugins():
|
|
||||||
self._ignorePluginModulePath.append(pluginModulePath)
|
|
||||||
else:
|
|
||||||
if pluginModulePath in self.getIgnoredPlugins():
|
|
||||||
self._ignorePluginModulePath.remove(pluginModulePath)
|
|
||||||
|
|
||||||
def setPluginActive(self, pluginInfo, active):
|
def setPluginActive(self, pluginInfo, active):
|
||||||
if self.isPluginBuildIn(pluginInfo):
|
"""Set the active state of a plugin."""
|
||||||
|
if pluginInfo.builtin:
|
||||||
active = True
|
active = True
|
||||||
pluginName = self.getPluginModuleName(pluginInfo)
|
|
||||||
|
pluginName = pluginInfo.get_module_name()
|
||||||
|
|
||||||
if active:
|
if active:
|
||||||
if not pluginName in self.getActivePlugins():
|
if pluginName not in self.getActivePlugins():
|
||||||
if self.loadPlugin(pluginInfo):
|
if self.loadPlugin(pluginInfo):
|
||||||
self._activePlugins.append(pluginName )
|
self._active_plugins.append(pluginName)
|
||||||
else:
|
else:
|
||||||
if pluginName in self.getActivePlugins():
|
if pluginName in self.getActivePlugins():
|
||||||
if self.unloadPlugin(pluginInfo):
|
if self.unloadPlugin(pluginInfo):
|
||||||
self._activePlugins.remove(pluginName )
|
self._active_plugins.remove(pluginName)
|
||||||
|
|
||||||
def isPluginActive(self, pluginInfo):
|
def isPluginActive(self, pluginInfo):
|
||||||
if self.isPluginBuildIn(pluginInfo):
|
"""Check if a plugin is active."""
|
||||||
|
module_name = pluginInfo.get_module_name()
|
||||||
|
|
||||||
|
# Builtin plugins are always active
|
||||||
|
if pluginInfo.builtin:
|
||||||
|
logger.debug(f"Plugin {module_name} is builtin, active by default")
|
||||||
return True
|
return True
|
||||||
if self.isPluginLoaded(pluginInfo):
|
|
||||||
|
# If a plugin is already loaded, it's active
|
||||||
|
if pluginInfo.loaded:
|
||||||
|
logger.debug(f"Plugin {module_name} is already loaded, considered active")
|
||||||
return True
|
return True
|
||||||
active_plugin_names = self.getActivePlugins()
|
|
||||||
return self.getPluginModuleName(pluginInfo) in active_plugin_names
|
|
||||||
def syncAllPluginsActive(self, ForceAllPlugins=False):
|
|
||||||
self.unloadAllPlugins(ForceAllPlugins)
|
|
||||||
self.loadAllPlugins(ForceAllPlugins)
|
|
||||||
|
|
||||||
def loadAllPlugins(self, ForceAllPlugins=False):
|
# Check case-insensitive match in active plugins list
|
||||||
"""Loads plugins from settings."""
|
active_plugins = self.getActivePlugins()
|
||||||
|
|
||||||
|
# Try exact match first
|
||||||
|
if module_name in active_plugins:
|
||||||
|
logger.debug(f"Plugin {module_name} found in active plugins list")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Try case-insensitive match
|
||||||
|
module_name_lower = module_name.lower()
|
||||||
|
is_active = any(plugin.lower() == module_name_lower for plugin in active_plugins)
|
||||||
|
|
||||||
|
if is_active:
|
||||||
|
logger.debug(f"Plugin {module_name} found in active plugins list (case-insensitive match)")
|
||||||
|
else:
|
||||||
|
logger.debug(f"Plugin {module_name} not found in active plugins list")
|
||||||
|
|
||||||
|
return is_active
|
||||||
|
|
||||||
|
def syncAllPluginsActive(self):
|
||||||
|
"""Sync the active state of all plugins."""
|
||||||
|
logger.info("Syncing active state of all plugins")
|
||||||
|
|
||||||
|
# Log plugin status before syncing
|
||||||
|
if PLUGIN_DEBUG:
|
||||||
|
for pluginInfo in self.plugins:
|
||||||
|
is_active = self.isPluginActive(pluginInfo)
|
||||||
|
is_loaded = pluginInfo.loaded
|
||||||
|
logger.debug(f"Plugin {pluginInfo.get_module_name()}: active={is_active}, loaded={is_loaded}")
|
||||||
|
|
||||||
|
# First unload inactive plugins
|
||||||
for pluginInfo in self.plugins:
|
for pluginInfo in self.plugins:
|
||||||
if self.isPluginActive(pluginInfo) or ForceAllPlugins:
|
if not self.isPluginActive(pluginInfo) and pluginInfo.loaded:
|
||||||
self.loadPlugin(pluginInfo)
|
logger.info(f"Unloading inactive plugin: {pluginInfo.get_module_name()}")
|
||||||
|
|
||||||
def loadPlugin(self, pluginInfo):
|
|
||||||
resourceManager = self.getApp().getResourceManager()
|
|
||||||
moduleName = pluginInfo.get_module_name()
|
|
||||||
try:
|
|
||||||
if pluginInfo not in self.plugins:
|
|
||||||
print("Plugin missing: {}".format(moduleName))
|
|
||||||
return False
|
|
||||||
resourceManager.addResourceContext(moduleName)
|
|
||||||
self.engine.load_plugin(pluginInfo)
|
|
||||||
except Exception as e:
|
|
||||||
print('loadPlugin:',e)
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def unloadAllPlugins(self, ForceAllPlugins=False):
|
|
||||||
"""Loads plugins from settings."""
|
|
||||||
for pluginInfo in self.plugins:
|
|
||||||
if not self.isPluginActive(pluginInfo) or ForceAllPlugins:
|
|
||||||
self.unloadPlugin(pluginInfo)
|
self.unloadPlugin(pluginInfo)
|
||||||
|
|
||||||
|
# Then load active plugins
|
||||||
|
for pluginInfo in self.plugins:
|
||||||
|
if self.isPluginActive(pluginInfo) and not pluginInfo.loaded:
|
||||||
|
logger.info(f"Loading active plugin: {pluginInfo.get_module_name()}")
|
||||||
|
result = self.loadPlugin(pluginInfo)
|
||||||
|
logger.info(f"Plugin {pluginInfo.get_module_name()} load result: {result}")
|
||||||
|
|
||||||
|
# Log final plugin status
|
||||||
|
active_plugins = [p.get_module_name() for p in self.plugins if p.loaded]
|
||||||
|
logger.info(f"Active plugins after sync: {active_plugins}")
|
||||||
|
inactive_plugins = [p.get_module_name() for p in self.plugins if not p.loaded]
|
||||||
|
logger.info(f"Inactive plugins after sync: {inactive_plugins}")
|
||||||
|
|
||||||
|
def loadPlugin(self, pluginInfo):
|
||||||
|
"""Load a plugin."""
|
||||||
|
# Skip if pluggy is not available
|
||||||
|
if not PLUGGY_AVAILABLE:
|
||||||
|
logger.info(f"Skipping plugin {pluginInfo.get_name()}: pluggy not available")
|
||||||
|
return False
|
||||||
|
|
||||||
|
module_name = pluginInfo.get_module_name()
|
||||||
|
logger.info(f"Attempting to load plugin: {module_name}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Already loaded?
|
||||||
|
if pluginInfo.loaded:
|
||||||
|
logger.info(f"Plugin {module_name} already loaded, skipping")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Try to find the plugin file
|
||||||
|
module_name = pluginInfo.get_module_name()
|
||||||
|
plugin_dir = pluginInfo.get_module_dir()
|
||||||
|
|
||||||
|
# Check for plugin.py first (standard format)
|
||||||
|
plugin_file = os.path.join(plugin_dir, "plugin.py")
|
||||||
|
|
||||||
|
# Fall back to [PluginName].py if plugin.py doesn't exist
|
||||||
|
if not os.path.exists(plugin_file):
|
||||||
|
alternative_plugin_file = os.path.join(plugin_dir, f"{module_name}.py")
|
||||||
|
if os.path.exists(alternative_plugin_file):
|
||||||
|
plugin_file = alternative_plugin_file
|
||||||
|
logger.info(f"Using alternative plugin file: {alternative_plugin_file}")
|
||||||
|
|
||||||
|
if not os.path.exists(plugin_file):
|
||||||
|
logger.error(f"Plugin file not found: {plugin_file}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
logger.info(f"Loading plugin from: {plugin_file}")
|
||||||
|
spec = importlib.util.spec_from_file_location(module_name, plugin_file)
|
||||||
|
if spec is None:
|
||||||
|
logger.error(f"Failed to create spec for plugin: {module_name}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
pluginInfo.module = module
|
||||||
|
|
||||||
|
# Find Plugin class
|
||||||
|
plugin_class = None
|
||||||
|
for attr_name in dir(module):
|
||||||
|
attr = getattr(module, attr_name)
|
||||||
|
if (inspect.isclass(attr) and
|
||||||
|
attr.__module__ == module.__name__ and
|
||||||
|
hasattr(attr, 'activate')):
|
||||||
|
plugin_class = attr
|
||||||
|
logger.info(f"Found plugin class: {attr.__name__} in {module_name}")
|
||||||
|
break
|
||||||
|
|
||||||
|
if not plugin_class:
|
||||||
|
logger.error(f"No plugin class found in {module_name}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Create and initialize plugin instance
|
||||||
|
logger.info(f"Creating instance of plugin class: {plugin_class.__name__}")
|
||||||
|
plugin_instance = plugin_class()
|
||||||
|
pluginInfo.instance = plugin_instance
|
||||||
|
|
||||||
|
# Ensure plugins have a reference to the app
|
||||||
|
plugin_instance.app = self.getApp()
|
||||||
|
logger.info(f"Set app reference for plugin: {module_name}")
|
||||||
|
|
||||||
|
if hasattr(plugin_instance, 'set_app'):
|
||||||
|
plugin_instance.set_app(self.getApp())
|
||||||
|
logger.info(f"Called set_app() for plugin: {module_name}")
|
||||||
|
|
||||||
|
if hasattr(plugin_instance, 'set_plugin_info'):
|
||||||
|
plugin_instance.set_plugin_info(pluginInfo)
|
||||||
|
logger.info(f"Called set_plugin_info() for plugin: {module_name}")
|
||||||
|
|
||||||
|
# Register with pluggy and activate
|
||||||
|
if self.plugin_manager is None:
|
||||||
|
logger.error(f"Plugin manager is None when loading {module_name}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
logger.info(f"Registering plugin with pluggy: {module_name}")
|
||||||
|
self.plugin_manager.register(plugin_instance)
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.info(f"Activating plugin: {module_name}")
|
||||||
|
self.plugin_manager.hook.activate(plugin=plugin_instance)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error activating plugin {module_name}: {e}")
|
||||||
|
import traceback
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return False
|
||||||
|
|
||||||
|
pluginInfo.loaded = True
|
||||||
|
logger.info(f"Successfully loaded plugin: {module_name}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to load plugin {module_name}: {e}")
|
||||||
|
import traceback
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return False
|
||||||
|
|
||||||
def unloadPlugin(self, pluginInfo):
|
def unloadPlugin(self, pluginInfo):
|
||||||
resourceManager = self.getApp().getResourceManager()
|
"""Unload a plugin."""
|
||||||
moduleName = pluginInfo.get_module_name()
|
# Skip if pluggy is not available
|
||||||
try:
|
if not PLUGGY_AVAILABLE:
|
||||||
if pluginInfo not in self.plugins:
|
|
||||||
print("Plugin missing: {}".format(moduleName))
|
|
||||||
return False
|
|
||||||
if self.isPluginBuildIn(pluginInfo):
|
|
||||||
return False
|
|
||||||
self.engine.unload_plugin(pluginInfo)
|
|
||||||
self.getApp().getResourceManager().removeResourceContext(moduleName)
|
|
||||||
self.engine.garbage_collect()
|
|
||||||
except Exception as e:
|
|
||||||
print('unloadPlugin:',e)
|
|
||||||
return False
|
return False
|
||||||
return True
|
|
||||||
def installPlugin(self, pluginFilePath, pluginType=PluginType.USER):
|
|
||||||
if not self.isValidPluginFile(pluginFilePath):
|
|
||||||
return False
|
|
||||||
pluginFolder = pluginType.get_root_dir()
|
|
||||||
if not pluginFolder.endswith('/'):
|
|
||||||
pluginFolder += '/'
|
|
||||||
if not os.path.exists(pluginFolder):
|
|
||||||
os.mkdir(pluginFolder)
|
|
||||||
else:
|
|
||||||
if not os.path.isdir(pluginFolder):
|
|
||||||
return False
|
|
||||||
try:
|
|
||||||
with tarfile.open(pluginFilePath) as tar:
|
|
||||||
tar.extractall(path=pluginFolder)
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
|
|
||||||
pluginModulePath = self.getModuleDirByPluginFile(pluginFilePath)
|
|
||||||
if pluginModulePath != '':
|
|
||||||
pluginModulePath = pluginFolder + pluginModulePath
|
|
||||||
self.setIgnoredPlugins(pluginModulePath[:-1], False) # without ending /
|
|
||||||
print('install', pluginFilePath)
|
|
||||||
self.callPackageTriggers(pluginModulePath, 'onPostInstall')
|
|
||||||
self.rescanPlugins()
|
|
||||||
|
|
||||||
return True
|
if pluginInfo.builtin:
|
||||||
def getModuleDirByPluginFile(self, pluginFilePath):
|
|
||||||
if not isinstance(pluginFilePath, str):
|
|
||||||
return ''
|
|
||||||
if pluginFilePath == '':
|
|
||||||
return ''
|
|
||||||
if not os.path.exists(pluginFilePath):
|
|
||||||
return ''
|
|
||||||
try:
|
|
||||||
with tarfile.open(pluginFilePath) as tar:
|
|
||||||
tarMembers = tar.getmembers()
|
|
||||||
for tarMember in tarMembers:
|
|
||||||
if tarMember.isdir():
|
|
||||||
return tarMember.name
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
return ''
|
|
||||||
def isValidPluginFile(self, pluginFilePath):
|
|
||||||
if not isinstance(pluginFilePath, str):
|
|
||||||
return False
|
return False
|
||||||
if pluginFilePath == '':
|
|
||||||
return False
|
|
||||||
if not os.path.exists(pluginFilePath):
|
|
||||||
return False
|
|
||||||
pluginFolder = ''
|
|
||||||
pluginFileExists = False
|
|
||||||
packageFileExists = False
|
|
||||||
try:
|
|
||||||
with tarfile.open(pluginFilePath) as tar:
|
|
||||||
tarMembers = tar.getmembers()
|
|
||||||
for tarMember in tarMembers:
|
|
||||||
if tarMember.isdir():
|
|
||||||
if pluginFolder == '':
|
|
||||||
pluginFolder = tarMember.name
|
|
||||||
if tarMember.isfile():
|
|
||||||
if tarMember.name.endswith('.plugin'):
|
|
||||||
pluginFileExists = True
|
|
||||||
if tarMember.name.endswith('package.py'):
|
|
||||||
pluginFileExists = True
|
|
||||||
if not tarMember.name.startswith(pluginFolder):
|
|
||||||
return False
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
return False
|
|
||||||
return pluginFileExists
|
|
||||||
def uninstallPlugin(self, pluginInfo):
|
|
||||||
if self.isPluginBuildIn(pluginInfo):
|
|
||||||
return False
|
|
||||||
# do we want to allow removing system plugins?
|
|
||||||
if PluginSystemManager.getPluginType(pluginInfo) == PluginType.SYSTEM:
|
|
||||||
return False
|
|
||||||
pluginFolder = pluginInfo.get_data_dir()
|
|
||||||
if not pluginFolder.endswith('/'):
|
|
||||||
pluginFolder += '/'
|
|
||||||
if not os.path.isdir(pluginFolder):
|
|
||||||
return False
|
|
||||||
if self.isPluginActive(pluginInfo):
|
|
||||||
self.setPluginActive(pluginInfo, False)
|
|
||||||
SettingsManager = self.app.getSettingsManager()
|
|
||||||
# TODO SettingsManager.set_settings_value_list('active-plugins', self.getActivePlugins())
|
|
||||||
self.callPackageTriggers(pluginFolder, 'onPreUninstall')
|
|
||||||
|
|
||||||
try:
|
|
||||||
shutil.rmtree(pluginFolder, ignore_errors=True)
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
return False
|
|
||||||
self.setIgnoredPlugins(pluginFolder, True)
|
|
||||||
self.rescanPlugins()
|
|
||||||
|
|
||||||
return True
|
module_name = pluginInfo.get_module_name()
|
||||||
def callPackageTriggers(self, pluginPath, trigger):
|
|
||||||
if not os.path.exists(pluginPath):
|
try:
|
||||||
|
# Not loaded?
|
||||||
|
if not pluginInfo.loaded:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Deactivate plugin
|
||||||
|
plugin_instance = pluginInfo.instance
|
||||||
|
if plugin_instance:
|
||||||
|
try:
|
||||||
|
self.plugin_manager.hook.deactivate(plugin=plugin_instance)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error deactivating plugin {module_name}: {e}")
|
||||||
|
|
||||||
|
# Unregister from pluggy
|
||||||
|
self.plugin_manager.unregister(plugin_instance)
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
pluginInfo.instance = None
|
||||||
|
pluginInfo.loaded = False
|
||||||
|
|
||||||
|
logger.info(f"Unloaded plugin: {module_name}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to unload plugin {module_name}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def unloadAllPlugins(self, ForceAllPlugins=False):
|
||||||
|
"""Unload all plugins."""
|
||||||
|
if not PLUGGY_AVAILABLE:
|
||||||
return
|
return
|
||||||
if not pluginPath.endswith('/'):
|
|
||||||
pluginPath += '/'
|
|
||||||
packageModulePath = pluginPath + 'package.py'
|
|
||||||
if not os.path.isfile(packageModulePath):
|
|
||||||
return
|
|
||||||
if not os.access(packageModulePath, os.R_OK):
|
|
||||||
return
|
|
||||||
package = self.getApp().getAPIHelper().importModule('package', packageModulePath)
|
|
||||||
|
|
||||||
if trigger == 'onPostInstall':
|
for pluginInfo in self.plugins:
|
||||||
try:
|
if ForceAllPlugins or pluginInfo.loaded:
|
||||||
package.onPostInstall(pluginPath, self.getApp())
|
self.unloadPlugin(pluginInfo)
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
elif trigger == 'onPreUninstall':
|
|
||||||
try:
|
|
||||||
package.onPreUninstall(pluginPath, self.getApp())
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
|
|
||||||
def _setupExtensionSet(self):
|
|
||||||
plugin_iface = API(self.getApp())
|
|
||||||
self.extension_set = Peas.ExtensionSet.new(self.engine,
|
|
||||||
Peas.Activatable,
|
|
||||||
["object"],
|
|
||||||
[plugin_iface])
|
|
||||||
self.extension_set.connect("extension-removed",
|
|
||||||
self.__extensionRemoved)
|
|
||||||
self.extension_set.connect("extension-added",
|
|
||||||
self.__extensionAdded)
|
|
||||||
|
|
||||||
def _setupPluginsDir(self):
|
|
||||||
system_plugins_dir = PluginType.SYSTEM.get_root_dir()
|
|
||||||
user_plugins_dir = PluginType.USER.get_root_dir()
|
|
||||||
if os.path.exists(user_plugins_dir):
|
|
||||||
self.engine.add_search_path(user_plugins_dir)
|
|
||||||
if os.path.exists(system_plugins_dir):
|
|
||||||
self.engine.add_search_path(system_plugins_dir)
|
|
||||||
|
|
||||||
def __extensionRemoved(self, unusedSet, pluginInfo, extension):
|
|
||||||
extension.deactivate()
|
|
||||||
|
|
||||||
def __extensionAdded(self, unusedSet, pluginInfo, extension):
|
|
||||||
extension.setApp(self.getApp())
|
|
||||||
extension.setPluginInfo(pluginInfo)
|
|
||||||
extension.activate()
|
|
||||||
def __loadedPlugins(self, engine, unusedSet):
|
|
||||||
"""Handles the changing of the loaded plugin list."""
|
|
||||||
self.getApp().settings.ActivePlugins = engine.get_property("loaded-plugins")
|
|
||||||
|
|
||||||
class APIHelper():
|
|
||||||
def __init__(self, app):
|
|
||||||
self.app = app
|
|
||||||
self.cthulhuKeyBindings = None
|
|
||||||
|
|
||||||
'''
|
|
||||||
_pluginAPIManager.seCthulhuAPI('Logger', _logger)
|
|
||||||
_pluginAPIManager.setCthulhuAPI('SettingsManager', _settingsManager)
|
|
||||||
_pluginAPIManager.setCthulhuAPI('ScriptManager', _scriptManager)
|
|
||||||
_pluginAPIManager.setCthulhuAPI('EventManager', _eventManager)
|
|
||||||
_pluginAPIManager.setCthulhuAPI('Speech', speech)
|
|
||||||
_pluginAPIManager.setCthulhuAPI('Sound', sound)
|
|
||||||
_pluginAPIManager.setCthulhuAPI('Braille', braille)
|
|
||||||
_pluginAPIManager.setCthulhuAPI('Debug', debug)
|
|
||||||
_pluginAPIManager.setCthulhuAPI('Messages', messages)
|
|
||||||
_pluginAPIManager.setCthulhuAPI('MouseReview', mouse_review)
|
|
||||||
_pluginAPIManager.setCthulhuAPI('NotificationMessages', notification_messages)
|
|
||||||
_pluginAPIManager.setCthulhuAPI('CthulhuState', cthulhu_state)
|
|
||||||
_pluginAPIManager.setCthulhuAPI('CthulhuPlatform', cthulhu_platform)
|
|
||||||
_pluginAPIManager.setCthulhuAPI('Settings', settings)
|
|
||||||
_pluginAPIManager.setCthulhuAPI('Keybindings', keybindings)
|
|
||||||
'''
|
|
||||||
def outputMessage(self, Message, interrupt=False):
|
|
||||||
settings = self.app.getDynamicApiManager().getAPI('Settings')
|
|
||||||
braille = self.app.getDynamicApiManager().getAPI('Braille')
|
|
||||||
speech = self.app.getDynamicApiManager().getAPI('Speech')
|
|
||||||
if speech != None:
|
|
||||||
if (settings.enableSpeech):
|
|
||||||
if interrupt:
|
|
||||||
speech.cancel()
|
|
||||||
if Message != '':
|
|
||||||
speech.speak(Message)
|
|
||||||
if braille != None:
|
|
||||||
if (settings.enableBraille):
|
|
||||||
braille.displayMessage(Message)
|
|
||||||
def createInputEventHandler(self, function, name, learnModeEnabled=True):
|
|
||||||
EventManager = self.app.getDynamicApiManager().getAPI('EventManager')
|
|
||||||
newInputEventHandler = EventManager.input_event.InputEventHandler(function, name, learnModeEnabled)
|
|
||||||
return newInputEventHandler
|
|
||||||
def registerGestureByString(self, function, name, gestureString, profile, application, learnModeEnabled = True, contextName = None):
|
|
||||||
gestureList = gestureString.split(',')
|
|
||||||
registeredGestures = []
|
|
||||||
for gesture in gestureList:
|
|
||||||
if gesture.startswith('kb:'):
|
|
||||||
shortcutString = gesture[3:]
|
|
||||||
registuredGesture = self.registerShortcutByString(function, name, shortcutString, profile, application, learnModeEnabled, contextName=contextName)
|
|
||||||
if registuredGesture:
|
|
||||||
registeredGestures.append(registuredGesture)
|
|
||||||
return registeredGestures
|
|
||||||
def registerShortcutByString(self, function, name, shortcutString, profile, application, learnModeEnabled = True, contextName = None):
|
|
||||||
keybindings = self.app.getDynamicApiManager().getAPI('Keybindings')
|
|
||||||
settings = self.app.getDynamicApiManager().getAPI('Settings')
|
|
||||||
resourceManager = self.app.getResourceManager()
|
|
||||||
|
|
||||||
clickCount = 0
|
|
||||||
cthulhuKey = False
|
|
||||||
shiftKey = False
|
|
||||||
ctrlKey = False
|
|
||||||
altKey = False
|
|
||||||
key = ''
|
|
||||||
shortcutList = shortcutString.split('+')
|
|
||||||
for shortcutElement in shortcutList:
|
|
||||||
shortcutElementLower = shortcutElement.lower()
|
|
||||||
if shortcutElementLower == 'press':
|
|
||||||
clickCount += 1
|
|
||||||
elif shortcutElement == 'cthulhu':
|
|
||||||
cthulhuKey = True
|
|
||||||
elif shortcutElementLower == 'shift':
|
|
||||||
shiftKey = True
|
|
||||||
elif shortcutElementLower == 'control':
|
|
||||||
ctrlKey = True
|
|
||||||
elif shortcutElementLower == 'alt':
|
|
||||||
altKey = True
|
|
||||||
else:
|
|
||||||
key = shortcutElementLower
|
|
||||||
if clickCount == 0:
|
|
||||||
clickCount = 1
|
|
||||||
|
|
||||||
if self.cthulhuKeyBindings == None:
|
|
||||||
self.cthulhuKeyBindings = keybindings.KeyBindings()
|
|
||||||
|
|
||||||
tryFunction = resource_manager.TryFunction(function)
|
|
||||||
newInputEventHandler = self.createInputEventHandler(tryFunction.runInputEvent, name, learnModeEnabled)
|
|
||||||
|
|
||||||
currModifierMask = keybindings.NO_MODIFIER_MASK
|
|
||||||
if cthulhuKey:
|
|
||||||
currModifierMask = currModifierMask | 1 << keybindings.MODIFIER_CTHULHU
|
|
||||||
if shiftKey:
|
|
||||||
currModifierMask = currModifierMask | 1 << Atspi.ModifierType.SHIFT
|
|
||||||
if altKey:
|
|
||||||
currModifierMask = currModifierMask | 1 << Atspi.ModifierType.ALT
|
|
||||||
if ctrlKey:
|
|
||||||
currModifierMask = currModifierMask | 1 << Atspi.ModifierType.CONTROL
|
|
||||||
|
|
||||||
newKeyBinding = keybindings.KeyBinding(key, keybindings.defaultModifierMask, currModifierMask, newInputEventHandler, clickCount)
|
|
||||||
self.cthulhuKeyBindings.add(newKeyBinding)
|
|
||||||
|
|
||||||
settings.keyBindingsMap["default"] = self.cthulhuKeyBindings
|
|
||||||
|
|
||||||
if contextName:
|
|
||||||
resourceContext = resourceManager.getResourceContext(contextName)
|
|
||||||
if resourceContext:
|
|
||||||
resourceEntry = resource_manager.ResourceEntry('keyboard', newKeyBinding, function, tryFunction, shortcutString)
|
|
||||||
resourceContext.addGesture(profile, application, newKeyBinding, resourceEntry)
|
|
||||||
|
|
||||||
return newKeyBinding
|
|
||||||
|
|
||||||
def unregisterShortcut(self, KeyBindingToRemove, contextName = None):
|
|
||||||
ok = False
|
|
||||||
keybindings = self.app.getDynamicApiManager().getAPI('Keybindings')
|
|
||||||
settings = self.app.getDynamicApiManager().getAPI('Settings')
|
|
||||||
resourceManager = self.app.getResourceManager()
|
|
||||||
|
|
||||||
if self.cthulhuKeyBindings == None:
|
|
||||||
self.cthulhuKeyBindings = keybindings.KeyBindings()
|
|
||||||
try:
|
|
||||||
self.cthulhuKeyBindings.remove(KeyBindingToRemove)
|
|
||||||
settings.keyBindingsMap["default"] = self.cthulhuKeyBindings
|
|
||||||
ok = True
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
if contextName:
|
|
||||||
resourceContext = resourceManager.getResourceContext(contextName)
|
|
||||||
if resourceContext:
|
|
||||||
resourceContext.removeGesture(KeyBindingToRemove)
|
|
||||||
|
|
||||||
return ok
|
|
||||||
def importModule(self, moduleName, moduleLocation):
|
|
||||||
if version in ["3.3","3.4"]:
|
|
||||||
return SourceFileLoader(moduleName, moduleLocation).load_module()
|
|
||||||
else:
|
|
||||||
spec = importlib.util.spec_from_file_location(moduleName, moduleLocation)
|
|
||||||
driver_mod = importlib.util.module_from_spec(spec)
|
|
||||||
spec.loader.exec_module(driver_mod)
|
|
||||||
return driver_mod
|
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
[Plugin]
|
|
||||||
Module=ByeCthulhu
|
|
||||||
Loader=python3
|
|
||||||
Name=Stop announcement for cthulhu
|
|
||||||
Description=Test plugin for cthulhu
|
|
||||||
Authors=Chrys chrys@linux-a11y.org
|
|
@ -1,52 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
#
|
|
||||||
# Copyright (c) 2024 Stormux
|
|
||||||
# Copyright (c) 2010-2012 The Orca Team
|
|
||||||
# Copyright (c) 2012 Igalia, S.L.
|
|
||||||
# Copyright (c) 2005-2010 Sun Microsystems Inc.
|
|
||||||
#
|
|
||||||
# This library is free software; you can redistribute it and/or
|
|
||||||
# modify it under the terms of the GNU Lesser General Public
|
|
||||||
# License as published by the Free Software Foundation; either
|
|
||||||
# version 2.1 of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This library is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
# Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Lesser General Public
|
|
||||||
# License along with this library; if not, write to the
|
|
||||||
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
|
|
||||||
# Boston MA 02110-1301 USA.
|
|
||||||
#
|
|
||||||
# Fork of Orca Screen Reader (GNOME)
|
|
||||||
# Original source: https://gitlab.gnome.org/GNOME/orca
|
|
||||||
|
|
||||||
from cthulhu import plugin
|
|
||||||
|
|
||||||
import gi
|
|
||||||
gi.require_version('Peas', '1.0')
|
|
||||||
from gi.repository import GObject
|
|
||||||
from gi.repository import Peas
|
|
||||||
import time
|
|
||||||
|
|
||||||
|
|
||||||
class ByeCthulhu(GObject.Object, Peas.Activatable, plugin.Plugin):
|
|
||||||
#__gtype_name__ = 'ByeCthulhu'
|
|
||||||
|
|
||||||
object = GObject.Property(type=GObject.Object)
|
|
||||||
def __init__(self):
|
|
||||||
plugin.Plugin.__init__(self)
|
|
||||||
def do_activate(self):
|
|
||||||
API = self.object
|
|
||||||
self.connectSignal("stop-application-completed", self.process)
|
|
||||||
def do_deactivate(self):
|
|
||||||
API = self.object
|
|
||||||
def do_update_state(self):
|
|
||||||
API = self.object
|
|
||||||
def process(self, app):
|
|
||||||
messages = app.getDynamicApiManager().getAPI('Messages')
|
|
||||||
activeScript = app.getDynamicApiManager().getAPI('CthulhuState').activeScript
|
|
||||||
activeScript.presentationInterrupt()
|
|
||||||
activeScript.presentMessage(messages.STOP_CTHULHU, resetStyles=False)
|
|
@ -1,7 +1,7 @@
|
|||||||
cthulhu_python_PYTHON = \
|
cthulhu_python_PYTHON = \
|
||||||
__init__.py \
|
__init__.py \
|
||||||
ByeCthulhu.plugin \
|
plugin.info \
|
||||||
ByeCthulhu.py
|
plugin.py
|
||||||
|
|
||||||
cthulhu_pythondir=$(pkgpythondir)/plugins/ByeCthulhu
|
cthulhu_pythondir=$(pkgpythondir)/plugins/ByeCthulhu
|
||||||
|
|
||||||
|
8
src/cthulhu/plugins/ByeCthulhu/plugin.info
Normal file
8
src/cthulhu/plugins/ByeCthulhu/plugin.info
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
name = Bye Cthulhu
|
||||||
|
version = 1.0.0
|
||||||
|
description = Says goodbye when Cthulhu is shutting down
|
||||||
|
authors = Stormux <storm_dragon@stormux.org>
|
||||||
|
website = https://stormux.org
|
||||||
|
copyright = Copyright 2025
|
||||||
|
builtin = false
|
||||||
|
hidden = false
|
75
src/cthulhu/plugins/ByeCthulhu/plugin.py
Normal file
75
src/cthulhu/plugins/ByeCthulhu/plugin.py
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
#!/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.
|
||||||
|
|
||||||
|
"""Bye Cthulhu plugin for Cthulhu."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
from cthulhu.plugin import Plugin, cthulhu_hookimpl
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class ByeCthulhu(Plugin):
|
||||||
|
"""Plugin that speaks a goodbye message when Cthulhu is shutting down."""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
"""Initialize the plugin."""
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
logger.info("ByeCthulhu plugin initialized")
|
||||||
|
self._signal_handler_id = None
|
||||||
|
|
||||||
|
@cthulhu_hookimpl
|
||||||
|
def activate(self, plugin=None):
|
||||||
|
"""Activate the plugin."""
|
||||||
|
# Skip if this activation call isn't for us
|
||||||
|
if plugin is not None and plugin is not self:
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info("Activating ByeCthulhu plugin")
|
||||||
|
try:
|
||||||
|
# Connect to the stop-application-completed signal
|
||||||
|
signal_manager = self.app.getSignalManager()
|
||||||
|
self._signal_handler_id = signal_manager.connectSignal(
|
||||||
|
"stop-application-completed",
|
||||||
|
self.process,
|
||||||
|
"default" # Add profile parameter
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error activating ByeCthulhu plugin: {e}")
|
||||||
|
|
||||||
|
@cthulhu_hookimpl
|
||||||
|
def deactivate(self, plugin=None):
|
||||||
|
"""Deactivate the plugin."""
|
||||||
|
# Skip if this deactivation call isn't for us
|
||||||
|
if plugin is not None and plugin is not self:
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info("Deactivating ByeCthulhu plugin")
|
||||||
|
try:
|
||||||
|
# Disconnect signal if we have an ID
|
||||||
|
if self._signal_handler_id is not None:
|
||||||
|
signal_manager = self.app.getSignalManager()
|
||||||
|
# Use disconnectSignalByFunction instead since disconnectSignal doesn't exist
|
||||||
|
signal_manager.disconnectSignalByFunction(
|
||||||
|
self.process
|
||||||
|
)
|
||||||
|
self._signal_handler_id = None
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error deactivating ByeCthulhu plugin: {e}")
|
||||||
|
|
||||||
|
def process(self, app):
|
||||||
|
"""Process the stop-application-completed signal."""
|
||||||
|
try:
|
||||||
|
messages = app.getDynamicApiManager().getAPI('Messages')
|
||||||
|
state = app.getDynamicApiManager().getAPI('CthulhuState')
|
||||||
|
if state.activeScript:
|
||||||
|
state.activeScript.presentationInterrupt()
|
||||||
|
state.activeScript.presentMessage(messages.STOP_CTHULHU, resetStyles=False)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in ByeCthulhu process: {e}")
|
@ -1,6 +0,0 @@
|
|||||||
[Plugin]
|
|
||||||
Module=CapsLockHack
|
|
||||||
Loader=python3
|
|
||||||
Name=Caps Lock Hack
|
|
||||||
Description=Fix Capslock sometimes switch on / off when its used as modifier
|
|
||||||
Authors=Chrys chrys@linux-a11y.org
|
|
@ -1,144 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
#
|
|
||||||
# Copyright (c) 2024 Stormux
|
|
||||||
# Copyright (c) 2010-2012 The Orca Team
|
|
||||||
# Copyright (c) 2012 Igalia, S.L.
|
|
||||||
# Copyright (c) 2005-2010 Sun Microsystems Inc.
|
|
||||||
#
|
|
||||||
# This library is free software; you can redistribute it and/or
|
|
||||||
# modify it under the terms of the GNU Lesser General Public
|
|
||||||
# License as published by the Free Software Foundation; either
|
|
||||||
# version 2.1 of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This library is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
# Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Lesser General Public
|
|
||||||
# License along with this library; if not, write to the
|
|
||||||
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
|
|
||||||
# Boston MA 02110-1301 USA.
|
|
||||||
#
|
|
||||||
# Fork of Orca Screen Reader (GNOME)
|
|
||||||
# Original source: https://gitlab.gnome.org/GNOME/orca
|
|
||||||
|
|
||||||
from cthulhu import plugin
|
|
||||||
|
|
||||||
import gi
|
|
||||||
gi.require_version('Peas', '1.0')
|
|
||||||
from gi.repository import GObject
|
|
||||||
from gi.repository import Peas
|
|
||||||
from threading import Thread, Lock
|
|
||||||
import subprocess, time, re, os
|
|
||||||
|
|
||||||
class CapsLockHack(GObject.Object, Peas.Activatable, plugin.Plugin):
|
|
||||||
__gtype_name__ = 'CapsLockHack'
|
|
||||||
|
|
||||||
object = GObject.Property(type=GObject.Object)
|
|
||||||
def __init__(self):
|
|
||||||
plugin.Plugin.__init__(self)
|
|
||||||
self.lock = Lock()
|
|
||||||
self.active = False
|
|
||||||
self.workerThread = Thread(target=self.worker)
|
|
||||||
def do_activate(self):
|
|
||||||
API = self.object
|
|
||||||
"""Enable or disable use of the caps lock key as an Cthulhu modifier key."""
|
|
||||||
self.interpretCapsLineProg = re.compile(
|
|
||||||
r'^\s*interpret\s+Caps[_+]Lock[_+]AnyOfOrNone\s*\(all\)\s*{\s*$', re.I)
|
|
||||||
self.normalCapsLineProg = re.compile(
|
|
||||||
r'^\s*action\s*=\s*LockMods\s*\(\s*modifiers\s*=\s*Lock\s*\)\s*;\s*$', re.I)
|
|
||||||
self.interpretShiftLineProg = re.compile(
|
|
||||||
r'^\s*interpret\s+Shift[_+]Lock[_+]AnyOf\s*\(\s*Shift\s*\+\s*Lock\s*\)\s*{\s*$', re.I)
|
|
||||||
self.normalShiftLineProg = re.compile(
|
|
||||||
r'^\s*action\s*=\s*LockMods\s*\(\s*modifiers\s*=\s*Shift\s*\)\s*;\s*$', re.I)
|
|
||||||
self.disabledModLineProg = re.compile(
|
|
||||||
r'^\s*action\s*=\s*NoAction\s*\(\s*\)\s*;\s*$', re.I)
|
|
||||||
self.normalCapsLine = ' action= LockMods(modifiers=Lock);'
|
|
||||||
self.normalShiftLine = ' action= LockMods(modifiers=Shift);'
|
|
||||||
self.disabledModLine = ' action= NoAction();'
|
|
||||||
self.activateWorker()
|
|
||||||
def do_deactivate(self):
|
|
||||||
API = self.object
|
|
||||||
self.deactivateWorker()
|
|
||||||
def do_update_state(self):
|
|
||||||
API = self.object
|
|
||||||
def deactivateWorker(self):
|
|
||||||
with self.lock:
|
|
||||||
self.active = False
|
|
||||||
self.workerThread.join()
|
|
||||||
def activateWorker(self):
|
|
||||||
with self.lock:
|
|
||||||
self.active = True
|
|
||||||
self.workerThread.start()
|
|
||||||
def isActive(self):
|
|
||||||
with self.lock:
|
|
||||||
return self.active
|
|
||||||
def worker(self):
|
|
||||||
"""Makes an Cthulhu-specific Xmodmap so that the keys behave as we
|
|
||||||
need them to do. This is especially the case for the Cthulhu modifier.
|
|
||||||
"""
|
|
||||||
API = self.object
|
|
||||||
capsLockCleared = False
|
|
||||||
settings = API.app.getDynamicApiManager().getAPI('Settings')
|
|
||||||
time.sleep(3)
|
|
||||||
while self.isActive():
|
|
||||||
if "Caps_Lock" in settings.cthulhuModifierKeys \
|
|
||||||
or "Shift_Lock" in settings.cthulhuModifierKeys:
|
|
||||||
self.setCapsLockAsCthulhuModifier(True)
|
|
||||||
capsLockCleared = True
|
|
||||||
elif capsLockCleared:
|
|
||||||
self.setCapsLockAsCthulhuModifier(False)
|
|
||||||
capsLockCleared = False
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
def setCapsLockAsCthulhuModifier(self, enable):
|
|
||||||
originalXmodmap = None
|
|
||||||
lines = None
|
|
||||||
try:
|
|
||||||
originalXmodmap = subprocess.check_output(['xkbcomp', os.environ['DISPLAY'], '-'])
|
|
||||||
lines = originalXmodmap.decode('UTF-8').split('\n')
|
|
||||||
except:
|
|
||||||
return
|
|
||||||
foundCapsInterpretSection = False
|
|
||||||
foundShiftInterpretSection = False
|
|
||||||
modified = False
|
|
||||||
for i, line in enumerate(lines):
|
|
||||||
if not foundCapsInterpretSection and not foundShiftInterpretSection:
|
|
||||||
if self.interpretCapsLineProg.match(line):
|
|
||||||
foundCapsInterpretSection = True
|
|
||||||
elif self.interpretShiftLineProg.match(line):
|
|
||||||
foundShiftInterpretSection = True
|
|
||||||
elif foundCapsInterpretSection:
|
|
||||||
if enable:
|
|
||||||
if self.normalCapsLineProg.match(line):
|
|
||||||
lines[i] = self.disabledModLine
|
|
||||||
modified = True
|
|
||||||
else:
|
|
||||||
if self.disabledModLineProg.match(line):
|
|
||||||
lines[i] = self.normalCapsLine
|
|
||||||
modified = True
|
|
||||||
if line.find('}'):
|
|
||||||
foundCapsInterpretSection = False
|
|
||||||
else: # foundShiftInterpretSection
|
|
||||||
if enable:
|
|
||||||
if self.normalShiftLineProg.match(line):
|
|
||||||
lines[i] = self.disabledModLine
|
|
||||||
modified = True
|
|
||||||
else:
|
|
||||||
if self.disabledModLineProg.match(line):
|
|
||||||
lines[i] = self.normalShiftLine
|
|
||||||
modified = True
|
|
||||||
if line.find('}'):
|
|
||||||
foundShiftInterpretSection = False
|
|
||||||
if modified:
|
|
||||||
newXmodMap = bytes('\n'.join(lines), 'UTF-8')
|
|
||||||
self.setXmodmap(newXmodMap)
|
|
||||||
def setXmodmap(self, xkbmap):
|
|
||||||
"""Set the keyboard map using xkbcomp."""
|
|
||||||
try:
|
|
||||||
p = subprocess.Popen(['xkbcomp', '-w0', '-', os.environ['DISPLAY']],
|
|
||||||
stdin=subprocess.PIPE, stdout=None, stderr=None)
|
|
||||||
p.communicate(xkbmap)
|
|
||||||
except:
|
|
||||||
pass
|
|
@ -1,7 +0,0 @@
|
|||||||
cthulhu_python_PYTHON = \
|
|
||||||
__init__.py \
|
|
||||||
CapsLockHack.plugin \
|
|
||||||
CapsLockHack.py
|
|
||||||
|
|
||||||
cthulhu_pythondir=$(pkgpythondir)/plugins/CapsLockHack
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
|||||||
[Plugin]
|
|
||||||
Module=Clipboard
|
|
||||||
Loader=python3
|
|
||||||
Name=Clipboard
|
|
||||||
Description=Present the content of the current clipboard
|
|
||||||
Authors=Chrys chrys@linux-a11y.org
|
|
@ -1,101 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
#
|
|
||||||
# Copyright (c) 2024 Stormux
|
|
||||||
# Copyright (c) 2010-2012 The Orca Team
|
|
||||||
# Copyright (c) 2012 Igalia, S.L.
|
|
||||||
# Copyright (c) 2005-2010 Sun Microsystems Inc.
|
|
||||||
#
|
|
||||||
# This library is free software; you can redistribute it and/or
|
|
||||||
# modify it under the terms of the GNU Lesser General Public
|
|
||||||
# License as published by the Free Software Foundation; either
|
|
||||||
# version 2.1 of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This library is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
# Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Lesser General Public
|
|
||||||
# License along with this library; if not, write to the
|
|
||||||
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
|
|
||||||
# Boston MA 02110-1301 USA.
|
|
||||||
#
|
|
||||||
# Fork of Orca Screen Reader (GNOME)
|
|
||||||
# Original source: https://gitlab.gnome.org/GNOME/orca
|
|
||||||
|
|
||||||
from cthulhu import plugin
|
|
||||||
|
|
||||||
import gi, os
|
|
||||||
gi.require_version('Peas', '1.0')
|
|
||||||
from gi.repository import GObject
|
|
||||||
from gi.repository import Peas
|
|
||||||
gi.require_version("Gtk", "3.0")
|
|
||||||
from gi.repository import Gtk, Gdk
|
|
||||||
|
|
||||||
class Clipboard(GObject.Object, Peas.Activatable, plugin.Plugin):
|
|
||||||
#__gtype_name__ = 'Clipboard'
|
|
||||||
|
|
||||||
object = GObject.Property(type=GObject.Object)
|
|
||||||
def __init__(self):
|
|
||||||
plugin.Plugin.__init__(self)
|
|
||||||
def do_activate(self):
|
|
||||||
API = self.object
|
|
||||||
self.registerGestureByString(self.speakClipboard, _('clipboard'), 'kb:cthulhu+c')
|
|
||||||
def do_deactivate(self):
|
|
||||||
API = self.object
|
|
||||||
def do_update_state(self):
|
|
||||||
API = self.object
|
|
||||||
def speakClipboard(self, script=None, inputEvent=None):
|
|
||||||
API = self.object
|
|
||||||
Message = self.getClipboard()
|
|
||||||
API.app.getDynamicApiManager().getAPI('CthulhuState').activeScript.presentMessage(Message, resetStyles=False)
|
|
||||||
return True
|
|
||||||
def getClipboard(self):
|
|
||||||
Message = ""
|
|
||||||
FoundClipboardContent = False
|
|
||||||
# Get Clipboard
|
|
||||||
ClipboardObj = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
|
|
||||||
|
|
||||||
ClipboardText = ClipboardObj.wait_for_text()
|
|
||||||
ClipboardImage = ClipboardObj.wait_for_image()
|
|
||||||
ClipboardURI = ClipboardObj.wait_for_uris()
|
|
||||||
if (ClipboardText != None):
|
|
||||||
FoundClipboardContent = True
|
|
||||||
if (ClipboardObj.wait_is_uris_available()):
|
|
||||||
noOfObjects = 0
|
|
||||||
noOfFolder = 0
|
|
||||||
noOfFiles = 0
|
|
||||||
noOfDisks = 0
|
|
||||||
noOfLinks = 0
|
|
||||||
for Uri in ClipboardURI:
|
|
||||||
if Uri == '':
|
|
||||||
continue
|
|
||||||
noOfObjects += 1
|
|
||||||
uriWithoutProtocoll = Uri[Uri.find('://') + 3:]
|
|
||||||
Message += " " + Uri[Uri.rfind('/') + 1:] + " "
|
|
||||||
if (os.path.isdir(uriWithoutProtocoll)):
|
|
||||||
noOfFolder += 1
|
|
||||||
Message = Message + _("Folder") #Folder
|
|
||||||
if (os.path.isfile(uriWithoutProtocoll)):
|
|
||||||
noOfFiles += 1
|
|
||||||
Message = Message + _("File") #File
|
|
||||||
if (os.path.ismount(uriWithoutProtocoll)):
|
|
||||||
noOfDisks += 1
|
|
||||||
Message = Message + _("Disk") #Mountpoint
|
|
||||||
if (os.path.islink(uriWithoutProtocoll)):
|
|
||||||
noOfLinks += 1
|
|
||||||
Message = Message + _("Link") #Link
|
|
||||||
if (noOfObjects > 1):
|
|
||||||
Message = str(noOfObjects) + _(" Objects in clipboard ") + Message # X Objects in Clipboard Object Object
|
|
||||||
else:
|
|
||||||
Message = str(noOfObjects) + _(" Object in clipboard ") + Message # 1 Object in Clipboard Object
|
|
||||||
else:
|
|
||||||
Message = _("Text in clipboard ") + ClipboardText # Text in Clipboard
|
|
||||||
|
|
||||||
if (ClipboardImage != None):
|
|
||||||
FoundClipboardContent = True
|
|
||||||
Message = _("The clipboard contains a image") # Image is in Clipboard
|
|
||||||
|
|
||||||
if (not FoundClipboardContent):
|
|
||||||
Message = _("The clipboard is empty")
|
|
||||||
return Message
|
|
@ -1,7 +1,7 @@
|
|||||||
cthulhu_python_PYTHON = \
|
cthulhu_python_PYTHON = \
|
||||||
__init__.py \
|
__init__.py \
|
||||||
Clipboard.plugin \
|
plugin.info \
|
||||||
Clipboard.py
|
plugin.py
|
||||||
|
|
||||||
cthulhu_pythondir=$(pkgpythondir)/plugins/Clipboard
|
cthulhu_pythondir=$(pkgpythondir)/plugins/Clipboard
|
||||||
|
|
||||||
|
7
src/cthulhu/plugins/Clipboard/plugin.info
Normal file
7
src/cthulhu/plugins/Clipboard/plugin.info
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
[Plugin]
|
||||||
|
Name = Clipboard
|
||||||
|
Module = Clipboard
|
||||||
|
Description = Present the content of the current clipboard
|
||||||
|
Authors = Storm Dragon <storm_dragon@stormux.org>
|
||||||
|
Version = 1.0
|
||||||
|
Category = Utilities
|
193
src/cthulhu/plugins/Clipboard/plugin.py
Normal file
193
src/cthulhu/plugins/Clipboard/plugin.py
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
#
|
||||||
|
# Copyright (c) 2024 Stormux
|
||||||
|
# Copyright (c) 2010-2012 The Orca Team
|
||||||
|
# Copyright (c) 2012 Igalia, S.L.
|
||||||
|
# Copyright (c) 2005-2010 Sun Microsystems Inc.
|
||||||
|
#
|
||||||
|
# This library is free software; you can redistribute it and/or
|
||||||
|
# modify it under the terms of the GNU Lesser General Public
|
||||||
|
# License as published by the Free Software Foundation; either
|
||||||
|
# version 2.1 of the License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
# Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public
|
||||||
|
# License along with this library; if not, write to the
|
||||||
|
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
|
||||||
|
# Boston MA 02110-1301 USA.
|
||||||
|
#
|
||||||
|
# Fork of Orca Screen Reader (GNOME)
|
||||||
|
# Original source: https://gitlab.gnome.org/GNOME/orca
|
||||||
|
|
||||||
|
"""Clipboard plugin for Cthulhu."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
import gettext
|
||||||
|
import gi
|
||||||
|
|
||||||
|
gi.require_version("Gtk", "3.0")
|
||||||
|
from gi.repository import Gtk, Gdk
|
||||||
|
|
||||||
|
from cthulhu.plugin import Plugin, cthulhu_hookimpl
|
||||||
|
|
||||||
|
# Set up translation function
|
||||||
|
_ = gettext.gettext
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class Clipboard(Plugin):
|
||||||
|
"""Plugin to read the clipboard contents."""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
"""Initialize the plugin."""
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
logger.info("Clipboard plugin initialized")
|
||||||
|
self._signal_handler_id = None
|
||||||
|
|
||||||
|
@cthulhu_hookimpl
|
||||||
|
def activate(self, plugin=None):
|
||||||
|
"""Activate the plugin."""
|
||||||
|
# Skip if this activation call isn't for us
|
||||||
|
if plugin is not None and plugin is not self:
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info("Activating Clipboard plugin")
|
||||||
|
try:
|
||||||
|
# Register keyboard shortcut
|
||||||
|
self.registerGestureByString(self.speakClipboard, _('clipboard'), 'kb:cthulhu+shift+c')
|
||||||
|
logger.debug("Registered shortcut for clipboard")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error activating Clipboard plugin: {e}")
|
||||||
|
|
||||||
|
@cthulhu_hookimpl
|
||||||
|
def deactivate(self, plugin=None):
|
||||||
|
"""Deactivate the plugin."""
|
||||||
|
# Skip if this deactivation call isn't for us
|
||||||
|
if plugin is not None and plugin is not self:
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info("Deactivating Clipboard plugin")
|
||||||
|
try:
|
||||||
|
# Unregister keyboard shortcut
|
||||||
|
if self.app:
|
||||||
|
api_helper = self.app.getAPIHelper()
|
||||||
|
if api_helper and hasattr(api_helper, 'unregisterShortcut'):
|
||||||
|
api_helper.unregisterShortcut('kb:cthulhu+shift+c')
|
||||||
|
logger.debug("Unregistered clipboard shortcut")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error deactivating Clipboard plugin: {e}")
|
||||||
|
"""Activate the plugin."""
|
||||||
|
# Skip if this activation call isn't for us
|
||||||
|
if plugin is not None and plugin is not self:
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info("Activating Clipboard plugin")
|
||||||
|
try:
|
||||||
|
# Register keyboard shortcut
|
||||||
|
self.registerGestureByString(self.speakClipboard, _('clipboard'), 'kb:cthulhu+shift+c')
|
||||||
|
logger.debug("Registered shortcut for clipboard")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error activating Clipboard plugin: {e}")
|
||||||
|
|
||||||
|
@cthulhu_hookimpl
|
||||||
|
def deactivate(self, plugin=None):
|
||||||
|
"""Deactivate the plugin."""
|
||||||
|
# Skip if this deactivation call isn't for us
|
||||||
|
if plugin is not None and plugin is not self:
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info("Deactivating Clipboard plugin")
|
||||||
|
try:
|
||||||
|
# Unregister keyboard shortcut
|
||||||
|
self.unregisterGestureByString('kb:cthulhu+shift+c')
|
||||||
|
logger.debug("Unregistered clipboard shortcut")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error deactivating Clipboard plugin: {e}")
|
||||||
|
|
||||||
|
def speakClipboard(self, script=None, inputEvent=None):
|
||||||
|
"""Present the contents of the clipboard."""
|
||||||
|
try:
|
||||||
|
message = self.getClipboard()
|
||||||
|
|
||||||
|
state = self.app.getDynamicApiManager().getAPI('CthulhuState')
|
||||||
|
if state and state.activeScript:
|
||||||
|
state.activeScript.presentMessage(message, resetStyles=False)
|
||||||
|
logger.debug("Presented clipboard contents")
|
||||||
|
else:
|
||||||
|
logger.warning("Could not present clipboard: no active script")
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in speakClipboard: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def getClipboard(self):
|
||||||
|
"""Get the contents of the clipboard."""
|
||||||
|
try:
|
||||||
|
message = ""
|
||||||
|
found_clipboard_content = False
|
||||||
|
|
||||||
|
# Get Clipboard
|
||||||
|
clipboard_obj = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
|
||||||
|
|
||||||
|
clipboard_text = clipboard_obj.wait_for_text()
|
||||||
|
clipboard_image = clipboard_obj.wait_for_image()
|
||||||
|
clipboard_uri = clipboard_obj.wait_for_uris()
|
||||||
|
|
||||||
|
if clipboard_text is not None:
|
||||||
|
found_clipboard_content = True
|
||||||
|
|
||||||
|
if clipboard_obj.wait_is_uris_available():
|
||||||
|
no_of_objects = 0
|
||||||
|
no_of_folder = 0
|
||||||
|
no_of_files = 0
|
||||||
|
no_of_disks = 0
|
||||||
|
no_of_links = 0
|
||||||
|
|
||||||
|
for uri in clipboard_uri:
|
||||||
|
if uri == '':
|
||||||
|
continue
|
||||||
|
|
||||||
|
no_of_objects += 1
|
||||||
|
uri_without_protocol = uri[uri.find('://') + 3:]
|
||||||
|
message += " " + uri[uri.rfind('/') + 1:] + " "
|
||||||
|
|
||||||
|
if os.path.isdir(uri_without_protocol):
|
||||||
|
no_of_folder += 1
|
||||||
|
message = message + _("Folder")
|
||||||
|
|
||||||
|
if os.path.isfile(uri_without_protocol):
|
||||||
|
no_of_files += 1
|
||||||
|
message = message + _("File")
|
||||||
|
|
||||||
|
if os.path.ismount(uri_without_protocol):
|
||||||
|
no_of_disks += 1
|
||||||
|
message = message + _("Disk")
|
||||||
|
|
||||||
|
if os.path.islink(uri_without_protocol):
|
||||||
|
no_of_links += 1
|
||||||
|
message = message + _("Link")
|
||||||
|
|
||||||
|
if no_of_objects > 1:
|
||||||
|
message = str(no_of_objects) + _(" Objects in clipboard ") + message
|
||||||
|
else:
|
||||||
|
message = str(no_of_objects) + _(" Object in clipboard ") + message
|
||||||
|
else:
|
||||||
|
message = _("Text in clipboard ") + clipboard_text
|
||||||
|
|
||||||
|
if clipboard_image is not None:
|
||||||
|
found_clipboard_content = True
|
||||||
|
message = _("The clipboard contains a image")
|
||||||
|
|
||||||
|
if not found_clipboard_content:
|
||||||
|
message = _("The clipboard is empty")
|
||||||
|
|
||||||
|
return message
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting clipboard content: {e}")
|
||||||
|
return _("Error accessing clipboard")
|
@ -1,6 +0,0 @@
|
|||||||
[Plugin]
|
|
||||||
Module=Date
|
|
||||||
Loader=python3
|
|
||||||
Name=Date
|
|
||||||
Description=Present the current date
|
|
||||||
Authors=Chrys chrys@linux-a11y.org
|
|
@ -1,58 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
#
|
|
||||||
# Copyright (c) 2024 Stormux
|
|
||||||
# Copyright (c) 2010-2012 The Orca Team
|
|
||||||
# Copyright (c) 2012 Igalia, S.L.
|
|
||||||
# Copyright (c) 2005-2010 Sun Microsystems Inc.
|
|
||||||
#
|
|
||||||
# This library is free software; you can redistribute it and/or
|
|
||||||
# modify it under the terms of the GNU Lesser General Public
|
|
||||||
# License as published by the Free Software Foundation; either
|
|
||||||
# version 2.1 of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This library is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
# Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Lesser General Public
|
|
||||||
# License along with this library; if not, write to the
|
|
||||||
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
|
|
||||||
# Boston MA 02110-1301 USA.
|
|
||||||
#
|
|
||||||
# Fork of Orca Screen Reader (GNOME)
|
|
||||||
# Original source: https://gitlab.gnome.org/GNOME/orca
|
|
||||||
|
|
||||||
from cthulhu import plugin
|
|
||||||
|
|
||||||
import gi, time
|
|
||||||
gi.require_version('Peas', '1.0')
|
|
||||||
from gi.repository import GObject
|
|
||||||
from gi.repository import Peas
|
|
||||||
|
|
||||||
class Date(GObject.Object, Peas.Activatable, plugin.Plugin):
|
|
||||||
#__gtype_name__ = 'Date'
|
|
||||||
|
|
||||||
object = GObject.Property(type=GObject.Object)
|
|
||||||
def __init__(self):
|
|
||||||
plugin.Plugin.__init__(self)
|
|
||||||
def do_activate(self):
|
|
||||||
API = self.object
|
|
||||||
self.connectSignal("setup-inputeventhandlers-completed", self.setupCompatBinding)
|
|
||||||
def setupCompatBinding(self, app):
|
|
||||||
cmdnames = app.getDynamicApiManager().getAPI('Cmdnames')
|
|
||||||
inputEventHandlers = app.getDynamicApiManager().getAPI('inputEventHandlers')
|
|
||||||
inputEventHandlers['presentDateHandler'] = app.getAPIHelper().createInputEventHandler(self.presentDate, cmdnames.PRESENT_CURRENT_DATE)
|
|
||||||
def do_deactivate(self):
|
|
||||||
API = self.object
|
|
||||||
inputEventHandlers = API.app.getDynamicApiManager().getAPI('inputEventHandlers')
|
|
||||||
del inputEventHandlers['presentDateHandler']
|
|
||||||
def presentDate(self, script=None, inputEvent=None):
|
|
||||||
""" Presents the current time. """
|
|
||||||
API = self.object
|
|
||||||
settings_manager = API.app.getDynamicApiManager().getAPI('SettingsManager')
|
|
||||||
_settingsManager = settings_manager.getManager()
|
|
||||||
dateFormat = _settingsManager.getSetting('presentDateFormat')
|
|
||||||
message = time.strftime(dateFormat, time.localtime())
|
|
||||||
API.app.getDynamicApiManager().getAPI('CthulhuState').activeScript.presentMessage(message, resetStyles=False)
|
|
||||||
return True
|
|
@ -1,7 +0,0 @@
|
|||||||
cthulhu_python_PYTHON = \
|
|
||||||
__init__.py \
|
|
||||||
Date.plugin \
|
|
||||||
Date.py
|
|
||||||
|
|
||||||
cthulhu_pythondir=$(pkgpythondir)/plugins/Date
|
|
||||||
|
|
7
src/cthulhu/plugins/DisplayVersion/Makefile.am
Normal file
7
src/cthulhu/plugins/DisplayVersion/Makefile.am
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
cthulhu_python_PYTHON = \
|
||||||
|
__init__.py \
|
||||||
|
plugin.info \
|
||||||
|
plugin.py
|
||||||
|
|
||||||
|
cthulhu_pythondir=$(pkgpythondir)/plugins/DisplayVersion
|
||||||
|
|
8
src/cthulhu/plugins/DisplayVersion/plugin.info
Normal file
8
src/cthulhu/plugins/DisplayVersion/plugin.info
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
name = Display Version
|
||||||
|
version = 1.0.0
|
||||||
|
description = Announces the Cthulhu version with Cthulhu+Shift+V
|
||||||
|
authors = Stormux <storm_dragon@stormux.org>
|
||||||
|
website = https://stormux.org
|
||||||
|
copyright = Copyright 2025
|
||||||
|
builtin = false
|
||||||
|
hidden = false
|
68
src/cthulhu/plugins/DisplayVersion/plugin.py
Normal file
68
src/cthulhu/plugins/DisplayVersion/plugin.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
#!/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.
|
||||||
|
|
||||||
|
"""Display Version plugin for Cthulhu."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from cthulhu.plugin import Plugin, cthulhu_hookimpl
|
||||||
|
from cthulhu import cthulhuVersion
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class DisplayVersion(Plugin):
|
||||||
|
"""Plugin that announces the current Cthulhu version."""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
"""Initialize the plugin."""
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
logger.info("DisplayVersion plugin initialized")
|
||||||
|
self._kb_binding = None
|
||||||
|
|
||||||
|
@cthulhu_hookimpl
|
||||||
|
def activate(self, plugin=None):
|
||||||
|
"""Activate the plugin."""
|
||||||
|
# Skip if this activation call isn't for us
|
||||||
|
if plugin is not None and plugin is not self:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.info("Activating DisplayVersion plugin")
|
||||||
|
|
||||||
|
# Register keyboard shortcut
|
||||||
|
self._kb_binding = self.registerGestureByString(
|
||||||
|
self.speakText,
|
||||||
|
f'Cthulhu screen reader version {cthulhuVersion.version}-{cthulhuVersion.codeName}',
|
||||||
|
'kb:cthulhu+shift+v'
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error activating DisplayVersion plugin: {e}")
|
||||||
|
|
||||||
|
@cthulhu_hookimpl
|
||||||
|
def deactivate(self, plugin=None):
|
||||||
|
"""Deactivate the plugin."""
|
||||||
|
# Skip if this deactivation call isn't for us
|
||||||
|
if plugin is not None and plugin is not self:
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info("Deactivating DisplayVersion plugin")
|
||||||
|
|
||||||
|
def speakText(self, script=None, inputEvent=None):
|
||||||
|
"""Speak the Cthulhu version when shortcut is pressed."""
|
||||||
|
try:
|
||||||
|
if self.app:
|
||||||
|
state = self.app.getDynamicApiManager().getAPI('CthulhuState')
|
||||||
|
if state.activeScript:
|
||||||
|
state.activeScript.presentMessage(
|
||||||
|
f'Cthulhu screen reader version {cthulhuVersion.version}-{cthulhuVersion.codeName}',
|
||||||
|
resetStyles=False
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in DisplayVersion speakText: {e}")
|
||||||
|
return False
|
@ -1,6 +0,0 @@
|
|||||||
[Plugin]
|
|
||||||
Module=HelloCthulhu
|
|
||||||
Loader=python3
|
|
||||||
Name=Cthulhu say hello
|
|
||||||
Description=startup announcement for Cthulhu
|
|
||||||
Authors=Chrys chrys@linux-a11y.org
|
|
@ -1,48 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
#
|
|
||||||
# Copyright (c) 2024 Stormux
|
|
||||||
# Copyright (c) 2010-2012 The Orca Team
|
|
||||||
# Copyright (c) 2012 Igalia, S.L.
|
|
||||||
# Copyright (c) 2005-2010 Sun Microsystems Inc.
|
|
||||||
#
|
|
||||||
# This library is free software; you can redistribute it and/or
|
|
||||||
# modify it under the terms of the GNU Lesser General Public
|
|
||||||
# License as published by the Free Software Foundation; either
|
|
||||||
# version 2.1 of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This library is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
# Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Lesser General Public
|
|
||||||
# License along with this library; if not, write to the
|
|
||||||
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
|
|
||||||
# Boston MA 02110-1301 USA.
|
|
||||||
#
|
|
||||||
# Fork of Orca Screen Reader (GNOME)
|
|
||||||
# Original source: https://gitlab.gnome.org/GNOME/orca
|
|
||||||
|
|
||||||
from cthulhu import plugin
|
|
||||||
|
|
||||||
import gi
|
|
||||||
gi.require_version('Peas', '1.0')
|
|
||||||
from gi.repository import GObject
|
|
||||||
from gi.repository import Peas
|
|
||||||
|
|
||||||
class HelloCthulhu(GObject.Object, Peas.Activatable, plugin.Plugin):
|
|
||||||
#__gtype_name__ = 'HelloCthulhu'
|
|
||||||
|
|
||||||
object = GObject.Property(type=GObject.Object)
|
|
||||||
def __init__(self):
|
|
||||||
plugin.Plugin.__init__(self)
|
|
||||||
def do_activate(self):
|
|
||||||
API = self.object
|
|
||||||
self.connectSignal("start-application-completed", self.process)
|
|
||||||
def do_deactivate(self):
|
|
||||||
API = self.object
|
|
||||||
def do_update_state(self):
|
|
||||||
API = self.object
|
|
||||||
def process(self, app):
|
|
||||||
messages = app.getDynamicApiManager().getAPI('Messages')
|
|
||||||
app.getDynamicApiManager().getAPI('CthulhuState').activeScript.presentMessage(messages.START_CTHULHU, resetStyles=False)
|
|
@ -1,7 +1,7 @@
|
|||||||
cthulhu_python_PYTHON = \
|
cthulhu_python_PYTHON = \
|
||||||
__init__.py \
|
__init__.py \
|
||||||
HelloCthulhu.plugin \
|
plugin.info \
|
||||||
HelloCthulhu.py
|
plugin.py
|
||||||
|
|
||||||
cthulhu_pythondir=$(pkgpythondir)/plugins/HelloCthulhu
|
cthulhu_pythondir=$(pkgpythondir)/plugins/HelloCthulhu
|
||||||
|
|
||||||
|
7
src/cthulhu/plugins/HelloCthulhu/plugin.info
Normal file
7
src/cthulhu/plugins/HelloCthulhu/plugin.info
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
[Plugin]
|
||||||
|
Name = Cthulhu say hello
|
||||||
|
Module = HelloCthulhu
|
||||||
|
Description = Startup announcement for Cthulhu
|
||||||
|
Authors = Storm Dragon <storm_dragon@stormux.org>
|
||||||
|
Version = 1.0
|
||||||
|
Category = Interaction
|
113
src/cthulhu/plugins/HelloCthulhu/plugin.py
Normal file
113
src/cthulhu/plugins/HelloCthulhu/plugin.py
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
#
|
||||||
|
# Copyright (c) 2024 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.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
# Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public
|
||||||
|
# License along with this library; if not, write to the
|
||||||
|
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
|
||||||
|
# Boston MA 02110-1301 USA.
|
||||||
|
#
|
||||||
|
|
||||||
|
"""Hello Cthulhu plugin for Cthulhu."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import weakref
|
||||||
|
from cthulhu.plugin import Plugin, cthulhu_hookimpl
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Class-level variable to track if the greeting has been presented
|
||||||
|
# This ensures the greeting is only shown once even if multiple instances exist
|
||||||
|
_greeting_shown = False
|
||||||
|
|
||||||
|
class HelloCthulhu(Plugin):
|
||||||
|
"""Plugin that speaks a welcome message when Cthulhu starts up."""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
"""Initialize the plugin."""
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
logger.info("HelloCthulhu plugin initialized")
|
||||||
|
self._signal_handler_id = None
|
||||||
|
self._is_connected = False
|
||||||
|
|
||||||
|
@cthulhu_hookimpl
|
||||||
|
def activate(self, plugin=None):
|
||||||
|
"""Activate the plugin."""
|
||||||
|
# Skip if this activation call isn't for us
|
||||||
|
if plugin is not None and plugin is not self:
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info("Activating HelloCthulhu plugin")
|
||||||
|
try:
|
||||||
|
# Only connect the signal if we haven't already
|
||||||
|
if not self._is_connected:
|
||||||
|
signal_manager = self.app.getSignalManager()
|
||||||
|
self._signal_handler_id = signal_manager.connectSignal(
|
||||||
|
"start-application-completed",
|
||||||
|
self.process,
|
||||||
|
"default" # Add profile parameter
|
||||||
|
)
|
||||||
|
self._is_connected = True
|
||||||
|
logger.debug("Connected to start-application-completed signal")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error activating HelloCthulhu plugin: {e}")
|
||||||
|
|
||||||
|
@cthulhu_hookimpl
|
||||||
|
def deactivate(self, plugin=None):
|
||||||
|
"""Deactivate the plugin."""
|
||||||
|
# Skip if this deactivation call isn't for us
|
||||||
|
if plugin is not None and plugin is not self:
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info("Deactivating HelloCthulhu plugin")
|
||||||
|
try:
|
||||||
|
# Only disconnect if we're connected
|
||||||
|
if self._is_connected and self._signal_handler_id is not None:
|
||||||
|
signal_manager = self.app.getSignalManager()
|
||||||
|
signal_manager.disconnectSignalByFunction(
|
||||||
|
self.process
|
||||||
|
)
|
||||||
|
self._signal_handler_id = None
|
||||||
|
self._is_connected = False
|
||||||
|
logger.debug("Disconnected from start-application-completed signal")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error deactivating HelloCthulhu plugin: {e}")
|
||||||
|
|
||||||
|
def process(self, app):
|
||||||
|
"""Process the start-application-completed signal."""
|
||||||
|
global _greeting_shown
|
||||||
|
|
||||||
|
# Only present the message if it hasn't been shown yet
|
||||||
|
if _greeting_shown:
|
||||||
|
logger.debug("Greeting already shown, skipping")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
messages = app.getDynamicApiManager().getAPI('Messages')
|
||||||
|
state = app.getDynamicApiManager().getAPI('CthulhuState')
|
||||||
|
if state.activeScript:
|
||||||
|
state.activeScript.presentMessage(messages.START_CTHULHU, resetStyles=False)
|
||||||
|
_greeting_shown = True
|
||||||
|
logger.info("Greeting message presented")
|
||||||
|
|
||||||
|
# Disconnect the signal after presenting the message
|
||||||
|
if self._is_connected and self._signal_handler_id is not None:
|
||||||
|
signal_manager = app.getSignalManager()
|
||||||
|
signal_manager.disconnectSignalByFunction(
|
||||||
|
self.process
|
||||||
|
)
|
||||||
|
self._signal_handler_id = None
|
||||||
|
self._is_connected = False
|
||||||
|
logger.debug("Disconnected signal after presenting greeting")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in HelloCthulhu process: {e}")
|
@ -1,6 +0,0 @@
|
|||||||
[Plugin]
|
|
||||||
Module=HelloWorld
|
|
||||||
Loader=python3
|
|
||||||
Name=Hello World (python3)
|
|
||||||
Description=Test plugin for cthulhu
|
|
||||||
Authors=Chrys chrys@linux-a11y.org
|
|
@ -1,49 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
#
|
|
||||||
# Copyright (c) 2024 Stormux
|
|
||||||
# Copyright (c) 2010-2012 The Orca Team
|
|
||||||
# Copyright (c) 2012 Igalia, S.L.
|
|
||||||
# Copyright (c) 2005-2010 Sun Microsystems Inc.
|
|
||||||
#
|
|
||||||
# This library is free software; you can redistribute it and/or
|
|
||||||
# modify it under the terms of the GNU Lesser General Public
|
|
||||||
# License as published by the Free Software Foundation; either
|
|
||||||
# version 2.1 of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This library is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
# Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Lesser General Public
|
|
||||||
# License along with this library; if not, write to the
|
|
||||||
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
|
|
||||||
# Boston MA 02110-1301 USA.
|
|
||||||
#
|
|
||||||
# Fork of Orca Screen Reader (GNOME)
|
|
||||||
# Original source: https://gitlab.gnome.org/GNOME/orca
|
|
||||||
|
|
||||||
from cthulhu import plugin
|
|
||||||
|
|
||||||
import gi
|
|
||||||
gi.require_version('Peas', '1.0')
|
|
||||||
from gi.repository import GObject
|
|
||||||
from gi.repository import Peas
|
|
||||||
|
|
||||||
class HelloWorld(GObject.Object, Peas.Activatable, plugin.Plugin):
|
|
||||||
__gtype_name__ = 'helloworld'
|
|
||||||
|
|
||||||
object = GObject.Property(type=GObject.Object)
|
|
||||||
def __init__(self):
|
|
||||||
plugin.Plugin.__init__(self)
|
|
||||||
def do_activate(self):
|
|
||||||
API = self.object
|
|
||||||
self.registerGestureByString(self.speakTest, _('hello world'), 'kb:cthulhu+z')
|
|
||||||
print('activate hello world plugin')
|
|
||||||
def do_deactivate(self):
|
|
||||||
API = self.object
|
|
||||||
print('deactivate hello world plugin')
|
|
||||||
def speakTest(self, script=None, inputEvent=None):
|
|
||||||
API = self.object
|
|
||||||
API.app.getDynamicApiManager().getAPI('CthulhuState').activeScript.presentMessage('hello world', resetStyles=False)
|
|
||||||
return True
|
|
@ -1,7 +0,0 @@
|
|||||||
cthulhu_python_PYTHON = \
|
|
||||||
__init__.py \
|
|
||||||
HelloWorld.plugin \
|
|
||||||
HelloWorld.py
|
|
||||||
|
|
||||||
cthulhu_pythondir=$(pkgpythondir)/plugins/HelloWorld
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
SUBDIRS = Clipboard HelloWorld SelfVoice Time MouseReview Date ByeCthulhu HelloCthulhu PluginManager CapsLockHack SimplePluginSystem
|
SUBDIRS = Clipboard DisplayVersion hello_world self_voice ByeCthulhu HelloCthulhu SimplePluginSystem
|
||||||
|
|
||||||
cthulhu_pythondir=$(pkgpythondir)/plugins
|
cthulhu_pythondir=$(pkgpythondir)/plugins
|
||||||
|
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
cthulhu_python_PYTHON = \
|
|
||||||
__init__.py \
|
|
||||||
MouseReview.plugin \
|
|
||||||
MouseReview.py
|
|
||||||
|
|
||||||
cthulhu_pythondir=$(pkgpythondir)/plugins/MouseReview
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
|||||||
[Plugin]
|
|
||||||
Module=MouseReview
|
|
||||||
Loader=python3
|
|
||||||
Name=Mouse Review
|
|
||||||
Description=Review whats below the mouse coursor
|
|
||||||
Authors=Chrys chrys@linux-a11y.org
|
|
@ -1,759 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
#
|
|
||||||
# Copyright (c) 2024 Stormux
|
|
||||||
# Copyright (c) 2010-2012 The Orca Team
|
|
||||||
# Copyright (c) 2012 Igalia, S.L.
|
|
||||||
# Copyright (c) 2005-2010 Sun Microsystems Inc.
|
|
||||||
#
|
|
||||||
# This library is free software; you can redistribute it and/or
|
|
||||||
# modify it under the terms of the GNU Lesser General Public
|
|
||||||
# License as published by the Free Software Foundation; either
|
|
||||||
# version 2.1 of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This library is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
# Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Lesser General Public
|
|
||||||
# License along with this library; if not, write to the
|
|
||||||
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
|
|
||||||
# Boston MA 02110-1301 USA.
|
|
||||||
#
|
|
||||||
# Fork of Orca Screen Reader (GNOME)
|
|
||||||
# Original source: https://gitlab.gnome.org/GNOME/orca
|
|
||||||
|
|
||||||
"""Mouse review mode."""
|
|
||||||
|
|
||||||
__id__ = "$Id$"
|
|
||||||
__version__ = "$Revision$"
|
|
||||||
__date__ = "$Date$"
|
|
||||||
__copyright__ = "Copyright (c) 2008 Eitan Isaacson" \
|
|
||||||
"Copyright (c) 2016 Igalia, S.L."
|
|
||||||
__license__ = "LGPL"
|
|
||||||
|
|
||||||
from cthulhu import plugin
|
|
||||||
|
|
||||||
import gi, math, time
|
|
||||||
gi.require_version('Peas', '1.0')
|
|
||||||
from gi.repository import GObject
|
|
||||||
from gi.repository import Peas
|
|
||||||
|
|
||||||
gi.require_version("Atspi", "2.0")
|
|
||||||
from gi.repository import Atspi
|
|
||||||
|
|
||||||
from gi.repository import Gdk
|
|
||||||
try:
|
|
||||||
gi.require_version("Wnck", "3.0")
|
|
||||||
from gi.repository import Wnck
|
|
||||||
_mouseReviewCapable = True
|
|
||||||
except Exception:
|
|
||||||
_mouseReviewCapable = False
|
|
||||||
|
|
||||||
# compatibility layer, see MouseReview.do_activate
|
|
||||||
debug = None
|
|
||||||
event_manager = None
|
|
||||||
cthulhu = None
|
|
||||||
cthulhu_state = None
|
|
||||||
script_manager = None
|
|
||||||
settings_manager = None
|
|
||||||
speech = None
|
|
||||||
messages = None
|
|
||||||
cmdnames = None
|
|
||||||
emitRegionChanged = None
|
|
||||||
_scriptManager = None
|
|
||||||
_settingsManager = None
|
|
||||||
AXObject = None
|
|
||||||
AXUtilities = None
|
|
||||||
keybindings = None
|
|
||||||
input_event = None
|
|
||||||
|
|
||||||
class MouseReview(GObject.Object, Peas.Activatable, plugin.Plugin):
|
|
||||||
#__gtype_name__ = 'MouseReview'
|
|
||||||
|
|
||||||
object = GObject.Property(type=GObject.Object)
|
|
||||||
def __init__(self):
|
|
||||||
plugin.Plugin.__init__(self)
|
|
||||||
def do_activate(self):
|
|
||||||
API = self.object
|
|
||||||
global _mouseReviewCapable
|
|
||||||
if not _mouseReviewCapable:
|
|
||||||
return
|
|
||||||
global debug
|
|
||||||
global event_manager
|
|
||||||
global cthulhu_state
|
|
||||||
global script_manager
|
|
||||||
global settings_manager
|
|
||||||
global speech
|
|
||||||
global _scriptManager
|
|
||||||
global _settingsManager
|
|
||||||
global emitRegionChanged
|
|
||||||
global messages
|
|
||||||
global cmdnames
|
|
||||||
global AXObject
|
|
||||||
global AXUtilities
|
|
||||||
global keybindings
|
|
||||||
global input_event
|
|
||||||
debug= API.app.getDynamicApiManager().getAPI('Debug')
|
|
||||||
event_manager = API.app.getDynamicApiManager().getAPI('EventManager')
|
|
||||||
messages = API.app.getDynamicApiManager().getAPI('Messages')
|
|
||||||
cmdnames = API.app.getDynamicApiManager().getAPI('Cmdnames')
|
|
||||||
cthulhu_state = API.app.getDynamicApiManager().getAPI('CthulhuState')
|
|
||||||
script_manager = API.app.getDynamicApiManager().getAPI('ScriptManager')
|
|
||||||
settings_manager = API.app.getDynamicApiManager().getAPI('SettingsManager')
|
|
||||||
speech = API.app.getDynamicApiManager().getAPI('Speech')
|
|
||||||
emitRegionChanged = API.app.getDynamicApiManager().getAPI('EmitRegionChanged')
|
|
||||||
_scriptManager = script_manager.getManager()
|
|
||||||
_settingsManager = settings_manager.getManager()
|
|
||||||
AXObject = API.app.getDynamicApiManager().getAPI('AXObject')
|
|
||||||
AXUtilities = API.app.getDynamicApiManager().getAPI('AXUtilities')
|
|
||||||
keybindings = API.app.getDynamicApiManager().getAPI('Keybindings')
|
|
||||||
input_event = API.app.getDynamicApiManager().getAPI('InputEvent')
|
|
||||||
mouse_review = MouseReviewer()
|
|
||||||
self.registerAPI('MouseReview', mouse_review)
|
|
||||||
self.Initialize(API.app)
|
|
||||||
self.connectSignal("setup-inputeventhandlers-completed", self.setupCompatBinding)
|
|
||||||
self.connectSignal("load-setting-completed", self.Initialize)
|
|
||||||
|
|
||||||
def do_deactivate(self):
|
|
||||||
API = self.object
|
|
||||||
global _mouseReviewCapable
|
|
||||||
if not _mouseReviewCapable:
|
|
||||||
return
|
|
||||||
mouse_review = API.app.getDynamicApiManager().getAPI('MouseReview')
|
|
||||||
|
|
||||||
mouse_review.deactivate()
|
|
||||||
def do_update_state(self):
|
|
||||||
API = self.object
|
|
||||||
def setupCompatBinding(self, app):
|
|
||||||
API = self.object
|
|
||||||
mouse_review = API.app.getDynamicApiManager().getAPI('MouseReview')
|
|
||||||
cmdnames = API.app.getDynamicApiManager().getAPI('Cmdnames')
|
|
||||||
inputEventHandlers = API.app.getDynamicApiManager().getAPI('inputEventHandlers')
|
|
||||||
inputEventHandlers['toggleMouseReviewHandler'] = API.app.getAPIHelper().createInputEventHandler(mouse_review.toggle, cmdnames.MOUSE_REVIEW_TOGGLE)
|
|
||||||
def Initialize(self, app):
|
|
||||||
mouse_review = app.getDynamicApiManager().getAPI('MouseReview')
|
|
||||||
settings_manager = app.getDynamicApiManager().getAPI('SettingsManager')
|
|
||||||
_settingsManager = settings_manager.getManager()
|
|
||||||
if _settingsManager.getSetting('enableMouseReview'):
|
|
||||||
mouse_review.activate()
|
|
||||||
else:
|
|
||||||
mouse_review.deactivate()
|
|
||||||
|
|
||||||
class _StringContext:
|
|
||||||
"""The textual information associated with an _ItemContext."""
|
|
||||||
|
|
||||||
def __init__(self, obj, script=None, string="", start=0, end=0):
|
|
||||||
"""Initialize the _StringContext.
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
- string: The human-consumable string
|
|
||||||
- obj: The accessible object associated with this string
|
|
||||||
- start: The start offset with respect to entire text, if one exists
|
|
||||||
- end: The end offset with respect to the entire text, if one exists
|
|
||||||
- script: The script associated with the accessible object
|
|
||||||
"""
|
|
||||||
|
|
||||||
self._obj = obj
|
|
||||||
self._script = script
|
|
||||||
self._string = string
|
|
||||||
self._start = start
|
|
||||||
self._end = end
|
|
||||||
self._boundingBox = 0, 0, 0, 0
|
|
||||||
if script:
|
|
||||||
self._boundingBox = script.utilities.getTextBoundingBox(obj, start, end)
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
return other is not None \
|
|
||||||
and self._obj == other._obj \
|
|
||||||
and self._string == other._string \
|
|
||||||
and self._start == other._start \
|
|
||||||
and self._end == other._end
|
|
||||||
|
|
||||||
def isSubstringOf(self, other):
|
|
||||||
"""Returns True if this is a substring of other."""
|
|
||||||
|
|
||||||
if other is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if not (self._obj and other._obj):
|
|
||||||
return False
|
|
||||||
|
|
||||||
thisBox = self.getBoundingBox()
|
|
||||||
if thisBox == (0, 0, 0, 0):
|
|
||||||
return False
|
|
||||||
|
|
||||||
otherBox = other.getBoundingBox()
|
|
||||||
if otherBox == (0, 0, 0, 0):
|
|
||||||
return False
|
|
||||||
|
|
||||||
# We get various and sundry results for the bounding box if the implementor
|
|
||||||
# included newline characters as part of the word or line at offset. Try to
|
|
||||||
# detect this and adjust the bounding boxes before getting the intersection.
|
|
||||||
if thisBox[3] != otherBox[3] and self._obj == other._obj:
|
|
||||||
thisNewLineCount = self._string.count("\n")
|
|
||||||
if thisNewLineCount and thisBox[3] / thisNewLineCount == otherBox[3]:
|
|
||||||
thisBox = *thisBox[0:3], otherBox[3]
|
|
||||||
|
|
||||||
if self._script.utilities.intersection(thisBox, otherBox) != thisBox:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if not (self._string and self._string.strip() in other._string):
|
|
||||||
return False
|
|
||||||
|
|
||||||
msg = f"MOUSE REVIEW: '{self._string}' is substring of '{other._string}'"
|
|
||||||
debug.println(debug.LEVEL_INFO, msg, True)
|
|
||||||
return True
|
|
||||||
|
|
||||||
def getBoundingBox(self):
|
|
||||||
"""Returns the bounding box associated with this context's range."""
|
|
||||||
|
|
||||||
return self._boundingBox
|
|
||||||
|
|
||||||
def getString(self):
|
|
||||||
"""Returns the string associated with this context."""
|
|
||||||
|
|
||||||
return self._string
|
|
||||||
|
|
||||||
def present(self):
|
|
||||||
"""Presents this context to the user."""
|
|
||||||
|
|
||||||
if not self._script:
|
|
||||||
msg = "MOUSE REVIEW: Not presenting due to lack of script"
|
|
||||||
debug.println(debug.LEVEL_INFO, msg, True)
|
|
||||||
return False
|
|
||||||
|
|
||||||
if not self._string:
|
|
||||||
msg = "MOUSE REVIEW: Not presenting due to lack of string"
|
|
||||||
debug.println(debug.LEVEL_INFO, msg, True)
|
|
||||||
return False
|
|
||||||
|
|
||||||
voice = self._script.speechGenerator.voice(obj=self._obj, string=self._string)
|
|
||||||
string = self._script.utilities.adjustForRepeats(self._string)
|
|
||||||
# TODO
|
|
||||||
#cthulhu.emitRegionChanged(self._obj, self._start, self._end, cthulhu.MOUSE_REVIEW)
|
|
||||||
emitRegionChanged(self._obj, self._start, self._end, "mouse-review")
|
|
||||||
|
|
||||||
|
|
||||||
self._script.speakMessage(string, voice=voice, interrupt=False)
|
|
||||||
self._script.displayBrailleMessage(self._string, -1)
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class _ItemContext:
|
|
||||||
"""Holds all the information of the item at a specified point."""
|
|
||||||
|
|
||||||
def __init__(self, x=0, y=0, obj=None, boundary=None, frame=None, script=None):
|
|
||||||
"""Initialize the _ItemContext.
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
- x: The X coordinate
|
|
||||||
- y: The Y coordinate
|
|
||||||
- obj: The accessible object of interest at that coordinate
|
|
||||||
- boundary: The accessible-text boundary type
|
|
||||||
- frame: The containing accessible object (often a top-level window)
|
|
||||||
- script: The script associated with the accessible object
|
|
||||||
"""
|
|
||||||
|
|
||||||
self._x = x
|
|
||||||
self._y = y
|
|
||||||
self._obj = obj
|
|
||||||
self._boundary = boundary
|
|
||||||
self._frame = frame
|
|
||||||
self._script = script
|
|
||||||
self._string = self._getStringContext()
|
|
||||||
self._time = time.time()
|
|
||||||
self._boundingBox = 0, 0, 0, 0
|
|
||||||
if script:
|
|
||||||
self._boundingBox = script.utilities.getBoundingBox(obj)
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
return other is not None \
|
|
||||||
and self._frame == other._frame \
|
|
||||||
and self._obj == other._obj \
|
|
||||||
and self._string == other._string
|
|
||||||
|
|
||||||
def _treatAsDuplicate(self, prior):
|
|
||||||
if self._obj != prior._obj or self._frame != prior._frame:
|
|
||||||
msg = "MOUSE REVIEW: Not a duplicate: different objects"
|
|
||||||
debug.println(debug.LEVEL_INFO, msg, True)
|
|
||||||
return False
|
|
||||||
|
|
||||||
if self.getString() and prior.getString() and not self._isSubstringOf(prior):
|
|
||||||
msg = "MOUSE REVIEW: Not a duplicate: not a substring of"
|
|
||||||
debug.println(debug.LEVEL_INFO, msg, True)
|
|
||||||
return False
|
|
||||||
|
|
||||||
if self._x == prior._x and self._y == prior._y:
|
|
||||||
msg = "MOUSE REVIEW: Treating as duplicate: mouse didn't move"
|
|
||||||
debug.println(debug.LEVEL_INFO, msg, True)
|
|
||||||
return True
|
|
||||||
|
|
||||||
interval = self._time - prior._time
|
|
||||||
if interval > 0.5:
|
|
||||||
msg = f"MOUSE REVIEW: Not a duplicate: was {interval:.2f}s ago"
|
|
||||||
debug.println(debug.LEVEL_INFO, msg, True)
|
|
||||||
return False
|
|
||||||
|
|
||||||
msg = "MOUSE REVIEW: Treating as duplicate"
|
|
||||||
debug.println(debug.LEVEL_INFO, msg, True)
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _treatAsSingleObject(self):
|
|
||||||
if not AXObject.supports_text(self._obj):
|
|
||||||
return True
|
|
||||||
|
|
||||||
if not self._obj.queryText().characterCount:
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _getStringContext(self):
|
|
||||||
"""Returns the _StringContext associated with the specified point."""
|
|
||||||
|
|
||||||
if not (self._script and self._obj):
|
|
||||||
return _StringContext(self._obj)
|
|
||||||
|
|
||||||
if self._treatAsSingleObject():
|
|
||||||
return _StringContext(self._obj, self._script)
|
|
||||||
|
|
||||||
string, start, end = self._script.utilities.textAtPoint(
|
|
||||||
self._obj, self._x, self._y, boundary=self._boundary)
|
|
||||||
if string:
|
|
||||||
string = self._script.utilities.expandEOCs(self._obj, start, end)
|
|
||||||
|
|
||||||
return _StringContext(self._obj, self._script, string, start, end)
|
|
||||||
|
|
||||||
def _getContainer(self):
|
|
||||||
roles = [Atspi.Role.DIALOG,
|
|
||||||
Atspi.Role.FRAME,
|
|
||||||
Atspi.Role.LAYERED_PANE,
|
|
||||||
Atspi.Role.MENU,
|
|
||||||
Atspi.Role.PAGE_TAB,
|
|
||||||
Atspi.Role.TOOL_BAR,
|
|
||||||
Atspi.Role.WINDOW]
|
|
||||||
return AXObject.find_ancestor(self._obj, lambda x: AXObject.get_role(x) in roles)
|
|
||||||
|
|
||||||
def _isSubstringOf(self, other):
|
|
||||||
"""Returns True if this is a substring of other."""
|
|
||||||
|
|
||||||
return self._string.isSubstringOf(other._string)
|
|
||||||
|
|
||||||
def getObject(self):
|
|
||||||
"""Returns the accessible object associated with this context."""
|
|
||||||
|
|
||||||
return self._obj
|
|
||||||
|
|
||||||
def getBoundingBox(self):
|
|
||||||
"""Returns the bounding box associated with this context."""
|
|
||||||
|
|
||||||
x, y, width, height = self._string.getBoundingBox()
|
|
||||||
if not (width or height):
|
|
||||||
return self._boundingBox
|
|
||||||
|
|
||||||
return x, y, width, height
|
|
||||||
|
|
||||||
def getString(self):
|
|
||||||
"""Returns the string associated with this context."""
|
|
||||||
|
|
||||||
return self._string.getString()
|
|
||||||
|
|
||||||
def getTime(self):
|
|
||||||
"""Returns the time associated with this context."""
|
|
||||||
|
|
||||||
return self._time
|
|
||||||
|
|
||||||
def _isInlineChild(self, prior):
|
|
||||||
if not self._obj or not prior._obj:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if AXObject.get_parent(prior._obj) != self._obj:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if self._treatAsSingleObject():
|
|
||||||
return False
|
|
||||||
|
|
||||||
return AXUtilities.is_link(prior._obj)
|
|
||||||
|
|
||||||
def present(self, prior):
|
|
||||||
"""Presents this context to the user."""
|
|
||||||
|
|
||||||
if self == prior or self._treatAsDuplicate(prior):
|
|
||||||
msg = "MOUSE REVIEW: Not presenting due to no change"
|
|
||||||
debug.println(debug.LEVEL_INFO, msg, True)
|
|
||||||
return False
|
|
||||||
|
|
||||||
interrupt = self._obj and self._obj != prior._obj \
|
|
||||||
or math.sqrt((self._x - prior._x)**2 + (self._y - prior._y)**2) > 25
|
|
||||||
|
|
||||||
if interrupt:
|
|
||||||
self._script.presentationInterrupt()
|
|
||||||
|
|
||||||
if self._frame and self._frame != prior._frame:
|
|
||||||
self._script.presentObject(self._frame,
|
|
||||||
alreadyFocused=True,
|
|
||||||
inMouseReview=True,
|
|
||||||
interrupt=True)
|
|
||||||
|
|
||||||
if self._script.utilities.containsOnlyEOCs(self._obj):
|
|
||||||
msg = "MOUSE REVIEW: Not presenting object which contains only EOCs"
|
|
||||||
debug.println(debug.LEVEL_INFO, msg, True)
|
|
||||||
return False
|
|
||||||
|
|
||||||
if self._obj and self._obj != prior._obj and not self._isInlineChild(prior):
|
|
||||||
priorObj = prior._obj or self._getContainer()
|
|
||||||
# TODO
|
|
||||||
#cthulhu.emitRegionChanged(self._obj, mode=cthulhu.MOUSE_REVIEW)
|
|
||||||
emitRegionChanged(self._obj, mode="mouse-review")
|
|
||||||
|
|
||||||
self._script.presentObject(self._obj, priorObj=priorObj, inMouseReview=True)
|
|
||||||
if self._string.getString() == AXObject.get_name(self._obj):
|
|
||||||
return True
|
|
||||||
if not self._script.utilities.isEditableTextArea(self._obj):
|
|
||||||
return True
|
|
||||||
if AXUtilities.is_table_cell(self._obj) \
|
|
||||||
and self._string.getString() == self._script.utilities.displayedText(self._obj):
|
|
||||||
return True
|
|
||||||
|
|
||||||
if self._string != prior._string and self._string.present():
|
|
||||||
return True
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class MouseReviewer:
|
|
||||||
"""Main class for the mouse-review feature."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self._active = _settingsManager.getSetting("enableMouseReview")
|
|
||||||
self._currentMouseOver = _ItemContext()
|
|
||||||
self._pointer = None
|
|
||||||
self._workspace = None
|
|
||||||
self._windows = []
|
|
||||||
self._all_windows = []
|
|
||||||
self._handlerIds = {}
|
|
||||||
self._eventListener = Atspi.EventListener.new(self._listener)
|
|
||||||
self.inMouseEvent = False
|
|
||||||
self._handlers = self._setup_handlers()
|
|
||||||
self._bindings = self._setup_bindings()
|
|
||||||
|
|
||||||
if not _mouseReviewCapable:
|
|
||||||
msg = "MOUSE REVIEW ERROR: Wnck is not available"
|
|
||||||
debug.println(debug.LEVEL_INFO, msg, True)
|
|
||||||
return
|
|
||||||
|
|
||||||
display = Gdk.Display.get_default()
|
|
||||||
try:
|
|
||||||
seat = Gdk.Display.get_default_seat(display)
|
|
||||||
self._pointer = seat.get_pointer()
|
|
||||||
except AttributeError:
|
|
||||||
msg = "MOUSE REVIEW ERROR: Gtk+ 3.20 is not available"
|
|
||||||
debug.println(debug.LEVEL_INFO, msg, True)
|
|
||||||
return
|
|
||||||
except Exception:
|
|
||||||
msg = "MOUSE REVIEW ERROR: Exception getting pointer for default seat."
|
|
||||||
debug.println(debug.LEVEL_INFO, msg, True)
|
|
||||||
return
|
|
||||||
|
|
||||||
if not self._pointer:
|
|
||||||
msg = "MOUSE REVIEW ERROR: No pointer for default seat."
|
|
||||||
debug.println(debug.LEVEL_INFO, msg, True)
|
|
||||||
return
|
|
||||||
|
|
||||||
if not self._active:
|
|
||||||
return
|
|
||||||
|
|
||||||
self.activate()
|
|
||||||
|
|
||||||
def get_bindings(self):
|
|
||||||
"""Returns the mouse-review keybindings."""
|
|
||||||
|
|
||||||
return self._bindings
|
|
||||||
|
|
||||||
def get_handlers(self):
|
|
||||||
"""Returns the mouse-review handlers."""
|
|
||||||
|
|
||||||
return self._handlers
|
|
||||||
|
|
||||||
def _setup_handlers(self):
|
|
||||||
"""Sets up and returns the mouse-review input event handlers."""
|
|
||||||
|
|
||||||
handlers = {}
|
|
||||||
|
|
||||||
handlers["toggleMouseReviewHandler"] = \
|
|
||||||
input_event.InputEventHandler(
|
|
||||||
self.toggle,
|
|
||||||
cmdnames.MOUSE_REVIEW_TOGGLE)
|
|
||||||
|
|
||||||
return handlers
|
|
||||||
|
|
||||||
def _setup_bindings(self):
|
|
||||||
"""Sets up and returns the mouse-review key bindings."""
|
|
||||||
|
|
||||||
bindings = keybindings.KeyBindings()
|
|
||||||
|
|
||||||
bindings.add(
|
|
||||||
keybindings.KeyBinding(
|
|
||||||
"",
|
|
||||||
keybindings.defaultModifierMask,
|
|
||||||
keybindings.NO_MODIFIER_MASK,
|
|
||||||
self._handlers.get("toggleMouseReviewHandler")))
|
|
||||||
|
|
||||||
return bindings
|
|
||||||
|
|
||||||
def activate(self):
|
|
||||||
"""Activates mouse review."""
|
|
||||||
|
|
||||||
if not _mouseReviewCapable:
|
|
||||||
msg = "MOUSE REVIEW ERROR: Wnck is not available"
|
|
||||||
debug.println(debug.LEVEL_INFO, msg, True)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Set up the initial object as the one with the focus to avoid
|
|
||||||
# presenting irrelevant info the first time.
|
|
||||||
obj = cthulhu_state.locusOfFocus
|
|
||||||
script = None
|
|
||||||
frame = None
|
|
||||||
if obj:
|
|
||||||
script = _scriptManager.getScript(AXObject.get_application(obj), obj)
|
|
||||||
if script:
|
|
||||||
frame = script.utilities.topLevelObject(obj)
|
|
||||||
self._currentMouseOver = _ItemContext(obj=obj, frame=frame, script=script)
|
|
||||||
|
|
||||||
self._eventListener.register("mouse:abs")
|
|
||||||
screen = Wnck.Screen.get_default()
|
|
||||||
if screen:
|
|
||||||
# On first startup windows and workspace are likely to be None,
|
|
||||||
# but the signals we connect to will get emitted when proper values
|
|
||||||
# become available; but in case we got disabled and re-enabled we
|
|
||||||
# have to get the initial values manually.
|
|
||||||
stacked = screen.get_windows_stacked()
|
|
||||||
if stacked:
|
|
||||||
stacked.reverse()
|
|
||||||
self._all_windows = stacked
|
|
||||||
self._workspace = screen.get_active_workspace()
|
|
||||||
if self._workspace:
|
|
||||||
self._update_workspace_windows()
|
|
||||||
|
|
||||||
i = screen.connect("window-stacking-changed", self._on_stacking_changed)
|
|
||||||
self._handlerIds[i] = screen
|
|
||||||
i = screen.connect("active-workspace-changed", self._on_workspace_changed)
|
|
||||||
self._handlerIds[i] = screen
|
|
||||||
|
|
||||||
self._active = True
|
|
||||||
|
|
||||||
def deactivate(self):
|
|
||||||
"""Deactivates mouse review."""
|
|
||||||
|
|
||||||
self._eventListener.deregister("mouse:abs")
|
|
||||||
for key, value in self._handlerIds.items():
|
|
||||||
value.disconnect(key)
|
|
||||||
self._handlerIds = {}
|
|
||||||
self._workspace = None
|
|
||||||
self._windows = []
|
|
||||||
self._all_windows = []
|
|
||||||
|
|
||||||
self._active = False
|
|
||||||
|
|
||||||
def getCurrentItem(self):
|
|
||||||
"""Returns the accessible object being reviewed."""
|
|
||||||
|
|
||||||
if not _mouseReviewCapable:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if not self._active:
|
|
||||||
return None
|
|
||||||
|
|
||||||
obj = self._currentMouseOver.getObject()
|
|
||||||
|
|
||||||
if time.time() - self._currentMouseOver.getTime() > 0.1:
|
|
||||||
msg = f"MOUSE REVIEW: Treating {obj} as stale"
|
|
||||||
debug.println(debug.LEVEL_INFO, msg, True)
|
|
||||||
return None
|
|
||||||
|
|
||||||
return obj
|
|
||||||
|
|
||||||
def toggle(self, script=None, event=None):
|
|
||||||
"""Toggle mouse reviewing on or off."""
|
|
||||||
|
|
||||||
if not _mouseReviewCapable:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._active = not self._active
|
|
||||||
_settingsManager.setSetting("enableMouseReview", self._active)
|
|
||||||
|
|
||||||
if not self._active:
|
|
||||||
self.deactivate()
|
|
||||||
msg = messages.MOUSE_REVIEW_DISABLED
|
|
||||||
else:
|
|
||||||
self.activate()
|
|
||||||
msg = messages.MOUSE_REVIEW_ENABLED
|
|
||||||
|
|
||||||
if cthulhu_state.activeScript:
|
|
||||||
cthulhu_state.activeScript.presentMessage(msg)
|
|
||||||
|
|
||||||
def _update_workspace_windows(self):
|
|
||||||
self._windows = [w for w in self._all_windows
|
|
||||||
if w.is_on_workspace(self._workspace)]
|
|
||||||
|
|
||||||
def _on_stacking_changed(self, screen):
|
|
||||||
"""Callback for Wnck's window-stacking-changed signal."""
|
|
||||||
|
|
||||||
stacked = screen.get_windows_stacked()
|
|
||||||
stacked.reverse()
|
|
||||||
self._all_windows = stacked
|
|
||||||
if self._workspace:
|
|
||||||
self._update_workspace_windows()
|
|
||||||
|
|
||||||
def _on_workspace_changed(self, screen, prev_ws=None):
|
|
||||||
"""Callback for Wnck's active-workspace-changed signal."""
|
|
||||||
|
|
||||||
self._workspace = screen.get_active_workspace()
|
|
||||||
self._update_workspace_windows()
|
|
||||||
|
|
||||||
def _contains_point(self, obj, x, y, coordType=None):
|
|
||||||
if coordType is None:
|
|
||||||
coordType = Atspi.CoordType.SCREEN
|
|
||||||
|
|
||||||
try:
|
|
||||||
return obj.queryComponent().contains(x, y, coordType)
|
|
||||||
except Exception:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _has_bounds(self, obj, bounds, coordType=None):
|
|
||||||
"""Returns True if the bounding box of obj is bounds."""
|
|
||||||
|
|
||||||
if coordType is None:
|
|
||||||
coordType = Atspi.CoordType.SCREEN
|
|
||||||
|
|
||||||
try:
|
|
||||||
extents = obj.queryComponent().getExtents(coordType)
|
|
||||||
except Exception:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return list(extents) == list(bounds)
|
|
||||||
|
|
||||||
def _accessible_window_at_point(self, pX, pY):
|
|
||||||
"""Returns the accessible window at the specified coordinates."""
|
|
||||||
|
|
||||||
window = None
|
|
||||||
for w in self._windows:
|
|
||||||
if w.is_minimized():
|
|
||||||
continue
|
|
||||||
|
|
||||||
x, y, width, height = w.get_geometry()
|
|
||||||
if x <= pX <= x + width and y <= pY <= y + height:
|
|
||||||
window = w
|
|
||||||
break
|
|
||||||
|
|
||||||
if not window:
|
|
||||||
return None
|
|
||||||
|
|
||||||
windowApp = window.get_application()
|
|
||||||
if not windowApp:
|
|
||||||
return None
|
|
||||||
|
|
||||||
app = AXUtilities.get_application_with_pid(windowApp.get_pid())
|
|
||||||
if not app:
|
|
||||||
return None
|
|
||||||
|
|
||||||
candidates = [o for o in AXObject.iter_children(
|
|
||||||
app, lambda x: self._contains_point(x, pX, pY))]
|
|
||||||
if len(candidates) == 1:
|
|
||||||
return candidates[0]
|
|
||||||
|
|
||||||
name = window.get_name()
|
|
||||||
matches = [o for o in candidates if AXObject.get_name(o) == name]
|
|
||||||
if len(matches) == 1:
|
|
||||||
return matches[0]
|
|
||||||
|
|
||||||
bbox = window.get_client_window_geometry()
|
|
||||||
matches = [o for o in candidates if self._has_bounds(o, bbox)]
|
|
||||||
if len(matches) == 1:
|
|
||||||
return matches[0]
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _on_mouse_moved(self, event):
|
|
||||||
"""Callback for mouse:abs events."""
|
|
||||||
|
|
||||||
screen, pX, pY = self._pointer.get_position()
|
|
||||||
window = self._accessible_window_at_point(pX, pY)
|
|
||||||
msg = "MOUSE REVIEW: Window at (%i, %i) is %s" % (pX, pY, window)
|
|
||||||
debug.println(debug.LEVEL_INFO, msg, True)
|
|
||||||
if not window:
|
|
||||||
return
|
|
||||||
|
|
||||||
script = _scriptManager.getScript(AXObject.get_application(window))
|
|
||||||
if not script:
|
|
||||||
return
|
|
||||||
|
|
||||||
if script.utilities.isDead(cthulhu_state.locusOfFocus):
|
|
||||||
menu = None
|
|
||||||
elif AXUtilities.is_menu(cthulhu_state.locusOfFocus):
|
|
||||||
menu = cthulhu_state.locusOfFocus
|
|
||||||
else:
|
|
||||||
menu = AXObject.find_ancestor(cthulhu_state.locusOfFocus, AXUtilities.is_menu)
|
|
||||||
|
|
||||||
screen, nowX, nowY = self._pointer.get_position()
|
|
||||||
if (pX, pY) != (nowX, nowY):
|
|
||||||
msg = "MOUSE REVIEW: Pointer moved again: (%i, %i)" % (nowX, nowY)
|
|
||||||
debug.println(debug.LEVEL_INFO, msg, True)
|
|
||||||
return
|
|
||||||
|
|
||||||
obj = script.utilities.descendantAtPoint(menu, pX, pY) \
|
|
||||||
or script.utilities.descendantAtPoint(window, pX, pY)
|
|
||||||
msg = "MOUSE REVIEW: Object at (%i, %i) is %s" % (pX, pY, obj)
|
|
||||||
debug.println(debug.LEVEL_INFO, msg, True)
|
|
||||||
|
|
||||||
script = _scriptManager.getScript(AXObject.get_application(window), obj)
|
|
||||||
if menu and obj and not AXObject.find_ancestor(obj, AXUtilities.is_menu):
|
|
||||||
if script.utilities.intersectingRegion(obj, menu) != (0, 0, 0, 0):
|
|
||||||
msg = f"MOUSE REVIEW: {obj} believed to be under {menu}"
|
|
||||||
debug.println(debug.LEVEL_INFO, msg, True)
|
|
||||||
return
|
|
||||||
|
|
||||||
objDocument = script.utilities.getTopLevelDocumentForObject(obj)
|
|
||||||
if objDocument and script.utilities.inDocumentContent():
|
|
||||||
document = script.utilities.activeDocument()
|
|
||||||
if document != objDocument:
|
|
||||||
msg = f"MOUSE REVIEW: {obj} is not in active document {document}"
|
|
||||||
debug.println(debug.LEVEL_INFO, msg, True)
|
|
||||||
return
|
|
||||||
|
|
||||||
screen, nowX, nowY = self._pointer.get_position()
|
|
||||||
if (pX, pY) != (nowX, nowY):
|
|
||||||
msg = "MOUSE REVIEW: Pointer moved again: (%i, %i)" % (nowX, nowY)
|
|
||||||
debug.println(debug.LEVEL_INFO, msg, True)
|
|
||||||
return
|
|
||||||
|
|
||||||
boundary = None
|
|
||||||
x, y, width, height = self._currentMouseOver.getBoundingBox()
|
|
||||||
if y <= pY <= y + height and self._currentMouseOver.getString():
|
|
||||||
boundary = Atspi.TextBoundaryType.WORD_START
|
|
||||||
elif obj == self._currentMouseOver.getObject():
|
|
||||||
boundary = Atspi.TextBoundaryType.LINE_START
|
|
||||||
elif AXUtilities.is_selectable(obj):
|
|
||||||
boundary = Atspi.TextBoundaryType.LINE_START
|
|
||||||
elif script.utilities.isMultiParagraphObject(obj):
|
|
||||||
boundary = Atspi.TextBoundaryType.LINE_START
|
|
||||||
|
|
||||||
new = _ItemContext(pX, pY, obj, boundary, window, script)
|
|
||||||
if new.present(self._currentMouseOver):
|
|
||||||
self._currentMouseOver = new
|
|
||||||
|
|
||||||
def _listener(self, event):
|
|
||||||
"""Generic listener, mainly to output debugging info."""
|
|
||||||
|
|
||||||
startTime = time.time()
|
|
||||||
msg = f"\nvvvvv PROCESS OBJECT EVENT {event.type} vvvvv"
|
|
||||||
debug.println(debug.LEVEL_INFO, msg, False)
|
|
||||||
|
|
||||||
if event.type.startswith("mouse:abs"):
|
|
||||||
self.inMouseEvent = True
|
|
||||||
self._on_mouse_moved(event)
|
|
||||||
self.inMouseEvent = False
|
|
||||||
|
|
||||||
msg = f"TOTAL PROCESSING TIME: {time.time() - startTime:.4f}\n"
|
|
||||||
msg += f"^^^^^ PROCESS OBJECT EVENT {event.type} ^^^^^\n"
|
|
||||||
debug.println(debug.LEVEL_INFO, msg, False)
|
|
@ -1,25 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
#
|
|
||||||
# Copyright (c) 2024 Stormux
|
|
||||||
# Copyright (c) 2010-2012 The Orca Team
|
|
||||||
# Copyright (c) 2012 Igalia, S.L.
|
|
||||||
# Copyright (c) 2005-2010 Sun Microsystems Inc.
|
|
||||||
#
|
|
||||||
# This library is free software; you can redistribute it and/or
|
|
||||||
# modify it under the terms of the GNU Lesser General Public
|
|
||||||
# License as published by the Free Software Foundation; either
|
|
||||||
# version 2.1 of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This library is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
# Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Lesser General Public
|
|
||||||
# License along with this library; if not, write to the
|
|
||||||
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
|
|
||||||
# Boston MA 02110-1301 USA.
|
|
||||||
#
|
|
||||||
# Fork of Orca Screen Reader (GNOME)
|
|
||||||
# Original source: https://gitlab.gnome.org/GNOME/orca
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
|||||||
cthulhu_python_PYTHON = \
|
|
||||||
__init__.py \
|
|
||||||
PluginManager.plugin \
|
|
||||||
PluginManager.py \
|
|
||||||
PluginManagerUi.py
|
|
||||||
|
|
||||||
cthulhu_pythondir=$(pkgpythondir)/plugins/PluginManager
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
|||||||
[Plugin]
|
|
||||||
Module=PluginManager
|
|
||||||
Loader=python3
|
|
||||||
Name=Plugin Manager
|
|
||||||
Description=Activate and Deactivate plugins
|
|
||||||
Authors=Chrys chrys@linux-a11y.org
|
|
||||||
Website=
|
|
||||||
Version=1.0
|
|
||||||
Copyright=
|
|
||||||
Builtin=true
|
|
||||||
Hidden=true
|
|
||||||
Depends=
|
|
||||||
Icon=
|
|
||||||
Help=
|
|
@ -1,61 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
#
|
|
||||||
# Copyright (c) 2024 Stormux
|
|
||||||
# Copyright (c) 2010-2012 The Orca Team
|
|
||||||
# Copyright (c) 2012 Igalia, S.L.
|
|
||||||
# Copyright (c) 2005-2010 Sun Microsystems Inc.
|
|
||||||
#
|
|
||||||
# This library is free software; you can redistribute it and/or
|
|
||||||
# modify it under the terms of the GNU Lesser General Public
|
|
||||||
# License as published by the Free Software Foundation; either
|
|
||||||
# version 2.1 of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This library is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
# Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Lesser General Public
|
|
||||||
# License along with this library; if not, write to the
|
|
||||||
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
|
|
||||||
# Boston MA 02110-1301 USA.
|
|
||||||
#
|
|
||||||
# Fork of Orca Screen Reader (GNOME)
|
|
||||||
# Original source: https://gitlab.gnome.org/GNOME/orca
|
|
||||||
|
|
||||||
from cthulhu import plugin
|
|
||||||
|
|
||||||
import gi
|
|
||||||
gi.require_version('Peas', '1.0')
|
|
||||||
from gi.repository import GObject
|
|
||||||
from gi.repository import Peas
|
|
||||||
|
|
||||||
import PluginManagerUi
|
|
||||||
|
|
||||||
class PluginManager(GObject.Object, Peas.Activatable, plugin.Plugin):
|
|
||||||
#__gtype_name__ = 'PluginManager'
|
|
||||||
|
|
||||||
object = GObject.Property(type=GObject.Object)
|
|
||||||
def __init__(self):
|
|
||||||
plugin.Plugin.__init__(self)
|
|
||||||
self.pluginManagerUi = None
|
|
||||||
def do_activate(self):
|
|
||||||
API = self.object
|
|
||||||
self.registerGestureByString(self.startPluginManagerUi, _('plugin manager'), 'kb:cthulhu+e')
|
|
||||||
|
|
||||||
def do_deactivate(self):
|
|
||||||
API = self.object
|
|
||||||
|
|
||||||
def startPluginManagerUi(self, script=None, inputEvent=None):
|
|
||||||
self.showUI()
|
|
||||||
return True
|
|
||||||
def showUI(self):
|
|
||||||
API = self.object
|
|
||||||
if self.pluginManagerUi == None:
|
|
||||||
self.pluginManagerUi = PluginManagerUi.PluginManagerUi(API.app)
|
|
||||||
self.pluginManagerUi.setTranslationContext(self.getTranslationContext())
|
|
||||||
self.pluginManagerUi.createUI()
|
|
||||||
self.pluginManagerUi.run()
|
|
||||||
self.pluginManagerUi = None
|
|
||||||
else:
|
|
||||||
self.pluginManagerUi.present()
|
|
@ -1,307 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
#
|
|
||||||
# Copyright (c) 2024 Stormux
|
|
||||||
# Copyright (c) 2010-2012 The Orca Team
|
|
||||||
# Copyright (c) 2012 Igalia, S.L.
|
|
||||||
# Copyright (c) 2005-2010 Sun Microsystems Inc.
|
|
||||||
#
|
|
||||||
# This library is free software; you can redistribute it and/or
|
|
||||||
# modify it under the terms of the GNU Lesser General Public
|
|
||||||
# License as published by the Free Software Foundation; either
|
|
||||||
# version 2.1 of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This library is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
# Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Lesser General Public
|
|
||||||
# License along with this library; if not, write to the
|
|
||||||
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
|
|
||||||
# Boston MA 02110-1301 USA.
|
|
||||||
#
|
|
||||||
# Fork of Orca Screen Reader (GNOME)
|
|
||||||
# Original source: https://gitlab.gnome.org/GNOME/orca
|
|
||||||
|
|
||||||
import gi
|
|
||||||
gi.require_version("Gtk", "3.0")
|
|
||||||
from gi.repository import Gtk, Gdk
|
|
||||||
|
|
||||||
class PluginManagerUi(Gtk.ApplicationWindow):
|
|
||||||
def __init__(self, app, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs, title=_("Cthulhu Plugin Manager"))
|
|
||||||
self.app = app
|
|
||||||
self.translationContext = None
|
|
||||||
self.connect("destroy", self._onCancelButtonClicked)
|
|
||||||
self.connect('key-press-event', self._onKeyPressWindow)
|
|
||||||
def createUI(self):
|
|
||||||
self.set_default_size(650, 650)
|
|
||||||
self.set_position(Gtk.WindowPosition.CENTER_ALWAYS)
|
|
||||||
|
|
||||||
# pluginInfo (object) = 0
|
|
||||||
# name (str) = 1
|
|
||||||
# active (bool) = 2
|
|
||||||
# buildIn (bool) = 3
|
|
||||||
# dataDir (str) = 4
|
|
||||||
# moduleDir (str) = 5
|
|
||||||
# dependencies (object) = 6
|
|
||||||
# moduleName (str) = 7
|
|
||||||
# description (str) = 8
|
|
||||||
# authors (object) = 9
|
|
||||||
# website (str) = 10
|
|
||||||
# copyright (str) = 11
|
|
||||||
# version (str) = 12
|
|
||||||
# helpUri (str) = 13
|
|
||||||
# iconName (str) = 14
|
|
||||||
self.listStore = Gtk.ListStore(object,str, bool, bool, str, str,object,str,str,object,str,str,str,str,str)
|
|
||||||
|
|
||||||
self.treeView = Gtk.TreeView(model=self.listStore)
|
|
||||||
self.treeView.connect("row-activated", self._rowActivated)
|
|
||||||
self.treeView.connect('key-press-event', self._onKeyPressTreeView)
|
|
||||||
|
|
||||||
self.rendererText = Gtk.CellRendererText()
|
|
||||||
self.columnText = Gtk.TreeViewColumn(_("Name"), self.rendererText, text=1)
|
|
||||||
self.treeView.append_column(self.columnText)
|
|
||||||
|
|
||||||
self.rendererToggle = Gtk.CellRendererToggle()
|
|
||||||
self.rendererToggle.connect("toggled", self._onCellToggled)
|
|
||||||
|
|
||||||
self.columnToggle = Gtk.TreeViewColumn(_("Active"), self.rendererToggle, active=2)
|
|
||||||
self.treeView.append_column(self.columnToggle)
|
|
||||||
|
|
||||||
|
|
||||||
self.buttomBox = Gtk.Box(spacing=6)
|
|
||||||
self.mainVBox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
|
||||||
self.mainVBox.pack_start(self.treeView, True, True, 0)
|
|
||||||
self.mainVBox.pack_start(self.buttomBox, False, True, 0)
|
|
||||||
|
|
||||||
self.add(self.mainVBox)
|
|
||||||
self.oKButton = Gtk.Button.new_with_mnemonic(_("_Details"))
|
|
||||||
self.oKButton.connect("clicked", self._onDetailsButtonClicked)
|
|
||||||
self.buttomBox.pack_start(self.oKButton, True, True, 0)
|
|
||||||
|
|
||||||
self.oKButton = Gtk.Button.new_with_mnemonic(_("_OK"))
|
|
||||||
self.oKButton.connect("clicked", self._onOkButtonClicked)
|
|
||||||
self.buttomBox.pack_start(self.oKButton, True, True, 0)
|
|
||||||
|
|
||||||
self.applyButton = Gtk.Button.new_with_mnemonic(_("_Apply"))
|
|
||||||
self.applyButton.connect("clicked", self._onApplyButtonClicked)
|
|
||||||
self.buttomBox.pack_start(self.applyButton, True, True, 0)
|
|
||||||
|
|
||||||
self.applyButton = Gtk.Button.new_with_mnemonic(_("_Install"))
|
|
||||||
self.applyButton.connect("clicked", self._onInstallButtonClicked)
|
|
||||||
self.buttomBox.pack_start(self.applyButton, True, True, 0)
|
|
||||||
|
|
||||||
self.applyButton = Gtk.Button.new_with_mnemonic(_("_Uninstall"))
|
|
||||||
self.applyButton.connect("clicked", self._onUninstallButtonClicked)
|
|
||||||
self.buttomBox.pack_start(self.applyButton, True, True, 0)
|
|
||||||
|
|
||||||
self.cancelButton = Gtk.Button.new_with_mnemonic(_("_Cancel"))
|
|
||||||
self.cancelButton.connect("clicked", self._onCancelButtonClicked)
|
|
||||||
self.buttomBox.pack_start(self.cancelButton, True, True, 0)
|
|
||||||
def setTranslationContext(self, translationContext):
|
|
||||||
self.translationContext = translationContext
|
|
||||||
global _
|
|
||||||
_ = translationContext.gettext
|
|
||||||
def closeWindow(self):
|
|
||||||
Gtk.main_quit()
|
|
||||||
def uninstallPlugin(self):
|
|
||||||
selection = self.treeView.get_selection()
|
|
||||||
model, list_iter = selection.get_selected()
|
|
||||||
try:
|
|
||||||
if model.get_value(list_iter,0):
|
|
||||||
pluginInfo = model.get_value(list_iter,0)
|
|
||||||
pluginName = self.app.getPluginSystemManager().getPluginName(pluginInfo)
|
|
||||||
dialog = Gtk.MessageDialog(None,
|
|
||||||
Gtk.DialogFlags.MODAL,
|
|
||||||
type=Gtk.MessageType.INFO,
|
|
||||||
buttons=Gtk.ButtonsType.YES_NO)
|
|
||||||
|
|
||||||
dialog.set_markup("<b>%s</b>" % _('Remove Plugin {}?').format(pluginName))
|
|
||||||
dialog.format_secondary_markup(_('Do you really want to remove Plugin {}?').format(pluginName))
|
|
||||||
response = dialog.run()
|
|
||||||
dialog.destroy()
|
|
||||||
if response != Gtk.ResponseType.YES:
|
|
||||||
return
|
|
||||||
self.app.getPluginSystemManager().uninstallPlugin(model.get_value(list_iter,0))
|
|
||||||
self.refreshPluginList()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def installPlugin(self):
|
|
||||||
ok, filePath = self.chooseFile()
|
|
||||||
if not ok:
|
|
||||||
return
|
|
||||||
self.app.getPluginSystemManager().installPlugin(filePath)
|
|
||||||
self.refreshPluginList()
|
|
||||||
|
|
||||||
def _onKeyPressWindow(self, _, event):
|
|
||||||
_, key_val = event.get_keyval()
|
|
||||||
if key_val == Gdk.KEY_Escape:
|
|
||||||
self.closeWindow()
|
|
||||||
def _onKeyPressTreeView(self, _, event):
|
|
||||||
_, key_val = event.get_keyval()
|
|
||||||
if key_val == Gdk.KEY_Return:
|
|
||||||
self.applySettings()
|
|
||||||
self.closeWindow()
|
|
||||||
if key_val == Gdk.KEY_Escape:
|
|
||||||
self.closeWindow()
|
|
||||||
# CTRL + Q
|
|
||||||
#modifiers = event.get_state()
|
|
||||||
#if modifiers == Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.MOD2_MASK:
|
|
||||||
# if key_val == Gdk.KEY_q:
|
|
||||||
# self._on_scan()
|
|
||||||
def applySettings(self):
|
|
||||||
for row in self.listStore:
|
|
||||||
pluginInfo = row[0]
|
|
||||||
isActive = row[2]
|
|
||||||
self.app.getPluginSystemManager().setPluginActive(pluginInfo, isActive)
|
|
||||||
gsettingsManager = self.app.getGsettingsManager()
|
|
||||||
gsettingsManager.set_settings_value_list('active-plugins', self.app.getPluginSystemManager().getActivePlugins())
|
|
||||||
|
|
||||||
self.app.getPluginSystemManager().syncAllPluginsActive()
|
|
||||||
self.refreshPluginList()
|
|
||||||
|
|
||||||
|
|
||||||
def _rowActivated(self, tree_view, path, column):
|
|
||||||
print('rowActivated')
|
|
||||||
def showDetails(self):
|
|
||||||
selection = self.treeView.get_selection()
|
|
||||||
model, list_iter = selection.get_selected()
|
|
||||||
try:
|
|
||||||
if model.get_value(list_iter,0):
|
|
||||||
pluginInfo = model.get_value(list_iter,0)
|
|
||||||
name = self.app.getPluginSystemManager().getPluginName(pluginInfo)
|
|
||||||
description = self.app.getPluginSystemManager().getPluginDescription(pluginInfo)
|
|
||||||
authors = self.app.getPluginSystemManager().getPluginAuthors(pluginInfo)
|
|
||||||
website =self.app.getPluginSystemManager().getPluginWebsite(pluginInfo)
|
|
||||||
copyright = self.app.getPluginSystemManager().getPluginCopyright(pluginInfo)
|
|
||||||
license = '' #self.app.getPluginSystemManager().getPluginName(pluginInfo)
|
|
||||||
version = self.app.getPluginSystemManager().getPluginVersion(pluginInfo)
|
|
||||||
dialog = Gtk.AboutDialog(self)
|
|
||||||
dialog.set_authors(authors)
|
|
||||||
dialog.set_website(website)
|
|
||||||
dialog.set_copyright(copyright)
|
|
||||||
dialog.set_license(license)
|
|
||||||
dialog.set_version(version)
|
|
||||||
dialog.set_program_name(name)
|
|
||||||
dialog.set_comments(description)
|
|
||||||
dialog.run()
|
|
||||||
dialog.destroy()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _onDetailsButtonClicked(self, widget):
|
|
||||||
self.showDetails()
|
|
||||||
|
|
||||||
def _onOkButtonClicked(self, widget):
|
|
||||||
self.applySettings()
|
|
||||||
self.closeWindow()
|
|
||||||
def _onApplyButtonClicked(self, widget):
|
|
||||||
self.applySettings()
|
|
||||||
def _onInstallButtonClicked(self, widget):
|
|
||||||
self.installPlugin()
|
|
||||||
def _onUninstallButtonClicked(self, widget):
|
|
||||||
self.uninstallPlugin()
|
|
||||||
def _onCancelButtonClicked(self, widget):
|
|
||||||
self.closeWindow()
|
|
||||||
def refreshPluginList(self):
|
|
||||||
self.clearPluginList()
|
|
||||||
pluginList = self.app.getPluginSystemManager().plugins
|
|
||||||
for pluginInfo in pluginList:
|
|
||||||
self.addPlugin(pluginInfo)
|
|
||||||
def clearPluginList(self):
|
|
||||||
self.listStore.clear()
|
|
||||||
|
|
||||||
def addPlugin(self, pluginInfo):
|
|
||||||
ignoredPlugins = self.app.getPluginSystemManager().getIgnoredPlugins()
|
|
||||||
moduleDir = self.app.getPluginSystemManager().getPluginModuleDir(pluginInfo)
|
|
||||||
if moduleDir in ignoredPlugins:
|
|
||||||
return
|
|
||||||
|
|
||||||
hidden = self.app.getPluginSystemManager().isPluginHidden(pluginInfo)
|
|
||||||
if hidden:
|
|
||||||
return
|
|
||||||
|
|
||||||
moduleName = self.app.getPluginSystemManager().getPluginModuleName(pluginInfo)
|
|
||||||
name = self.app.getPluginSystemManager().getPluginName(pluginInfo)
|
|
||||||
version = self.app.getPluginSystemManager().getPluginVersion(pluginInfo)
|
|
||||||
website = self.app.getPluginSystemManager().getPluginWebsite(pluginInfo)
|
|
||||||
authors = self.app.getPluginSystemManager().getPluginAuthors(pluginInfo)
|
|
||||||
buildIn = self.app.getPluginSystemManager().isPluginBuildIn(pluginInfo)
|
|
||||||
description = self.app.getPluginSystemManager().getPluginDescription(pluginInfo)
|
|
||||||
iconName = self.app.getPluginSystemManager().getPluginIconName(pluginInfo)
|
|
||||||
copyright = self.app.getPluginSystemManager().getPluginCopyright(pluginInfo)
|
|
||||||
dependencies = self.app.getPluginSystemManager().getPluginDependencies(pluginInfo)
|
|
||||||
|
|
||||||
#settings = self.app.getPluginSystemManager().getPluginSettings(pluginInfo)
|
|
||||||
#hasDependencies = self.app.getPluginSystemManager().hasPluginDependency(pluginInfo)
|
|
||||||
loaded = self.app.getPluginSystemManager().isPluginLoaded(pluginInfo)
|
|
||||||
available = self.app.getPluginSystemManager().isPluginAvailable(pluginInfo)
|
|
||||||
active = self.app.getPluginSystemManager().isPluginActive(pluginInfo)
|
|
||||||
|
|
||||||
#externalData = self.app.getPluginSystemManager().getPluginExternalData(pluginInfo)
|
|
||||||
helpUri = self.app.getPluginSystemManager().getPlugingetHelpUri(pluginInfo)
|
|
||||||
dataDir = self.app.getPluginSystemManager().getPluginDataDir(pluginInfo)
|
|
||||||
|
|
||||||
# pluginInfo (object) = 0
|
|
||||||
# name (str) = 1
|
|
||||||
# active (bool) = 2
|
|
||||||
# buildIn (bool) = 3
|
|
||||||
# dataDir (str) = 4
|
|
||||||
# moduleDir (str) = 5
|
|
||||||
# dependencies (object) = 6
|
|
||||||
# moduleName (str) = 7
|
|
||||||
# description (str) = 8
|
|
||||||
# authors (object) = 9
|
|
||||||
# website (str) = 10
|
|
||||||
# copyright (str) = 11
|
|
||||||
# version (str) = 12
|
|
||||||
# helpUri (str) = 13
|
|
||||||
# iconName (str) = 14
|
|
||||||
self.listStore.append([pluginInfo, name, active, buildIn, dataDir, moduleDir, dependencies, moduleName, description, authors, website, copyright, version, helpUri, iconName])
|
|
||||||
def chooseFile(self):
|
|
||||||
dialog = Gtk.FileChooserDialog(
|
|
||||||
title=_("Please choose a file"), parent=self, action=Gtk.FileChooserAction.OPEN
|
|
||||||
)
|
|
||||||
dialog.add_buttons(
|
|
||||||
Gtk.STOCK_CANCEL,
|
|
||||||
Gtk.ResponseType.CANCEL,
|
|
||||||
Gtk.STOCK_OPEN,
|
|
||||||
Gtk.ResponseType.OK,
|
|
||||||
)
|
|
||||||
|
|
||||||
filter_plugin = Gtk.FileFilter()
|
|
||||||
filter_plugin.set_name(_("Plugin Archive"))
|
|
||||||
filter_plugin.add_mime_type("application/gzip")
|
|
||||||
dialog.add_filter(filter_plugin)
|
|
||||||
|
|
||||||
response = dialog.run()
|
|
||||||
filePath = ''
|
|
||||||
ok = False
|
|
||||||
|
|
||||||
if response == Gtk.ResponseType.OK:
|
|
||||||
ok = True
|
|
||||||
filePath = dialog.get_filename()
|
|
||||||
|
|
||||||
dialog.destroy()
|
|
||||||
return ok, filePath
|
|
||||||
def _onCellToggled(self, widget, path):
|
|
||||||
|
|
||||||
self.listStore[path][2] = not self.listStore[path][2]
|
|
||||||
def present(self):
|
|
||||||
cthulhu_state = self.app.getDynamicApiManager().getAPI('CthulhuState')
|
|
||||||
ts = 0
|
|
||||||
try:
|
|
||||||
ts = cthulhu_state.lastInputEvent.timestamp
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
if ts == 0:
|
|
||||||
ts = Gtk.get_current_event_time()
|
|
||||||
self.present_with_time(ts)
|
|
||||||
def run(self):
|
|
||||||
self.refreshPluginList()
|
|
||||||
self.present()
|
|
||||||
self.show_all()
|
|
||||||
Gtk.main()
|
|
||||||
self.destroy()
|
|
@ -1,107 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
#
|
|
||||||
# Copyright (c) 2024 Stormux
|
|
||||||
# Copyright (c) 2010-2012 The Orca Team
|
|
||||||
# Copyright (c) 2012 Igalia, S.L.
|
|
||||||
# Copyright (c) 2005-2010 Sun Microsystems Inc.
|
|
||||||
#
|
|
||||||
# This library is free software; you can redistribute it and/or
|
|
||||||
# modify it under the terms of the GNU Lesser General Public
|
|
||||||
# License as published by the Free Software Foundation; either
|
|
||||||
# version 2.1 of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This library is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
# Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Lesser General Public
|
|
||||||
# License along with this library; if not, write to the
|
|
||||||
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
|
|
||||||
# Boston MA 02110-1301 USA.
|
|
||||||
#
|
|
||||||
# Fork of Orca Screen Reader (GNOME)
|
|
||||||
# Original source: https://gitlab.gnome.org/GNOME/orca
|
|
||||||
|
|
||||||
import gi
|
|
||||||
gi.require_version('Gtk', '3.0')
|
|
||||||
from gi.repository import Gtk
|
|
||||||
|
|
||||||
class ListBoxRowWithData(Gtk.ListBoxRow):
|
|
||||||
def __init__(self, data):
|
|
||||||
super(Gtk.ListBoxRow, self).__init__()
|
|
||||||
self.data = data
|
|
||||||
self.add(Gtk.Label(label=data))
|
|
||||||
|
|
||||||
class PluginManagerUi(Gtk.Window):
|
|
||||||
def __init__(self):
|
|
||||||
Gtk.Window.__init__(self)
|
|
||||||
self.pluginList = []
|
|
||||||
self.set_default_size(200, -1)
|
|
||||||
self.connect("destroy", Gtk.main_quit)
|
|
||||||
self.listBox = Gtk.ListBox()
|
|
||||||
|
|
||||||
self.buttomBox = Gtk.Box(spacing=6)
|
|
||||||
self.mainVBox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=20)
|
|
||||||
self.mainVBox.pack_start(self.listBox, True, True, 0)
|
|
||||||
self.mainVBox.pack_start(self.buttomBox, True, True, 0)
|
|
||||||
|
|
||||||
self.add(self.mainVBox)
|
|
||||||
|
|
||||||
self.oKButton = Gtk.Button(label="OK")
|
|
||||||
self.oKButton.connect("clicked", self.on_oKButton_clicked)
|
|
||||||
self.buttomBox.pack_start(self.oKButton, True, True, 0)
|
|
||||||
|
|
||||||
self.applyButton = Gtk.Button(label="Apply")
|
|
||||||
self.applyButton.connect("clicked", self.on_applyButton_clicked)
|
|
||||||
self.buttomBox.pack_start(self.applyButton, True, True, 0)
|
|
||||||
|
|
||||||
self.cancelButton = Gtk.Button(label="Cancel")
|
|
||||||
self.cancelButton.connect("clicked", self.on_cancelButton_clicked)
|
|
||||||
self.buttomBox.pack_start(self.cancelButton, True, True, 0)
|
|
||||||
|
|
||||||
self.listBox.connect("row-activated", self.on_row_activated)
|
|
||||||
|
|
||||||
def on_row_activated(self, listBox, listboxrow):
|
|
||||||
print("Row %i activated" % (listboxrow.get_index()))
|
|
||||||
|
|
||||||
def on_oKButton_clicked(self, widget):
|
|
||||||
print("OK")
|
|
||||||
|
|
||||||
def on_applyButton_clicked(self, widget):
|
|
||||||
print("Apply")
|
|
||||||
|
|
||||||
def on_cancelButton_clicked(self, widget):
|
|
||||||
print("Cancel")
|
|
||||||
|
|
||||||
|
|
||||||
def addPlugin(self, Name, Active, Description = ''):
|
|
||||||
self.pluginList.append([Name, Active, Description])
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
for plugin in self.pluginList:
|
|
||||||
print(plugin)
|
|
||||||
box = Gtk.Box(spacing=10)
|
|
||||||
pluginNameLabel = Gtk.Label(plugin[0])
|
|
||||||
#pluginActiveCheckButton = Gtk.CheckButton(label="_Active", use_underline=True)
|
|
||||||
#pluginActiveCheckButton.set_active(plugin[1])
|
|
||||||
pluginActiveSwitch = Gtk.Switch()
|
|
||||||
pluginActiveSwitch.set_active(plugin[1])
|
|
||||||
|
|
||||||
|
|
||||||
pluginDescriptionLabel = Gtk.Label(plugin[2])
|
|
||||||
|
|
||||||
box.pack_start(pluginNameLabel, True, True, 0)
|
|
||||||
box.pack_start(pluginActiveSwitch, True, True, 0)
|
|
||||||
box.pack_start(pluginDescriptionLabel, True, True, 0)
|
|
||||||
|
|
||||||
self.listBox.add(box)
|
|
||||||
self.show_all()
|
|
||||||
Gtk.main()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
ui = PluginManagerUi()
|
|
||||||
ui.addPlugin('plugin1', True, 'bla')
|
|
||||||
ui.addPlugin('plugin2', True, 'bla')
|
|
||||||
ui.addPlugin('plugin3', True, 'bla')
|
|
||||||
ui.run()
|
|
@ -1,117 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
#
|
|
||||||
# Copyright (c) 2024 Stormux
|
|
||||||
# Copyright (c) 2010-2012 The Orca Team
|
|
||||||
# Copyright (c) 2012 Igalia, S.L.
|
|
||||||
# Copyright (c) 2005-2010 Sun Microsystems Inc.
|
|
||||||
#
|
|
||||||
# This library is free software; you can redistribute it and/or
|
|
||||||
# modify it under the terms of the GNU Lesser General Public
|
|
||||||
# License as published by the Free Software Foundation; either
|
|
||||||
# version 2.1 of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This library is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
# Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Lesser General Public
|
|
||||||
# License along with this library; if not, write to the
|
|
||||||
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
|
|
||||||
# Boston MA 02110-1301 USA.
|
|
||||||
#
|
|
||||||
# Fork of Orca Screen Reader (GNOME)
|
|
||||||
# Original source: https://gitlab.gnome.org/GNOME/orca
|
|
||||||
|
|
||||||
import gi
|
|
||||||
|
|
||||||
gi.require_version("Gtk", "3.0")
|
|
||||||
from gi.repository import Gtk
|
|
||||||
|
|
||||||
|
|
||||||
class ListBoxRowWithData(Gtk.ListBoxRow):
|
|
||||||
def __init__(self, data):
|
|
||||||
super(Gtk.ListBoxRow, self).__init__()
|
|
||||||
self.data = data
|
|
||||||
self.add(Gtk.Label(label=data))
|
|
||||||
|
|
||||||
|
|
||||||
class ListBoxWindow(Gtk.Window):
|
|
||||||
def __init__(self):
|
|
||||||
Gtk.Window.__init__(self, title="ListBox Demo")
|
|
||||||
self.set_border_width(10)
|
|
||||||
|
|
||||||
box_outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
|
||||||
self.add(box_outer)
|
|
||||||
|
|
||||||
listbox = Gtk.ListBox()
|
|
||||||
listbox.set_selection_mode(Gtk.SelectionMode.NONE)
|
|
||||||
box_outer.pack_start(listbox, True, True, 0)
|
|
||||||
|
|
||||||
row = Gtk.ListBoxRow()
|
|
||||||
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
|
|
||||||
row.add(hbox)
|
|
||||||
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
|
||||||
hbox.pack_start(vbox, True, True, 0)
|
|
||||||
|
|
||||||
label1 = Gtk.Label(label="Automatic Date & Time", xalign=0)
|
|
||||||
label2 = Gtk.Label(label="Requires internet access", xalign=0)
|
|
||||||
vbox.pack_start(label1, True, True, 0)
|
|
||||||
vbox.pack_start(label2, True, True, 0)
|
|
||||||
|
|
||||||
switch = Gtk.Switch()
|
|
||||||
switch.props.valign = Gtk.Align.CENTER
|
|
||||||
hbox.pack_start(switch, False, True, 0)
|
|
||||||
|
|
||||||
listbox.add(row)
|
|
||||||
|
|
||||||
row = Gtk.ListBoxRow()
|
|
||||||
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
|
|
||||||
row.add(hbox)
|
|
||||||
label = Gtk.Label(label="Enable Automatic Update", xalign=0)
|
|
||||||
check = Gtk.CheckButton()
|
|
||||||
hbox.pack_start(label, True, True, 0)
|
|
||||||
hbox.pack_start(check, False, True, 0)
|
|
||||||
|
|
||||||
listbox.add(row)
|
|
||||||
|
|
||||||
row = Gtk.ListBoxRow()
|
|
||||||
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
|
|
||||||
row.add(hbox)
|
|
||||||
label = Gtk.Label(label="Date Format", xalign=0)
|
|
||||||
combo = Gtk.ComboBoxText()
|
|
||||||
combo.insert(0, "0", "24-hour")
|
|
||||||
combo.insert(1, "1", "AM/PM")
|
|
||||||
hbox.pack_start(label, True, True, 0)
|
|
||||||
hbox.pack_start(combo, False, True, 0)
|
|
||||||
|
|
||||||
listbox.add(row)
|
|
||||||
|
|
||||||
listbox_2 = Gtk.ListBox()
|
|
||||||
items = "This is a sorted ListBox Fail".split()
|
|
||||||
|
|
||||||
for item in items:
|
|
||||||
listbox_2.add(ListBoxRowWithData(item))
|
|
||||||
|
|
||||||
def sort_func(row_1, row_2, data, notify_destroy):
|
|
||||||
return row_1.data.lower() > row_2.data.lower()
|
|
||||||
|
|
||||||
def filter_func(row, data, notify_destroy):
|
|
||||||
return False if row.data == "Fail" else True
|
|
||||||
|
|
||||||
listbox_2.set_sort_func(sort_func, None, False)
|
|
||||||
listbox_2.set_filter_func(filter_func, None, False)
|
|
||||||
|
|
||||||
def on_row_activated(listbox_widget, row):
|
|
||||||
print(row.data)
|
|
||||||
|
|
||||||
listbox_2.connect("row-activated", on_row_activated)
|
|
||||||
|
|
||||||
box_outer.pack_start(listbox_2, True, True, 0)
|
|
||||||
listbox_2.show_all()
|
|
||||||
|
|
||||||
|
|
||||||
win = ListBoxWindow()
|
|
||||||
win.connect("destroy", Gtk.main_quit)
|
|
||||||
win.show_all()
|
|
||||||
Gtk.main()
|
|
@ -1,25 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
#
|
|
||||||
# Copyright (c) 2024 Stormux
|
|
||||||
# Copyright (c) 2010-2012 The Orca Team
|
|
||||||
# Copyright (c) 2012 Igalia, S.L.
|
|
||||||
# Copyright (c) 2005-2010 Sun Microsystems Inc.
|
|
||||||
#
|
|
||||||
# This library is free software; you can redistribute it and/or
|
|
||||||
# modify it under the terms of the GNU Lesser General Public
|
|
||||||
# License as published by the Free Software Foundation; either
|
|
||||||
# version 2.1 of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This library is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
# Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Lesser General Public
|
|
||||||
# License along with this library; if not, write to the
|
|
||||||
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
|
|
||||||
# Boston MA 02110-1301 USA.
|
|
||||||
#
|
|
||||||
# Fork of Orca Screen Reader (GNOME)
|
|
||||||
# Original source: https://gitlab.gnome.org/GNOME/orca
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
|||||||
cthulhu_python_PYTHON = \
|
|
||||||
__init__.py \
|
|
||||||
SelfVoice.plugin \
|
|
||||||
SelfVoice.py
|
|
||||||
|
|
||||||
cthulhu_pythondir=$(pkgpythondir)/plugins/SelfVoice
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
|||||||
[Plugin]
|
|
||||||
Module=SelfVoice
|
|
||||||
Loader=python3
|
|
||||||
Name=Self Voice Plugin
|
|
||||||
Description=use cthulhu text / braile from using unix sockets
|
|
||||||
Authors=Chrys chrys@linux-a11y.org
|
|
@ -1,135 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
#
|
|
||||||
# Copyright (c) 2024 Stormux
|
|
||||||
# Copyright (c) 2010-2012 The Orca Team
|
|
||||||
# Copyright (c) 2012 Igalia, S.L.
|
|
||||||
# Copyright (c) 2005-2010 Sun Microsystems Inc.
|
|
||||||
#
|
|
||||||
# This library is free software; you can redistribute it and/or
|
|
||||||
# modify it under the terms of the GNU Lesser General Public
|
|
||||||
# License as published by the Free Software Foundation; either
|
|
||||||
# version 2.1 of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This library is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
# Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Lesser General Public
|
|
||||||
# License along with this library; if not, write to the
|
|
||||||
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
|
|
||||||
# Boston MA 02110-1301 USA.
|
|
||||||
#
|
|
||||||
# Fork of Orca Screen Reader (GNOME)
|
|
||||||
# Original source: https://gitlab.gnome.org/GNOME/orca
|
|
||||||
|
|
||||||
from cthulhu import plugin
|
|
||||||
|
|
||||||
import gi
|
|
||||||
gi.require_version('Peas', '1.0')
|
|
||||||
from gi.repository import GObject
|
|
||||||
from gi.repository import Peas
|
|
||||||
import select, socket, os, os.path
|
|
||||||
from threading import Thread, Lock
|
|
||||||
|
|
||||||
APPEND_CODE = '<#APPEND#>'
|
|
||||||
PERSISTENT_CODE = '<#PERSISTENT#>'
|
|
||||||
|
|
||||||
class SelfVoice(GObject.Object, Peas.Activatable, plugin.Plugin):
|
|
||||||
__gtype_name__ = 'SelfVoice'
|
|
||||||
|
|
||||||
object = GObject.Property(type=GObject.Object)
|
|
||||||
def __init__(self):
|
|
||||||
plugin.Plugin.__init__(self)
|
|
||||||
self.lock = Lock()
|
|
||||||
self.active = False
|
|
||||||
self.voiceThread = Thread(target=self.voiceWorker)
|
|
||||||
def do_activate(self):
|
|
||||||
API = self.object
|
|
||||||
self.activateWorker()
|
|
||||||
def do_deactivate(self):
|
|
||||||
API = self.object
|
|
||||||
self.deactivateWorker()
|
|
||||||
def do_update_state(self):
|
|
||||||
API = self.object
|
|
||||||
def deactivateWorker(self):
|
|
||||||
with self.lock:
|
|
||||||
self.active = False
|
|
||||||
self.voiceThread.join()
|
|
||||||
def activateWorker(self):
|
|
||||||
with self.lock:
|
|
||||||
self.active = True
|
|
||||||
self.voiceThread.start()
|
|
||||||
def isActive(self):
|
|
||||||
with self.lock:
|
|
||||||
return self.active
|
|
||||||
def outputMessage(self, Message):
|
|
||||||
# Prepare
|
|
||||||
API = self.object
|
|
||||||
append = Message.startswith(APPEND_CODE)
|
|
||||||
if append:
|
|
||||||
Message = Message[len(APPEND_CODE):]
|
|
||||||
if Message.endswith(PERSISTENT_CODE):
|
|
||||||
Message = Message[:len(Message)-len(PERSISTENT_CODE)]
|
|
||||||
API.app.getAPIHelper().outputMessage(Message, not append)
|
|
||||||
else:
|
|
||||||
script_manager = API.app.getDynamicApiManager().getAPI('ScriptManager')
|
|
||||||
scriptManager = script_manager.getManager()
|
|
||||||
scriptManager.getDefaultScript().presentMessage(Message, resetStyles=False)
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
settings = API.app.getDynamicApiManager().getAPI('Settings')
|
|
||||||
braille = API.app.getDynamicApiManager().getAPI('Braille')
|
|
||||||
speech = API.app.getDynamicApiManager().getAPI('Speech')
|
|
||||||
# Speak
|
|
||||||
if speech != None:
|
|
||||||
if (settings.enableSpeech):
|
|
||||||
if not append:
|
|
||||||
speech.cancel()
|
|
||||||
if Message != '':
|
|
||||||
speech.speak(Message)
|
|
||||||
# Braille
|
|
||||||
if braille != None:
|
|
||||||
if (settings.enableBraille):
|
|
||||||
braille.displayMessage(Message)
|
|
||||||
except e as Exception:
|
|
||||||
print(e)
|
|
||||||
|
|
||||||
def voiceWorker(self):
|
|
||||||
socketFile = '/tmp/cthulhu.sock'
|
|
||||||
# for testing purposes
|
|
||||||
#socketFile = '/tmp/cthulhu-plugin.sock'
|
|
||||||
|
|
||||||
if os.path.exists(socketFile):
|
|
||||||
os.unlink(socketFile)
|
|
||||||
cthulhuSock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
||||||
cthulhuSock.bind(socketFile)
|
|
||||||
os.chmod(socketFile, 0o222)
|
|
||||||
cthulhuSock.listen(1)
|
|
||||||
while self.isActive():
|
|
||||||
# Check if the client is still connected and if data is available:
|
|
||||||
try:
|
|
||||||
r, _, _ = select.select([cthulhuSock], [], [], 0.8)
|
|
||||||
except select.error:
|
|
||||||
break
|
|
||||||
if r == []:
|
|
||||||
continue
|
|
||||||
if cthulhuSock in r:
|
|
||||||
client_sock, client_addr = cthulhuSock.accept()
|
|
||||||
try:
|
|
||||||
rawdata = client_sock.recv(8129)
|
|
||||||
data = rawdata.decode("utf-8").rstrip().lstrip()
|
|
||||||
self.outputMessage(data)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
client_sock.close()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
if cthulhuSock:
|
|
||||||
cthulhuSock.close()
|
|
||||||
cthulhuSock = None
|
|
||||||
if os.path.exists(socketFile):
|
|
||||||
os.unlink(socketFile)
|
|
||||||
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
#
|
|
||||||
# Copyright (c) 2024 Stormux
|
|
||||||
# Copyright (c) 2010-2012 The Orca Team
|
|
||||||
# Copyright (c) 2012 Igalia, S.L.
|
|
||||||
# Copyright (c) 2005-2010 Sun Microsystems Inc.
|
|
||||||
#
|
|
||||||
# This library is free software; you can redistribute it and/or
|
|
||||||
# modify it under the terms of the GNU Lesser General Public
|
|
||||||
# License as published by the Free Software Foundation; either
|
|
||||||
# version 2.1 of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This library is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
# Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Lesser General Public
|
|
||||||
# License along with this library; if not, write to the
|
|
||||||
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
|
|
||||||
# Boston MA 02110-1301 USA.
|
|
||||||
#
|
|
||||||
# Fork of Orca Screen Reader (GNOME)
|
|
||||||
# Original source: https://gitlab.gnome.org/GNOME/orca
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
|||||||
cthulhu_python_PYTHON = \
|
cthulhu_python_PYTHON = \
|
||||||
__init__.py \
|
__init__.py \
|
||||||
SimplePluginSystem.plugin \
|
plugin.info \
|
||||||
SimplePluginSystem.py
|
plugin.py
|
||||||
|
|
||||||
cthulhu_pythondir=$(pkgpythondir)/plugins/SimplePluginSystem
|
cthulhu_pythondir=$(pkgpythondir)/plugins/SimplePluginSystem
|
||||||
|
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
[Plugin]
|
|
||||||
Module=SimplePluginSystem
|
|
||||||
Loader=python3
|
|
||||||
Name=Simple Plugin System
|
|
||||||
Description=Simple plugin system implementation for Cthulhu
|
|
||||||
Authors=Chrys <chrys@linux-a11y.org>;Storm Dragon <storm_dragon@stormux.org>
|
|
||||||
Copyright=Copyright Â2024 Chrys, Storm Dragon
|
|
||||||
Website=https://git.stormux.org/storm/cthulhu
|
|
||||||
Version=1.0
|
|
||||||
Builtin=true
|
|
||||||
|
|
@ -1,291 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
#
|
|
||||||
# Copyright (c) 2024 Stormux
|
|
||||||
# Copyright (c) 2010-2012 The Orca Team
|
|
||||||
# Copyright (c) 2012 Igalia, S.L.
|
|
||||||
# Copyright (c) 2005-2010 Sun Microsystems Inc.
|
|
||||||
#
|
|
||||||
# This library is free software; you can redistribute it and/or
|
|
||||||
# modify it under the terms of the GNU Lesser General Public
|
|
||||||
# License as published by the Free Software Foundation; either
|
|
||||||
# version 2.1 of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This library is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
# Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Lesser General Public
|
|
||||||
# License along with this library; if not, write to the
|
|
||||||
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
|
|
||||||
# Boston MA 02110-1301 USA.
|
|
||||||
#
|
|
||||||
# Fork of Orca Screen Reader (GNOME)
|
|
||||||
# Original source: https://gitlab.gnome.org/GNOME/orca
|
|
||||||
|
|
||||||
from cthulhu import plugin
|
|
||||||
|
|
||||||
from gi.repository import GObject, Peas
|
|
||||||
import glob
|
|
||||||
import os
|
|
||||||
import importlib.util
|
|
||||||
import random
|
|
||||||
import string
|
|
||||||
import _thread
|
|
||||||
from subprocess import Popen, PIPE
|
|
||||||
|
|
||||||
settings = None
|
|
||||||
speech = None
|
|
||||||
braille = None
|
|
||||||
input_event = None
|
|
||||||
|
|
||||||
def outputMessage( Message):
|
|
||||||
if (settings.enableSpeech):
|
|
||||||
speech.speak(Message)
|
|
||||||
if (settings.enableBraille):
|
|
||||||
braille.displayMessage(Message)
|
|
||||||
|
|
||||||
class SimplePluginSystem(GObject.Object, Peas.Activatable, plugin.Plugin):
|
|
||||||
__gtype_name__ = 'SimplePluginSystem'
|
|
||||||
object = GObject.Property(type=GObject.Object)
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
plugin.Plugin.__init__(self)
|
|
||||||
self.plugin_list = []
|
|
||||||
self.loaded = False
|
|
||||||
self.plugin_repo = os.path.expanduser('~') + "/.local/share/cthulhu/simple-plugins-enabled/"
|
|
||||||
|
|
||||||
def do_activate(self):
|
|
||||||
API = self.object
|
|
||||||
global settings
|
|
||||||
global speech
|
|
||||||
global braille
|
|
||||||
global input_event
|
|
||||||
settings = API.app.getDynamicApiManager().getAPI('Settings')
|
|
||||||
speech = API.app.getDynamicApiManager().getAPI('Speech')
|
|
||||||
braille = API.app.getDynamicApiManager().getAPI('Braille')
|
|
||||||
input_event = API.app.getDynamicApiManager().getAPI('InputEvent')
|
|
||||||
"""Required method for plugins"""
|
|
||||||
if not self.loaded:
|
|
||||||
self.load_plugins()
|
|
||||||
|
|
||||||
def do_deactivate(self):
|
|
||||||
"""Required method for plugins"""
|
|
||||||
# Remove all registered keybindings
|
|
||||||
for plugin in self.plugin_list:
|
|
||||||
self.unregisterShortcut(plugin['function'], plugin['shortcut'])
|
|
||||||
self.loaded = False
|
|
||||||
self.plugin_list = []
|
|
||||||
|
|
||||||
def SetupShortcutAndHandle(self, currPluginSetting):
|
|
||||||
shortcut = ''
|
|
||||||
# just the modifier
|
|
||||||
if not currPluginSetting['shiftkey'] and not currPluginSetting['ctrlkey'] and not currPluginSetting['altkey']:
|
|
||||||
shortcut = 'kb:cthulhu+' + currPluginSetting['key']
|
|
||||||
# cthulhu + alt
|
|
||||||
if not currPluginSetting['shiftkey'] and not currPluginSetting['ctrlkey'] and currPluginSetting['altkey']:
|
|
||||||
shortcut = 'kb:cthulhu+alt+' + currPluginSetting['key']
|
|
||||||
# cthulhu + CTRL
|
|
||||||
if not currPluginSetting['shiftkey'] and currPluginSetting['ctrlkey'] and not currPluginSetting['altkey']:
|
|
||||||
shortcut = 'kb:cthulhu+control+' + currPluginSetting['key']
|
|
||||||
# cthulhu + alt + CTRL
|
|
||||||
if not currPluginSetting['shiftkey'] and currPluginSetting['ctrlkey'] and currPluginSetting['altkey']:
|
|
||||||
shortcut = 'kb:cthulhu+alt+control+ ' + currPluginSetting['key']
|
|
||||||
# cthulhu + shift
|
|
||||||
if currPluginSetting['shiftkey'] and not currPluginSetting['ctrlkey'] and not currPluginSetting['altkey']:
|
|
||||||
shortcut = 'kb:cthulhu+shift+' + currPluginSetting['key']
|
|
||||||
# alt + shift
|
|
||||||
if currPluginSetting['shiftkey'] and not currPluginSetting['ctrlkey'] and currPluginSetting['altkey']:
|
|
||||||
shortcut = 'kb:alt+shift+' + currPluginSetting['key']
|
|
||||||
if shortcut != '':
|
|
||||||
print(shortcut)
|
|
||||||
currPluginSetting['shortcut'] = shortcut
|
|
||||||
self.registerGestureByString(currPluginSetting['function'], _(currPluginSetting['pluginname']), shortcut)
|
|
||||||
return currPluginSetting
|
|
||||||
|
|
||||||
def id_generator(self, size=7, chars=string.ascii_letters):
|
|
||||||
return ''.join(random.choice(chars) for _ in range(size))
|
|
||||||
|
|
||||||
def initSettings(self):
|
|
||||||
currPluginSetting={
|
|
||||||
'pluginname':'',
|
|
||||||
'functionname':'',
|
|
||||||
'key':'',
|
|
||||||
'shiftkey':False,
|
|
||||||
'ctrlkey':False,
|
|
||||||
'altkey':False,
|
|
||||||
'startnotify':False,
|
|
||||||
'stopnotify':False,
|
|
||||||
'blockcall':False,
|
|
||||||
'error':False,
|
|
||||||
'exec': False,
|
|
||||||
'parameters':'',
|
|
||||||
'function':None,
|
|
||||||
'inputeventhandler':None,
|
|
||||||
'valid':False,
|
|
||||||
'supressoutput':False,
|
|
||||||
'shortcut': ''
|
|
||||||
}
|
|
||||||
return currPluginSetting
|
|
||||||
|
|
||||||
def getPluginSettings(self, filepath, currPluginSetting):
|
|
||||||
try:
|
|
||||||
currPluginSetting['file'] = filepath
|
|
||||||
fileName, fileExtension = os.path.splitext(filepath)
|
|
||||||
if (fileExtension and (fileExtension != '')): #if there is an extension
|
|
||||||
currPluginSetting['loadable'] = (fileExtension.lower() == '.py') # only python is loadable
|
|
||||||
filename = os.path.basename(filepath) #filename
|
|
||||||
filename = os.path.splitext(filename)[0] #remove extension if we have one
|
|
||||||
#remove pluginname seperated by __-__
|
|
||||||
filenamehelper = filename.split('__-__')
|
|
||||||
filename = filenamehelper[len(filenamehelper) - 1 ]
|
|
||||||
currPluginSetting['permission'] = os.access(filepath, os.X_OK )
|
|
||||||
currPluginSetting['pluginname'] = 'NoNameAvailable'
|
|
||||||
if len(filenamehelper) == 2:
|
|
||||||
currPluginSetting['pluginname'] = filenamehelper[0]
|
|
||||||
#now get shortcuts seperated by __+__
|
|
||||||
filenamehelper = filename.split('__+__')
|
|
||||||
if len([y for y in filenamehelper if 'parameters_' in y.lower()]) == 1 and\
|
|
||||||
len([y for y in filenamehelper if 'parameters_' in y.lower()][0]) > 11:
|
|
||||||
currPluginSetting['parameters'] = [y for y in filenamehelper if 'parameters_' in y.lower()][0][11:]
|
|
||||||
if len([y for y in filenamehelper if 'key_' in y.lower()]) == 1 and\
|
|
||||||
len([y for y in filenamehelper if 'key_' in y.lower()][0]) > 4 :
|
|
||||||
currPluginSetting['key'] = [y for y in filenamehelper if 'key_' in y.lower()][0][4]
|
|
||||||
if currPluginSetting['key'] == '':
|
|
||||||
settcurrPluginSetting = 'shift' in map(str.lower, filenamehelper)
|
|
||||||
currPluginSetting['ctrlkey'] = 'control' in map(str.lower, filenamehelper)
|
|
||||||
currPluginSetting['altkey'] = 'alt' in map(str.lower, filenamehelper)
|
|
||||||
currPluginSetting['startnotify'] = 'startnotify' in map(str.lower, filenamehelper)
|
|
||||||
currPluginSetting['stopnotify'] = 'stopnotify' in map(str.lower, filenamehelper)
|
|
||||||
currPluginSetting['blockcall'] = 'blockcall' in map(str.lower, filenamehelper)
|
|
||||||
currPluginSetting['error'] = 'error' in map(str.lower, filenamehelper)
|
|
||||||
currPluginSetting['supressoutput'] = 'supressoutput' in map(str.lower, filenamehelper)
|
|
||||||
currPluginSetting['exec'] = 'exec' in map(str.lower, filenamehelper)
|
|
||||||
currPluginSetting['loadmodule'] = 'loadmodule' in map(str.lower, filenamehelper)
|
|
||||||
currPluginSetting = self.readSettingsFromPlugin(currPluginSetting)
|
|
||||||
if not currPluginSetting['loadmodule']:
|
|
||||||
if not currPluginSetting['permission']: #subprocessing only works with exec permission
|
|
||||||
return self.initSettings()
|
|
||||||
if currPluginSetting['loadmodule'] and not currPluginSetting['loadable']: #sorry.. its not loadable only .py is loadable
|
|
||||||
return self.initSettings()
|
|
||||||
if (len(currPluginSetting['key']) > 1): #no shortcut
|
|
||||||
if not currPluginSetting['exec']: # and no exec -> the plugin make no sense because it isnt hooked anywhere
|
|
||||||
return self.initSettings() #so not load it (sets valid = False)
|
|
||||||
else:
|
|
||||||
currPluginSetting['key'] = '' #there is a strange key, but exec? ignore the key..
|
|
||||||
currPluginSetting['valid'] = True # we could load everything
|
|
||||||
return currPluginSetting
|
|
||||||
except:
|
|
||||||
return self.initSettings()
|
|
||||||
|
|
||||||
def readSettingsFromPlugin(self, currPluginSetting):
|
|
||||||
if not os.access(currPluginSetting['file'], os.R_OK ):
|
|
||||||
return currPluginSetting
|
|
||||||
fileName, fileExtension = os.path.splitext(currPluginSetting['file'])
|
|
||||||
if (fileExtension and (fileExtension != '')): #if there is an extension
|
|
||||||
if (fileExtension.lower() != '.py') and \
|
|
||||||
(fileExtension.lower() != '.sh'):
|
|
||||||
return currPluginSetting
|
|
||||||
else:
|
|
||||||
return currPluginSetting
|
|
||||||
|
|
||||||
with open(currPluginSetting['file'], "r") as pluginFile:
|
|
||||||
for line in pluginFile:
|
|
||||||
currPluginSetting['shiftkey'] = ('sopsproperty:shift' in line.lower().replace(" ", "")) or currPluginSetting['shiftkey']
|
|
||||||
currPluginSetting['ctrlkey'] = ('sopsproperty:control' in line.lower().replace(" ", "")) or currPluginSetting['ctrlkey']
|
|
||||||
currPluginSetting['altkey'] = ('sopsproperty:alt' in line.lower().replace(" ", "")) or currPluginSetting['altkey']
|
|
||||||
currPluginSetting['startnotify'] = ('sopsproperty:startnotify' in line.lower().replace(" ", "")) or currPluginSetting['startnotify']
|
|
||||||
currPluginSetting['stopnotify'] = ('sopsproperty:stopnotify' in line.lower().replace(" ", "")) or currPluginSetting['stopnotify']
|
|
||||||
currPluginSetting['blockcall'] = ('sopsproperty:blockcall' in line.lower().replace(" ", "")) or currPluginSetting['blockcall']
|
|
||||||
currPluginSetting['error'] = ('sopsproperty:error' in line.lower().replace(" ", "")) or currPluginSetting['error']
|
|
||||||
currPluginSetting['supressoutput'] = ('sopsproperty:supressoutput' in line.lower().replace(" ", "")) or currPluginSetting['supressoutput']
|
|
||||||
currPluginSetting['exec'] = ('sopsproperty:exec' in line.lower().replace(" ", "")) or currPluginSetting['exec']
|
|
||||||
currPluginSetting['loadmodule'] = ('sopsproperty:loadmodule' in line.lower().replace(" ", "")) or currPluginSetting['loadmodule']
|
|
||||||
return currPluginSetting
|
|
||||||
|
|
||||||
def buildPluginSubprocess(self, currPluginSetting):
|
|
||||||
currplugin = "\'\"" + currPluginSetting['file'] + "\" " + currPluginSetting['parameters'] + "\'"
|
|
||||||
pluginname = currPluginSetting['pluginname']
|
|
||||||
if currPluginSetting['blockcall']:
|
|
||||||
pluginname = "blocking " + pluginname
|
|
||||||
fun_body = "global " + currPluginSetting['functionname']+"\n"
|
|
||||||
fun_body += "def " + currPluginSetting['functionname'] + "(script=None, inputEvent=None):\n"
|
|
||||||
if currPluginSetting['startnotify']:
|
|
||||||
fun_body +=" outputMessage('start " + pluginname + "')\n"
|
|
||||||
fun_body +=" p = Popen(" + currplugin + ", stdout=PIPE, stderr=PIPE, shell=True)\n"
|
|
||||||
fun_body +=" stdout, stderr = p.communicate()\n"
|
|
||||||
fun_body +=" message = ''\n"
|
|
||||||
fun_body +=" if not " + str(currPluginSetting['supressoutput']) + " and stdout:\n"
|
|
||||||
fun_body +=" message += str(stdout, \"utf-8\")\n"
|
|
||||||
fun_body +=" if " + str(currPluginSetting['error']) + " and stderr:\n"
|
|
||||||
fun_body +=" message += ' error: ' + str(stderr, \"utf-8\")\n"
|
|
||||||
fun_body +=" outputMessage( message)\n"
|
|
||||||
if currPluginSetting['stopnotify']:
|
|
||||||
fun_body +=" outputMessage('finish " + pluginname + "')\n"
|
|
||||||
fun_body +=" return True\n\n"
|
|
||||||
fun_body += "global " + currPluginSetting['functionname']+"T\n"
|
|
||||||
fun_body +="def " + currPluginSetting['functionname'] + "T(script=None, inputEvent=None):\n"
|
|
||||||
fun_body +=" _thread.start_new_thread("+ currPluginSetting['functionname'] + ",(script, inputEvent))\n\n"
|
|
||||||
return fun_body
|
|
||||||
|
|
||||||
def buildPluginExec(self, currPluginSetting):
|
|
||||||
pluginname = currPluginSetting['pluginname']
|
|
||||||
if currPluginSetting['blockcall']:
|
|
||||||
pluginname = "blocking " + pluginname
|
|
||||||
fun_body = "global " + currPluginSetting['functionname']+"\n"
|
|
||||||
fun_body += "def " + currPluginSetting['functionname'] + "(script=None, inputEvent=None):\n"
|
|
||||||
if currPluginSetting['startnotify']:
|
|
||||||
fun_body +=" outputMessage('start " + pluginname + "')\n"
|
|
||||||
fun_body += " try:\n"
|
|
||||||
fun_body += " spec = importlib.util.spec_from_file_location(\"" + currPluginSetting['functionname'] + "\",\""+ currPluginSetting['file']+"\")\n"
|
|
||||||
fun_body += " "+currPluginSetting['functionname'] + "Module = importlib.util.module_from_spec(spec)\n"
|
|
||||||
fun_body += " spec.loader.exec_module(" + currPluginSetting['functionname'] + "Module)\n"
|
|
||||||
fun_body += " except:\n"
|
|
||||||
fun_body += " pass\n"
|
|
||||||
if currPluginSetting['error']:
|
|
||||||
fun_body += " outputMessage(\"Error while executing " + pluginname + "\")\n"
|
|
||||||
if currPluginSetting['stopnotify']:
|
|
||||||
fun_body +=" outputMessage('finish " + pluginname + "')\n"
|
|
||||||
fun_body += " return True\n\n"
|
|
||||||
fun_body += "global " + currPluginSetting['functionname']+"T\n"
|
|
||||||
fun_body +="def " + currPluginSetting['functionname'] + "T(script=None, inputEvent=None):\n"
|
|
||||||
fun_body +=" _thread.start_new_thread("+ currPluginSetting['functionname'] + ",(script, inputEvent))\n\n"
|
|
||||||
return fun_body
|
|
||||||
|
|
||||||
def getFunctionName(self, currPluginSetting):
|
|
||||||
currPluginSetting['functionname'] = ''
|
|
||||||
while currPluginSetting['functionname'] == '' or currPluginSetting['functionname'] + 'T' in globals() or currPluginSetting['functionname'] in globals():
|
|
||||||
currPluginSetting['functionname'] = self.id_generator()
|
|
||||||
return currPluginSetting
|
|
||||||
|
|
||||||
def load_plugins(self):
|
|
||||||
if not self.loaded:
|
|
||||||
self.plugin_list = glob.glob(self.plugin_repo+'*')
|
|
||||||
for currplugin in self.plugin_list:
|
|
||||||
currPluginSetting = self.initSettings()
|
|
||||||
currPluginSetting = self.getPluginSettings(currplugin, currPluginSetting)
|
|
||||||
|
|
||||||
if not currPluginSetting['valid']:
|
|
||||||
continue
|
|
||||||
|
|
||||||
currPluginSetting = self.getFunctionName(currPluginSetting)
|
|
||||||
|
|
||||||
if currPluginSetting['loadmodule']:
|
|
||||||
exec(self.buildPluginExec(currPluginSetting)) # load as python module
|
|
||||||
else:
|
|
||||||
exec(self.buildPluginSubprocess(currPluginSetting)) # run as subprocess
|
|
||||||
|
|
||||||
if currPluginSetting['blockcall']:
|
|
||||||
currPluginSetting['function'] = globals()[currPluginSetting['functionname']] # non threaded
|
|
||||||
else:
|
|
||||||
currPluginSetting['function'] = globals()[currPluginSetting['functionname']+"T"] # T = Threaded
|
|
||||||
|
|
||||||
|
|
||||||
if currPluginSetting['exec']: # exec on load if we want
|
|
||||||
currPluginSetting['function']()
|
|
||||||
|
|
||||||
if not currPluginSetting['key'] == '':
|
|
||||||
currPluginSetting = self.SetupShortcutAndHandle(currPluginSetting)
|
|
||||||
print(currPluginSetting)
|
|
||||||
self.plugin_list.append(currPluginSetting) # store in a list
|
|
||||||
self.loaded = True
|
|
9
src/cthulhu/plugins/SimplePluginSystem/plugin.info
Normal file
9
src/cthulhu/plugins/SimplePluginSystem/plugin.info
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
[Plugin]
|
||||||
|
Name = Simple Plugin System
|
||||||
|
Module = SimplePluginSystem
|
||||||
|
Description = Simple plugin system implementation for Cthulhu
|
||||||
|
Authors = Storm Dragon <storm_dragon@stormux.org>
|
||||||
|
Copyright = Copyright (c) 2025 Stormux
|
||||||
|
Website = https://git.stormux.org/storm/cthulhu
|
||||||
|
Version = 1.0
|
||||||
|
Category = System
|
407
src/cthulhu/plugins/SimplePluginSystem/plugin.py
Normal file
407
src/cthulhu/plugins/SimplePluginSystem/plugin.py
Normal file
@ -0,0 +1,407 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
#
|
||||||
|
# Copyright (c) 2024 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.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
# Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public
|
||||||
|
# License along with this library; if not, write to the
|
||||||
|
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
|
||||||
|
# Boston MA 02110-1301 USA.
|
||||||
|
#
|
||||||
|
|
||||||
|
"""Simple Plugin System for Cthulhu."""
|
||||||
|
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
import importlib.util
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
import _thread
|
||||||
|
import logging
|
||||||
|
from subprocess import Popen, PIPE
|
||||||
|
import gettext
|
||||||
|
|
||||||
|
from cthulhu.plugin import Plugin, cthulhu_hookimpl
|
||||||
|
|
||||||
|
# Set up translation function
|
||||||
|
_ = gettext.gettext
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Global variables for API access
|
||||||
|
settings = None
|
||||||
|
speech = None
|
||||||
|
braille = None
|
||||||
|
input_event = None
|
||||||
|
|
||||||
|
def outputMessage(Message):
|
||||||
|
"""Output a message via speech and/or braille depending on settings."""
|
||||||
|
if (settings.enableSpeech):
|
||||||
|
speech.speak(Message)
|
||||||
|
if (settings.enableBraille):
|
||||||
|
braille.displayMessage(Message)
|
||||||
|
|
||||||
|
class SimplePluginSystem(Plugin):
|
||||||
|
"""Simple plugin system implementation for Cthulhu.
|
||||||
|
|
||||||
|
This plugin allows loading and managing simple script-based plugins
|
||||||
|
from a designated directory.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
"""Initialize the plugin system."""
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
logger.info("SimplePluginSystem plugin initialized")
|
||||||
|
self.plugin_list = []
|
||||||
|
self.loaded = False
|
||||||
|
self.plugin_repo = os.path.expanduser('~') + "/.local/share/cthulhu/simple-plugins-enabled/"
|
||||||
|
self._signal_handler_id = None
|
||||||
|
|
||||||
|
@cthulhu_hookimpl
|
||||||
|
def activate(self, plugin=None):
|
||||||
|
"""Activate the plugin system."""
|
||||||
|
# Skip if this activation call isn't for us
|
||||||
|
if plugin is not None and plugin is not self:
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info("Activating SimplePluginSystem plugin")
|
||||||
|
try:
|
||||||
|
global settings
|
||||||
|
global speech
|
||||||
|
global braille
|
||||||
|
global input_event
|
||||||
|
|
||||||
|
settings = self.app.getDynamicApiManager().getAPI('Settings')
|
||||||
|
speech = self.app.getDynamicApiManager().getAPI('Speech')
|
||||||
|
braille = self.app.getDynamicApiManager().getAPI('Braille')
|
||||||
|
input_event = self.app.getDynamicApiManager().getAPI('InputEvent')
|
||||||
|
|
||||||
|
if not self.loaded:
|
||||||
|
self.load_plugins()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error activating SimplePluginSystem plugin: {e}")
|
||||||
|
|
||||||
|
@cthulhu_hookimpl
|
||||||
|
def deactivate(self, plugin=None):
|
||||||
|
"""Deactivate the plugin system."""
|
||||||
|
# Skip if this deactivation call isn't for us
|
||||||
|
if plugin is not None and plugin is not self:
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info("Deactivating SimplePluginSystem plugin")
|
||||||
|
try:
|
||||||
|
# Remove all registered keybindings
|
||||||
|
for plugin in self.plugin_list:
|
||||||
|
self.unregisterShortcut(plugin['function'], plugin['shortcut'])
|
||||||
|
self.loaded = False
|
||||||
|
self.plugin_list = []
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error deactivating SimplePluginSystem plugin: {e}")
|
||||||
|
|
||||||
|
def SetupShortcutAndHandle(self, currPluginSetting):
|
||||||
|
"""Set up keyboard shortcuts for a plugin."""
|
||||||
|
shortcut = ''
|
||||||
|
# just the modifier
|
||||||
|
if not currPluginSetting['shiftkey'] and not currPluginSetting['ctrlkey'] and not currPluginSetting['altkey']:
|
||||||
|
shortcut = 'kb:cthulhu+' + currPluginSetting['key']
|
||||||
|
# cthulhu + alt
|
||||||
|
if not currPluginSetting['shiftkey'] and not currPluginSetting['ctrlkey'] and currPluginSetting['altkey']:
|
||||||
|
shortcut = 'kb:cthulhu+alt+' + currPluginSetting['key']
|
||||||
|
# cthulhu + CTRL
|
||||||
|
if not currPluginSetting['shiftkey'] and currPluginSetting['ctrlkey'] and not currPluginSetting['altkey']:
|
||||||
|
shortcut = 'kb:cthulhu+control+' + currPluginSetting['key']
|
||||||
|
# cthulhu + alt + CTRL
|
||||||
|
if not currPluginSetting['shiftkey'] and currPluginSetting['ctrlkey'] and currPluginSetting['altkey']:
|
||||||
|
shortcut = 'kb:cthulhu+alt+control+ ' + currPluginSetting['key']
|
||||||
|
# cthulhu + shift
|
||||||
|
if currPluginSetting['shiftkey'] and not currPluginSetting['ctrlkey'] and not currPluginSetting['altkey']:
|
||||||
|
shortcut = 'kb:cthulhu+shift+' + currPluginSetting['key']
|
||||||
|
# alt + shift
|
||||||
|
if currPluginSetting['shiftkey'] and not currPluginSetting['ctrlkey'] and currPluginSetting['altkey']:
|
||||||
|
shortcut = 'kb:alt+shift+' + currPluginSetting['key']
|
||||||
|
if shortcut != '':
|
||||||
|
logger.debug(f"Registering shortcut: {shortcut}")
|
||||||
|
currPluginSetting['shortcut'] = shortcut
|
||||||
|
try:
|
||||||
|
# Try to use the translation function, fall back to plain text if it fails
|
||||||
|
plugin_name = _(currPluginSetting['pluginname'])
|
||||||
|
except Exception:
|
||||||
|
# If translation fails, use the original name
|
||||||
|
plugin_name = currPluginSetting['pluginname']
|
||||||
|
logger.warning(f"Translation failed for plugin: {currPluginSetting['pluginname']}")
|
||||||
|
|
||||||
|
self.registerGestureByString(currPluginSetting['function'], plugin_name, shortcut)
|
||||||
|
return currPluginSetting
|
||||||
|
|
||||||
|
def id_generator(self, size=7, chars=string.ascii_letters):
|
||||||
|
"""Generate a random ID string."""
|
||||||
|
return ''.join(random.choice(chars) for _ in range(size))
|
||||||
|
|
||||||
|
def initSettings(self):
|
||||||
|
"""Initialize default settings for a plugin."""
|
||||||
|
currPluginSetting = {
|
||||||
|
'pluginname': '',
|
||||||
|
'functionname': '',
|
||||||
|
'key': '',
|
||||||
|
'shiftkey': False,
|
||||||
|
'ctrlkey': False,
|
||||||
|
'altkey': False,
|
||||||
|
'startnotify': False,
|
||||||
|
'stopnotify': False,
|
||||||
|
'blockcall': False,
|
||||||
|
'error': False,
|
||||||
|
'exec': False,
|
||||||
|
'parameters': '',
|
||||||
|
'function': None,
|
||||||
|
'inputeventhandler': None,
|
||||||
|
'valid': False,
|
||||||
|
'supressoutput': False,
|
||||||
|
'shortcut': ''
|
||||||
|
}
|
||||||
|
return currPluginSetting
|
||||||
|
|
||||||
|
def getPluginSettings(self, filepath, currPluginSetting):
|
||||||
|
"""Parse plugin settings from filename and content."""
|
||||||
|
try:
|
||||||
|
currPluginSetting['file'] = filepath
|
||||||
|
fileName, fileExtension = os.path.splitext(filepath)
|
||||||
|
if (fileExtension and (fileExtension != '')): # if there is an extension
|
||||||
|
currPluginSetting['loadable'] = (fileExtension.lower() == '.py') # only python is loadable
|
||||||
|
filename = os.path.basename(filepath) # filename
|
||||||
|
filename = os.path.splitext(filename)[0] # remove extension if we have one
|
||||||
|
# remove pluginname seperated by __-__
|
||||||
|
filenamehelper = filename.split('__-__')
|
||||||
|
filename = filenamehelper[len(filenamehelper) - 1]
|
||||||
|
currPluginSetting['permission'] = os.access(filepath, os.X_OK)
|
||||||
|
currPluginSetting['pluginname'] = 'NoNameAvailable'
|
||||||
|
if len(filenamehelper) == 2:
|
||||||
|
currPluginSetting['pluginname'] = filenamehelper[0]
|
||||||
|
# now get shortcuts seperated by __+__
|
||||||
|
filenamehelper = filename.split('__+__')
|
||||||
|
if len([y for y in filenamehelper if 'parameters_' in y.lower()]) == 1 and\
|
||||||
|
len([y for y in filenamehelper if 'parameters_' in y.lower()][0]) > 11:
|
||||||
|
currPluginSetting['parameters'] = [y for y in filenamehelper if 'parameters_' in y.lower()][0][11:]
|
||||||
|
if len([y for y in filenamehelper if 'key_' in y.lower()]) == 1 and\
|
||||||
|
len([y for y in filenamehelper if 'key_' in y.lower()][0]) > 4:
|
||||||
|
currPluginSetting['key'] = [y for y in filenamehelper if 'key_' in y.lower()][0][4]
|
||||||
|
if currPluginSetting['key'] == '':
|
||||||
|
settcurrPluginSetting = 'shift' in map(str.lower, filenamehelper)
|
||||||
|
currPluginSetting['ctrlkey'] = 'control' in map(str.lower, filenamehelper)
|
||||||
|
currPluginSetting['altkey'] = 'alt' in map(str.lower, filenamehelper)
|
||||||
|
currPluginSetting['startnotify'] = 'startnotify' in map(str.lower, filenamehelper)
|
||||||
|
currPluginSetting['stopnotify'] = 'stopnotify' in map(str.lower, filenamehelper)
|
||||||
|
currPluginSetting['blockcall'] = 'blockcall' in map(str.lower, filenamehelper)
|
||||||
|
currPluginSetting['error'] = 'error' in map(str.lower, filenamehelper)
|
||||||
|
currPluginSetting['supressoutput'] = 'supressoutput' in map(str.lower, filenamehelper)
|
||||||
|
currPluginSetting['exec'] = 'exec' in map(str.lower, filenamehelper)
|
||||||
|
currPluginSetting['loadmodule'] = 'loadmodule' in map(str.lower, filenamehelper)
|
||||||
|
currPluginSetting = self.readSettingsFromPlugin(currPluginSetting)
|
||||||
|
if not currPluginSetting['loadmodule']:
|
||||||
|
if not currPluginSetting['permission']: # subprocessing only works with exec permission
|
||||||
|
return self.initSettings()
|
||||||
|
if currPluginSetting['loadmodule'] and not currPluginSetting['loadable']: # sorry.. its not loadable only .py is loadable
|
||||||
|
return self.initSettings()
|
||||||
|
if (len(currPluginSetting['key']) > 1): # no shortcut
|
||||||
|
if not currPluginSetting['exec']: # and no exec -> the plugin make no sense because it isnt hooked anywhere
|
||||||
|
return self.initSettings() # so not load it (sets valid = False)
|
||||||
|
else:
|
||||||
|
currPluginSetting['key'] = '' # there is a strange key, but exec? ignore the key..
|
||||||
|
currPluginSetting['valid'] = True # we could load everything
|
||||||
|
return currPluginSetting
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting plugin settings: {e}")
|
||||||
|
return self.initSettings()
|
||||||
|
|
||||||
|
def readSettingsFromPlugin(self, currPluginSetting):
|
||||||
|
"""Read settings from plugin file content."""
|
||||||
|
if not os.access(currPluginSetting['file'], os.R_OK):
|
||||||
|
return currPluginSetting
|
||||||
|
fileName, fileExtension = os.path.splitext(currPluginSetting['file'])
|
||||||
|
if (fileExtension and (fileExtension != '')): # if there is an extension
|
||||||
|
if (fileExtension.lower() != '.py') and \
|
||||||
|
(fileExtension.lower() != '.sh'):
|
||||||
|
return currPluginSetting
|
||||||
|
else:
|
||||||
|
return currPluginSetting
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(currPluginSetting['file'], "r") as pluginFile:
|
||||||
|
for line in pluginFile:
|
||||||
|
currPluginSetting['shiftkey'] = ('sopsproperty:shift' in line.lower().replace(" ", "")) or currPluginSetting['shiftkey']
|
||||||
|
currPluginSetting['ctrlkey'] = ('sopsproperty:control' in line.lower().replace(" ", "")) or currPluginSetting['ctrlkey']
|
||||||
|
currPluginSetting['altkey'] = ('sopsproperty:alt' in line.lower().replace(" ", "")) or currPluginSetting['altkey']
|
||||||
|
currPluginSetting['startnotify'] = ('sopsproperty:startnotify' in line.lower().replace(" ", "")) or currPluginSetting['startnotify']
|
||||||
|
currPluginSetting['stopnotify'] = ('sopsproperty:stopnotify' in line.lower().replace(" ", "")) or currPluginSetting['stopnotify']
|
||||||
|
currPluginSetting['blockcall'] = ('sopsproperty:blockcall' in line.lower().replace(" ", "")) or currPluginSetting['blockcall']
|
||||||
|
currPluginSetting['error'] = ('sopsproperty:error' in line.lower().replace(" ", "")) or currPluginSetting['error']
|
||||||
|
currPluginSetting['supressoutput'] = ('sopsproperty:supressoutput' in line.lower().replace(" ", "")) or currPluginSetting['supressoutput']
|
||||||
|
currPluginSetting['exec'] = ('sopsproperty:exec' in line.lower().replace(" ", "")) or currPluginSetting['exec']
|
||||||
|
currPluginSetting['loadmodule'] = ('sopsproperty:loadmodule' in line.lower().replace(" ", "")) or currPluginSetting['loadmodule']
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error reading plugin file: {e}")
|
||||||
|
|
||||||
|
return currPluginSetting
|
||||||
|
|
||||||
|
def buildPluginSubprocess(self, currPluginSetting):
|
||||||
|
"""Build a function to execute a plugin as a subprocess."""
|
||||||
|
currplugin = "\'\"" + currPluginSetting['file'] + "\" " + currPluginSetting['parameters'] + "\'"
|
||||||
|
pluginname = currPluginSetting['pluginname']
|
||||||
|
if currPluginSetting['blockcall']:
|
||||||
|
pluginname = "blocking " + pluginname
|
||||||
|
fun_body = "global " + currPluginSetting['functionname'] + "\n"
|
||||||
|
fun_body += "def " + currPluginSetting['functionname'] + "(script=None, inputEvent=None):\n"
|
||||||
|
if currPluginSetting['startnotify']:
|
||||||
|
fun_body += " outputMessage('start " + pluginname + "')\n"
|
||||||
|
fun_body += " p = Popen(" + currplugin + ", stdout=PIPE, stderr=PIPE, shell=True)\n"
|
||||||
|
fun_body += " stdout, stderr = p.communicate()\n"
|
||||||
|
fun_body += " message = ''\n"
|
||||||
|
fun_body += " if not " + str(currPluginSetting['supressoutput']) + " and stdout:\n"
|
||||||
|
fun_body += " message += str(stdout, \"utf-8\")\n"
|
||||||
|
fun_body += " if " + str(currPluginSetting['error']) + " and stderr:\n"
|
||||||
|
fun_body += " message += ' error: ' + str(stderr, \"utf-8\")\n"
|
||||||
|
fun_body += " outputMessage(message)\n"
|
||||||
|
if currPluginSetting['stopnotify']:
|
||||||
|
fun_body += " outputMessage('finish " + pluginname + "')\n"
|
||||||
|
fun_body += " return True\n\n"
|
||||||
|
fun_body += "global " + currPluginSetting['functionname'] + "T\n"
|
||||||
|
fun_body += "def " + currPluginSetting['functionname'] + "T(script=None, inputEvent=None):\n"
|
||||||
|
fun_body += " _thread.start_new_thread(" + currPluginSetting['functionname'] + ",(script, inputEvent))\n\n"
|
||||||
|
return fun_body
|
||||||
|
|
||||||
|
def buildPluginExec(self, currPluginSetting):
|
||||||
|
"""Build a function to execute a plugin as a Python module."""
|
||||||
|
pluginname = currPluginSetting['pluginname']
|
||||||
|
if currPluginSetting['blockcall']:
|
||||||
|
pluginname = "blocking " + pluginname
|
||||||
|
fun_body = "global " + currPluginSetting['functionname'] + "\n"
|
||||||
|
fun_body += "def " + currPluginSetting['functionname'] + "(script=None, inputEvent=None):\n"
|
||||||
|
if currPluginSetting['startnotify']:
|
||||||
|
fun_body += " outputMessage('start " + pluginname + "')\n"
|
||||||
|
fun_body += " try:\n"
|
||||||
|
fun_body += " spec = importlib.util.spec_from_file_location(\"" + currPluginSetting['functionname'] + "\",\"" + currPluginSetting['file'] + "\")\n"
|
||||||
|
fun_body += " " + currPluginSetting['functionname'] + "Module = importlib.util.module_from_spec(spec)\n"
|
||||||
|
fun_body += " spec.loader.exec_module(" + currPluginSetting['functionname'] + "Module)\n"
|
||||||
|
fun_body += " except Exception as e:\n"
|
||||||
|
fun_body += " logger.error(f\"Error executing plugin {pluginname}: {e}\")\n"
|
||||||
|
if currPluginSetting['error']:
|
||||||
|
fun_body += " outputMessage(\"Error while executing " + pluginname + "\")\n"
|
||||||
|
if currPluginSetting['stopnotify']:
|
||||||
|
fun_body += " outputMessage('finish " + pluginname + "')\n"
|
||||||
|
fun_body += " return True\n\n"
|
||||||
|
fun_body += "global " + currPluginSetting['functionname'] + "T\n"
|
||||||
|
fun_body += "def " + currPluginSetting['functionname'] + "T(script=None, inputEvent=None):\n"
|
||||||
|
fun_body += " _thread.start_new_thread(" + currPluginSetting['functionname'] + ",(script, inputEvent))\n\n"
|
||||||
|
return fun_body
|
||||||
|
|
||||||
|
def getFunctionName(self, currPluginSetting):
|
||||||
|
"""Generate a unique function name for a plugin."""
|
||||||
|
currPluginSetting['functionname'] = ''
|
||||||
|
while currPluginSetting['functionname'] == '' or currPluginSetting['functionname'] + 'T' in globals() or currPluginSetting['functionname'] in globals():
|
||||||
|
currPluginSetting['functionname'] = self.id_generator()
|
||||||
|
return currPluginSetting
|
||||||
|
|
||||||
|
def registerGestureByString(self, function, description, shortcut):
|
||||||
|
"""Register a keyboard shortcut for a function.
|
||||||
|
|
||||||
|
This is a compatibility wrapper for the new plugin system.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if self.app:
|
||||||
|
api_helper = self.app.getAPIHelper()
|
||||||
|
if api_helper:
|
||||||
|
api_helper.registerGestureByString(
|
||||||
|
function,
|
||||||
|
description,
|
||||||
|
shortcut,
|
||||||
|
'default',
|
||||||
|
'cthulhu',
|
||||||
|
True,
|
||||||
|
contextName=self.module_name
|
||||||
|
)
|
||||||
|
logger.debug(f"Registered shortcut {shortcut} for {description}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.error("Could not get APIHelper")
|
||||||
|
else:
|
||||||
|
logger.error("No app reference available")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error registering shortcut {shortcut}: {e}")
|
||||||
|
|
||||||
|
def unregisterShortcut(self, function, shortcut):
|
||||||
|
"""Unregister a keyboard shortcut for a function.
|
||||||
|
|
||||||
|
This is a compatibility wrapper for the new plugin system.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if self.app:
|
||||||
|
api_helper = self.app.getAPIHelper()
|
||||||
|
if api_helper and hasattr(api_helper, 'unregisterShortcut'):
|
||||||
|
api_helper.unregisterShortcut(shortcut)
|
||||||
|
logger.debug(f"Unregistered shortcut {shortcut}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.error("Could not get APIHelper or unregisterShortcut method")
|
||||||
|
else:
|
||||||
|
logger.error("No app reference available")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error unregistering shortcut {shortcut}: {e}")
|
||||||
|
|
||||||
|
def load_plugins(self):
|
||||||
|
"""Load and setup all plugins in the plugin repository."""
|
||||||
|
if not self.loaded:
|
||||||
|
try:
|
||||||
|
logger.info(f"Loading plugins from {self.plugin_repo}")
|
||||||
|
plugin_files = glob.glob(self.plugin_repo + '*')
|
||||||
|
self.plugin_list = [] # Reset the plugin list to avoid confusion
|
||||||
|
|
||||||
|
for currplugin in plugin_files:
|
||||||
|
try:
|
||||||
|
# Ensure currplugin is a valid path string
|
||||||
|
if not isinstance(currplugin, (str, bytes, os.PathLike)):
|
||||||
|
logger.error(f"Invalid plugin path: {type(currplugin)}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
currPluginSetting = self.initSettings()
|
||||||
|
currPluginSetting = self.getPluginSettings(currplugin, currPluginSetting)
|
||||||
|
|
||||||
|
if not currPluginSetting['valid']:
|
||||||
|
logger.debug(f"Skipping invalid plugin: {currplugin}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
currPluginSetting = self.getFunctionName(currPluginSetting)
|
||||||
|
|
||||||
|
if currPluginSetting['loadmodule']:
|
||||||
|
exec(self.buildPluginExec(currPluginSetting)) # load as python module
|
||||||
|
else:
|
||||||
|
exec(self.buildPluginSubprocess(currPluginSetting)) # run as subprocess
|
||||||
|
|
||||||
|
if currPluginSetting['blockcall']:
|
||||||
|
currPluginSetting['function'] = globals()[currPluginSetting['functionname']] # non threaded
|
||||||
|
else:
|
||||||
|
currPluginSetting['function'] = globals()[currPluginSetting['functionname'] + "T"] # T = Threaded
|
||||||
|
|
||||||
|
if currPluginSetting['exec']: # exec on load if we want
|
||||||
|
currPluginSetting['function']()
|
||||||
|
|
||||||
|
if not currPluginSetting['key'] == '':
|
||||||
|
currPluginSetting = self.SetupShortcutAndHandle(currPluginSetting)
|
||||||
|
|
||||||
|
logger.debug(f"Loaded plugin: {currPluginSetting['pluginname']}")
|
||||||
|
self.plugin_list.append(currPluginSetting) # store in a list
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error loading plugin {currplugin}: {e}")
|
||||||
|
|
||||||
|
self.loaded = True
|
||||||
|
logger.info(f"Loaded {len(self.plugin_list)} plugins")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in load_plugins: {e}")
|
@ -1,7 +0,0 @@
|
|||||||
cthulhu_python_PYTHON = \
|
|
||||||
__init__.py \
|
|
||||||
Time.plugin \
|
|
||||||
Time.py
|
|
||||||
|
|
||||||
cthulhu_pythondir=$(pkgpythondir)/plugins/Time
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
|||||||
[Plugin]
|
|
||||||
Module=Time
|
|
||||||
Loader=python3
|
|
||||||
Name=Time
|
|
||||||
Description=Present current time
|
|
||||||
Authors=Chrys chrys@linux-a11y.org
|
|
@ -1,60 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
#
|
|
||||||
# Copyright (c) 2024 Stormux
|
|
||||||
# Copyright (c) 2010-2012 The Orca Team
|
|
||||||
# Copyright (c) 2012 Igalia, S.L.
|
|
||||||
# Copyright (c) 2005-2010 Sun Microsystems Inc.
|
|
||||||
#
|
|
||||||
# This library is free software; you can redistribute it and/or
|
|
||||||
# modify it under the terms of the GNU Lesser General Public
|
|
||||||
# License as published by the Free Software Foundation; either
|
|
||||||
# version 2.1 of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This library is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
# Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Lesser General Public
|
|
||||||
# License along with this library; if not, write to the
|
|
||||||
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
|
|
||||||
# Boston MA 02110-1301 USA.
|
|
||||||
#
|
|
||||||
# Fork of Orca Screen Reader (GNOME)
|
|
||||||
# Original source: https://gitlab.gnome.org/GNOME/orca
|
|
||||||
|
|
||||||
import gi, time
|
|
||||||
gi.require_version('Peas', '1.0')
|
|
||||||
from gi.repository import GObject
|
|
||||||
from gi.repository import Peas
|
|
||||||
|
|
||||||
from cthulhu import plugin
|
|
||||||
|
|
||||||
class Time(GObject.Object, Peas.Activatable, plugin.Plugin):
|
|
||||||
#__gtype_name__ = 'Time'
|
|
||||||
|
|
||||||
object = GObject.Property(type=GObject.Object)
|
|
||||||
def __init__(self):
|
|
||||||
plugin.Plugin.__init__(self)
|
|
||||||
def do_activate(self):
|
|
||||||
API = self.object
|
|
||||||
self.connectSignal("setup-inputeventhandlers-completed", self.setupCompatBinding)
|
|
||||||
def setupCompatBinding(self, app):
|
|
||||||
cmdnames = app.getDynamicApiManager().getAPI('Cmdnames')
|
|
||||||
inputEventHandlers = app.getDynamicApiManager().getAPI('inputEventHandlers')
|
|
||||||
inputEventHandlers['presentTimeHandler'] = app.getAPIHelper().createInputEventHandler(self.presentTime, cmdnames.PRESENT_CURRENT_TIME)
|
|
||||||
def do_deactivate(self):
|
|
||||||
API = self.object
|
|
||||||
inputEventHandlers = API.app.getDynamicApiManager().getAPI('inputEventHandlers')
|
|
||||||
del inputEventHandlers['presentTimeHandler']
|
|
||||||
def do_update_state(self):
|
|
||||||
API = self.object
|
|
||||||
def presentTime(self, script=None, inputEvent=None):
|
|
||||||
""" Presents the current time. """
|
|
||||||
API = self.object
|
|
||||||
settings_manager = API.app.getDynamicApiManager().getAPI('SettingsManager')
|
|
||||||
_settingsManager = settings_manager.getManager()
|
|
||||||
timeFormat = _settingsManager.getSetting('presentTimeFormat')
|
|
||||||
message = time.strftime(timeFormat, time.localtime())
|
|
||||||
API.app.getDynamicApiManager().getAPI('CthulhuState').activeScript.presentMessage(message, resetStyles=False)
|
|
||||||
return True
|
|
@ -1,25 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
#
|
|
||||||
# Copyright (c) 2024 Stormux
|
|
||||||
# Copyright (c) 2010-2012 The Orca Team
|
|
||||||
# Copyright (c) 2012 Igalia, S.L.
|
|
||||||
# Copyright (c) 2005-2010 Sun Microsystems Inc.
|
|
||||||
#
|
|
||||||
# This library is free software; you can redistribute it and/or
|
|
||||||
# modify it under the terms of the GNU Lesser General Public
|
|
||||||
# License as published by the Free Software Foundation; either
|
|
||||||
# version 2.1 of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This library is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
# Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Lesser General Public
|
|
||||||
# License along with this library; if not, write to the
|
|
||||||
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
|
|
||||||
# Boston MA 02110-1301 USA.
|
|
||||||
#
|
|
||||||
# Fork of Orca Screen Reader (GNOME)
|
|
||||||
# Original source: https://gitlab.gnome.org/GNOME/orca
|
|
||||||
|
|
6
src/cthulhu/plugins/hello_world/Makefile.am
Normal file
6
src/cthulhu/plugins/hello_world/Makefile.am
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
cthulhu_python_PYTHON = \
|
||||||
|
__init__.py \
|
||||||
|
plugin.info \
|
||||||
|
plugin.py
|
||||||
|
|
||||||
|
cthulhu_pythondir=$(pkgpythondir)/plugins/hello_world
|
33
src/cthulhu/plugins/hello_world/README.md
Normal file
33
src/cthulhu/plugins/hello_world/README.md
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# Hello World Plugin for Cthulhu
|
||||||
|
|
||||||
|
This is a simple test plugin for the Cthulhu screen reader that demonstrates how to use the pluggy-based plugin system.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Registers a keyboard shortcut (Cthulhu+z) that speaks "hello world" when pressed
|
||||||
|
- Demonstrates how to use the Plugin base class
|
||||||
|
- Shows how to use cthulhu_hookimpl for hook implementation
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
The plugin uses the following key features:
|
||||||
|
- `pluggy` for hook specification and implementation
|
||||||
|
- The `Plugin` base class from cthulhu.plugin
|
||||||
|
- The `cthulhu_hookimpl` decorator to mark functions as plugin hooks
|
||||||
|
- The `registerGestureByString` method to register keyboard shortcuts
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
- `plugin.py`: The main plugin implementation
|
||||||
|
- `plugin.info`: Metadata about the plugin
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
Requires the pluggy package to be installed:
|
||||||
|
```
|
||||||
|
pip install pluggy
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
The plugin will be automatically loaded when Cthulhu starts if it's listed in the activePlugins setting.
|
8
src/cthulhu/plugins/hello_world/plugin.info
Normal file
8
src/cthulhu/plugins/hello_world/plugin.info
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
name = Hello World
|
||||||
|
version = 1.0.0
|
||||||
|
description = Test plugin for Cthulhu
|
||||||
|
authors = Storm Dragon storm_dragon@stormux.org
|
||||||
|
website = https://stormux.org
|
||||||
|
copyright = Copyright 2025
|
||||||
|
builtin = false
|
||||||
|
hidden = false
|
70
src/cthulhu/plugins/hello_world/plugin.py
Normal file
70
src/cthulhu/plugins/hello_world/plugin.py
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
#!/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.
|
||||||
|
|
||||||
|
"""Hello World plugin for Cthulhu."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from cthulhu.plugin import Plugin, cthulhu_hookimpl
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class HelloWorld(Plugin):
|
||||||
|
"""Hello World plugin."""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
"""Initialize the plugin."""
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
print("Plugin hello world initialized.")
|
||||||
|
logger.info("HelloWorld plugin initialized")
|
||||||
|
|
||||||
|
@cthulhu_hookimpl
|
||||||
|
def activate(self, plugin=None):
|
||||||
|
"""Activate the plugin."""
|
||||||
|
# Skip if this activation call isn't for us
|
||||||
|
if plugin is not None and plugin is not self:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.info("Activating Hello World plugin")
|
||||||
|
|
||||||
|
# Register our keyboard shortcut
|
||||||
|
self.registerGestureByString(
|
||||||
|
self.speakTest,
|
||||||
|
"hello world",
|
||||||
|
"kb:cthulhu+z",
|
||||||
|
learnModeEnabled=True
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error activating Hello World plugin: {e}")
|
||||||
|
|
||||||
|
@cthulhu_hookimpl
|
||||||
|
def deactivate(self, plugin=None):
|
||||||
|
"""Deactivate the plugin."""
|
||||||
|
# Skip if this deactivation call isn't for us
|
||||||
|
if plugin is not None and plugin is not self:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.info("Deactivating Hello World plugin")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error deactivating Hello World plugin: {e}")
|
||||||
|
|
||||||
|
def speakTest(self, script=None, inputEvent=None):
|
||||||
|
"""Speak a test message."""
|
||||||
|
try:
|
||||||
|
if self.app:
|
||||||
|
self.app.getDynamicApiManager().getAPI('CthulhuState').activeScript.presentMessage(
|
||||||
|
'hello world',
|
||||||
|
resetStyles=False
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in speakTest: {e}")
|
||||||
|
return False
|
7
src/cthulhu/plugins/self_voice/Makefile.am
Normal file
7
src/cthulhu/plugins/self_voice/Makefile.am
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
cthulhu_python_PYTHON = \
|
||||||
|
__init__.py \
|
||||||
|
plugin.info \
|
||||||
|
plugin.py
|
||||||
|
|
||||||
|
cthulhu_pythondir=$(pkgpythondir)/plugins/self_voice
|
||||||
|
|
12
src/cthulhu/plugins/self_voice/plugin.info
Normal file
12
src/cthulhu/plugins/self_voice/plugin.info
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
[Plugin]
|
||||||
|
name=Self Voice
|
||||||
|
module_name=self_voice
|
||||||
|
version=1.0.0
|
||||||
|
description=Use Cthulhu speech and braille from external applications via Unix sockets
|
||||||
|
authors=Stormux
|
||||||
|
copyright=Copyright (c) 2024 Stormux
|
||||||
|
website=https://stormux.org
|
||||||
|
icon_name=audio-speakers
|
||||||
|
builtin=false
|
||||||
|
hidden=false
|
||||||
|
help_uri=https://stormux.org
|
194
src/cthulhu/plugins/self_voice/plugin.py
Normal file
194
src/cthulhu/plugins/self_voice/plugin.py
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
#
|
||||||
|
# Copyright (c) 2024 Stormux
|
||||||
|
# Copyright (c) 2010-2012 The Orca Team
|
||||||
|
# Copyright (c) 2012 Igalia, S.L.
|
||||||
|
# Copyright (c) 2005-2010 Sun Microsystems Inc.
|
||||||
|
#
|
||||||
|
# This library is free software; you can redistribute it and/or
|
||||||
|
# modify it under the terms of the GNU Lesser General Public
|
||||||
|
# License as published by the Free Software Foundation; either
|
||||||
|
# version 2.1 of the License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
# Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public
|
||||||
|
# License along with this library; if not, write to the
|
||||||
|
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
|
||||||
|
# Boston MA 02110-1301 USA.
|
||||||
|
#
|
||||||
|
# Fork of Orca Screen Reader (GNOME)
|
||||||
|
# Original source: https://gitlab.gnome.org/GNOME/orca
|
||||||
|
|
||||||
|
"""Self Voice plugin for Cthulhu screen reader."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import select
|
||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
from threading import Thread, Lock
|
||||||
|
from cthulhu.plugin import Plugin, cthulhu_hookimpl
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Special codes for message handling
|
||||||
|
APPEND_CODE = '<#APPEND#>'
|
||||||
|
PERSISTENT_CODE = '<#PERSISTENT#>'
|
||||||
|
|
||||||
|
class SelfVoice(Plugin):
|
||||||
|
"""Plugin that provides a socket interface for external applications to send text to Cthulhu."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize the plugin."""
|
||||||
|
super().__init__()
|
||||||
|
self.lock = Lock()
|
||||||
|
self.active = False
|
||||||
|
self.voiceThread = Thread(target=self.voiceWorker)
|
||||||
|
self.voiceThread.daemon = True # Make thread exit when main thread exits
|
||||||
|
|
||||||
|
@cthulhu_hookimpl
|
||||||
|
def activate(self):
|
||||||
|
"""Activate the self-voice plugin."""
|
||||||
|
super().activate()
|
||||||
|
logger.info("Activating Self Voice Plugin")
|
||||||
|
self.activateWorker()
|
||||||
|
|
||||||
|
@cthulhu_hookimpl
|
||||||
|
def deactivate(self):
|
||||||
|
"""Deactivate the self-voice plugin."""
|
||||||
|
logger.info("Deactivating Self Voice Plugin")
|
||||||
|
self.deactivateWorker()
|
||||||
|
super().deactivate()
|
||||||
|
|
||||||
|
def activateWorker(self):
|
||||||
|
"""Start the voice worker thread."""
|
||||||
|
with self.lock:
|
||||||
|
self.active = True
|
||||||
|
|
||||||
|
# Only start if not already running
|
||||||
|
if not self.voiceThread.is_alive():
|
||||||
|
self.voiceThread = Thread(target=self.voiceWorker)
|
||||||
|
self.voiceThread.daemon = True
|
||||||
|
self.voiceThread.start()
|
||||||
|
|
||||||
|
def deactivateWorker(self):
|
||||||
|
"""Stop the voice worker thread."""
|
||||||
|
with self.lock:
|
||||||
|
self.active = False
|
||||||
|
|
||||||
|
# Try to join the thread if it's alive, with a timeout
|
||||||
|
if self.voiceThread.is_alive():
|
||||||
|
try:
|
||||||
|
self.voiceThread.join(timeout=2.0)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Error stopping voice worker thread: {e}")
|
||||||
|
|
||||||
|
def isActive(self):
|
||||||
|
"""Check if the worker is active."""
|
||||||
|
with self.lock:
|
||||||
|
return self.active
|
||||||
|
|
||||||
|
def outputMessage(self, message):
|
||||||
|
"""Output a message through Cthulhu's speech and braille systems.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: The message to output. May include special codes.
|
||||||
|
"""
|
||||||
|
# Process special codes
|
||||||
|
append = message.startswith(APPEND_CODE)
|
||||||
|
if append:
|
||||||
|
message = message[len(APPEND_CODE):]
|
||||||
|
|
||||||
|
persistent = False
|
||||||
|
if message.endswith(PERSISTENT_CODE):
|
||||||
|
message = message[:len(message)-len(PERSISTENT_CODE)]
|
||||||
|
persistent = True
|
||||||
|
|
||||||
|
# Output through appropriate channel
|
||||||
|
if persistent:
|
||||||
|
# Use the APIHelper for persistent messages
|
||||||
|
self.app.getAPIHelper().outputMessage(message, not append)
|
||||||
|
else:
|
||||||
|
# Use the script manager for standard messages
|
||||||
|
script_manager = self.app.getDynamicApiManager().getAPI('ScriptManager')
|
||||||
|
scriptManager = script_manager.getManager()
|
||||||
|
scriptManager.getDefaultScript().presentMessage(message, resetStyles=False)
|
||||||
|
|
||||||
|
def voiceWorker(self):
|
||||||
|
"""Worker thread that listens on a socket for messages to speak."""
|
||||||
|
socketFile = '/tmp/cthulhu.sock'
|
||||||
|
# For testing purposes
|
||||||
|
# socketFile = '/tmp/cthulhu-plugin.sock'
|
||||||
|
|
||||||
|
# Clean up any existing socket file
|
||||||
|
if os.path.exists(socketFile):
|
||||||
|
try:
|
||||||
|
os.unlink(socketFile)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error removing existing socket file: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create and set up the socket
|
||||||
|
cthulhu_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||||
|
cthulhu_sock.bind(socketFile)
|
||||||
|
os.chmod(socketFile, 0o222) # Write-only for everyone
|
||||||
|
cthulhu_sock.listen(1)
|
||||||
|
|
||||||
|
logger.info(f"Self Voice plugin listening on {socketFile}")
|
||||||
|
|
||||||
|
# Main loop - listen for connections
|
||||||
|
while self.isActive():
|
||||||
|
# Check if data is available with a timeout
|
||||||
|
try:
|
||||||
|
r, _, _ = select.select([cthulhu_sock], [], [], 0.8)
|
||||||
|
except select.error as e:
|
||||||
|
logger.error(f"Select error: {e}")
|
||||||
|
break
|
||||||
|
|
||||||
|
if not r: # No data available
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Accept connection
|
||||||
|
if cthulhu_sock in r:
|
||||||
|
try:
|
||||||
|
client_sock, _ = cthulhu_sock.accept()
|
||||||
|
client_sock.settimeout(0.5) # Set a timeout for receiving data
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Receive and process data
|
||||||
|
raw_data = client_sock.recv(8192)
|
||||||
|
if raw_data:
|
||||||
|
data = raw_data.decode("utf-8").strip()
|
||||||
|
if data:
|
||||||
|
self.outputMessage(data)
|
||||||
|
except socket.timeout:
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error receiving data: {e}")
|
||||||
|
finally:
|
||||||
|
client_sock.close()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error accepting connection: {e}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Socket error: {e}")
|
||||||
|
finally:
|
||||||
|
# Clean up
|
||||||
|
if 'cthulhu_sock' in locals():
|
||||||
|
try:
|
||||||
|
cthulhu_sock.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if os.path.exists(socketFile):
|
||||||
|
try:
|
||||||
|
os.unlink(socketFile)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error removing socket file: {e}")
|
||||||
|
|
||||||
|
logger.info("Self Voice plugin socket closed")
|
@ -113,7 +113,7 @@ class ResourceContext():
|
|||||||
self.settings[profile][application] = {}
|
self.settings[profile][application] = {}
|
||||||
# add entry
|
# add entry
|
||||||
self.settings[profile][application][sub_setting_name] = entry
|
self.settings[profile][application][sub_setting_name] = entry
|
||||||
|
|
||||||
print('add', 'settings', self.getName(), profile, application, entry.getResourceText())
|
print('add', 'settings', self.getName(), profile, application, entry.getResourceText())
|
||||||
|
|
||||||
|
|
||||||
@ -214,7 +214,7 @@ class ResourceContext():
|
|||||||
self.unregisterAllAPI()
|
self.unregisterAllAPI()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
|
|
||||||
def unregisterAllAPI(self):
|
def unregisterAllAPI(self):
|
||||||
dynamicApiManager = self.app.getDynamicApiManager()
|
dynamicApiManager = self.app.getDynamicApiManager()
|
||||||
for application, value in self.getAPIs().copy().items():
|
for application, value in self.getAPIs().copy().items():
|
||||||
@ -226,7 +226,7 @@ class ResourceContext():
|
|||||||
print('unregister api ', self.getName(), entry.getEntryType(), entry.getResourceText())
|
print('unregister api ', self.getName(), entry.getEntryType(), entry.getResourceText())
|
||||||
def unregisterAllGestures(self):
|
def unregisterAllGestures(self):
|
||||||
APIHelper = self.app.getAPIHelper()
|
APIHelper = self.app.getAPIHelper()
|
||||||
|
|
||||||
for profile, profileValue in self.getGestures().copy().items():
|
for profile, profileValue in self.getGestures().copy().items():
|
||||||
for application, applicationValue in profileValue.copy().items():
|
for application, applicationValue in profileValue.copy().items():
|
||||||
for gesture, entry in applicationValue.copy().items():
|
for gesture, entry in applicationValue.copy().items():
|
||||||
@ -272,12 +272,12 @@ class ResourceManager():
|
|||||||
def removeResourceContext(self, contextName):
|
def removeResourceContext(self, contextName):
|
||||||
if not contextName:
|
if not contextName:
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.resourceContextDict[contextName].unregisterAllResources()
|
self.resourceContextDict[contextName].unregisterAllResources()
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# temp
|
# temp
|
||||||
try:
|
try:
|
||||||
print('_________', 'summery', self.resourceContextDict[contextName].getName(), '_________')
|
print('_________', 'summery', self.resourceContextDict[contextName].getName(), '_________')
|
||||||
@ -302,7 +302,7 @@ class ResourceManager():
|
|||||||
return self.resourceContextDict[contextName]
|
return self.resourceContextDict[contextName]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def addAPI(self, application, api, contextName = None):
|
def addAPI(self, application, api, contextName = None):
|
||||||
if not contextName:
|
if not contextName:
|
||||||
return
|
return
|
||||||
|
@ -3558,10 +3558,10 @@ class Utilities:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def unicodeValueString(character):
|
def unicodeValueString(character):
|
||||||
""" Returns a four hex digit representation of the given character
|
""" Returns a four hex digit representation of the given character
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
- The character to return representation
|
- The character to return representation
|
||||||
|
|
||||||
Returns a string representaition of the given character unicode vlue
|
Returns a string representaition of the given character unicode vlue
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
SUBDIRS = apps toolkits switcher terminal web
|
SUBDIRS = apps toolkits sleepmode switcher terminal web
|
||||||
|
|
||||||
cthulhu_python_PYTHON = \
|
cthulhu_python_PYTHON = \
|
||||||
__init__.py \
|
__init__.py \
|
||||||
|
@ -204,6 +204,11 @@ class Script(script.Script):
|
|||||||
Script.processBrailleCutLine,
|
Script.processBrailleCutLine,
|
||||||
cmdnames.PROCESS_BRAILLE_CUT_LINE)
|
cmdnames.PROCESS_BRAILLE_CUT_LINE)
|
||||||
|
|
||||||
|
self.inputEventHandlers["toggleSleepModeHandler"] = \
|
||||||
|
input_event.InputEventHandler(
|
||||||
|
Script.toggleSleepMode,
|
||||||
|
cmdnames.TOGGLE_SLEEP_MODE)
|
||||||
|
|
||||||
self.inputEventHandlers["shutdownHandler"] = \
|
self.inputEventHandlers["shutdownHandler"] = \
|
||||||
input_event.InputEventHandler(
|
input_event.InputEventHandler(
|
||||||
cthulhu.quitCthulhu,
|
cthulhu.quitCthulhu,
|
||||||
@ -732,6 +737,13 @@ class Script(script.Script):
|
|||||||
# #
|
# #
|
||||||
########################################################################
|
########################################################################
|
||||||
|
|
||||||
|
def toggleSleepMode(self, input_event=None):
|
||||||
|
"""Toggles between sleep mode and regular mode."""
|
||||||
|
script_manager = _scriptManager
|
||||||
|
sleepScript = script_manager.createScript("sleepmode", self.app)
|
||||||
|
script_manager.setActiveScript(sleepScript, "Sleep mode toggled")
|
||||||
|
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.
|
||||||
|
6
src/cthulhu/scripts/sleepmode/Makefile.am
Normal file
6
src/cthulhu/scripts/sleepmode/Makefile.am
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
cthulhu_python_PYTHON = \
|
||||||
|
__init__.py \
|
||||||
|
script.py \
|
||||||
|
script_utilities.py
|
||||||
|
|
||||||
|
cthulhu_pythondir=$(pkgpythondir)/scripts/terminal
|
0
src/cthulhu/scripts/sleepmode/__init__.py
Normal file
0
src/cthulhu/scripts/sleepmode/__init__.py
Normal file
216
src/cthulhu/scripts/sleepmode/script.py
Normal file
216
src/cthulhu/scripts/sleepmode/script.py
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
#
|
||||||
|
# Copyright (c) 2024 Stormux
|
||||||
|
# Copyright (c) 2010-2012 The Orca Team
|
||||||
|
# Copyright (c) 2012 Igalia, S.L.
|
||||||
|
# Copyright (c) 2005-2010 Sun Microsystems Inc.
|
||||||
|
#
|
||||||
|
# This library is free software; you can redistribute it and/or
|
||||||
|
# modify it under the terms of the GNU Lesser General Public
|
||||||
|
# License as published by the Free Software Foundation; either
|
||||||
|
# version 2.1 of the License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
# Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public
|
||||||
|
# License along with this library; if not, write to the
|
||||||
|
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
|
||||||
|
# Boston MA 02110-1301 USA.
|
||||||
|
#
|
||||||
|
# Fork of Orca Screen Reader (GNOME)
|
||||||
|
# Original source: https://gitlab.gnome.org/GNOME/orca
|
||||||
|
|
||||||
|
"""Script for sleep mode where Cthulhu ignores events and commands."""
|
||||||
|
|
||||||
|
__id__ = "$Id$"
|
||||||
|
__version__ = "$Revision$"
|
||||||
|
__date__ = "$Date$"
|
||||||
|
__copyright__ = "Copyright (c) 2024 Stormux"
|
||||||
|
__license__ = "LGPL"
|
||||||
|
|
||||||
|
import cthulhu.debug as debug
|
||||||
|
import cthulhu.scripts.default as default
|
||||||
|
import cthulhu.input_event as input_event
|
||||||
|
import cthulhu.keybindings as keybindings
|
||||||
|
import cthulhu.messages as messages
|
||||||
|
from cthulhu.ax_object import AXObject
|
||||||
|
from cthulhu.ax_utilities import AXUtilities
|
||||||
|
|
||||||
|
class Script(default.Script):
|
||||||
|
"""The sleep-mode script."""
|
||||||
|
|
||||||
|
def __init__(self, app):
|
||||||
|
super().__init__(app)
|
||||||
|
self.presentIfInactive = True
|
||||||
|
|
||||||
|
def activate(self):
|
||||||
|
"""Called when this script is activated."""
|
||||||
|
tokens = ["SLEEP MODE: Activating script for", self.app]
|
||||||
|
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||||
|
|
||||||
|
# Only keep the sleep mode toggle binding active
|
||||||
|
self.removeKeyGrabs()
|
||||||
|
self.addKeyGrabs()
|
||||||
|
|
||||||
|
# Present sleep mode status
|
||||||
|
self.clearBraille()
|
||||||
|
self.presentMessage(messages.SLEEP_MODE_ENABLED_FOR % AXObject.get_name(self.app))
|
||||||
|
|
||||||
|
def deactivate(self):
|
||||||
|
"""Called when this script is deactivated."""
|
||||||
|
tokens = ["SLEEP MODE: De-activating script for", self.app]
|
||||||
|
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||||
|
self.removeKeyGrabs()
|
||||||
|
|
||||||
|
def getKeyBindings(self):
|
||||||
|
"""Only provide the key binding needed to exit sleep mode."""
|
||||||
|
keyBindings = keybindings.KeyBindings()
|
||||||
|
|
||||||
|
keyBindings.load([("q", keybindings.defaultModifierMask,
|
||||||
|
keybindings.CTHULHU_CTRL_ALT_MODIFIER_MASK | keybindings.SHIFT_ALT_MODIFIER_MASK,
|
||||||
|
"toggleSleepModeHandler")],
|
||||||
|
self.inputEventHandlers)
|
||||||
|
return keyBindings
|
||||||
|
|
||||||
|
def setupInputEventHandlers(self):
|
||||||
|
"""Sets up the input event handlers for sleep mode."""
|
||||||
|
super().setupInputEventHandlers()
|
||||||
|
self.inputEventHandlers["toggleSleepModeHandler"] = \
|
||||||
|
input_event.InputEventHandler(
|
||||||
|
Script.toggleSleepMode,
|
||||||
|
"Toggles sleep mode on/off")
|
||||||
|
|
||||||
|
def toggleSleepMode(self, input_event=None):
|
||||||
|
"""Toggles between sleep mode and regular mode."""
|
||||||
|
script_manager = _scriptManager
|
||||||
|
script_manager.setActiveScript(script_manager.getDefaultScript(), "Sleep mode toggled")
|
||||||
|
self.presentMessage(messages.SLEEP_MODE_DISABLED_FOR % AXObject.get_name(self.app))
|
||||||
|
return True
|
||||||
|
|
||||||
|
def locusOfFocusChanged(self, event, oldLocusOfFocus, newLocusOfFocus):
|
||||||
|
"""Handles changes of focus of interest to the script."""
|
||||||
|
tokens = ["SLEEP MODE: focus changed from", oldLocusOfFocus, "to", newLocusOfFocus]
|
||||||
|
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||||
|
if oldLocusOfFocus is None and AXUtilities.is_application(AXObject.get_parent(newLocusOfFocus)):
|
||||||
|
self.clearBraille()
|
||||||
|
self.presentMessage(messages.SLEEP_MODE_ENABLED_FOR % AXObject.get_name(self.app))
|
||||||
|
return
|
||||||
|
|
||||||
|
msg = "SLEEP MODE: Ignoring event."
|
||||||
|
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||||
|
|
||||||
|
def presentKeyboardEvent(self, event):
|
||||||
|
"""Prevents keyboard echo in sleep mode."""
|
||||||
|
msg = "SLEEP MODE: Not presenting keyboard event."
|
||||||
|
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def updateBraille(self, obj, **args):
|
||||||
|
"""Don't update braille in sleep mode."""
|
||||||
|
msg = "SLEEP MODE: Not updating braille."
|
||||||
|
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||||
|
|
||||||
|
# Event handler overrides - all do nothing
|
||||||
|
def onActiveChanged(self, event):
|
||||||
|
msg = "SLEEP MODE: Ignoring event."
|
||||||
|
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||||
|
|
||||||
|
def onActiveDescendantChanged(self, event):
|
||||||
|
msg = "SLEEP MODE: Ignoring event."
|
||||||
|
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||||
|
|
||||||
|
def onBusyChanged(self, event):
|
||||||
|
msg = "SLEEP MODE: Ignoring event."
|
||||||
|
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||||
|
|
||||||
|
def onCaretMoved(self, event):
|
||||||
|
msg = "SLEEP MODE: Ignoring event."
|
||||||
|
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||||
|
|
||||||
|
def onCheckedChanged(self, event):
|
||||||
|
msg = "SLEEP MODE: Ignoring event."
|
||||||
|
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||||
|
|
||||||
|
def onChildrenAdded(self, event):
|
||||||
|
msg = "SLEEP MODE: Ignoring event."
|
||||||
|
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||||
|
|
||||||
|
def onChildrenRemoved(self, event):
|
||||||
|
msg = "SLEEP MODE: Ignoring event."
|
||||||
|
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||||
|
|
||||||
|
def onColumnReordered(self, event):
|
||||||
|
msg = "SLEEP MODE: Ignoring event."
|
||||||
|
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||||
|
|
||||||
|
def onDocumentLoadComplete(self, event):
|
||||||
|
msg = "SLEEP MODE: Ignoring event."
|
||||||
|
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||||
|
|
||||||
|
def onDocumentLoadStopped(self, event):
|
||||||
|
msg = "SLEEP MODE: Ignoring event."
|
||||||
|
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||||
|
|
||||||
|
def onDocumentReload(self, event):
|
||||||
|
msg = "SLEEP MODE: Ignoring event."
|
||||||
|
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||||
|
|
||||||
|
def onExpandedChanged(self, event):
|
||||||
|
msg = "SLEEP MODE: Ignoring event."
|
||||||
|
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||||
|
|
||||||
|
def onFocus(self, event):
|
||||||
|
msg = "SLEEP MODE: Ignoring event."
|
||||||
|
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||||
|
|
||||||
|
def onFocusedChanged(self, event):
|
||||||
|
msg = "SLEEP MODE: Ignoring event."
|
||||||
|
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||||
|
|
||||||
|
def onMouseButton(self, event):
|
||||||
|
msg = "SLEEP MODE: Ignoring event."
|
||||||
|
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||||
|
|
||||||
|
def onNameChanged(self, event):
|
||||||
|
msg = "SLEEP MODE: Ignoring event."
|
||||||
|
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||||
|
|
||||||
|
def onSelectedChanged(self, event):
|
||||||
|
msg = "SLEEP MODE: Ignoring event."
|
||||||
|
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||||
|
|
||||||
|
def onSelectionChanged(self, event):
|
||||||
|
msg = "SLEEP MODE: Ignoring event."
|
||||||
|
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||||
|
|
||||||
|
def onShowingChanged(self, event):
|
||||||
|
msg = "SLEEP MODE: Ignoring event."
|
||||||
|
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||||
|
|
||||||
|
def onTextAttributesChanged(self, event):
|
||||||
|
msg = "SLEEP MODE: Ignoring event."
|
||||||
|
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||||
|
|
||||||
|
def onTextDeleted(self, event):
|
||||||
|
msg = "SLEEP MODE: Ignoring event."
|
||||||
|
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||||
|
|
||||||
|
def onTextInserted(self, event):
|
||||||
|
msg = "SLEEP MODE: Ignoring event."
|
||||||
|
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||||
|
|
||||||
|
def onTextSelectionChanged(self, event):
|
||||||
|
msg = "SLEEP MODE: Ignoring event."
|
||||||
|
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||||
|
|
||||||
|
def onWindowActivated(self, event):
|
||||||
|
"""Callback for window:activate accessibility events."""
|
||||||
|
self.clearBraille()
|
||||||
|
self.presentMessage(messages.SLEEP_MODE_ENABLED_FOR % AXObject.get_name(self.app))
|
||||||
|
|
||||||
|
def onWindowDeactivated(self, event):
|
||||||
|
msg = "SLEEP MODE: Ignoring event."
|
||||||
|
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
59
src/cthulhu/scripts/sleepmode/script_utilities.py
Normal file
59
src/cthulhu/scripts/sleepmode/script_utilities.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
#
|
||||||
|
# Copyright (c) 2024 Stormux
|
||||||
|
# Copyright (c) 2010-2012 The Orca Team
|
||||||
|
# Copyright (c) 2012 Igalia, S.L.
|
||||||
|
# Copyright (c) 2005-2010 Sun Microsystems Inc.
|
||||||
|
#
|
||||||
|
# This library is free software; you can redistribute it and/or
|
||||||
|
# modify it under the terms of the GNU Lesser General Public
|
||||||
|
# License as published by the Free Software Foundation; either
|
||||||
|
# version 2.1 of the License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
# Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public
|
||||||
|
# License along with this library; if not, write to the
|
||||||
|
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
|
||||||
|
# Boston MA 02110-1301 USA.
|
||||||
|
#
|
||||||
|
# Fork of Orca Screen Reader (GNOME)
|
||||||
|
# Original source: https://gitlab.gnome.org/GNOME/orca
|
||||||
|
|
||||||
|
"""Utilities for Sleep Mode. Helps ensure we do nothing. When nothing is done, nothing is left undone."""
|
||||||
|
|
||||||
|
__id__ = "$Id$"
|
||||||
|
__version__ = "$Revision$"
|
||||||
|
__date__ = "$Date$"
|
||||||
|
__copyright__ = "Copyright (c) 2024 Stormux"
|
||||||
|
__license__ = "LGPL"
|
||||||
|
|
||||||
|
import cthulhu.debug as debug
|
||||||
|
import cthulhu.script_utilities as script_utilities
|
||||||
|
|
||||||
|
class Utilities(script_utilities.Utilities):
|
||||||
|
"""Utilities class for the sleep-mode script."""
|
||||||
|
|
||||||
|
def willEchoCharacter(self, event):
|
||||||
|
"""Returns True if we'll echo this character."""
|
||||||
|
|
||||||
|
msg = "SLEEP MODE: Will not echo character."
|
||||||
|
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def displayRegionChanged(self, obj, start, end, mode):
|
||||||
|
"""Don't track changes in sleep mode."""
|
||||||
|
|
||||||
|
msg = "SLEEP MODE: Will not track display changes."
|
||||||
|
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def shouldPresentContext(self):
|
||||||
|
"""Don't present context in sleep mode."""
|
||||||
|
|
||||||
|
msg = "SLEEP MODE: Will not present context."
|
||||||
|
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||||
|
return False
|
@ -413,4 +413,4 @@ presentChatRoomLast = False
|
|||||||
presentLiveRegionFromInactiveTab = False
|
presentLiveRegionFromInactiveTab = False
|
||||||
|
|
||||||
# Plugins
|
# Plugins
|
||||||
activePlugins = ['Clipboard', 'MouseReview', 'Date', 'ByeCthulhu', 'Time', 'HelloCthulhu', 'HelloWorld', 'SelfVoice', 'PluginManager', 'SimplePluginSystem']
|
activePlugins = ['Clipboard', 'DisplayVersion', 'ByeCthulhu', 'HelloCthulhu', 'hello_world', 'self_voice', 'SimplePluginSystem']
|
||||||
|
@ -69,7 +69,7 @@ class SpeechServer(speechserver.SpeechServer):
|
|||||||
# See the parent class for documentation.
|
# See the parent class for documentation.
|
||||||
|
|
||||||
_active_servers = {}
|
_active_servers = {}
|
||||||
|
|
||||||
DEFAULT_SERVER_ID = 'default'
|
DEFAULT_SERVER_ID = 'default'
|
||||||
_SERVER_NAMES = {DEFAULT_SERVER_ID: guilabels.DEFAULT_SYNTHESIZER}
|
_SERVER_NAMES = {DEFAULT_SERVER_ID: guilabels.DEFAULT_SYNTHESIZER}
|
||||||
|
|
||||||
@ -93,7 +93,7 @@ class SpeechServer(speechserver.SpeechServer):
|
|||||||
|
|
||||||
Attempt to create the server if it doesn't exist yet. Returns None
|
Attempt to create the server if it doesn't exist yet. Returns None
|
||||||
when it is not possible to create the server.
|
when it is not possible to create the server.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if serverId not in cls._active_servers:
|
if serverId not in cls._active_servers:
|
||||||
cls(serverId)
|
cls(serverId)
|
||||||
@ -781,16 +781,16 @@ class SpeechServer(speechserver.SpeechServer):
|
|||||||
def reset(self, text=None, acss=None):
|
def reset(self, text=None, acss=None):
|
||||||
self._client.close()
|
self._client.close()
|
||||||
self._init()
|
self._init()
|
||||||
|
|
||||||
def list_output_modules(self):
|
def list_output_modules(self):
|
||||||
"""Return names of available output modules as a tuple of strings.
|
"""Return names of available output modules as a tuple of strings.
|
||||||
|
|
||||||
This method is not a part of Cthulhu speech API, but is used internally
|
This method is not a part of Cthulhu speech API, but is used internally
|
||||||
by the Speech Dispatcher backend.
|
by the Speech Dispatcher backend.
|
||||||
|
|
||||||
The returned tuple can be empty if the information can not be
|
The returned tuple can be empty if the information can not be
|
||||||
obtained (e.g. with an older Speech Dispatcher version).
|
obtained (e.g. with an older Speech Dispatcher version).
|
||||||
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
return self._send_command(self._client.list_output_modules)
|
return self._send_command(self._client.list_output_modules)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user