Merge remote-tracking branch 'origin/web-and-punctuation-fixes' into testing
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
# Maintainer: Storm Dragon <storm_dragon@stormux.org>
|
# Maintainer: Storm Dragon <storm_dragon@stormux.org>
|
||||||
|
|
||||||
pkgname=cthulhu
|
pkgname=cthulhu
|
||||||
pkgver=2026.02.16
|
pkgver=2026.02.17
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Desktop-agnostic screen reader with plugin system, forked from Orca"
|
pkgdesc="Desktop-agnostic screen reader with plugin system, forked from Orca"
|
||||||
url="https://git.stormux.org/storm/cthulhu"
|
url="https://git.stormux.org/storm/cthulhu"
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
project('cthulhu',
|
project('cthulhu',
|
||||||
version: '2026.02.16-testing',
|
version: '2026.02.17-master',
|
||||||
meson_version: '>= 1.0.0',
|
meson_version: '>= 1.0.0',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -23,5 +23,5 @@
|
|||||||
# Forked from Orca screen reader.
|
# Forked from Orca screen reader.
|
||||||
# Cthulhu project: https://git.stormux.org/storm/cthulhu
|
# Cthulhu project: https://git.stormux.org/storm/cthulhu
|
||||||
|
|
||||||
version = "2026.02.16"
|
version = "2026.02.17"
|
||||||
codeName = "testing"
|
codeName = "master"
|
||||||
|
|||||||
@@ -276,7 +276,9 @@ class LiveRegionManager:
|
|||||||
utts = message['labels'] + message['content']
|
utts = message['labels'] + message['content']
|
||||||
|
|
||||||
if self.monitoring:
|
if self.monitoring:
|
||||||
self._script.presentMessage(utts)
|
# Live region content is user-generated text, not system messages.
|
||||||
|
# Use resetStyles=False to preserve the user's punctuation settings.
|
||||||
|
self._script.presentMessage(utts, resetStyles=False)
|
||||||
else:
|
else:
|
||||||
msg = "INFO: Not presenting message because monitoring is off"
|
msg = "INFO: Not presenting message because monitoring is off"
|
||||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||||
|
|||||||
@@ -1473,7 +1473,9 @@ class Script(script.Script):
|
|||||||
"""Callback for object:announcement events."""
|
"""Callback for object:announcement events."""
|
||||||
|
|
||||||
if isinstance(event.any_data, str):
|
if isinstance(event.any_data, str):
|
||||||
self.presentMessage(event.any_data)
|
# AT-SPI announcements contain application content, not system messages.
|
||||||
|
# Use resetStyles=False to preserve the user's punctuation settings.
|
||||||
|
self.presentMessage(event.any_data, resetStyles=False)
|
||||||
|
|
||||||
def onNameChanged(self, event):
|
def onNameChanged(self, event):
|
||||||
"""Callback for object:property-change:accessible-name events."""
|
"""Callback for object:property-change:accessible-name events."""
|
||||||
|
|||||||
@@ -1648,9 +1648,21 @@ class Script(default.Script):
|
|||||||
elif self.utilities.isContentEditableWithEmbeddedObjects(newFocus) \
|
elif self.utilities.isContentEditableWithEmbeddedObjects(newFocus) \
|
||||||
and (self._lastCommandWasCaretNav or self._lastCommandWasStructNav) \
|
and (self._lastCommandWasCaretNav or self._lastCommandWasStructNav) \
|
||||||
and not (AXUtilities.is_table_cell(newFocus) and AXObject.get_name(newFocus)):
|
and not (AXUtilities.is_table_cell(newFocus) and AXObject.get_name(newFocus)):
|
||||||
tokens = ["WEB: New focus", newFocus, "content editable. Generating line."]
|
# Check if we're entering the content editable from outside (e.g. down arrow
|
||||||
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
# from a message list into a message entry). In that case, generate full object
|
||||||
contents = self.utilities.getLineContentsAtOffset(newFocus, caretOffset)
|
# speech (with label and role) rather than just line contents.
|
||||||
|
enteredFromOutside = oldFocus is not None \
|
||||||
|
and oldFocus != newFocus \
|
||||||
|
and not AXObject.find_ancestor(oldFocus, lambda x: x == newFocus)
|
||||||
|
if enteredFromOutside:
|
||||||
|
tokens = ["WEB: New focus", newFocus,
|
||||||
|
"content editable entered from outside. Generating speech."]
|
||||||
|
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||||
|
args['priorObj'] = oldFocus
|
||||||
|
else:
|
||||||
|
tokens = ["WEB: New focus", newFocus, "content editable. Generating line."]
|
||||||
|
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||||
|
contents = self.utilities.getLineContentsAtOffset(newFocus, caretOffset)
|
||||||
elif self.utilities.isAnchor(newFocus):
|
elif self.utilities.isAnchor(newFocus):
|
||||||
tokens = ["WEB: New focus", newFocus, "is anchor. Generating line."]
|
tokens = ["WEB: New focus", newFocus, "is anchor. Generating line."]
|
||||||
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||||
|
|||||||
@@ -1834,6 +1834,16 @@ class Utilities(script_utilities.Utilities):
|
|||||||
prevObj, pOffset = self.findPreviousCaretInOrder(firstObj, firstStart)
|
prevObj, pOffset = self.findPreviousCaretInOrder(firstObj, firstStart)
|
||||||
nextObj, nOffset = self.findNextCaretInOrder(lastObj, lastEnd - 1)
|
nextObj, nOffset = self.findNextCaretInOrder(lastObj, lastEnd - 1)
|
||||||
|
|
||||||
|
# If we're inside a content editable, don't expand line contents beyond
|
||||||
|
# its boundaries (e.g. don't include a "More options" button adjacent to
|
||||||
|
# a message entry just because it's on the same visual line).
|
||||||
|
contentEditableBoundary = None
|
||||||
|
if self.isContentEditableWithEmbeddedObjects(obj):
|
||||||
|
contentEditableBoundary = obj
|
||||||
|
else:
|
||||||
|
contentEditableBoundary = AXObject.find_ancestor(
|
||||||
|
obj, self.isContentEditableWithEmbeddedObjects)
|
||||||
|
|
||||||
# Check for things on the same line to the left of this object.
|
# Check for things on the same line to the left of this object.
|
||||||
prevStartTime = time.time()
|
prevStartTime = time.time()
|
||||||
while prevObj and self.getDocumentForObject(prevObj) == document:
|
while prevObj and self.getDocumentForObject(prevObj) == document:
|
||||||
@@ -1848,6 +1858,10 @@ class Utilities(script_utilities.Utilities):
|
|||||||
if objRow != AXObject.find_ancestor(prevObj, AXUtilities.is_table_row):
|
if objRow != AXObject.find_ancestor(prevObj, AXUtilities.is_table_row):
|
||||||
break
|
break
|
||||||
|
|
||||||
|
if contentEditableBoundary and prevObj != contentEditableBoundary \
|
||||||
|
and not AXObject.find_ancestor(prevObj, lambda x: x == contentEditableBoundary):
|
||||||
|
break
|
||||||
|
|
||||||
onLeft = self._getContentsForObj(prevObj, pOffset, boundary)
|
onLeft = self._getContentsForObj(prevObj, pOffset, boundary)
|
||||||
onLeft = list(filter(_include, onLeft))
|
onLeft = list(filter(_include, onLeft))
|
||||||
if not onLeft:
|
if not onLeft:
|
||||||
@@ -1878,6 +1892,10 @@ class Utilities(script_utilities.Utilities):
|
|||||||
if objRow != AXObject.find_ancestor(nextObj, AXUtilities.is_table_row):
|
if objRow != AXObject.find_ancestor(nextObj, AXUtilities.is_table_row):
|
||||||
break
|
break
|
||||||
|
|
||||||
|
if contentEditableBoundary and nextObj != contentEditableBoundary \
|
||||||
|
and not AXObject.find_ancestor(nextObj, lambda x: x == contentEditableBoundary):
|
||||||
|
break
|
||||||
|
|
||||||
onRight = self._getContentsForObj(nextObj, nOffset, boundary)
|
onRight = self._getContentsForObj(nextObj, nOffset, boundary)
|
||||||
if onRight and self._contentIsSubsetOf(objects[0], onRight[-1]):
|
if onRight and self._contentIsSubsetOf(objects[0], onRight[-1]):
|
||||||
onRight = onRight[0:-1]
|
onRight = onRight[0:-1]
|
||||||
|
|||||||
@@ -361,7 +361,13 @@ class SpeechGenerator(speech_generator.SpeechGenerator):
|
|||||||
if self._script.utilities.isContentEditableWithEmbeddedObjects(obj) \
|
if self._script.utilities.isContentEditableWithEmbeddedObjects(obj) \
|
||||||
or self._script.utilities.isDocument(obj):
|
or self._script.utilities.isDocument(obj):
|
||||||
if input_event_manager.get_manager().last_event_was_caret_navigation():
|
if input_event_manager.get_manager().last_event_was_caret_navigation():
|
||||||
return []
|
# Still generate the label if we just entered this object from outside
|
||||||
|
# (e.g. down arrow from message list into message entry in Discord).
|
||||||
|
enteredFromOutside = priorObj is not None \
|
||||||
|
and priorObj != obj \
|
||||||
|
and not AXObject.find_ancestor(priorObj, lambda x: x == obj)
|
||||||
|
if not enteredFromOutside:
|
||||||
|
return []
|
||||||
|
|
||||||
if AXUtilities.is_page_tab(priorObj) and AXObject.get_name(priorObj) == objName:
|
if AXUtilities.is_page_tab(priorObj) and AXObject.get_name(priorObj) == objName:
|
||||||
return []
|
return []
|
||||||
@@ -554,6 +560,9 @@ class SpeechGenerator(speech_generator.SpeechGenerator):
|
|||||||
if roledescription:
|
if roledescription:
|
||||||
result = [roledescription]
|
result = [roledescription]
|
||||||
result.extend(self.voice(speech_generator.SYSTEM, obj=obj, **args))
|
result.extend(self.voice(speech_generator.SYSTEM, obj=obj, **args))
|
||||||
|
# aria-roledescription replaces the standard role name, so return
|
||||||
|
# early to avoid announcing both (e.g. "Message" + "article").
|
||||||
|
return result
|
||||||
|
|
||||||
role = args.get('role', AXObject.get_role(obj))
|
role = args.get('role', AXObject.get_role(obj))
|
||||||
roleSoundPresentation = cthulhu.cthulhuApp.settingsManager.getSetting('roleSoundPresentation')
|
roleSoundPresentation = cthulhu.cthulhuApp.settingsManager.getSetting('roleSoundPresentation')
|
||||||
|
|||||||
Reference in New Issue
Block a user