Fixed multiline edit box weirdness.
This commit is contained in:
@@ -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
|
||||
|
||||
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:
|
||||
|
||||
@@ -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,6 +1170,9 @@ class Script(default.Script):
|
||||
|
||||
obj, offset = self.utilities.getCaretContext(documentFrame=document)
|
||||
contents = self.utilities.getLineContentsAtOffset(obj, offset, useCache=True)
|
||||
if isNativeMultilineEditable:
|
||||
self.speakContents(contents, priorObj=priorObj, alreadyFocused=True)
|
||||
else:
|
||||
self.speakContents(contents, priorObj=priorObj)
|
||||
self.pointOfReference["lastTextUnitSpoken"] = "line"
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user