Fix stuff, apply ruff rules.
This commit is contained in:
+175
-6
@@ -64,7 +64,9 @@ from .cthulhu_platform import tablesdir
|
|||||||
_logger = None
|
_logger = None
|
||||||
log = None
|
log = None
|
||||||
_monitor = None
|
_monitor = None
|
||||||
|
_monitorCallback = None
|
||||||
_settingsManager = None
|
_settingsManager = None
|
||||||
|
_enableComputerBrailleAtCursor = True
|
||||||
|
|
||||||
def _ensureLogger():
|
def _ensureLogger():
|
||||||
"""Ensure logger is initialized."""
|
"""Ensure logger is initialized."""
|
||||||
@@ -99,6 +101,30 @@ else:
|
|||||||
tokens = ["BRAILLE: brlapi imported", brlapi]
|
tokens = ["BRAILLE: brlapi imported", brlapi]
|
||||||
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||||
|
|
||||||
|
|
||||||
|
def _brlapi_command(name):
|
||||||
|
if not _brlAPIAvailable:
|
||||||
|
return None
|
||||||
|
return getattr(brlapi, name, None)
|
||||||
|
|
||||||
|
|
||||||
|
BRLAPI_KEY_CMD_HWINLT = _brlapi_command("KEY_CMD_HWINLT")
|
||||||
|
BRLAPI_KEY_CMD_FWINLT = _brlapi_command("KEY_CMD_FWINLT")
|
||||||
|
BRLAPI_KEY_CMD_FWINLTSKIP = _brlapi_command("KEY_CMD_FWINLTSKIP")
|
||||||
|
BRLAPI_KEY_CMD_HWINRT = _brlapi_command("KEY_CMD_HWINRT")
|
||||||
|
BRLAPI_KEY_CMD_FWINRT = _brlapi_command("KEY_CMD_FWINRT")
|
||||||
|
BRLAPI_KEY_CMD_FWINRTSKIP = _brlapi_command("KEY_CMD_FWINRTSKIP")
|
||||||
|
BRLAPI_KEY_CMD_LNUP = _brlapi_command("KEY_CMD_LNUP")
|
||||||
|
BRLAPI_KEY_CMD_LNDN = _brlapi_command("KEY_CMD_LNDN")
|
||||||
|
BRLAPI_KEY_CMD_FREEZE = _brlapi_command("KEY_CMD_FREEZE")
|
||||||
|
BRLAPI_KEY_CMD_TOP_LEFT = _brlapi_command("KEY_CMD_TOP_LEFT")
|
||||||
|
BRLAPI_KEY_CMD_BOT_LEFT = _brlapi_command("KEY_CMD_BOT_LEFT")
|
||||||
|
BRLAPI_KEY_CMD_HOME = _brlapi_command("KEY_CMD_HOME")
|
||||||
|
BRLAPI_KEY_CMD_SIXDOTS = _brlapi_command("KEY_CMD_SIXDOTS")
|
||||||
|
BRLAPI_KEY_CMD_ROUTE = _brlapi_command("KEY_CMD_ROUTE")
|
||||||
|
BRLAPI_KEY_CMD_CUTBEGIN = _brlapi_command("KEY_CMD_CUTBEGIN")
|
||||||
|
BRLAPI_KEY_CMD_CUTLINE = _brlapi_command("KEY_CMD_CUTLINE")
|
||||||
|
|
||||||
BRLAPI_PRIORITY_IDLE = 0
|
BRLAPI_PRIORITY_IDLE = 0
|
||||||
BRLAPI_PRIORITY_DEFAULT = 50
|
BRLAPI_PRIORITY_DEFAULT = 50
|
||||||
BRLAPI_PRIORITY_HIGH = 70
|
BRLAPI_PRIORITY_HIGH = 70
|
||||||
@@ -936,9 +962,15 @@ class Line:
|
|||||||
def addRegion(self, region):
|
def addRegion(self, region):
|
||||||
self.regions.append(region)
|
self.regions.append(region)
|
||||||
|
|
||||||
|
def add_region(self, region):
|
||||||
|
self.addRegion(region)
|
||||||
|
|
||||||
def addRegions(self, regions):
|
def addRegions(self, regions):
|
||||||
self.regions.extend(regions)
|
self.regions.extend(regions)
|
||||||
|
|
||||||
|
def add_regions(self, regions):
|
||||||
|
self.addRegions(regions)
|
||||||
|
|
||||||
def getLineInfo(self, getLinkMask=True):
|
def getLineInfo(self, getLinkMask=True):
|
||||||
"""Computes the complete string for this line as well as a
|
"""Computes the complete string for this line as well as a
|
||||||
0-based index where the focused region starts on this line.
|
0-based index where the focused region starts on this line.
|
||||||
@@ -998,6 +1030,9 @@ class Line:
|
|||||||
|
|
||||||
return [string, focusOffset, attributeMask, ranges]
|
return [string, focusOffset, attributeMask, ranges]
|
||||||
|
|
||||||
|
def get_line_info(self, get_link_mask=True):
|
||||||
|
return self.getLineInfo(get_link_mask)
|
||||||
|
|
||||||
def getRegionAtOffset(self, offset):
|
def getRegionAtOffset(self, offset):
|
||||||
"""Finds the Region at the given 0-based offset in this line.
|
"""Finds the Region at the given 0-based offset in this line.
|
||||||
|
|
||||||
@@ -1500,18 +1535,20 @@ def refresh(panToCursor=True, targetCursorCell=0, getLinkMask=True, stopFlash=Tr
|
|||||||
shutdown()
|
shutdown()
|
||||||
|
|
||||||
if settings.enableBrailleMonitor:
|
if settings.enableBrailleMonitor:
|
||||||
if not _monitor:
|
if attributeMask:
|
||||||
|
subMask = attributeMask[startPos:endPos]
|
||||||
|
else:
|
||||||
|
subMask = None
|
||||||
|
if _monitorCallback:
|
||||||
|
_monitorCallback(cursorCell, substring, subMask, _displaySize[0])
|
||||||
|
elif not _monitor:
|
||||||
try:
|
try:
|
||||||
_monitor = brlmon.BrlMon(_displaySize[0])
|
_monitor = brlmon.BrlMon(_displaySize[0])
|
||||||
_monitor.show_all()
|
_monitor.show_all()
|
||||||
except Exception:
|
except Exception:
|
||||||
debug.printMessage(debug.LEVEL_WARNING, "brlmon failed")
|
debug.printMessage(debug.LEVEL_WARNING, "brlmon failed")
|
||||||
_monitor = None
|
_monitor = None
|
||||||
if attributeMask:
|
if _monitor and not _monitorCallback:
|
||||||
subMask = attributeMask[startPos:endPos]
|
|
||||||
else:
|
|
||||||
subMask = None
|
|
||||||
if _monitor:
|
|
||||||
_monitor.writeText(cursorCell, substring, subMask)
|
_monitor.writeText(cursorCell, substring, subMask)
|
||||||
elif _monitor:
|
elif _monitor:
|
||||||
_monitor.destroy()
|
_monitor.destroy()
|
||||||
@@ -2110,3 +2147,135 @@ def shutdown():
|
|||||||
msg = "BRAILLE: Braille shutdown complete."
|
msg = "BRAILLE: Braille shutdown complete."
|
||||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def check_braille_setting():
|
||||||
|
return checkBrailleSetting()
|
||||||
|
|
||||||
|
|
||||||
|
def disable_braille():
|
||||||
|
return disableBraille()
|
||||||
|
|
||||||
|
|
||||||
|
def display_line(
|
||||||
|
line,
|
||||||
|
focused_region=None,
|
||||||
|
pan_to_cursor=True,
|
||||||
|
indicate_links=True,
|
||||||
|
stop_flash=True,
|
||||||
|
):
|
||||||
|
clear()
|
||||||
|
addLine(line)
|
||||||
|
setFocus(focused_region, getLinkMask=indicate_links)
|
||||||
|
refresh(
|
||||||
|
panToCursor=pan_to_cursor,
|
||||||
|
getLinkMask=indicate_links,
|
||||||
|
stopFlash=stop_flash,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def display_message(message, cursor=-1, flash_time=0):
|
||||||
|
return displayMessage(message, cursor=cursor, flashTime=flash_time)
|
||||||
|
|
||||||
|
|
||||||
|
def get_caret_context(event):
|
||||||
|
return getCaretContext(event)
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_contraction_table():
|
||||||
|
return globals().get("_defaultContractionTable", "")
|
||||||
|
|
||||||
|
|
||||||
|
def get_region_at_cell(cell):
|
||||||
|
return getRegionAtCell(cell)
|
||||||
|
|
||||||
|
|
||||||
|
def is_flash_active():
|
||||||
|
return bool(_flashEventSourceId)
|
||||||
|
|
||||||
|
|
||||||
|
def kill_flash(restore_saved=True):
|
||||||
|
return killFlash(restoreSaved=restore_saved)
|
||||||
|
|
||||||
|
|
||||||
|
def pan_left(pan_amount=0):
|
||||||
|
return panLeft(pan_amount)
|
||||||
|
|
||||||
|
|
||||||
|
def pan_right(pan_amount=0):
|
||||||
|
return panRight(pan_amount)
|
||||||
|
|
||||||
|
|
||||||
|
def process_routing_key(event):
|
||||||
|
return processRoutingKey(event)
|
||||||
|
|
||||||
|
|
||||||
|
def return_to_region_with_focus(input_event=None):
|
||||||
|
return returnToRegionWithFocus(input_event)
|
||||||
|
|
||||||
|
|
||||||
|
def set_brlapi_priority(level=BRLAPI_PRIORITY_DEFAULT):
|
||||||
|
return setBrlapiPriority(level)
|
||||||
|
|
||||||
|
|
||||||
|
def set_contraction_table(file_path):
|
||||||
|
settings.brailleContractionTable = file_path
|
||||||
|
|
||||||
|
|
||||||
|
def set_enable_braille(value):
|
||||||
|
settings.enableBraille = bool(value)
|
||||||
|
|
||||||
|
|
||||||
|
def set_enable_computer_braille_at_cursor(value):
|
||||||
|
global _enableComputerBrailleAtCursor
|
||||||
|
_enableComputerBrailleAtCursor = bool(value)
|
||||||
|
|
||||||
|
|
||||||
|
def set_enable_contracted_braille(value):
|
||||||
|
settings.enableContractedBraille = bool(value)
|
||||||
|
for line in _lines:
|
||||||
|
line.setContractedBraille(settings.enableContractedBraille)
|
||||||
|
|
||||||
|
|
||||||
|
def set_enable_eol(value):
|
||||||
|
settings.disableBrailleEOL = not bool(value)
|
||||||
|
|
||||||
|
|
||||||
|
def set_enable_word_wrap(value):
|
||||||
|
settings.enableBrailleWordWrap = bool(value)
|
||||||
|
|
||||||
|
|
||||||
|
def set_link_indicator(value):
|
||||||
|
settings.brailleLinkIndicator = value
|
||||||
|
|
||||||
|
|
||||||
|
def set_monitor_callback(callback):
|
||||||
|
global _monitorCallback
|
||||||
|
_monitorCallback = callback
|
||||||
|
|
||||||
|
|
||||||
|
def set_selector_indicator(value):
|
||||||
|
settings.brailleSelectorIndicator = value
|
||||||
|
|
||||||
|
|
||||||
|
def set_text_attributes_indicator(value):
|
||||||
|
settings.textAttributesBrailleIndicator = value
|
||||||
|
|
||||||
|
|
||||||
|
def setup_key_ranges(keys):
|
||||||
|
return setupKeyRanges(keys)
|
||||||
|
|
||||||
|
|
||||||
|
def toggle_contracted_braille(event):
|
||||||
|
return setContractedBraille(event)
|
||||||
|
|
||||||
|
|
||||||
|
def try_reposition_cursor(obj):
|
||||||
|
if not _regionWithFocus or not isinstance(_regionWithFocus, Text):
|
||||||
|
return False
|
||||||
|
if _regionWithFocus.accessible != obj:
|
||||||
|
return False
|
||||||
|
if not _regionWithFocus.repositionCursor():
|
||||||
|
return False
|
||||||
|
refresh()
|
||||||
|
return True
|
||||||
|
|||||||
@@ -37,6 +37,28 @@ __license__ = "LGPL"
|
|||||||
|
|
||||||
from .cthulhu_i18n import _
|
from .cthulhu_i18n import _
|
||||||
|
|
||||||
|
BYPASS_MODE_TOGGLE = _("Toggle bypass mode")
|
||||||
|
CHAT_NEXT_MESSAGE = _("Speak and braille the next chat room message")
|
||||||
|
CLIPBOARD_PRESENT_CONTENTS = _("Present clipboard contents")
|
||||||
|
DEBUG_CLEAR_ATSPI_CACHE_FOR_APPLICATION = _("Clear the AT-SPI cache for the current application")
|
||||||
|
DEBUG_CYCLE_LEVEL = _("Cycle the debug level at run time")
|
||||||
|
LIVE_REGIONS_ARE_ANNOUNCED = _("Toggle live region announcements")
|
||||||
|
LIVE_REGIONS_NEXT = _("Speak the next live region announcement")
|
||||||
|
LIVE_REGIONS_PREVIOUS = _("Speak the previous live region announcement")
|
||||||
|
PRESENT_BATTERY_STATUS = _("Present battery status")
|
||||||
|
PRESENT_CELL_FORMULA = _("Present spreadsheet cell formula")
|
||||||
|
PRESENT_CPU_AND_MEMORY_USAGE = _("Present CPU and memory usage")
|
||||||
|
PRESENT_CURRENT_PROFILE = _("Present current settings profile")
|
||||||
|
STRUCTURAL_NAVIGATION_MODE_CYCLE = _("Cycle structural navigation mode")
|
||||||
|
TABLE_CELL_BEGINNING_OF_ROW = _("Go to the beginning of the row")
|
||||||
|
TABLE_CELL_BOTTOM_OF_COLUMN = _("Go to the bottom of the column")
|
||||||
|
TABLE_CELL_END_OF_ROW = _("Go to the end of the row")
|
||||||
|
TABLE_CELL_TOP_OF_COLUMN = _("Go to the top of the column")
|
||||||
|
TABLE_NAVIGATION_TOGGLE = _("Toggle table navigation keys")
|
||||||
|
TOGGLE_BRAILLE_MONITOR = _("Toggle the braille monitor")
|
||||||
|
TOGGLE_KEYBOARD_LAYOUT = _("Toggle keyboard layout")
|
||||||
|
TOGGLE_SPEECH_MONITOR = _("Toggle the speech monitor")
|
||||||
|
|
||||||
# Translators: this command will move the mouse pointer to the current item
|
# Translators: this command will move the mouse pointer to the current item
|
||||||
# without clicking on it.
|
# without clicking on it.
|
||||||
ROUTE_POINTER_TO_ITEM = _("Route the pointer to the current item")
|
ROUTE_POINTER_TO_ITEM = _("Route the pointer to the current item")
|
||||||
|
|||||||
@@ -122,6 +122,11 @@ class FocusManager:
|
|||||||
appName = (AXObject.get_name(app) or "").lower()
|
appName = (AXObject.get_name(app) or "").lower()
|
||||||
return appName == "cthulhu"
|
return appName == "cthulhu"
|
||||||
|
|
||||||
|
def is_in_preferences_window(self) -> bool:
|
||||||
|
"""Returns True if focus is inside Cthulhu's preferences window."""
|
||||||
|
|
||||||
|
return self.active_window_is_cthulhu()
|
||||||
|
|
||||||
def focus_and_window_are_unknown(self) -> bool:
|
def focus_and_window_are_unknown(self) -> bool:
|
||||||
"""Returns True if we have no knowledge about what is focused."""
|
"""Returns True if we have no knowledge about what is focused."""
|
||||||
|
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ class InputEventManager:
|
|||||||
def add_grabs_for_keybinding(
|
def add_grabs_for_keybinding(
|
||||||
self,
|
self,
|
||||||
binding: keybindings.KeyBinding,
|
binding: keybindings.KeyBinding,
|
||||||
cthulhu_modifiers: list[str],
|
cthulhu_modifiers: list[str] | None = None,
|
||||||
) -> list[int]:
|
) -> list[int]:
|
||||||
"""Adds grabs for binding, returns grab IDs."""
|
"""Adds grabs for binding, returns grab IDs."""
|
||||||
|
|
||||||
|
|||||||
+63
-11
@@ -67,6 +67,9 @@ CTHULHU_CTRL_MODIFIER_MASK = (1 << MODIFIER_CTHULHU |
|
|||||||
CTHULHU_CTRL_ALT_MODIFIER_MASK = (1 << MODIFIER_CTHULHU |
|
CTHULHU_CTRL_ALT_MODIFIER_MASK = (1 << MODIFIER_CTHULHU |
|
||||||
1 << Atspi.ModifierType.CONTROL |
|
1 << Atspi.ModifierType.CONTROL |
|
||||||
1 << Atspi.ModifierType.ALT)
|
1 << Atspi.ModifierType.ALT)
|
||||||
|
CTHULHU_ALT_SHIFT_MODIFIER_MASK = (1 << MODIFIER_CTHULHU |
|
||||||
|
1 << Atspi.ModifierType.ALT |
|
||||||
|
1 << Atspi.ModifierType.SHIFT)
|
||||||
CTHULHU_SHIFT_MODIFIER_MASK = (1 << MODIFIER_CTHULHU |
|
CTHULHU_SHIFT_MODIFIER_MASK = (1 << MODIFIER_CTHULHU |
|
||||||
1 << Atspi.ModifierType.SHIFT)
|
1 << Atspi.ModifierType.SHIFT)
|
||||||
SHIFT_MODIFIER_MASK = 1 << Atspi.ModifierType.SHIFT
|
SHIFT_MODIFIER_MASK = 1 << Atspi.ModifierType.SHIFT
|
||||||
@@ -171,6 +174,8 @@ def getModifierNames(mods):
|
|||||||
text += _("Shift") + "+"
|
text += _("Shift") + "+"
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
get_modifier_names = getModifierNames
|
||||||
|
|
||||||
def get_click_countString(count):
|
def get_click_countString(count):
|
||||||
"""Returns a human-consumable string representing the number of
|
"""Returns a human-consumable string representing the number of
|
||||||
clicks, such as 'double click' and 'triple click'."""
|
clicks, such as 'double click' and 'triple click'."""
|
||||||
@@ -189,7 +194,7 @@ def get_click_countString(count):
|
|||||||
return _("triple click")
|
return _("triple click")
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def create_key_definitions(keycode, keyval, modifiers):
|
def create_key_definitions(keycode, keyval, modifiers, cthulhu_modifiers=None):
|
||||||
"""Returns a list of Atspi key definitions for the given keycode, keyval, and modifiers."""
|
"""Returns a list of Atspi key definitions for the given keycode, keyval, and modifiers."""
|
||||||
ret = []
|
ret = []
|
||||||
if modifiers & CTHULHU_MODIFIER_MASK:
|
if modifiers & CTHULHU_MODIFIER_MASK:
|
||||||
@@ -197,7 +202,7 @@ def create_key_definitions(keycode, keyval, modifiers):
|
|||||||
other_modifiers = modifiers & ~CTHULHU_MODIFIER_MASK
|
other_modifiers = modifiers & ~CTHULHU_MODIFIER_MASK
|
||||||
from . import input_event_manager
|
from . import input_event_manager
|
||||||
manager = input_event_manager.get_manager()
|
manager = input_event_manager.get_manager()
|
||||||
for key in settings.cthulhuModifierKeys:
|
for key in cthulhu_modifiers or settings.cthulhuModifierKeys:
|
||||||
mod_keyval, mod_keycode = get_keycodes(key)
|
mod_keyval, mod_keycode = get_keycodes(key)
|
||||||
if mod_keycode == 0 and key == "Shift_Lock":
|
if mod_keycode == 0 and key == "Shift_Lock":
|
||||||
mod_keyval, mod_keycode = get_keycodes("Caps_Lock")
|
mod_keyval, mod_keycode = get_keycodes("Caps_Lock")
|
||||||
@@ -224,14 +229,22 @@ class KeyBinding:
|
|||||||
and the InputEventHandler.
|
and the InputEventHandler.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, keysymstring, modifier_mask, modifiers, handler,
|
def __init__(
|
||||||
click_count = 1, enabled=True):
|
self,
|
||||||
|
keysymstring,
|
||||||
|
modifier_mask_or_modifiers,
|
||||||
|
*args,
|
||||||
|
click_count=1,
|
||||||
|
enabled=True,
|
||||||
|
):
|
||||||
"""Creates a new key binding.
|
"""Creates a new key binding.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
- keysymstring: the keysymstring - this is typically a string
|
- keysymstring: the keysymstring - this is typically a string
|
||||||
from /usr/include/X11/keysymdef.h with the preceding 'XK_'
|
from /usr/include/X11/keysymdef.h with the preceding 'XK_'
|
||||||
removed (e.g., XK_KP_Enter becomes the string 'KP_Enter').
|
removed (e.g., XK_KP_Enter becomes the string 'KP_Enter').
|
||||||
|
- modifier_mask_or_modifiers: either the modifier mask in the legacy form,
|
||||||
|
or the required modifier state in the lightweight command-manager form.
|
||||||
- modifier_mask: bit mask where a set bit tells us what modifiers
|
- modifier_mask: bit mask where a set bit tells us what modifiers
|
||||||
we care about (see Atspi.ModifierType.*)
|
we care about (see Atspi.ModifierType.*)
|
||||||
- modifiers: the state the modifiers we care about must be in for
|
- modifiers: the state the modifiers we care about must be in for
|
||||||
@@ -240,6 +253,24 @@ class KeyBinding:
|
|||||||
- handler: the InputEventHandler for this key binding
|
- handler: the InputEventHandler for this key binding
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if len(args) >= 2:
|
||||||
|
modifier_mask = modifier_mask_or_modifiers
|
||||||
|
modifiers = args[0]
|
||||||
|
handler = args[1]
|
||||||
|
if len(args) >= 3:
|
||||||
|
click_count = args[2]
|
||||||
|
if len(args) >= 4:
|
||||||
|
enabled = args[3]
|
||||||
|
elif len(args) == 1:
|
||||||
|
modifier_mask = defaultModifierMask
|
||||||
|
modifiers = modifier_mask_or_modifiers
|
||||||
|
handler = None
|
||||||
|
click_count = args[0]
|
||||||
|
else:
|
||||||
|
modifier_mask = defaultModifierMask
|
||||||
|
modifiers = modifier_mask_or_modifiers
|
||||||
|
handler = None
|
||||||
|
|
||||||
self.keysymstring = keysymstring
|
self.keysymstring = keysymstring
|
||||||
self.modifier_mask = modifier_mask
|
self.modifier_mask = modifier_mask
|
||||||
self.modifiers = modifiers
|
self.modifiers = modifiers
|
||||||
@@ -296,14 +327,21 @@ class KeyBinding:
|
|||||||
"""Returns the grab IDs for this KeyBinding."""
|
"""Returns the grab IDs for this KeyBinding."""
|
||||||
return self._grab_ids
|
return self._grab_ids
|
||||||
|
|
||||||
|
def set_grab_ids(self, grab_ids):
|
||||||
|
"""Sets the grab IDs for this KeyBinding."""
|
||||||
|
self._grab_ids = grab_ids
|
||||||
|
|
||||||
def has_grabs(self):
|
def has_grabs(self):
|
||||||
"""Returns True if there are existing grabs associated with this KeyBinding."""
|
"""Returns True if there are existing grabs associated with this KeyBinding."""
|
||||||
return bool(self._grab_ids)
|
return bool(self._grab_ids)
|
||||||
|
|
||||||
def add_grabs(self):
|
def add_grabs(self, cthulhu_modifiers=None):
|
||||||
"""Adds key grabs for this KeyBinding."""
|
"""Adds key grabs for this KeyBinding."""
|
||||||
from . import input_event_manager
|
from . import input_event_manager
|
||||||
self._grab_ids = input_event_manager.get_manager().add_grabs_for_keybinding(self)
|
self._grab_ids = input_event_manager.get_manager().add_grabs_for_keybinding(
|
||||||
|
self,
|
||||||
|
cthulhu_modifiers or settings.cthulhuModifierKeys,
|
||||||
|
)
|
||||||
|
|
||||||
def remove_grabs(self):
|
def remove_grabs(self):
|
||||||
"""Removes key grabs for this KeyBinding."""
|
"""Removes key grabs for this KeyBinding."""
|
||||||
@@ -311,21 +349,35 @@ class KeyBinding:
|
|||||||
input_event_manager.get_manager().remove_grabs_for_keybinding(self)
|
input_event_manager.get_manager().remove_grabs_for_keybinding(self)
|
||||||
self._grab_ids = []
|
self._grab_ids = []
|
||||||
|
|
||||||
def key_definitions(self):
|
def key_definitions(self, cthulhu_modifiers=None):
|
||||||
"""Return a list of Atspi key definitions for the given binding."""
|
"""Return a list of Atspi key definitions for the given binding."""
|
||||||
ret = []
|
ret = []
|
||||||
if not self.keycode:
|
if not self.keycode:
|
||||||
self.keyval, self.keycode = get_keycodes(self.keysymstring)
|
self.keyval, self.keycode = get_keycodes(self.keysymstring)
|
||||||
ret.extend(create_key_definitions(self.keycode, self.keyval, self.modifiers))
|
ret.extend(
|
||||||
|
create_key_definitions(
|
||||||
|
self.keycode,
|
||||||
|
self.keyval,
|
||||||
|
self.modifiers,
|
||||||
|
cthulhu_modifiers,
|
||||||
|
)
|
||||||
|
)
|
||||||
if CAN_USE_KEYSYMS and self.modifiers & SHIFT_MODIFIER_MASK:
|
if CAN_USE_KEYSYMS and self.modifiers & SHIFT_MODIFIER_MASK:
|
||||||
upper_keyval = Gdk.keyval_to_upper(self.keyval)
|
upper_keyval = Gdk.keyval_to_upper(self.keyval)
|
||||||
if upper_keyval != self.keyval:
|
if upper_keyval != self.keyval:
|
||||||
ret.extend(create_key_definitions(self.keycode, upper_keyval, self.modifiers))
|
ret.extend(
|
||||||
|
create_key_definitions(
|
||||||
|
self.keycode,
|
||||||
|
upper_keyval,
|
||||||
|
self.modifiers,
|
||||||
|
cthulhu_modifiers,
|
||||||
|
)
|
||||||
|
)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def keyDefs(self):
|
def keyDefs(self, cthulhu_modifiers=None):
|
||||||
"""Legacy wrapper. Use key_definitions() instead."""
|
"""Legacy wrapper. Use key_definitions() instead."""
|
||||||
return self.key_definitions()
|
return self.key_definitions(cthulhu_modifiers)
|
||||||
|
|
||||||
class KeyBindings:
|
class KeyBindings:
|
||||||
"""Structure that maintains a set of KeyBinding instances.
|
"""Structure that maintains a set of KeyBinding instances.
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ class LearnModePresenter:
|
|||||||
self.app = app
|
self.app = app
|
||||||
self._handlers = self._setup_handlers()
|
self._handlers = self._setup_handlers()
|
||||||
self._bindings = self._setup_bindings()
|
self._bindings = self._setup_bindings()
|
||||||
|
self._initialized = False
|
||||||
self._is_active = False
|
self._is_active = False
|
||||||
self._gui = None
|
self._gui = None
|
||||||
|
|
||||||
@@ -82,6 +83,27 @@ class LearnModePresenter:
|
|||||||
|
|
||||||
return self._handlers
|
return self._handlers
|
||||||
|
|
||||||
|
def set_up_commands(self):
|
||||||
|
"""Sets up commands with CommandManager."""
|
||||||
|
|
||||||
|
if self._initialized:
|
||||||
|
return
|
||||||
|
self._initialized = True
|
||||||
|
|
||||||
|
from . import command_manager
|
||||||
|
|
||||||
|
kb = keybindings.KeyBinding("h", keybindings.CTHULHU_MODIFIER_MASK)
|
||||||
|
command_manager.get_manager().add_command(
|
||||||
|
command_manager.KeyboardCommand(
|
||||||
|
"enter_learn_mode",
|
||||||
|
self.start,
|
||||||
|
guilabels.KB_GROUP_LEARN_MODE,
|
||||||
|
cmdnames.ENTER_LEARN_MODE,
|
||||||
|
desktop_keybinding=kb,
|
||||||
|
laptop_keybinding=kb,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
def _setup_handlers(self):
|
def _setup_handlers(self):
|
||||||
"""Sets up and returns the learn-mode-presenter input event handlers."""
|
"""Sets up and returns the learn-mode-presenter input event handlers."""
|
||||||
|
|
||||||
@@ -405,3 +427,5 @@ def getPresenter():
|
|||||||
_presenter = LearnModePresenter(cthulhu.cthulhuApp)
|
_presenter = LearnModePresenter(cthulhu.cthulhuApp)
|
||||||
return _presenter
|
return _presenter
|
||||||
|
|
||||||
|
|
||||||
|
get_presenter = getPresenter
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import html
|
import html
|
||||||
import re
|
import re
|
||||||
|
import sys
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
from gi.repository import Gio, GLib
|
from gi.repository import Gio, GLib
|
||||||
@@ -188,7 +189,8 @@ class MakoNotificationMonitor:
|
|||||||
return self._generation
|
return self._generation
|
||||||
|
|
||||||
def _get_presenter(self):
|
def _get_presenter(self):
|
||||||
presenter = self._presenter or notification_presenter.getPresenter()
|
presenter_module = sys.modules.get("cthulhu.notification_presenter", notification_presenter)
|
||||||
|
presenter = self._presenter or presenter_module.getPresenter()
|
||||||
if presenter is not self._presenter:
|
if presenter is not self._presenter:
|
||||||
self._presenter = presenter
|
self._presenter = presenter
|
||||||
|
|
||||||
|
|||||||
@@ -364,6 +364,7 @@ class MouseReviewer:
|
|||||||
self._useAtspi = False
|
self._useAtspi = False
|
||||||
self._handlers = self._setup_handlers()
|
self._handlers = self._setup_handlers()
|
||||||
self._bindings = self._setup_bindings()
|
self._bindings = self._setup_bindings()
|
||||||
|
self._initialized = False
|
||||||
|
|
||||||
atspiVersion = Atspi.get_version()
|
atspiVersion = Atspi.get_version()
|
||||||
capabilityEnum = getattr(Atspi, "DeviceCapability", None)
|
capabilityEnum = getattr(Atspi, "DeviceCapability", None)
|
||||||
@@ -434,6 +435,24 @@ class MouseReviewer:
|
|||||||
|
|
||||||
return self._handlers
|
return self._handlers
|
||||||
|
|
||||||
|
def set_up_commands(self):
|
||||||
|
"""Sets up commands with CommandManager."""
|
||||||
|
|
||||||
|
if self._initialized:
|
||||||
|
return
|
||||||
|
self._initialized = True
|
||||||
|
|
||||||
|
from . import command_manager, guilabels
|
||||||
|
|
||||||
|
command_manager.get_manager().add_command(
|
||||||
|
command_manager.KeyboardCommand(
|
||||||
|
"toggle_mouse_review",
|
||||||
|
self.toggle,
|
||||||
|
guilabels.KB_GROUP_MOUSE_REVIEW,
|
||||||
|
cmdnames.MOUSE_REVIEW_TOGGLE,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
def _setup_handlers(self):
|
def _setup_handlers(self):
|
||||||
"""Sets up and returns the mouse-review input event handlers."""
|
"""Sets up and returns the mouse-review input event handlers."""
|
||||||
|
|
||||||
@@ -831,3 +850,6 @@ def getReviewer():
|
|||||||
from . import cthulhu
|
from . import cthulhu
|
||||||
_reviewer = MouseReviewer(cthulhu.cthulhuApp)
|
_reviewer = MouseReviewer(cthulhu.cthulhuApp)
|
||||||
return _reviewer
|
return _reviewer
|
||||||
|
|
||||||
|
|
||||||
|
get_reviewer = getReviewer
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import time
|
import time
|
||||||
|
from dataclasses import dataclass, field
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
@@ -54,6 +55,28 @@ if TYPE_CHECKING:
|
|||||||
from .scripts import default
|
from .scripts import default
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class NotificationEntry:
|
||||||
|
"""A notification history entry."""
|
||||||
|
|
||||||
|
message: str
|
||||||
|
timestamp: float
|
||||||
|
source: str = ""
|
||||||
|
source_generation: int = 0
|
||||||
|
notification_id: int | None = None
|
||||||
|
live: bool = False
|
||||||
|
actions: dict[str, str] = field(default_factory=dict)
|
||||||
|
app_name: str = ""
|
||||||
|
summary: str = ""
|
||||||
|
body: str = ""
|
||||||
|
urgency: int = -1
|
||||||
|
desktop_entry: str = ""
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
yield self.message
|
||||||
|
yield self.timestamp
|
||||||
|
|
||||||
|
|
||||||
class NotificationPresenter:
|
class NotificationPresenter:
|
||||||
"""Provides access to the notification history."""
|
"""Provides access to the notification history."""
|
||||||
|
|
||||||
@@ -65,9 +88,10 @@ class NotificationPresenter:
|
|||||||
# the list. The current index is relative to, and used directly, with the
|
# the list. The current index is relative to, and used directly, with the
|
||||||
# python list, i.e. self._notifications[-3] would return the third-to-last
|
# python list, i.e. self._notifications[-3] would return the third-to-last
|
||||||
# notification message.
|
# notification message.
|
||||||
self._notifications: list[tuple[str, float]] = []
|
self._notifications: list[NotificationEntry] = []
|
||||||
self._current_index: int = -1
|
self._current_index: int = -1
|
||||||
self._initialized: bool = False
|
self._initialized: bool = False
|
||||||
|
self._mako_monitor = None
|
||||||
|
|
||||||
msg = "NOTIFICATION PRESENTER: Registering D-Bus commands."
|
msg = "NOTIFICATION PRESENTER: Registering D-Bus commands."
|
||||||
debug.print_message(debug.LEVEL_INFO, msg, True)
|
debug.print_message(debug.LEVEL_INFO, msg, True)
|
||||||
@@ -122,14 +146,42 @@ class NotificationPresenter:
|
|||||||
msg = "NOTIFICATION PRESENTER: Commands set up."
|
msg = "NOTIFICATION PRESENTER: Commands set up."
|
||||||
debug.print_message(debug.LEVEL_INFO, msg, True)
|
debug.print_message(debug.LEVEL_INFO, msg, True)
|
||||||
|
|
||||||
def save_notification(self, message: str) -> None:
|
def save_notification(
|
||||||
|
self,
|
||||||
|
message: str,
|
||||||
|
source: str = "",
|
||||||
|
source_generation: int = 0,
|
||||||
|
notification_id: int | None = None,
|
||||||
|
live: bool = False,
|
||||||
|
actions: dict[str, str] | None = None,
|
||||||
|
app_name: str = "",
|
||||||
|
summary: str = "",
|
||||||
|
body: str = "",
|
||||||
|
urgency: int = -1,
|
||||||
|
desktop_entry: str = "",
|
||||||
|
) -> NotificationEntry:
|
||||||
"""Adds message to the list of notification messages."""
|
"""Adds message to the list of notification messages."""
|
||||||
|
|
||||||
tokens = ["NOTIFICATION PRESENTER: Adding '", message, "'."]
|
tokens = ["NOTIFICATION PRESENTER: Adding '", message, "'."]
|
||||||
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
|
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
|
||||||
to_remove = max(len(self._notifications) - self._max_size + 1, 0)
|
entry = NotificationEntry(
|
||||||
|
message=message,
|
||||||
|
timestamp=time.time(),
|
||||||
|
source=source,
|
||||||
|
source_generation=source_generation,
|
||||||
|
notification_id=notification_id,
|
||||||
|
live=live,
|
||||||
|
actions=dict(actions or {}),
|
||||||
|
app_name=app_name,
|
||||||
|
summary=summary,
|
||||||
|
body=body,
|
||||||
|
urgency=urgency,
|
||||||
|
desktop_entry=desktop_entry,
|
||||||
|
)
|
||||||
|
self._notifications.append(entry)
|
||||||
|
to_remove = max(len(self._notifications) - self._max_size, 0)
|
||||||
self._notifications = self._notifications[to_remove:]
|
self._notifications = self._notifications[to_remove:]
|
||||||
self._notifications.append((message, time.time()))
|
return entry
|
||||||
|
|
||||||
def clear_list(self) -> None:
|
def clear_list(self) -> None:
|
||||||
"""Clears the notifications list."""
|
"""Clears the notifications list."""
|
||||||
@@ -155,6 +207,101 @@ class NotificationPresenter:
|
|||||||
days = round(diff / 86400)
|
days = round(diff / 86400)
|
||||||
return messages.days_ago(days)
|
return messages.days_ago(days)
|
||||||
|
|
||||||
|
def set_mako_monitor(self, monitor) -> None:
|
||||||
|
"""Sets the monitor used to control live Mako notifications."""
|
||||||
|
|
||||||
|
self._mako_monitor = monitor
|
||||||
|
|
||||||
|
def mark_source_unavailable(self, source: str) -> None:
|
||||||
|
"""Marks all entries from source as no longer live."""
|
||||||
|
|
||||||
|
for entry in self._notifications:
|
||||||
|
if entry.source == source:
|
||||||
|
entry.live = False
|
||||||
|
|
||||||
|
def sync_live_notifications(
|
||||||
|
self,
|
||||||
|
source: str,
|
||||||
|
notifications: dict[int, dict],
|
||||||
|
source_generation: int,
|
||||||
|
) -> None:
|
||||||
|
"""Updates the live state for notifications from source."""
|
||||||
|
|
||||||
|
for entry in self._notifications:
|
||||||
|
if entry.source != source or entry.source_generation != source_generation:
|
||||||
|
continue
|
||||||
|
|
||||||
|
data = notifications.get(entry.notification_id)
|
||||||
|
entry.live = data is not None
|
||||||
|
if data is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
entry.message = data.get("message", entry.message)
|
||||||
|
entry.actions = dict(data.get("actions", entry.actions) or {})
|
||||||
|
entry.app_name = data.get("app_name", entry.app_name)
|
||||||
|
entry.summary = data.get("summary", entry.summary)
|
||||||
|
entry.body = data.get("body", entry.body)
|
||||||
|
entry.urgency = data.get("urgency", entry.urgency)
|
||||||
|
entry.desktop_entry = data.get("desktop_entry", entry.desktop_entry)
|
||||||
|
|
||||||
|
def can_control_entry(self, entry: NotificationEntry | None) -> bool:
|
||||||
|
"""Returns True if entry is a live notification we can control."""
|
||||||
|
|
||||||
|
if entry is None or not entry.live or self._mako_monitor is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return self._mako_monitor.is_current_entry(entry)
|
||||||
|
|
||||||
|
def get_actions_for_entry(self, entry: NotificationEntry | None) -> dict[str, str]:
|
||||||
|
"""Returns actions for entry if it can still be controlled."""
|
||||||
|
|
||||||
|
if not self.can_control_entry(entry):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
return dict(entry.actions)
|
||||||
|
|
||||||
|
def dismiss_entry(self, script: default.Script, entry: NotificationEntry | None) -> bool:
|
||||||
|
"""Dismisses entry if it is still live."""
|
||||||
|
|
||||||
|
if (
|
||||||
|
not self.can_control_entry(entry)
|
||||||
|
or entry is None
|
||||||
|
or entry.notification_id is None
|
||||||
|
or self._mako_monitor is None
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not self._mako_monitor.dismiss_notification(entry.notification_id):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if entry in self._notifications:
|
||||||
|
self._notifications.remove(entry)
|
||||||
|
script.presentMessage(messages.NOTIFICATION_DISMISSED)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def invoke_action_for_entry(
|
||||||
|
self,
|
||||||
|
script: default.Script,
|
||||||
|
entry: NotificationEntry | None,
|
||||||
|
action_key: str,
|
||||||
|
) -> bool:
|
||||||
|
"""Invokes action_key for entry if it is still live."""
|
||||||
|
|
||||||
|
if (
|
||||||
|
not self.can_control_entry(entry)
|
||||||
|
or entry is None
|
||||||
|
or entry.notification_id is None
|
||||||
|
or action_key not in entry.actions
|
||||||
|
or self._mako_monitor is None
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not self._mako_monitor.invoke_action(entry.notification_id, action_key):
|
||||||
|
return False
|
||||||
|
|
||||||
|
script.presentMessage(messages.NOTIFICATION_ACTION_INVOKED)
|
||||||
|
return True
|
||||||
|
|
||||||
@dbus_service.command
|
@dbus_service.command
|
||||||
def present_last_notification(
|
def present_last_notification(
|
||||||
self,
|
self,
|
||||||
@@ -316,8 +463,8 @@ class NotificationPresenter:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
rows = [
|
rows = [
|
||||||
(message, self._timestamp_to_string(timestamp))
|
(entry.message, self._timestamp_to_string(entry.timestamp), entry)
|
||||||
for message, timestamp in reversed(self._notifications)
|
for entry in reversed(self._notifications)
|
||||||
]
|
]
|
||||||
title = guilabels.notifications_count(len(self._notifications))
|
title = guilabels.notifications_count(len(self._notifications))
|
||||||
column_headers = [
|
column_headers = [
|
||||||
@@ -330,6 +477,7 @@ class NotificationPresenter:
|
|||||||
column_headers,
|
column_headers,
|
||||||
rows,
|
rows,
|
||||||
self.on_dialog_destroyed,
|
self.on_dialog_destroyed,
|
||||||
|
self,
|
||||||
)
|
)
|
||||||
self._gui.show_gui()
|
self._gui.show_gui()
|
||||||
return True
|
return True
|
||||||
@@ -348,11 +496,17 @@ class NotificationListGUI:
|
|||||||
script: default.Script,
|
script: default.Script,
|
||||||
title: str,
|
title: str,
|
||||||
column_headers: list[str],
|
column_headers: list[str],
|
||||||
rows: list[tuple[str, str]],
|
rows: list[tuple[str, str, NotificationEntry]],
|
||||||
destroyed_callback: Callable[[Gtk.Dialog], None],
|
destroyed_callback: Callable[[Gtk.Dialog], None],
|
||||||
|
presenter: NotificationPresenter | None = None,
|
||||||
):
|
):
|
||||||
self._script: default.Script = script
|
self._script: default.Script = script
|
||||||
|
self._presenter: NotificationPresenter = presenter or get_presenter()
|
||||||
self._model: Gtk.ListStore | None = None
|
self._model: Gtk.ListStore | None = None
|
||||||
|
self._selection = None
|
||||||
|
self._dismiss_button = None
|
||||||
|
self._actions_box = None
|
||||||
|
self._actions_status_label = None
|
||||||
self._gui: Gtk.Dialog = self._create_dialog(title, column_headers, rows)
|
self._gui: Gtk.Dialog = self._create_dialog(title, column_headers, rows)
|
||||||
self._gui.connect("destroy", destroyed_callback)
|
self._gui.connect("destroy", destroyed_callback)
|
||||||
|
|
||||||
@@ -360,7 +514,7 @@ class NotificationListGUI:
|
|||||||
self,
|
self,
|
||||||
title: str,
|
title: str,
|
||||||
column_headers: list[str],
|
column_headers: list[str],
|
||||||
rows: list[tuple[str, str]],
|
rows: list[tuple[str, str, NotificationEntry]],
|
||||||
) -> Gtk.Dialog:
|
) -> Gtk.Dialog:
|
||||||
dialog = Gtk.Dialog(
|
dialog = Gtk.Dialog(
|
||||||
title,
|
title,
|
||||||
@@ -381,8 +535,9 @@ class NotificationListGUI:
|
|||||||
tree.set_hexpand(True)
|
tree.set_hexpand(True)
|
||||||
tree.set_vexpand(True)
|
tree.set_vexpand(True)
|
||||||
scrolled_window.add(tree) # pylint: disable=no-member
|
scrolled_window.add(tree) # pylint: disable=no-member
|
||||||
|
self._selection = tree.get_selection()
|
||||||
|
|
||||||
cols = len(column_headers) * [GObject.TYPE_STRING]
|
cols = len(column_headers) * [GObject.TYPE_STRING] + [GObject.TYPE_PYOBJECT]
|
||||||
for i, header in enumerate(column_headers):
|
for i, header in enumerate(column_headers):
|
||||||
cell = Gtk.CellRendererText()
|
cell = Gtk.CellRendererText()
|
||||||
column = Gtk.TreeViewColumn(header, cell, text=i)
|
column = Gtk.TreeViewColumn(header, cell, text=i)
|
||||||
@@ -400,6 +555,93 @@ class NotificationListGUI:
|
|||||||
dialog.connect("response", self.on_response)
|
dialog.connect("response", self.on_response)
|
||||||
return dialog
|
return dialog
|
||||||
|
|
||||||
|
def _get_selected_entry(self) -> NotificationEntry | None:
|
||||||
|
if self._model is None or self._selection is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
model, paths = self._selection.get_selected_rows()
|
||||||
|
if not paths:
|
||||||
|
return None
|
||||||
|
|
||||||
|
row_iter = model.get_iter(paths[0])
|
||||||
|
if row_iter is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return model.get_value(row_iter, 2)
|
||||||
|
|
||||||
|
def _dismiss_selected_notification(self) -> None:
|
||||||
|
if self._model is None or self._selection is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
model, paths = self._selection.get_selected_rows()
|
||||||
|
if not paths:
|
||||||
|
return
|
||||||
|
|
||||||
|
row_iter = model.get_iter(paths[0])
|
||||||
|
if row_iter is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
entry = model.get_value(row_iter, 2)
|
||||||
|
if not self._presenter.dismiss_entry(self._script, entry):
|
||||||
|
self._update_action_buttons()
|
||||||
|
return
|
||||||
|
|
||||||
|
has_next_row = model.remove(row_iter)
|
||||||
|
if model.iter_n_children(None):
|
||||||
|
if has_next_row:
|
||||||
|
self._selection.select_path(row_iter)
|
||||||
|
else:
|
||||||
|
self._selection.select_path(model.iter_n_children(None) - 1)
|
||||||
|
|
||||||
|
self._update_action_buttons()
|
||||||
|
|
||||||
|
def _clear_action_buttons(self) -> None:
|
||||||
|
if self._actions_box is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
for child in self._actions_box.get_children():
|
||||||
|
self._actions_box.remove(child)
|
||||||
|
|
||||||
|
def _on_action_button_clicked(
|
||||||
|
self,
|
||||||
|
_button: Gtk.Button,
|
||||||
|
entry: NotificationEntry,
|
||||||
|
action_key: str,
|
||||||
|
) -> None:
|
||||||
|
self._presenter.invoke_action_for_entry(self._script, entry, action_key)
|
||||||
|
|
||||||
|
def _update_action_buttons(self) -> None:
|
||||||
|
if (
|
||||||
|
self._dismiss_button is None
|
||||||
|
or self._actions_box is None
|
||||||
|
or self._actions_status_label is None
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
self._clear_action_buttons()
|
||||||
|
entry = self._get_selected_entry()
|
||||||
|
can_control = self._presenter.can_control_entry(entry)
|
||||||
|
self._dismiss_button.set_sensitive(can_control)
|
||||||
|
|
||||||
|
if not can_control:
|
||||||
|
self._actions_status_label.set_text(messages.NOTIFICATION_UNAVAILABLE)
|
||||||
|
self._actions_status_label.set_visible(True)
|
||||||
|
return
|
||||||
|
|
||||||
|
actions = self._presenter.get_actions_for_entry(entry)
|
||||||
|
if not actions:
|
||||||
|
self._actions_status_label.set_text(messages.NOTIFICATION_NO_ACTIONS)
|
||||||
|
self._actions_status_label.set_visible(True)
|
||||||
|
return
|
||||||
|
|
||||||
|
self._actions_status_label.set_visible(False)
|
||||||
|
for action_key, label in actions.items():
|
||||||
|
button = Gtk.Button(label=label)
|
||||||
|
button.set_margin_top(6)
|
||||||
|
button.connect("clicked", self._on_action_button_clicked, entry, action_key)
|
||||||
|
self._actions_box.pack_start(button, False, False, 0)
|
||||||
|
self._actions_box.show_all()
|
||||||
|
|
||||||
def on_response(self, _dialog: Gtk.Dialog, response: int) -> None:
|
def on_response(self, _dialog: Gtk.Dialog, response: int) -> None:
|
||||||
"""The handler for the 'response' signal."""
|
"""The handler for the 'response' signal."""
|
||||||
|
|
||||||
@@ -428,3 +670,6 @@ def get_presenter() -> NotificationPresenter:
|
|||||||
"""Returns the Notification Presenter"""
|
"""Returns the Notification Presenter"""
|
||||||
|
|
||||||
return _presenter
|
return _presenter
|
||||||
|
|
||||||
|
|
||||||
|
getPresenter = get_presenter
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ from . import (
|
|||||||
braille_generator,
|
braille_generator,
|
||||||
chat_presenter,
|
chat_presenter,
|
||||||
debug,
|
debug,
|
||||||
|
formatting,
|
||||||
script_utilities,
|
script_utilities,
|
||||||
sound_generator,
|
sound_generator,
|
||||||
speech_generator,
|
speech_generator,
|
||||||
@@ -62,6 +63,7 @@ class Script:
|
|||||||
|
|
||||||
self.listeners = self.get_listeners()
|
self.listeners = self.get_listeners()
|
||||||
self.utilities = self.get_utilities()
|
self.utilities = self.get_utilities()
|
||||||
|
self.formatting = self.get_formatting()
|
||||||
|
|
||||||
self._braille_generator = self._create_braille_generator()
|
self._braille_generator = self._create_braille_generator()
|
||||||
self._sound_generator = self._create_sound_generator()
|
self._sound_generator = self._create_sound_generator()
|
||||||
@@ -137,6 +139,18 @@ class Script:
|
|||||||
|
|
||||||
return braille_generator.BrailleGenerator(self)
|
return braille_generator.BrailleGenerator(self)
|
||||||
|
|
||||||
|
def get_formatting(self) -> formatting.Formatting:
|
||||||
|
"""Returns the formatting strings for this script."""
|
||||||
|
|
||||||
|
if type(self).getFormatting is not Script.getFormatting:
|
||||||
|
return self.getFormatting()
|
||||||
|
return formatting.Formatting(self)
|
||||||
|
|
||||||
|
def getFormatting(self) -> formatting.Formatting:
|
||||||
|
"""Returns the formatting strings for this script."""
|
||||||
|
|
||||||
|
return formatting.Formatting(self)
|
||||||
|
|
||||||
def _create_sound_generator(self) -> sound_generator.SoundGenerator:
|
def _create_sound_generator(self) -> sound_generator.SoundGenerator:
|
||||||
"""Creates and returns the sound generator for this script."""
|
"""Creates and returns the sound generator for this script."""
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ class SleepModeManager:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._handlers = self.getHandlers(True)
|
self._handlers = self.getHandlers(True)
|
||||||
self._bindings = keybindings.KeyBindings()
|
self._bindings = keybindings.KeyBindings()
|
||||||
|
self._initialized = False
|
||||||
self._apps = set()
|
self._apps = set()
|
||||||
self._disabledAutoSleepApps = set()
|
self._disabledAutoSleepApps = set()
|
||||||
self._autoSleepAppNames = set()
|
self._autoSleepAppNames = set()
|
||||||
@@ -80,6 +81,31 @@ class SleepModeManager:
|
|||||||
|
|
||||||
return self._handlers
|
return self._handlers
|
||||||
|
|
||||||
|
def set_up_commands(self):
|
||||||
|
"""Sets up commands with CommandManager."""
|
||||||
|
|
||||||
|
if self._initialized:
|
||||||
|
return
|
||||||
|
self._initialized = True
|
||||||
|
|
||||||
|
import cthulhu.command_manager as command_manager
|
||||||
|
import cthulhu.guilabels as guilabels
|
||||||
|
|
||||||
|
kb = keybindings.KeyBinding(
|
||||||
|
"q",
|
||||||
|
keybindings.CTHULHU_CTRL_MODIFIER_MASK | keybindings.SHIFT_MODIFIER_MASK,
|
||||||
|
)
|
||||||
|
command_manager.get_manager().add_command(
|
||||||
|
command_manager.KeyboardCommand(
|
||||||
|
"toggle_sleep_mode",
|
||||||
|
self.toggleSleepMode,
|
||||||
|
guilabels.KB_GROUP_SLEEP_MODE,
|
||||||
|
cmdnames.TOGGLE_SLEEP_MODE,
|
||||||
|
desktop_keybinding=kb,
|
||||||
|
laptop_keybinding=kb,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
def isActiveForApp(self, app):
|
def isActiveForApp(self, app):
|
||||||
"""Returns True if sleep mode is active for app."""
|
"""Returns True if sleep mode is active for app."""
|
||||||
|
|
||||||
@@ -96,6 +122,11 @@ class SleepModeManager:
|
|||||||
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def is_active_for_app(self, app):
|
||||||
|
"""Returns True if sleep mode is active for app."""
|
||||||
|
|
||||||
|
return self.isActiveForApp(app)
|
||||||
|
|
||||||
def _getAutoSleepPath(self):
|
def _getAutoSleepPath(self):
|
||||||
prefsDir = os.path.join(GLib.get_user_data_dir(), "cthulhu")
|
prefsDir = os.path.join(GLib.get_user_data_dir(), "cthulhu")
|
||||||
try:
|
try:
|
||||||
@@ -301,3 +332,6 @@ _manager = SleepModeManager()
|
|||||||
def getManager():
|
def getManager():
|
||||||
"""Returns the Sleep Mode Manager singleton."""
|
"""Returns the Sleep Mode Manager singleton."""
|
||||||
return _manager
|
return _manager
|
||||||
|
|
||||||
|
|
||||||
|
get_manager = getManager
|
||||||
|
|||||||
+29
-2
@@ -76,6 +76,9 @@ _timestamp: float = 0.0
|
|||||||
|
|
||||||
# Optional callback for live monitoring of spoken text.
|
# Optional callback for live monitoring of spoken text.
|
||||||
_monitorWriteTextCallback: Optional[Callable[[str], None]] = None
|
_monitorWriteTextCallback: Optional[Callable[[str], None]] = None
|
||||||
|
_monitorWriteKeyCallback: Optional[Callable[[str], None]] = None
|
||||||
|
_monitorBeginGroupCallback: Optional[Callable[[], None]] = None
|
||||||
|
_monitorEndGroupCallback: Optional[Callable[[], None]] = None
|
||||||
|
|
||||||
def _isSpeechDispatcherFactory(moduleName: Optional[str]) -> bool:
|
def _isSpeechDispatcherFactory(moduleName: Optional[str]) -> bool:
|
||||||
if not moduleName:
|
if not moduleName:
|
||||||
@@ -310,6 +313,10 @@ def getSpeechServer() -> Optional[SpeechServer]:
|
|||||||
"""Returns the speech server instance."""
|
"""Returns the speech server instance."""
|
||||||
return _speechserver
|
return _speechserver
|
||||||
|
|
||||||
|
def get_speech_server() -> Optional[SpeechServer]:
|
||||||
|
"""Returns the speech server instance."""
|
||||||
|
return getSpeechServer()
|
||||||
|
|
||||||
def setSpeechServer(speechServer: SpeechServer) -> None:
|
def setSpeechServer(speechServer: SpeechServer) -> None:
|
||||||
"""Sets the speech server to be used.
|
"""Sets the speech server to be used.
|
||||||
|
|
||||||
@@ -320,10 +327,30 @@ def setSpeechServer(speechServer: SpeechServer) -> None:
|
|||||||
_speechserver = speechServer
|
_speechserver = speechServer
|
||||||
_refreshEchoSpeechServer()
|
_refreshEchoSpeechServer()
|
||||||
|
|
||||||
def set_monitor_callbacks(writeText: Optional[Callable[[str], None]] = None) -> None:
|
def set_server(speech_server: Optional[SpeechServer]) -> None:
|
||||||
|
"""Sets the speech server to be used."""
|
||||||
|
setSpeechServer(speech_server)
|
||||||
|
|
||||||
|
def set_monitor_callbacks(
|
||||||
|
writeText: Optional[Callable[[str], None]] = None,
|
||||||
|
writeKey: Optional[Callable[[str], None]] = None,
|
||||||
|
beginGroup: Optional[Callable[[], None]] = None,
|
||||||
|
endGroup: Optional[Callable[[], None]] = None,
|
||||||
|
*,
|
||||||
|
write_text: Optional[Callable[[str], None]] = None,
|
||||||
|
write_key: Optional[Callable[[str], None]] = None,
|
||||||
|
begin_group: Optional[Callable[[], None]] = None,
|
||||||
|
end_group: Optional[Callable[[], None]] = None,
|
||||||
|
) -> None:
|
||||||
"""Sets runtime callbacks for live speech monitoring."""
|
"""Sets runtime callbacks for live speech monitoring."""
|
||||||
global _monitorWriteTextCallback
|
global _monitorWriteTextCallback
|
||||||
_monitorWriteTextCallback = writeText
|
global _monitorWriteKeyCallback
|
||||||
|
global _monitorBeginGroupCallback
|
||||||
|
global _monitorEndGroupCallback
|
||||||
|
_monitorWriteTextCallback = write_text if write_text is not None else writeText
|
||||||
|
_monitorWriteKeyCallback = write_key if write_key is not None else writeKey
|
||||||
|
_monitorBeginGroupCallback = begin_group if begin_group is not None else beginGroup
|
||||||
|
_monitorEndGroupCallback = end_group if end_group is not None else endGroup
|
||||||
|
|
||||||
def _write_to_monitor(text: str) -> None:
|
def _write_to_monitor(text: str) -> None:
|
||||||
"""Writes text to the active speech monitor callback if set."""
|
"""Writes text to the active speech monitor callback if set."""
|
||||||
|
|||||||
@@ -207,6 +207,11 @@ class SpeechServer(object):
|
|||||||
"""Returns a localized name describing this factory."""
|
"""Returns a localized name describing this factory."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_factory_name(cls):
|
||||||
|
"""Returns a localized name describing this factory."""
|
||||||
|
return cls.getFactoryName()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def getSpeechServers():
|
def getSpeechServers():
|
||||||
"""Gets available speech servers as a list. The caller
|
"""Gets available speech servers as a list. The caller
|
||||||
@@ -215,6 +220,11 @@ class SpeechServer(object):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_speech_servers(cls):
|
||||||
|
"""Gets available speech servers as a list."""
|
||||||
|
return cls.getSpeechServers()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def getSpeechServer(info):
|
def getSpeechServer(info):
|
||||||
"""Gets a given SpeechServer based upon the info.
|
"""Gets a given SpeechServer based upon the info.
|
||||||
@@ -222,12 +232,22 @@ class SpeechServer(object):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_speech_server(cls, info=None):
|
||||||
|
"""Gets a given SpeechServer based upon the info."""
|
||||||
|
return cls.getSpeechServer(info)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def shutdownActiveServers():
|
def shutdownActiveServers():
|
||||||
"""Cleans up and shuts down this factory.
|
"""Cleans up and shuts down this factory.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def shutdown_active_servers(cls):
|
||||||
|
"""Cleans up and shuts down this factory."""
|
||||||
|
return cls.shutdownActiveServers()
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -236,11 +256,72 @@ class SpeechServer(object):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def get_info(self):
|
||||||
|
"""Returns [name, id]."""
|
||||||
|
return self.getInfo()
|
||||||
|
|
||||||
def getVoiceFamilies(self):
|
def getVoiceFamilies(self):
|
||||||
"""Returns a list of VoiceFamily instances representing all
|
"""Returns a list of VoiceFamily instances representing all
|
||||||
voice families known by the speech server."""
|
voice families known by the speech server."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def get_voice_families(self):
|
||||||
|
"""Returns a list of VoiceFamily instances representing all voice families."""
|
||||||
|
return self.getVoiceFamilies()
|
||||||
|
|
||||||
|
def get_voice_families_for_language(self, language, dialect="", variant="", maximum=None):
|
||||||
|
"""Returns available voice families for the specified language."""
|
||||||
|
if not dialect and variant:
|
||||||
|
dialect = variant
|
||||||
|
method = getattr(self, "getVoiceFamiliesForLanguage", None)
|
||||||
|
if method is None:
|
||||||
|
return []
|
||||||
|
return method(language, dialect, maximum=maximum)
|
||||||
|
|
||||||
|
def _get_current_voice_properties(self):
|
||||||
|
if hasattr(self, "_current_voice_properties"):
|
||||||
|
return self._current_voice_properties
|
||||||
|
if hasattr(self, "_currentVoiceProperties"):
|
||||||
|
return self._currentVoiceProperties
|
||||||
|
self._current_voice_properties = {}
|
||||||
|
return self._current_voice_properties
|
||||||
|
|
||||||
|
def set_default_voice(self, voice):
|
||||||
|
"""Sets the default voice properties."""
|
||||||
|
if voice is None:
|
||||||
|
voice = {}
|
||||||
|
elif hasattr(voice, "copy"):
|
||||||
|
voice = voice.copy()
|
||||||
|
else:
|
||||||
|
voice = dict(voice)
|
||||||
|
|
||||||
|
if hasattr(self, "_currentVoiceProperties"):
|
||||||
|
self._currentVoiceProperties = voice
|
||||||
|
else:
|
||||||
|
self._current_voice_properties = voice
|
||||||
|
|
||||||
|
def clear_cached_voice_properties(self):
|
||||||
|
"""Clears cached voice properties so they are applied on the next utterance."""
|
||||||
|
self._get_current_voice_properties().clear()
|
||||||
|
|
||||||
|
def get_voice_family(self):
|
||||||
|
"""Returns the active voice family."""
|
||||||
|
from .acss import ACSS
|
||||||
|
|
||||||
|
return self._get_current_voice_properties().get(ACSS.FAMILY)
|
||||||
|
|
||||||
|
def set_voice_family(self, voice_family):
|
||||||
|
"""Sets the active voice family."""
|
||||||
|
from .acss import ACSS
|
||||||
|
|
||||||
|
voice = self._get_current_voice_properties().copy()
|
||||||
|
voice[ACSS.FAMILY] = voice_family
|
||||||
|
self.set_default_voice(voice)
|
||||||
|
|
||||||
|
setter = getattr(self, "_set_family", None) or getattr(self, "_setFamily", None)
|
||||||
|
if setter is not None:
|
||||||
|
setter(voice_family)
|
||||||
|
|
||||||
def speakCharacter(self, character, acss=None):
|
def speakCharacter(self, character, acss=None):
|
||||||
"""Speaks a single character immediately.
|
"""Speaks a single character immediately.
|
||||||
|
|
||||||
@@ -254,6 +335,10 @@ class SpeechServer(object):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def speak_character(self, character, acss=None):
|
||||||
|
"""Speaks a single character immediately."""
|
||||||
|
return self.speakCharacter(character, acss)
|
||||||
|
|
||||||
def speakKeyEvent(self, event, acss=None):
|
def speakKeyEvent(self, event, acss=None):
|
||||||
"""Speaks a key event immediately.
|
"""Speaks a key event immediately.
|
||||||
|
|
||||||
@@ -262,6 +347,10 @@ class SpeechServer(object):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def speak_key_event(self, event, acss=None):
|
||||||
|
"""Speaks a key event immediately."""
|
||||||
|
return self.speakKeyEvent(event, acss)
|
||||||
|
|
||||||
def speak(self, text=None, acss=None, interrupt=True):
|
def speak(self, text=None, acss=None, interrupt=True):
|
||||||
"""Speaks all queued text immediately. If text is not None,
|
"""Speaks all queued text immediately. If text is not None,
|
||||||
it is added to the queue before speaking.
|
it is added to the queue before speaking.
|
||||||
@@ -294,34 +383,84 @@ class SpeechServer(object):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def say_all(self, utterance_iterator, progress_callback):
|
||||||
|
"""Speaks each utterance from the given iterator."""
|
||||||
|
return self.sayAll(utterance_iterator, progress_callback)
|
||||||
|
|
||||||
def increaseSpeechRate(self, step=5):
|
def increaseSpeechRate(self, step=5):
|
||||||
"""Increases the speech rate.
|
"""Increases the speech rate.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def increase_speech_rate(self, step=5):
|
||||||
|
"""Increases the speech rate."""
|
||||||
|
return self.increaseSpeechRate(step)
|
||||||
|
|
||||||
def decreaseSpeechRate(self, step=5):
|
def decreaseSpeechRate(self, step=5):
|
||||||
"""Decreases the speech rate.
|
"""Decreases the speech rate.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def decrease_speech_rate(self, step=5):
|
||||||
|
"""Decreases the speech rate."""
|
||||||
|
return self.decreaseSpeechRate(step)
|
||||||
|
|
||||||
def increaseSpeechPitch(self, step=0.5):
|
def increaseSpeechPitch(self, step=0.5):
|
||||||
"""Increases the speech pitch.
|
"""Increases the speech pitch.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def increase_speech_pitch(self, step=0.5):
|
||||||
|
"""Increases the speech pitch."""
|
||||||
|
return self.increaseSpeechPitch(step)
|
||||||
|
|
||||||
def decreaseSpeechPitch(self, step=0.5):
|
def decreaseSpeechPitch(self, step=0.5):
|
||||||
"""Decreases the speech pitch.
|
"""Decreases the speech pitch.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def decrease_speech_pitch(self, step=0.5):
|
||||||
|
"""Decreases the speech pitch."""
|
||||||
|
return self.decreaseSpeechPitch(step)
|
||||||
|
|
||||||
|
def increase_speech_volume(self, step=0.5):
|
||||||
|
"""Increases the speech volume."""
|
||||||
|
method = getattr(self, "increaseSpeechVolume", None)
|
||||||
|
if method is not None:
|
||||||
|
return method(step)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def decrease_speech_volume(self, step=0.5):
|
||||||
|
"""Decreases the speech volume."""
|
||||||
|
method = getattr(self, "decreaseSpeechVolume", None)
|
||||||
|
if method is not None:
|
||||||
|
return method(step)
|
||||||
|
return None
|
||||||
|
|
||||||
def updateCapitalizationStyle(self):
|
def updateCapitalizationStyle(self):
|
||||||
"""Updates the capitalization style used by the speech server."""
|
"""Updates the capitalization style used by the speech server."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def update_capitalization_style(self, style=None):
|
||||||
|
"""Updates the capitalization style used by the speech server."""
|
||||||
|
if style is not None:
|
||||||
|
from . import settings
|
||||||
|
|
||||||
|
settings.capitalizationStyle = getattr(style, "value", style)
|
||||||
|
return self.updateCapitalizationStyle()
|
||||||
|
|
||||||
def updatePunctuationLevel(self):
|
def updatePunctuationLevel(self):
|
||||||
"""Punctuation level changed, inform this speechServer."""
|
"""Punctuation level changed, inform this speechServer."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def update_punctuation_level(self, level=None):
|
||||||
|
"""Updates the punctuation level used by the speech server."""
|
||||||
|
if level is not None:
|
||||||
|
from . import settings
|
||||||
|
|
||||||
|
settings.verbalizePunctuationStyle = getattr(level, "value", level)
|
||||||
|
return self.updatePunctuationLevel()
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
"""Stops ongoing speech and flushes the queue."""
|
"""Stops ongoing speech and flushes the queue."""
|
||||||
pass
|
pass
|
||||||
@@ -333,3 +472,21 @@ class SpeechServer(object):
|
|||||||
def reset(self, text=None, acss=None):
|
def reset(self, text=None, acss=None):
|
||||||
"""Resets the speech engine."""
|
"""Resets the speech engine."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def get_output_module(self):
|
||||||
|
"""Returns the current output module."""
|
||||||
|
method = getattr(self, "getOutputModule", None)
|
||||||
|
if method is not None:
|
||||||
|
return method()
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def set_output_module(self, module):
|
||||||
|
"""Sets the current output module."""
|
||||||
|
method = getattr(self, "setOutputModule", None)
|
||||||
|
if method is not None:
|
||||||
|
return method(module)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def list_output_modules(self):
|
||||||
|
"""Returns available output modules."""
|
||||||
|
return ()
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
"""Regression tests for BrlAPI command exports."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from cthulhu import braille
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_braille_exports_brlapi_command_aliases():
|
||||||
|
expected_names = [
|
||||||
|
"BRLAPI_KEY_CMD_HWINLT",
|
||||||
|
"BRLAPI_KEY_CMD_FWINLT",
|
||||||
|
"BRLAPI_KEY_CMD_FWINLTSKIP",
|
||||||
|
"BRLAPI_KEY_CMD_HWINRT",
|
||||||
|
"BRLAPI_KEY_CMD_FWINRT",
|
||||||
|
"BRLAPI_KEY_CMD_FWINRTSKIP",
|
||||||
|
"BRLAPI_KEY_CMD_LNUP",
|
||||||
|
"BRLAPI_KEY_CMD_LNDN",
|
||||||
|
"BRLAPI_KEY_CMD_FREEZE",
|
||||||
|
"BRLAPI_KEY_CMD_TOP_LEFT",
|
||||||
|
"BRLAPI_KEY_CMD_BOT_LEFT",
|
||||||
|
"BRLAPI_KEY_CMD_HOME",
|
||||||
|
"BRLAPI_KEY_CMD_SIXDOTS",
|
||||||
|
"BRLAPI_KEY_CMD_ROUTE",
|
||||||
|
"BRLAPI_KEY_CMD_CUTBEGIN",
|
||||||
|
"BRLAPI_KEY_CMD_CUTLINE",
|
||||||
|
]
|
||||||
|
|
||||||
|
for name in expected_names:
|
||||||
|
assert hasattr(braille, name), name
|
||||||
|
|
||||||
|
if braille._brlAPIAvailable:
|
||||||
|
assert braille.BRLAPI_KEY_CMD_HWINLT == braille.brlapi.KEY_CMD_HWINLT
|
||||||
|
assert braille.BRLAPI_KEY_CMD_ROUTE == braille.brlapi.KEY_CMD_ROUTE
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
"""Regression tests for braille API compatibility."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from cthulhu import braille
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_braille_exports_snake_case_compatibility_apis():
|
||||||
|
expected_names = [
|
||||||
|
"check_braille_setting",
|
||||||
|
"disable_braille",
|
||||||
|
"display_line",
|
||||||
|
"display_message",
|
||||||
|
"get_caret_context",
|
||||||
|
"get_default_contraction_table",
|
||||||
|
"get_region_at_cell",
|
||||||
|
"is_flash_active",
|
||||||
|
"kill_flash",
|
||||||
|
"pan_left",
|
||||||
|
"pan_right",
|
||||||
|
"process_routing_key",
|
||||||
|
"return_to_region_with_focus",
|
||||||
|
"set_brlapi_priority",
|
||||||
|
"set_contraction_table",
|
||||||
|
"set_enable_braille",
|
||||||
|
"set_enable_computer_braille_at_cursor",
|
||||||
|
"set_enable_contracted_braille",
|
||||||
|
"set_enable_eol",
|
||||||
|
"set_enable_word_wrap",
|
||||||
|
"set_link_indicator",
|
||||||
|
"set_monitor_callback",
|
||||||
|
"set_selector_indicator",
|
||||||
|
"set_text_attributes_indicator",
|
||||||
|
"setup_key_ranges",
|
||||||
|
"toggle_contracted_braille",
|
||||||
|
"try_reposition_cursor",
|
||||||
|
]
|
||||||
|
|
||||||
|
for name in expected_names:
|
||||||
|
assert hasattr(braille, name), name
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_line_exports_snake_case_helpers():
|
||||||
|
line = braille.Line()
|
||||||
|
first = braille.Region("first")
|
||||||
|
second = braille.Region("second")
|
||||||
|
|
||||||
|
line.add_region(first)
|
||||||
|
line.add_regions([second])
|
||||||
|
|
||||||
|
assert line.regions == [first, second]
|
||||||
|
assert line.get_line_info() == line.getLineInfo()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_setup_key_ranges_delegates_to_legacy_api(monkeypatch):
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
def setup_key_ranges(keys):
|
||||||
|
calls.append(keys)
|
||||||
|
|
||||||
|
monkeypatch.setattr(braille, "setupKeyRanges", setup_key_ranges)
|
||||||
|
|
||||||
|
braille.setup_key_ranges({1, 2})
|
||||||
|
|
||||||
|
assert calls == [{1, 2}]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_snake_case_runtime_setters_update_legacy_settings(monkeypatch):
|
||||||
|
monkeypatch.setattr(braille.settings, "enableBraille", True)
|
||||||
|
monkeypatch.setattr(braille.settings, "enableContractedBraille", False)
|
||||||
|
monkeypatch.setattr(braille.settings, "brailleContractionTable", "")
|
||||||
|
monkeypatch.setattr(braille.settings, "disableBrailleEOL", False)
|
||||||
|
monkeypatch.setattr(braille.settings, "enableBrailleWordWrap", False)
|
||||||
|
monkeypatch.setattr(braille.settings, "brailleSelectorIndicator", 0)
|
||||||
|
monkeypatch.setattr(braille.settings, "brailleLinkIndicator", 0)
|
||||||
|
monkeypatch.setattr(braille.settings, "textAttributesBrailleIndicator", 0)
|
||||||
|
|
||||||
|
braille.set_enable_braille(False)
|
||||||
|
braille.set_enable_contracted_braille(True)
|
||||||
|
braille.set_contraction_table("/tmp/table.ctb")
|
||||||
|
braille.set_enable_eol(False)
|
||||||
|
braille.set_enable_word_wrap(True)
|
||||||
|
braille.set_selector_indicator(64)
|
||||||
|
braille.set_link_indicator(128)
|
||||||
|
braille.set_text_attributes_indicator(192)
|
||||||
|
|
||||||
|
assert braille.settings.enableBraille is False
|
||||||
|
assert braille.settings.enableContractedBraille is True
|
||||||
|
assert braille.settings.brailleContractionTable == "/tmp/table.ctb"
|
||||||
|
assert braille.settings.disableBrailleEOL is True
|
||||||
|
assert braille.settings.enableBrailleWordWrap is True
|
||||||
|
assert braille.settings.brailleSelectorIndicator == 64
|
||||||
|
assert braille.settings.brailleLinkIndicator == 128
|
||||||
|
assert braille.settings.textAttributesBrailleIndicator == 192
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_display_message_accepts_snake_case_keyword(monkeypatch):
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
def display_message(message, cursor=-1, flashTime=0):
|
||||||
|
calls.append((message, cursor, flashTime))
|
||||||
|
|
||||||
|
monkeypatch.setattr(braille, "displayMessage", display_message)
|
||||||
|
|
||||||
|
braille.display_message("hello", flash_time=250)
|
||||||
|
|
||||||
|
assert calls == [("hello", -1, 250)]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_kill_flash_accepts_snake_case_keyword(monkeypatch):
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
def kill_flash(restoreSaved=True):
|
||||||
|
calls.append(restoreSaved)
|
||||||
|
|
||||||
|
monkeypatch.setattr(braille, "killFlash", kill_flash)
|
||||||
|
|
||||||
|
braille.kill_flash(restore_saved=False)
|
||||||
|
|
||||||
|
assert calls == [False]
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
"""Regression tests for command-name exports."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from cthulhu import cmdnames
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"name",
|
||||||
|
[
|
||||||
|
"BYPASS_MODE_TOGGLE",
|
||||||
|
"CHAT_NEXT_MESSAGE",
|
||||||
|
"CLIPBOARD_PRESENT_CONTENTS",
|
||||||
|
"DEBUG_CLEAR_ATSPI_CACHE_FOR_APPLICATION",
|
||||||
|
"DEBUG_CYCLE_LEVEL",
|
||||||
|
"LIVE_REGIONS_ARE_ANNOUNCED",
|
||||||
|
"LIVE_REGIONS_NEXT",
|
||||||
|
"LIVE_REGIONS_PREVIOUS",
|
||||||
|
"PRESENT_BATTERY_STATUS",
|
||||||
|
"PRESENT_CELL_FORMULA",
|
||||||
|
"PRESENT_CPU_AND_MEMORY_USAGE",
|
||||||
|
"PRESENT_CURRENT_PROFILE",
|
||||||
|
"STRUCTURAL_NAVIGATION_MODE_CYCLE",
|
||||||
|
"TABLE_CELL_BEGINNING_OF_ROW",
|
||||||
|
"TABLE_CELL_BOTTOM_OF_COLUMN",
|
||||||
|
"TABLE_CELL_END_OF_ROW",
|
||||||
|
"TABLE_CELL_TOP_OF_COLUMN",
|
||||||
|
"TABLE_NAVIGATION_TOGGLE",
|
||||||
|
"TOGGLE_BRAILLE_MONITOR",
|
||||||
|
"TOGGLE_KEYBOARD_LAYOUT",
|
||||||
|
"TOGGLE_SPEECH_MONITOR",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_referenced_command_name_is_exported(name: str) -> None:
|
||||||
|
assert getattr(cmdnames, name)
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
"""Regression tests for focus manager API compatibility."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from cthulhu import focus_manager
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_is_in_preferences_window_tracks_cthulhu_active_window(monkeypatch):
|
||||||
|
manager = focus_manager.FocusManager.__new__(focus_manager.FocusManager)
|
||||||
|
window = object()
|
||||||
|
app = object()
|
||||||
|
manager._window = window
|
||||||
|
|
||||||
|
monkeypatch.setattr(focus_manager.AXObject, "get_application", lambda obj: app)
|
||||||
|
monkeypatch.setattr(focus_manager.AXObject, "get_name", lambda obj: "Cthulhu")
|
||||||
|
|
||||||
|
assert manager.is_in_preferences_window() is True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_is_in_preferences_window_is_false_for_non_cthulhu_window(monkeypatch):
|
||||||
|
manager = focus_manager.FocusManager.__new__(focus_manager.FocusManager)
|
||||||
|
window = object()
|
||||||
|
app = object()
|
||||||
|
manager._window = window
|
||||||
|
|
||||||
|
monkeypatch.setattr(focus_manager.AXObject, "get_application", lambda obj: app)
|
||||||
|
monkeypatch.setattr(focus_manager.AXObject, "get_name", lambda obj: "Terminal")
|
||||||
|
|
||||||
|
assert manager.is_in_preferences_window() is False
|
||||||
@@ -348,6 +348,29 @@ class TestInputEventManager:
|
|||||||
if case["expects_debug_call"]:
|
if case["expects_debug_call"]:
|
||||||
essential_modules["cthulhu.debug"].print_tokens.assert_called()
|
essential_modules["cthulhu.debug"].print_tokens.assert_called()
|
||||||
|
|
||||||
|
def test_add_grabs_for_keybinding_allows_default_cthulhu_modifiers(
|
||||||
|
self,
|
||||||
|
test_context: CthulhuTestContext,
|
||||||
|
) -> None:
|
||||||
|
"""Plugin bindings can be grabbed before a script supplies modifier keys."""
|
||||||
|
|
||||||
|
input_event_manager, _essential_modules = self._setup_input_event_manager(test_context)
|
||||||
|
|
||||||
|
mock_device = test_context.Mock()
|
||||||
|
mock_device.add_key_grab.return_value = 123
|
||||||
|
input_event_manager._device = mock_device
|
||||||
|
|
||||||
|
mock_kd = test_context.Mock()
|
||||||
|
mock_binding = test_context.Mock()
|
||||||
|
mock_binding.has_grabs.return_value = False
|
||||||
|
mock_binding.key_definitions.return_value = [mock_kd]
|
||||||
|
|
||||||
|
result = input_event_manager.add_grabs_for_keybinding(mock_binding)
|
||||||
|
|
||||||
|
assert result == [123]
|
||||||
|
mock_binding.key_definitions.assert_called_once_with(None)
|
||||||
|
mock_device.add_key_grab.assert_called_once_with(mock_kd, None)
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"case",
|
"case",
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
"""Regression tests for keybinding API compatibility."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from cthulhu import keybindings
|
||||||
|
|
||||||
|
|
||||||
|
class DummyHandler:
|
||||||
|
description = "dummy handler"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_keybinding_accepts_lightweight_command_manager_form():
|
||||||
|
binding = keybindings.KeyBinding("KP_Divide", keybindings.CTHULHU_MODIFIER_MASK)
|
||||||
|
|
||||||
|
assert binding.keysymstring == "KP_Divide"
|
||||||
|
assert binding.modifier_mask == keybindings.defaultModifierMask
|
||||||
|
assert binding.modifiers == keybindings.CTHULHU_MODIFIER_MASK
|
||||||
|
assert binding.handler is None
|
||||||
|
assert binding.click_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_keybinding_accepts_lightweight_positional_click_count():
|
||||||
|
binding = keybindings.KeyBinding("a", keybindings.CTHULHU_MODIFIER_MASK, 2)
|
||||||
|
|
||||||
|
assert binding.modifier_mask == keybindings.defaultModifierMask
|
||||||
|
assert binding.modifiers == keybindings.CTHULHU_MODIFIER_MASK
|
||||||
|
assert binding.click_count == 2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_keybinding_preserves_legacy_handler_form():
|
||||||
|
handler = DummyHandler()
|
||||||
|
|
||||||
|
binding = keybindings.KeyBinding(
|
||||||
|
"b",
|
||||||
|
keybindings.defaultModifierMask,
|
||||||
|
keybindings.CTHULHU_SHIFT_MODIFIER_MASK,
|
||||||
|
handler,
|
||||||
|
3,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert binding.modifier_mask == keybindings.defaultModifierMask
|
||||||
|
assert binding.modifiers == keybindings.CTHULHU_SHIFT_MODIFIER_MASK
|
||||||
|
assert binding.handler is handler
|
||||||
|
assert binding.click_count == 3
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_keybinding_exposes_command_manager_helpers():
|
||||||
|
binding = keybindings.KeyBinding("c", keybindings.CTHULHU_MODIFIER_MASK)
|
||||||
|
|
||||||
|
assert keybindings.get_modifier_names is keybindings.getModifierNames
|
||||||
|
assert keybindings.CTHULHU_ALT_SHIFT_MODIFIER_MASK == (
|
||||||
|
keybindings.CTHULHU_MODIFIER_MASK
|
||||||
|
| keybindings.ALT_MODIFIER_MASK
|
||||||
|
| keybindings.SHIFT_MODIFIER_MASK
|
||||||
|
)
|
||||||
|
binding.set_grab_ids([42])
|
||||||
|
assert binding.get_grab_ids() == [42]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_keybinding_add_grabs_accepts_explicit_cthulhu_modifiers(monkeypatch):
|
||||||
|
binding = keybindings.KeyBinding("d", keybindings.CTHULHU_MODIFIER_MASK)
|
||||||
|
captured = {}
|
||||||
|
|
||||||
|
class FakeInputEventManager:
|
||||||
|
def add_grabs_for_keybinding(self, grab_binding, cthulhu_modifiers):
|
||||||
|
captured["binding"] = grab_binding
|
||||||
|
captured["cthulhu_modifiers"] = cthulhu_modifiers
|
||||||
|
return [7]
|
||||||
|
|
||||||
|
from cthulhu import input_event_manager
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
input_event_manager,
|
||||||
|
"get_manager",
|
||||||
|
lambda: FakeInputEventManager(),
|
||||||
|
)
|
||||||
|
|
||||||
|
binding.add_grabs(["Insert"])
|
||||||
|
|
||||||
|
assert captured == {"binding": binding, "cthulhu_modifiers": ["Insert"]}
|
||||||
|
assert binding.get_grab_ids() == [7]
|
||||||
@@ -17,6 +17,12 @@ from cthulhu import learn_mode_presenter
|
|||||||
|
|
||||||
|
|
||||||
class LearnModePresenterRegressionTests(unittest.TestCase):
|
class LearnModePresenterRegressionTests(unittest.TestCase):
|
||||||
|
def test_get_presenter_alias_matches_legacy_get_presenter(self):
|
||||||
|
self.assertIs(learn_mode_presenter.get_presenter, learn_mode_presenter.getPresenter)
|
||||||
|
|
||||||
|
def test_exposes_default_extension_setup_hook(self):
|
||||||
|
self.assertTrue(hasattr(learn_mode_presenter.LearnModePresenter, "set_up_commands"))
|
||||||
|
|
||||||
def test_escape_keyval_exits_learn_mode_when_event_string_is_control_character(self):
|
def test_escape_keyval_exits_learn_mode_when_event_string_is_control_character(self):
|
||||||
presenter = learn_mode_presenter.LearnModePresenter(mock.Mock())
|
presenter = learn_mode_presenter.LearnModePresenter(mock.Mock())
|
||||||
presenter._is_active = True
|
presenter._is_active = True
|
||||||
|
|||||||
@@ -59,6 +59,12 @@ class InputEventManagerPointerMonitorTests(unittest.TestCase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.manager = input_event_manager.InputEventManager()
|
self.manager = input_event_manager.InputEventManager()
|
||||||
|
|
||||||
|
def test_get_reviewer_alias_matches_legacy_get_reviewer(self):
|
||||||
|
self.assertIs(mouse_review.get_reviewer, mouse_review.getReviewer)
|
||||||
|
|
||||||
|
def test_exposes_default_extension_setup_hook(self):
|
||||||
|
self.assertTrue(hasattr(mouse_review.MouseReviewer, "set_up_commands"))
|
||||||
|
|
||||||
def test_activate_device_creates_the_device_only_once(self):
|
def test_activate_device_creates_the_device_only_once(self):
|
||||||
device = FakeDevice()
|
device = FakeDevice()
|
||||||
deviceFactory = FakeDeviceFactory(device)
|
deviceFactory = FakeDeviceFactory(device)
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
"""Compatibility tests for the notification presenter."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from cthulhu import notification_presenter
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_get_presenter_alias_matches_legacy_get_presenter() -> None:
|
||||||
|
"""The Mako monitor still uses the legacy camelCase singleton accessor."""
|
||||||
|
|
||||||
|
assert notification_presenter.getPresenter is notification_presenter.get_presenter
|
||||||
|
assert notification_presenter.getPresenter() is notification_presenter.get_presenter()
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
"""Regression tests for script startup ordering."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from cthulhu.scripts import default
|
||||||
|
|
||||||
|
script_module = default.script
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_script_initializes_formatting_before_generators(monkeypatch):
|
||||||
|
"""Generator construction needs script.formatting during startup."""
|
||||||
|
|
||||||
|
monkeypatch.setattr(script_module.AXObject, "get_name", staticmethod(lambda _app: "test-app"))
|
||||||
|
init_order = []
|
||||||
|
formatting = {"sentinel": True}
|
||||||
|
|
||||||
|
class FormattingAwareScript(script_module.Script):
|
||||||
|
def get_formatting(self):
|
||||||
|
init_order.append("formatting")
|
||||||
|
return formatting
|
||||||
|
|
||||||
|
def get_listeners(self):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def get_utilities(self):
|
||||||
|
return object()
|
||||||
|
|
||||||
|
def _create_braille_generator(self):
|
||||||
|
init_order.append("braille")
|
||||||
|
assert self.formatting is formatting
|
||||||
|
return "braille"
|
||||||
|
|
||||||
|
def _create_sound_generator(self):
|
||||||
|
init_order.append("sound")
|
||||||
|
assert self.formatting is formatting
|
||||||
|
return "sound"
|
||||||
|
|
||||||
|
def _create_speech_generator(self):
|
||||||
|
init_order.append("speech")
|
||||||
|
assert self.formatting is formatting
|
||||||
|
return "speech"
|
||||||
|
|
||||||
|
def get_label_inference(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_chat(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def set_up_commands(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
test_script = FormattingAwareScript(object())
|
||||||
|
|
||||||
|
assert init_order == ["formatting", "braille", "sound", "speech"]
|
||||||
|
assert test_script.formatting is formatting
|
||||||
|
assert test_script.get_braille_generator() == "braille"
|
||||||
|
assert test_script.get_sound_generator() == "sound"
|
||||||
|
assert test_script.get_speech_generator() == "speech"
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
"""Compatibility tests for the sleep mode manager."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
import sys
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .cthulhu_test_context import CthulhuTestContext
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_get_manager_alias_matches_legacy_get_manager(
|
||||||
|
test_context: CthulhuTestContext,
|
||||||
|
) -> None:
|
||||||
|
"""The default script expects the snake_case singleton accessor."""
|
||||||
|
|
||||||
|
test_context.setup_shared_dependencies()
|
||||||
|
sys.modules.pop("cthulhu.sleep_mode_manager", None)
|
||||||
|
sleep_mode_manager = importlib.import_module("cthulhu.sleep_mode_manager")
|
||||||
|
|
||||||
|
assert sleep_mode_manager.get_manager is sleep_mode_manager.getManager
|
||||||
|
assert sleep_mode_manager.get_manager() is sleep_mode_manager.getManager()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_sleep_mode_manager_exposes_default_extension_setup_hook(
|
||||||
|
test_context: CthulhuTestContext,
|
||||||
|
) -> None:
|
||||||
|
"""The default script calls set_up_commands() on all extensions."""
|
||||||
|
|
||||||
|
test_context.setup_shared_dependencies()
|
||||||
|
sys.modules.pop("cthulhu.sleep_mode_manager", None)
|
||||||
|
sleep_mode_manager = importlib.import_module("cthulhu.sleep_mode_manager")
|
||||||
|
|
||||||
|
assert hasattr(sleep_mode_manager.get_manager(), "set_up_commands")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_sleep_mode_manager_exposes_snake_case_app_state_accessor(
|
||||||
|
test_context: CthulhuTestContext,
|
||||||
|
) -> None:
|
||||||
|
"""The script manager checks sleep mode using the snake_case method name."""
|
||||||
|
|
||||||
|
test_context.setup_shared_dependencies()
|
||||||
|
sys.modules.pop("cthulhu.sleep_mode_manager", None)
|
||||||
|
sleep_mode_manager = importlib.import_module("cthulhu.sleep_mode_manager")
|
||||||
|
manager = sleep_mode_manager.get_manager()
|
||||||
|
|
||||||
|
app = object()
|
||||||
|
|
||||||
|
assert manager.is_active_for_app(app) == manager.isActiveForApp(app)
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
"""Regression tests for speech monitor callback registration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from cthulhu import speech
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_set_monitor_callbacks_accepts_snake_case_keywords() -> None:
|
||||||
|
captured: list[str] = []
|
||||||
|
|
||||||
|
speech.set_monitor_callbacks(
|
||||||
|
write_text=captured.append,
|
||||||
|
write_key=lambda _key: None,
|
||||||
|
begin_group=lambda: None,
|
||||||
|
end_group=lambda: None,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
speech._write_to_monitor("hello")
|
||||||
|
finally:
|
||||||
|
speech.set_monitor_callbacks()
|
||||||
|
|
||||||
|
assert captured == ["hello"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_set_monitor_callbacks_preserves_legacy_write_text_argument() -> None:
|
||||||
|
captured: list[str] = []
|
||||||
|
|
||||||
|
speech.set_monitor_callbacks(writeText=captured.append)
|
||||||
|
try:
|
||||||
|
speech._write_to_monitor("legacy")
|
||||||
|
finally:
|
||||||
|
speech.set_monitor_callbacks()
|
||||||
|
|
||||||
|
assert captured == ["legacy"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_speech_server_snake_case_accessors_delegate_to_legacy_api(monkeypatch) -> None:
|
||||||
|
monkeypatch.setattr(speech, "_refreshEchoSpeechServer", lambda: None)
|
||||||
|
original_server = speech.getSpeechServer()
|
||||||
|
server = object()
|
||||||
|
|
||||||
|
try:
|
||||||
|
speech.set_server(server)
|
||||||
|
|
||||||
|
assert speech.get_speech_server() is server
|
||||||
|
assert speech.getSpeechServer() is server
|
||||||
|
finally:
|
||||||
|
speech.setSpeechServer(original_server)
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
"""Regression tests for speech server API compatibility."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from cthulhu import speechserver
|
||||||
|
from cthulhu.acss import ACSS
|
||||||
|
|
||||||
|
|
||||||
|
class DummySpeechServer(speechserver.SpeechServer):
|
||||||
|
factory_calls = []
|
||||||
|
active_servers_shutdown = False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def getFactoryName():
|
||||||
|
return "Dummy"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def getSpeechServers():
|
||||||
|
return ["server-a"]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def getSpeechServer(info=None):
|
||||||
|
DummySpeechServer.factory_calls.append(info)
|
||||||
|
return DummySpeechServer()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def shutdownActiveServers():
|
||||||
|
DummySpeechServer.active_servers_shutdown = True
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.calls = []
|
||||||
|
self._current_voice_properties = {}
|
||||||
|
|
||||||
|
def getInfo(self):
|
||||||
|
return ["Dummy", "dummy"]
|
||||||
|
|
||||||
|
def getVoiceFamilies(self):
|
||||||
|
return ["voice-a"]
|
||||||
|
|
||||||
|
def getVoiceFamiliesForLanguage(self, language, dialect="", maximum=None):
|
||||||
|
return [(language, dialect, maximum)]
|
||||||
|
|
||||||
|
def getOutputModule(self):
|
||||||
|
return "module-a"
|
||||||
|
|
||||||
|
def setOutputModule(self, module):
|
||||||
|
self.calls.append(("setOutputModule", module))
|
||||||
|
|
||||||
|
def updateCapitalizationStyle(self):
|
||||||
|
self.calls.append(("updateCapitalizationStyle",))
|
||||||
|
|
||||||
|
def updatePunctuationLevel(self):
|
||||||
|
self.calls.append(("updatePunctuationLevel",))
|
||||||
|
|
||||||
|
def increaseSpeechRate(self, step=5):
|
||||||
|
self.calls.append(("increaseSpeechRate", step))
|
||||||
|
|
||||||
|
def decreaseSpeechVolume(self, step=0.5):
|
||||||
|
self.calls.append(("decreaseSpeechVolume", step))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_snake_case_factory_methods_delegate_to_legacy_methods():
|
||||||
|
DummySpeechServer.factory_calls.clear()
|
||||||
|
DummySpeechServer.active_servers_shutdown = False
|
||||||
|
|
||||||
|
server = DummySpeechServer.get_speech_server(("Dummy", "dummy"))
|
||||||
|
|
||||||
|
assert isinstance(server, DummySpeechServer)
|
||||||
|
assert DummySpeechServer.factory_calls == [("Dummy", "dummy")]
|
||||||
|
assert DummySpeechServer.get_speech_servers() == ["server-a"]
|
||||||
|
assert DummySpeechServer.get_factory_name() == "Dummy"
|
||||||
|
|
||||||
|
DummySpeechServer.shutdown_active_servers()
|
||||||
|
|
||||||
|
assert DummySpeechServer.active_servers_shutdown is True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_snake_case_instance_methods_delegate_to_legacy_methods():
|
||||||
|
server = DummySpeechServer()
|
||||||
|
|
||||||
|
assert server.get_info() == ["Dummy", "dummy"]
|
||||||
|
assert server.get_voice_families() == ["voice-a"]
|
||||||
|
assert server.get_voice_families_for_language("en", variant="us") == [("en", "us", None)]
|
||||||
|
assert server.get_output_module() == "module-a"
|
||||||
|
|
||||||
|
server.set_output_module("module-b")
|
||||||
|
server.update_capitalization_style("none")
|
||||||
|
server.update_punctuation_level(speechserver.PunctuationStyle.SOME)
|
||||||
|
server.increase_speech_rate(9)
|
||||||
|
server.decrease_speech_volume(1.5)
|
||||||
|
|
||||||
|
assert server.calls == [
|
||||||
|
("setOutputModule", "module-b"),
|
||||||
|
("updateCapitalizationStyle",),
|
||||||
|
("updatePunctuationLevel",),
|
||||||
|
("increaseSpeechRate", 9),
|
||||||
|
("decreaseSpeechVolume", 1.5),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_voice_property_helpers_store_and_clear_default_voice():
|
||||||
|
server = DummySpeechServer()
|
||||||
|
default_voice = {ACSS.FAMILY: {speechserver.VoiceFamily.NAME: "Voice A"}}
|
||||||
|
next_family = {speechserver.VoiceFamily.NAME: "Voice B"}
|
||||||
|
|
||||||
|
server.set_default_voice(default_voice)
|
||||||
|
|
||||||
|
assert server.get_voice_family() == default_voice[ACSS.FAMILY]
|
||||||
|
|
||||||
|
server.set_voice_family(next_family)
|
||||||
|
|
||||||
|
assert server.get_voice_family() == next_family
|
||||||
|
|
||||||
|
server.clear_cached_voice_properties()
|
||||||
|
|
||||||
|
assert server.get_voice_family() is None
|
||||||
Reference in New Issue
Block a user