From 6b4756c9d0dbbc210833168048002614096467d4 Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Wed, 17 Jul 2024 14:58:52 +0200 Subject: [PATCH 01/11] Use moveToCodePointOffset again --- source/braille.py | 13 ++++++++++--- user_docs/en/changes.md | 1 + 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/source/braille.py b/source/braille.py index 2a1757ed578..1317182d73d 100644 --- a/source/braille.py +++ b/source/braille.py @@ -1429,11 +1429,18 @@ def getTextInfoForBraillePos(self, braillePos: int) -> textInfos.TextInfo: """Fetches a collapsed TextInfo at the specified braille position in the region.""" pos = self._rawToContentPos[self.brailleToRawPos[braillePos]] # pos is relative to the start of the reading unit. - # Therefore, get the start of the reading unit... + if pos > 0: + # Move pos code points from the start. + # Note that, as liblouis uses 32 bit encoding internally, + # it is really safe to assume that one code point offset is equal to one character within liblouis. + try: + return self._readingInfo.moveToCodepointOffset(pos) + except (ValueError, RuntimeError): + log.exception(f"Error in moveToCodepointOffset, falling back to moving by {pos} characters") dest = self._readingInfo.copy() dest.collapse() - # and move pos characters from there. - dest.move(textInfos.UNIT_CHARACTER, pos) + if pos > 0: + dest.move(textInfos.UNIT_CHARACTER, pos) return dest def routeTo(self, braillePos: int): diff --git a/user_docs/en/changes.md b/user_docs/en/changes.md index 7ce36415774..85cd00f9e16 100644 --- a/user_docs/en/changes.md +++ b/user_docs/en/changes.md @@ -18,6 +18,7 @@ * NVDA once again relies on events for caret movement in several cases, rather than only on manual querying of the caret position. * UIA for XAML and WPF text controls. (#16817, @LeonarddeR) * IAccessible2 for browsers such as Firefox and Chromium based browsers. (#11545, #16815, @LeonarddeR) +* Braille cursor routing is now much more reliable when a line contains one or more Unicode variation selectors or decomposed characters. (#10960, @mltony, @LeonarddeR) ### Changes for Developers From c5ad7aca654aa2556da6baafe694ac70ec2658d3 Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Wed, 17 Jul 2024 15:55:59 +0200 Subject: [PATCH 02/11] Provide workaround for Word with UIA --- source/NVDAObjects/UIA/wordDocument.py | 14 +++++++++++++- source/textInfos/__init__.py | 10 +++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/source/NVDAObjects/UIA/wordDocument.py b/source/NVDAObjects/UIA/wordDocument.py index 96ca9fd4aec..a1836880f85 100644 --- a/source/NVDAObjects/UIA/wordDocument.py +++ b/source/NVDAObjects/UIA/wordDocument.py @@ -270,6 +270,18 @@ def _getTextFromUIARange(self, textRange): t = t.replace(END_OF_ROW_MARK, "") return t + def _getTextForCodepointMovement(self) -> str: + """ + #8576, #10960: In Word, list bullets are exposed in text but are ignored when moving by character. + Therefore in `getTextWithFields`, the bullets are stripped from the text and exposed in the `line-prefix` field. + To stay compatible with this, we can't simply use the `text` property, + as it can potentially contain bullets that should be stripped. + """ + t = super()._getTextForCodepointMovement() + if not t: + return t + return "".join(f for f in self.getTextWithFields(formatConfig=dict()) if isinstance(f, str)) + def _isEndOfRow(self): """Is this textInfo positioned on an end-of-row mark?""" info = self.copy() @@ -648,7 +660,7 @@ def _caretMoveBySentenceHelper(self, gesture, direction): description=_( # Translators: a description for a script that reports the comment at the caret. "Reports the text of the comment where the system caret is located." - " If pressed twice, presents the information in a browsable message" + " If pressed twice, presents the information in a browsable message", ), category=SCRCAT_SYSTEMCARET, speakOnDemand=True, diff --git a/source/textInfos/__init__.py b/source/textInfos/__init__.py index 56d487f6d91..3d5dfdbe494 100755 --- a/source/textInfos/__init__.py +++ b/source/textInfos/__init__.py @@ -711,6 +711,10 @@ def getMathMl(self, field): """ raise NotImplementedError + def _getTextForCodepointMovement(self) -> str: + """Gets the text as used in moveToCodepointOffset.""" + return self.text + def moveToCodepointOffset( self, codepointOffset: int, @@ -803,7 +807,7 @@ def moveToCodepointOffset( we reduce the count of characters in order to make sure the algorithm makes some progress on each iteration. """ - text = self.text + text = self._getTextForCodepointMovement() if codepointOffset < 0 or codepointOffset > len(text): raise ValueError if codepointOffset == 0 or codepointOffset == len(text): @@ -845,7 +849,7 @@ def moveToCodepointOffset( moveCharacters = codepointOffsetLeft code = tmpInfo.move(UNIT_CHARACTER, moveCharacters, endPoint="end") lastMove = moveCharacters - tmpText = tmpInfo.text + tmpText = tmpInfo._getTextForCodepointMovement() actualCodepointOffset = len(tmpText) if not text.startswith(tmpText): raise RuntimeError( @@ -865,7 +869,7 @@ def moveToCodepointOffset( moveCharacters = -codepointOffsetRight code = tmpInfo.move(UNIT_CHARACTER, moveCharacters, endPoint="start") lastMove = moveCharacters - tmpText = tmpInfo.text + tmpText = tmpInfo._getTextForCodepointMovement() actualCodepointOffset = totalCodepointOffset - len(tmpText) if not text.endswith(tmpText): raise RuntimeError( From 66e8ca61d8aa1f0dc8a7af7f5e62ed6a8998030c Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Wed, 17 Jul 2024 16:17:23 +0200 Subject: [PATCH 03/11] Add unit test --- tests/unit/test_braille/test_routing.py | 34 ++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_braille/test_routing.py b/tests/unit/test_braille/test_routing.py index 5c984dfe013..d552d20d538 100644 --- a/tests/unit/test_braille/test_routing.py +++ b/tests/unit/test_braille/test_routing.py @@ -1,16 +1,16 @@ # A part of NonVisual Desktop Access (NVDA) # This file is covered by the GNU General Public License. # See the file COPYING for more details. -# Copyright (C) 2023 NV Access Limited, Leonard de Ruijter +# Copyright (C) 2023-2024 NV Access Limited, Leonard de Ruijter -"""Unit tests for the move system caret when routing review cursor braille setting.""" +"""Unit tests for braille cursor routing.""" import config import braille import textInfos import api import controlTypes -from ..textProvider import CursorManager +from ..textProvider import CursorManager, BasicTextProvider import unittest import time from config.featureFlagEnums import ReviewRoutingMovesSystemCaretFlag @@ -147,3 +147,31 @@ def test_moveCaret_always_instantActivate(self): self.assertGreaterEqual(self.cm.lastActivateTime, curTime) caret = self.cm.makeTextInfo(textInfos.POSITION_CARET) self.assertEquals(caret, review) + + +class TestTextInfoRegionRouting(unittest.TestCase): + """A test for TextInfoRegion.getTextInfoForBraillePos, which is used in braille cursor routing. + This test ensures that braille routes to the expected character when dealing with emoji + containing modifier characters. + These emoji are threated as one character by uniscribe, however they span multiple characters + on a braille display. + Note that due to the nature of this test, it relies on uniscribe to be available. + """ + + def test_routeToEmoji(self): + testText = "⚠️test" + obj = BasicTextProvider(text=testText) + ti = obj.makeTextInfo(textInfos.POSITION_CARET) + ti.expand(textInfos.UNIT_CHARACTER) + self.assertEqual(ti.text, testText[:2]) + ti.collapse(end=True) + ti.expand(textInfos.UNIT_CHARACTER) + self.assertEqual(ti.text, testText[2]) + region = braille.TextInfoRegion(obj) + region.update() + index = 3 # Position of e + pos = region.rawToBraillePos[index] + region.routeTo(pos) + ti = obj.makeTextInfo(textInfos.POSITION_CARET) + ti.expand(textInfos.UNIT_CHARACTER) + self.assertEqual(ti.text, testText[index]) From 87273d52ea2c805ee95e27a8cd8daf39900a62d1 Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Wed, 17 Jul 2024 16:22:11 +0200 Subject: [PATCH 04/11] Enable uniscribe in textProvider --- tests/unit/textProvider.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/unit/textProvider.py b/tests/unit/textProvider.py index 6b3d08bd909..8e072f047ae 100644 --- a/tests/unit/textProvider.py +++ b/tests/unit/textProvider.py @@ -18,8 +18,6 @@ class BasicTextInfo(NVDAObjectTextInfo): - # NVDAHelper is not initialized, so we can't use Uniscribe. - useUniscribe = False # Most of our code use UTF-16 as internal encoding. # Mimic this behavior, so we can also implicitly test textUtils module code encoding = textUtils.WCHAR_ENCODING From c9694dc68e22e70681975c28fa7e0374a809c88f Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Wed, 17 Jul 2024 20:56:37 +0200 Subject: [PATCH 05/11] Handle composites --- source/braille.py | 29 ++++++++++++++++------- tests/unit/test_braille/test_routing.py | 31 +++++++++++++++++++++++-- 2 files changed, 50 insertions(+), 10 deletions(-) diff --git a/source/braille.py b/source/braille.py index 1317182d73d..2094282ff58 100644 --- a/source/braille.py +++ b/source/braille.py @@ -1426,21 +1426,34 @@ def update(self): self._brailleInputIndStart = None def getTextInfoForBraillePos(self, braillePos: int) -> textInfos.TextInfo: - """Fetches a collapsed TextInfo at the specified braille position in the region.""" + """Fetches a collapsed TextInfo at the specified braille position in the region. + :param braillePos: The braille position. + If no textInfo could be found at braillePos, + try to find one at braillePos - 1 until a position has been found. + """ pos = self._rawToContentPos[self.brailleToRawPos[braillePos]] # pos is relative to the start of the reading unit. - if pos > 0: - # Move pos code points from the start. + for i, curPos in enumerate(range(pos, max(-1, pos - 3), -1)): + if curPos == 0: + # Not necessary to find offset. + break + # Move curPos code points from the start. # Note that, as liblouis uses 32 bit encoding internally, # it is really safe to assume that one code point offset is equal to one character within liblouis. + # If an attempt fails, we try to move to the previous character try: - return self._readingInfo.moveToCodepointOffset(pos) - except (ValueError, RuntimeError): - log.exception(f"Error in moveToCodepointOffset, falling back to moving by {pos} characters") + return self._readingInfo.moveToCodepointOffset(curPos) + except RuntimeError: + if i < 2: + logFunc = log.debug + else: + logFunc = log.error + curPos = pos + logFunc(f"Error in moveToCodepointOffset in iteration {i + 1} (position {curPos}") dest = self._readingInfo.copy() dest.collapse() - if pos > 0: - dest.move(textInfos.UNIT_CHARACTER, pos) + if curPos > 0: + dest.move(textInfos.UNIT_CHARACTER, curPos) return dest def routeTo(self, braillePos: int): diff --git a/tests/unit/test_braille/test_routing.py b/tests/unit/test_braille/test_routing.py index d552d20d538..cc827c78608 100644 --- a/tests/unit/test_braille/test_routing.py +++ b/tests/unit/test_braille/test_routing.py @@ -152,8 +152,8 @@ def test_moveCaret_always_instantActivate(self): class TestTextInfoRegionRouting(unittest.TestCase): """A test for TextInfoRegion.getTextInfoForBraillePos, which is used in braille cursor routing. This test ensures that braille routes to the expected character when dealing with emoji - containing modifier characters. - These emoji are threated as one character by uniscribe, however they span multiple characters + or other composites. + These glyphs are threated as one character by uniscribe, however they span multiple characters on a braille display. Note that due to the nature of this test, it relies on uniscribe to be available. """ @@ -175,3 +175,30 @@ def test_routeToEmoji(self): ti = obj.makeTextInfo(textInfos.POSITION_CARET) ti.expand(textInfos.UNIT_CHARACTER) self.assertEqual(ti.text, testText[index]) + + def test_routeToComposite(self): + testText = "רבְּר" + obj = BasicTextProvider(text=testText) + ti = obj.makeTextInfo(textInfos.POSITION_CARET) + ti.expand(textInfos.UNIT_CHARACTER) + self.assertEqual(ti.text, testText[0]) + ti.collapse(end=True) + ti.expand(textInfos.UNIT_CHARACTER) + self.assertEqual(ti.text, testText[1:4]) + ti.collapse(end=True) + ti.expand(textInfos.UNIT_CHARACTER) + self.assertEqual(ti.text, testText[4]) + region = braille.TextInfoRegion(obj) + region.update() + index = 1 # Position of ב + pos = region.rawToBraillePos[index] + region.routeTo(pos) + ti = obj.makeTextInfo(textInfos.POSITION_CARET) + ti.expand(textInfos.UNIT_CHARACTER) + self.assertEqual(ti.text, testText[1:4]) + index = 3 # Position of ּ + pos = region.rawToBraillePos[index] + region.routeTo(pos) + ti = obj.makeTextInfo(textInfos.POSITION_CARET) + ti.expand(textInfos.UNIT_CHARACTER) + self.assertEqual(ti.text, testText[1:4]) From baade86c6cb786e5829b37ed5fc2dede4d9c630e Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Wed, 17 Jul 2024 21:00:40 +0200 Subject: [PATCH 06/11] Use a max of 10 iterations --- source/braille.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/source/braille.py b/source/braille.py index 2094282ff58..ab0b0924d56 100644 --- a/source/braille.py +++ b/source/braille.py @@ -1433,7 +1433,8 @@ def getTextInfoForBraillePos(self, braillePos: int) -> textInfos.TextInfo: """ pos = self._rawToContentPos[self.brailleToRawPos[braillePos]] # pos is relative to the start of the reading unit. - for i, curPos in enumerate(range(pos, max(-1, pos - 3), -1)): + maxIterations = 10 + for i, curPos in enumerate(range(pos, max(-1, pos - maxIterations), -1)): if curPos == 0: # Not necessary to find offset. break @@ -1444,7 +1445,7 @@ def getTextInfoForBraillePos(self, braillePos: int) -> textInfos.TextInfo: try: return self._readingInfo.moveToCodepointOffset(curPos) except RuntimeError: - if i < 2: + if i + 1 < maxIterations: logFunc = log.debug else: logFunc = log.error From 7bc812653ca6fa20e363631efd6f779d4791a1d1 Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Thu, 18 Jul 2024 08:31:22 +0200 Subject: [PATCH 07/11] Add suggestions from @coderabbitai code review --- tests/unit/test_braille/test_routing.py | 78 +++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/tests/unit/test_braille/test_routing.py b/tests/unit/test_braille/test_routing.py index cc827c78608..72916aaf11f 100644 --- a/tests/unit/test_braille/test_routing.py +++ b/tests/unit/test_braille/test_routing.py @@ -202,3 +202,81 @@ def test_routeToComposite(self): ti = obj.makeTextInfo(textInfos.POSITION_CARET) ti.expand(textInfos.UNIT_CHARACTER) self.assertEqual(ti.text, testText[1:4]) + + def test_routeToMultipleEmoji(self): + testText = "👩🏽‍🚀👨🏻‍🚒" + obj = BasicTextProvider(text=testText) + ti = obj.makeTextInfo(textInfos.POSITION_CARET) + ti.expand(textInfos.UNIT_CHARACTER) + self.assertEqual(ti.text, testText[:2]) + ti.collapse(end=True) + ti.expand(textInfos.UNIT_CHARACTER) + self.assertEqual(ti.text, testText[2:4]) + region = braille.TextInfoRegion(obj) + region.update() + index = 4 # Position of the second emoji + pos = region.rawToBraillePos[index] + region.routeTo(pos) + ti = obj.makeTextInfo(textInfos.POSITION_CARET) + ti.expand(textInfos.UNIT_CHARACTER) + self.assertEqual(ti.text, testText[4:6]) + + def test_routeToZeroWidthJoiner(self): + testText = "👨‍👩‍👧‍👦" + obj = BasicTextProvider(text=testText) + ti = obj.makeTextInfo(textInfos.POSITION_CARET) + ti.expand(textInfos.UNIT_CHARACTER) + self.assertEqual(ti.text, testText[:1]) + ti.collapse(end=True) + ti.expand(textInfos.UNIT_CHARACTER) + self.assertEqual(ti.text, testText[1:2]) + region = braille.TextInfoRegion(obj) + region.update() + index = 2 # Position of the second family member + pos = region.rawToBraillePos[index] + region.routeTo(pos) + ti = obj.makeTextInfo(textInfos.POSITION_CARET) + ti.expand(textInfos.UNIT_CHARACTER) + self.assertEqual(ti.text, testText[2:3]) + + def test_routeToVariationSelector(self): + testText = "✌️" + obj = BasicTextProvider(text=testText) + ti = obj.makeTextInfo(textInfos.POSITION_CARET) + ti.expand(textInfos.UNIT_CHARACTER) + self.assertEqual(ti.text, testText[:1]) + ti.collapse(end=True) + ti.expand(textInfos.UNIT_CHARACTER) + self.assertEqual(ti.text, testText[1:2]) + region = braille.TextInfoRegion(obj) + region.update() + index = 1 # Position of the variation selector + pos = region.rawToBraillePos[index] + region.routeTo(pos) + ti = obj.makeTextInfo(textInfos.POSITION_CARET) + ti.expand(textInfos.UNIT_CHARACTER) + self.assertEqual(ti.text, testText[1:2]) + + def test_routeToMixedContent(self): + testText = "Hello 👋, how are you? רָבּ" + obj = BasicTextProvider(text=testText) + ti = obj.makeTextInfo(textInfos.POSITION_CARET) + ti.expand(textInfos.UNIT_CHARACTER) + self.assertEqual(ti.text, testText[:1]) + ti.collapse(end=True) + ti.expand(textInfos.UNIT_CHARACTER) + self.assertEqual(ti.text, testText[1:2]) + region = braille.TextInfoRegion(obj) + region.update() + index = 6 # Position of the emoji + pos = region.rawToBraillePos[index] + region.routeTo(pos) + ti = obj.makeTextInfo(textInfos.POSITION_CARET) + ti.expand(textInfos.UNIT_CHARACTER) + self.assertEqual(ti.text, testText[6:7]) + index = 18 # Position of the Hebrew composite character + pos = region.rawToBraillePos[index] + region.routeTo(pos) + ti = obj.makeTextInfo(textInfos.POSITION_CARET) + ti.expand(textInfos.UNIT_CHARACTER) + self.assertEqual(ti.text, testText[18:21]) From 2fb01605186be3d37b6ea337ae31d7ca8677a785 Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Thu, 18 Jul 2024 08:38:47 +0200 Subject: [PATCH 08/11] Add time check --- source/braille.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/source/braille.py b/source/braille.py index ab0b0924d56..97dd2d1deee 100644 --- a/source/braille.py +++ b/source/braille.py @@ -1434,6 +1434,7 @@ def getTextInfoForBraillePos(self, braillePos: int) -> textInfos.TextInfo: pos = self._rawToContentPos[self.brailleToRawPos[braillePos]] # pos is relative to the start of the reading unit. maxIterations = 10 + start_time = time.time() for i, curPos in enumerate(range(pos, max(-1, pos - maxIterations), -1)): if curPos == 0: # Not necessary to find offset. @@ -1445,12 +1446,15 @@ def getTextInfoForBraillePos(self, braillePos: int) -> textInfos.TextInfo: try: return self._readingInfo.moveToCodepointOffset(curPos) except RuntimeError: - if i + 1 < maxIterations: - logFunc = log.debug - else: - logFunc = log.error + msg = f"Error in moveToCodepointOffset in iteration {i + 1} (position {curPos}" + if i + 1 >= maxIterations or (exceeded := time.time() - start_time > 0.5): + logFunc = log.exception curPos = pos - logFunc(f"Error in moveToCodepointOffset in iteration {i + 1} (position {curPos}") + if exceeded: + msg += ", exceeded time limit of 0.5 seconds" + else: + logFunc = log.debug + logFunc(msg) dest = self._readingInfo.copy() dest.collapse() if curPos > 0: From eee85fa9fdbae3e357bb894007e37dd0f5721dad Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Thu, 18 Jul 2024 19:50:25 +0200 Subject: [PATCH 09/11] Stick to one unit test because uniscribe doesn't offer uniformity when testing --- tests/unit/test_braille/test_routing.py | 78 ------------------------- 1 file changed, 78 deletions(-) diff --git a/tests/unit/test_braille/test_routing.py b/tests/unit/test_braille/test_routing.py index 72916aaf11f..cc827c78608 100644 --- a/tests/unit/test_braille/test_routing.py +++ b/tests/unit/test_braille/test_routing.py @@ -202,81 +202,3 @@ def test_routeToComposite(self): ti = obj.makeTextInfo(textInfos.POSITION_CARET) ti.expand(textInfos.UNIT_CHARACTER) self.assertEqual(ti.text, testText[1:4]) - - def test_routeToMultipleEmoji(self): - testText = "👩🏽‍🚀👨🏻‍🚒" - obj = BasicTextProvider(text=testText) - ti = obj.makeTextInfo(textInfos.POSITION_CARET) - ti.expand(textInfos.UNIT_CHARACTER) - self.assertEqual(ti.text, testText[:2]) - ti.collapse(end=True) - ti.expand(textInfos.UNIT_CHARACTER) - self.assertEqual(ti.text, testText[2:4]) - region = braille.TextInfoRegion(obj) - region.update() - index = 4 # Position of the second emoji - pos = region.rawToBraillePos[index] - region.routeTo(pos) - ti = obj.makeTextInfo(textInfos.POSITION_CARET) - ti.expand(textInfos.UNIT_CHARACTER) - self.assertEqual(ti.text, testText[4:6]) - - def test_routeToZeroWidthJoiner(self): - testText = "👨‍👩‍👧‍👦" - obj = BasicTextProvider(text=testText) - ti = obj.makeTextInfo(textInfos.POSITION_CARET) - ti.expand(textInfos.UNIT_CHARACTER) - self.assertEqual(ti.text, testText[:1]) - ti.collapse(end=True) - ti.expand(textInfos.UNIT_CHARACTER) - self.assertEqual(ti.text, testText[1:2]) - region = braille.TextInfoRegion(obj) - region.update() - index = 2 # Position of the second family member - pos = region.rawToBraillePos[index] - region.routeTo(pos) - ti = obj.makeTextInfo(textInfos.POSITION_CARET) - ti.expand(textInfos.UNIT_CHARACTER) - self.assertEqual(ti.text, testText[2:3]) - - def test_routeToVariationSelector(self): - testText = "✌️" - obj = BasicTextProvider(text=testText) - ti = obj.makeTextInfo(textInfos.POSITION_CARET) - ti.expand(textInfos.UNIT_CHARACTER) - self.assertEqual(ti.text, testText[:1]) - ti.collapse(end=True) - ti.expand(textInfos.UNIT_CHARACTER) - self.assertEqual(ti.text, testText[1:2]) - region = braille.TextInfoRegion(obj) - region.update() - index = 1 # Position of the variation selector - pos = region.rawToBraillePos[index] - region.routeTo(pos) - ti = obj.makeTextInfo(textInfos.POSITION_CARET) - ti.expand(textInfos.UNIT_CHARACTER) - self.assertEqual(ti.text, testText[1:2]) - - def test_routeToMixedContent(self): - testText = "Hello 👋, how are you? רָבּ" - obj = BasicTextProvider(text=testText) - ti = obj.makeTextInfo(textInfos.POSITION_CARET) - ti.expand(textInfos.UNIT_CHARACTER) - self.assertEqual(ti.text, testText[:1]) - ti.collapse(end=True) - ti.expand(textInfos.UNIT_CHARACTER) - self.assertEqual(ti.text, testText[1:2]) - region = braille.TextInfoRegion(obj) - region.update() - index = 6 # Position of the emoji - pos = region.rawToBraillePos[index] - region.routeTo(pos) - ti = obj.makeTextInfo(textInfos.POSITION_CARET) - ti.expand(textInfos.UNIT_CHARACTER) - self.assertEqual(ti.text, testText[6:7]) - index = 18 # Position of the Hebrew composite character - pos = region.rawToBraillePos[index] - region.routeTo(pos) - ti = obj.makeTextInfo(textInfos.POSITION_CARET) - ti.expand(textInfos.UNIT_CHARACTER) - self.assertEqual(ti.text, testText[18:21]) From 1de44fc8fe269e35292fb081d3217cba000e0f8f Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter <3049216+LeonarddeR@users.noreply.github.com> Date: Tue, 30 Jul 2024 08:53:31 +0200 Subject: [PATCH 10/11] Update tests/unit/test_braille/test_routing.py --- tests/unit/test_braille/test_routing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_braille/test_routing.py b/tests/unit/test_braille/test_routing.py index cc827c78608..debbeb72a98 100644 --- a/tests/unit/test_braille/test_routing.py +++ b/tests/unit/test_braille/test_routing.py @@ -196,7 +196,7 @@ def test_routeToComposite(self): ti = obj.makeTextInfo(textInfos.POSITION_CARET) ti.expand(textInfos.UNIT_CHARACTER) self.assertEqual(ti.text, testText[1:4]) - index = 3 # Position of ּ + index = 3 # Position of ּ (\u5bc) pos = region.rawToBraillePos[index] region.routeTo(pos) ti = obj.makeTextInfo(textInfos.POSITION_CARET) From 3d83711db3cb209cd78835c8b6677bf9507ade1f Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter <3049216+LeonarddeR@users.noreply.github.com> Date: Tue, 30 Jul 2024 08:54:26 +0200 Subject: [PATCH 11/11] Apply suggestions from code review Co-authored-by: Sean Budd --- source/braille.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/braille.py b/source/braille.py index 97dd2d1deee..48247bc132a 100644 --- a/source/braille.py +++ b/source/braille.py @@ -1434,7 +1434,7 @@ def getTextInfoForBraillePos(self, braillePos: int) -> textInfos.TextInfo: pos = self._rawToContentPos[self.brailleToRawPos[braillePos]] # pos is relative to the start of the reading unit. maxIterations = 10 - start_time = time.time() + startTime = time.time() for i, curPos in enumerate(range(pos, max(-1, pos - maxIterations), -1)): if curPos == 0: # Not necessary to find offset. @@ -1447,7 +1447,7 @@ def getTextInfoForBraillePos(self, braillePos: int) -> textInfos.TextInfo: return self._readingInfo.moveToCodepointOffset(curPos) except RuntimeError: msg = f"Error in moveToCodepointOffset in iteration {i + 1} (position {curPos}" - if i + 1 >= maxIterations or (exceeded := time.time() - start_time > 0.5): + if i + 1 >= maxIterations or (exceeded := time.time() - startTime > 0.5): logFunc = log.exception curPos = pos if exceeded: