Fixed multiline edit box weirdness.

This commit is contained in:
Storm Dragon
2026-07-29 03:22:10 -04:00
parent 8b90a020c4
commit f4ee5e4468
6 changed files with 133 additions and 20 deletions
+11 -5
View File
@@ -608,6 +608,8 @@ class InputEventManager:
except Exception as error:
msg = f"INPUT EVENT MANAGER: Could not read EWMH active window: {error}"
debug.print_message(debug.LEVEL_INFO, msg, True)
self._close_x11_display()
self._didAttemptX11Display = False
return None
def _close_x11_display(self) -> None:
@@ -769,11 +771,15 @@ class InputEventManager:
self._xtermRecoverySourceId = 0
return False
match = self._active_x11_window_xterm_match()
if match is True:
self._suspend_key_grabs_for_xterm()
elif match is False:
self._restore_key_grabs_after_xterm()
try:
match = self._active_x11_window_xterm_match()
if match is True:
self._suspend_key_grabs_for_xterm()
elif match is False:
self._restore_key_grabs_after_xterm()
except Exception as error:
msg = f"INPUT EVENT MANAGER: XTerm recovery poll failed: {error}"
debug.print_message(debug.LEVEL_INFO, msg, True)
return True
def _on_active_x11_window_changed(self, screen: Any, _previousWindow: Any) -> None:
+11 -2
View File
@@ -1152,7 +1152,13 @@ class Script(default.Script):
def sayLine(self, obj):
"""Speaks the line at the current caret position."""
isEditable = self.utilities.isContentEditableWithEmbeddedObjects(obj)
isNativeMultilineEditable = (
self.utilities.inDocumentContent(obj)
and AXUtilities.is_editable(obj)
and AXUtilities.is_multi_line(obj)
)
isEditable = self.utilities.isContentEditableWithEmbeddedObjects(obj) \
or isNativeMultilineEditable
if not (self._lastCommandWasCaretNav or self._lastCommandWasStructNav) and not isEditable:
super().sayLine(obj)
return
@@ -1164,7 +1170,10 @@ class Script(default.Script):
obj, offset = self.utilities.getCaretContext(documentFrame=document)
contents = self.utilities.getLineContentsAtOffset(obj, offset, useCache=True)
self.speakContents(contents, priorObj=priorObj)
if isNativeMultilineEditable:
self.speakContents(contents, priorObj=priorObj, alreadyFocused=True)
else:
self.speakContents(contents, priorObj=priorObj)
self.pointOfReference["lastTextUnitSpoken"] = "line"
def presentObject(self, obj, **args):
+19 -13
View File
@@ -1822,6 +1822,17 @@ class Utilities(script_utilities.Utilities):
string = AXText.get_substring(obj, offset, firstEnd)
return [[firstObj, offset, firstEnd, string]] + contents[1:]
def _getEditableLineBoundary(self, obj):
if self.isContentEditableWithEmbeddedObjects(obj):
return obj
if self.inDocumentContent(obj) \
and AXUtilities.is_editable(obj) \
and AXUtilities.is_multi_line(obj):
return obj
return AXObject.find_ancestor(obj, self.isContentEditableWithEmbeddedObjects)
def _getLineContentsAtOffset(self, obj, offset, layoutMode=None, useCache=True):
startTime = time.time()
if not obj:
@@ -1930,15 +1941,10 @@ class Utilities(script_utilities.Utilities):
prevObj, pOffset = self.findPreviousCaretInOrder(firstObj, firstStart)
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)
# If we're inside an editable, don't expand line contents beyond its
# boundaries (e.g. don't include an adjacent button just because it's
# on the same visual line).
editableBoundary = self._getEditableLineBoundary(obj)
# Check for things on the same line to the left of this object.
prevStartTime = time.time()
@@ -1954,8 +1960,8 @@ class Utilities(script_utilities.Utilities):
if objRow != AXObject.find_ancestor(prevObj, AXUtilities.is_table_row):
break
if contentEditableBoundary and prevObj != contentEditableBoundary \
and not AXObject.find_ancestor(prevObj, lambda x: x == contentEditableBoundary):
if editableBoundary and prevObj != editableBoundary \
and not AXObject.find_ancestor(prevObj, lambda x: x == editableBoundary):
break
onLeft = self._getContentsForObj(prevObj, pOffset, boundary)
@@ -1988,8 +1994,8 @@ class Utilities(script_utilities.Utilities):
if objRow != AXObject.find_ancestor(nextObj, AXUtilities.is_table_row):
break
if contentEditableBoundary and nextObj != contentEditableBoundary \
and not AXObject.find_ancestor(nextObj, lambda x: x == contentEditableBoundary):
if editableBoundary and nextObj != editableBoundary \
and not AXObject.find_ancestor(nextObj, lambda x: x == editableBoundary):
break
onRight = self._getContentsForObj(nextObj, nOffset, boundary)
@@ -690,6 +690,43 @@ class InputEventManagerX11FocusRegressionTests(unittest.TestCase):
restore.assert_called_once_with()
suspend.assert_not_called()
def test_xterm_focus_poll_survives_transient_matcher_failure(self):
manager = input_event_manager.InputEventManager()
manager._device = object()
manager._xtermRecoverySourceId = 23
with (
mock.patch.object(
manager,
"_active_x11_window_xterm_match",
side_effect=RuntimeError("transient matcher failure"),
),
mock.patch.object(input_event_manager.debug, "print_message") as printMessage,
):
result = manager._poll_xterm_grab_recovery()
self.assertTrue(result)
self.assertEqual(manager._xtermRecoverySourceId, 23)
printMessage.assert_called_once()
def test_ewmh_read_failure_reconnects_on_next_check(self):
manager = input_event_manager.InputEventManager()
x11Display = mock.Mock()
x11Root = mock.Mock()
x11Root.get_full_property.side_effect = RuntimeError("display connection failed")
manager._x11Display = x11Display
manager._x11Root = x11Root
manager._netActiveWindowAtom = object()
manager._x11AnyPropertyType = object()
manager._didAttemptX11Display = True
with mock.patch.object(input_event_manager.debug, "print_message"):
result = manager._get_ewmh_active_x11_window_id()
self.assertIsNone(result)
x11Display.close.assert_called_once_with()
self.assertFalse(manager._didAttemptX11Display)
def test_xterm_suppression_does_not_check_x11_without_active_handoff(self):
manager = input_event_manager.InputEventManager()
+54
View File
@@ -622,6 +622,60 @@ class WebCaretMovedRegressionTests(unittest.TestCase):
setLocusOfFocus.assert_not_called()
class WebMultilineEditorLinePresentationRegressionTests(unittest.TestCase):
def test_native_multiline_editor_uses_visual_web_line(self):
testScript = web_script.Script.__new__(web_script.Script)
testScript._lastCommandWasCaretNav = False
testScript._lastCommandWasStructNav = False
testScript.pointOfReference = {}
testScript.utilities = mock.Mock()
testScript.utilities.isContentEditableWithEmbeddedObjects.return_value = False
testScript.utilities.inDocumentContent.return_value = True
testScript.utilities.getTopLevelDocumentForObject.return_value = "document"
testScript.utilities.getPriorContext.return_value = ("prior", 20)
testScript.utilities.getCaretContext.return_value = ("editor", 42)
visualLine = [["editor", 40, 55, "wrapped line"]]
testScript.utilities.getLineContentsAtOffset.return_value = visualLine
testScript.speakContents = mock.Mock()
with (
mock.patch.object(web_script.AXUtilities, "is_editable", return_value=True),
mock.patch.object(web_script.AXUtilities, "is_multi_line", return_value=True),
mock.patch.object(web_script.default.Script, "sayLine") as defaultSayLine,
):
web_script.Script.sayLine(testScript, "editor")
defaultSayLine.assert_not_called()
testScript.utilities.getLineContentsAtOffset.assert_called_once_with(
"editor",
42,
useCache=True,
)
testScript.speakContents.assert_called_once_with(
visualLine,
priorObj="prior",
alreadyFocused=True,
)
def test_native_multiline_editor_is_visual_line_boundary(self):
editor = object()
utilities = web_script_utilities.Utilities.__new__(
web_script_utilities.Utilities
)
utilities.isContentEditableWithEmbeddedObjects = mock.Mock(return_value=False)
utilities.inDocumentContent = mock.Mock(return_value=True)
with (
mock.patch.object(web_script_utilities.AXUtilities, "is_editable", return_value=True),
mock.patch.object(web_script_utilities.AXUtilities, "is_multi_line", return_value=True),
mock.patch.object(web_script_utilities.AXObject, "find_ancestor") as findAncestor,
):
boundary = utilities._getEditableLineBoundary(editor)
self.assertIs(boundary, editor)
findAncestor.assert_not_called()
class TextEventReasonRegressionTests(unittest.TestCase):
@staticmethod
def _make_input_manager():
@@ -85,6 +85,7 @@ class WebLayoutModeLineRegressionTests(unittest.TestCase):
utilities.findPreviousCaretInOrder = mock.Mock(return_value=(None, -1))
utilities.findNextCaretInOrder = mock.Mock(return_value=(None, -1))
utilities.isContentEditableWithEmbeddedObjects = mock.Mock(return_value=False)
utilities.inDocumentContent = mock.Mock(return_value=False)
return utilities, obj, contents
def _get_line_for_setting(self, layoutMode):