Skip to content

Commit e3b89e7

Browse files
authored
Merge 3d83711 into 5d04b49
2 parents 5d04b49 + 3d83711 commit e3b89e7

6 files changed

Lines changed: 108 additions & 13 deletions

File tree

source/NVDAObjects/UIA/wordDocument.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,18 @@ def _getTextFromUIARange(self, textRange):
270270
t = t.replace(END_OF_ROW_MARK, "")
271271
return t
272272

273+
def _getTextForCodepointMovement(self) -> str:
274+
"""
275+
#8576, #10960: In Word, list bullets are exposed in text but are ignored when moving by character.
276+
Therefore in `getTextWithFields`, the bullets are stripped from the text and exposed in the `line-prefix` field.
277+
To stay compatible with this, we can't simply use the `text` property,
278+
as it can potentially contain bullets that should be stripped.
279+
"""
280+
t = super()._getTextForCodepointMovement()
281+
if not t:
282+
return t
283+
return "".join(f for f in self.getTextWithFields(formatConfig=dict()) if isinstance(f, str))
284+
273285
def _isEndOfRow(self):
274286
"""Is this textInfo positioned on an end-of-row mark?"""
275287
info = self.copy()
@@ -648,7 +660,7 @@ def _caretMoveBySentenceHelper(self, gesture, direction):
648660
description=_(
649661
# Translators: a description for a script that reports the comment at the caret.
650662
"Reports the text of the comment where the system caret is located."
651-
" If pressed twice, presents the information in a browsable message"
663+
" If pressed twice, presents the information in a browsable message",
652664
),
653665
category=SCRCAT_SYSTEMCARET,
654666
speakOnDemand=True,

source/braille.py

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1544,14 +1544,39 @@ def update(self):
15441544
self._brailleInputIndStart = None
15451545

15461546
def getTextInfoForBraillePos(self, braillePos: int) -> textInfos.TextInfo:
1547-
"""Fetches a collapsed TextInfo at the specified braille position in the region."""
1547+
"""Fetches a collapsed TextInfo at the specified braille position in the region.
1548+
:param braillePos: The braille position.
1549+
If no textInfo could be found at braillePos,
1550+
try to find one at braillePos - 1 until a position has been found.
1551+
"""
15481552
pos = self._rawToContentPos[self.brailleToRawPos[braillePos]]
15491553
# pos is relative to the start of the reading unit.
1550-
# Therefore, get the start of the reading unit...
1554+
maxIterations = 10
1555+
startTime = time.time()
1556+
for i, curPos in enumerate(range(pos, max(-1, pos - maxIterations), -1)):
1557+
if curPos == 0:
1558+
# Not necessary to find offset.
1559+
break
1560+
# Move curPos code points from the start.
1561+
# Note that, as liblouis uses 32 bit encoding internally,
1562+
# it is really safe to assume that one code point offset is equal to one character within liblouis.
1563+
# If an attempt fails, we try to move to the previous character
1564+
try:
1565+
return self._readingInfo.moveToCodepointOffset(curPos)
1566+
except RuntimeError:
1567+
msg = f"Error in moveToCodepointOffset in iteration {i + 1} (position {curPos}"
1568+
if i + 1 >= maxIterations or (exceeded := time.time() - startTime > 0.5):
1569+
logFunc = log.exception
1570+
curPos = pos
1571+
if exceeded:
1572+
msg += ", exceeded time limit of 0.5 seconds"
1573+
else:
1574+
logFunc = log.debug
1575+
logFunc(msg)
15511576
dest = self._readingInfo.copy()
15521577
dest.collapse()
1553-
# and move pos characters from there.
1554-
dest.move(textInfos.UNIT_CHARACTER, pos)
1578+
if curPos > 0:
1579+
dest.move(textInfos.UNIT_CHARACTER, curPos)
15551580
return dest
15561581

15571582
def routeTo(self, braillePos: int):

source/textInfos/__init__.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -711,6 +711,10 @@ def getMathMl(self, field):
711711
"""
712712
raise NotImplementedError
713713

714+
def _getTextForCodepointMovement(self) -> str:
715+
"""Gets the text as used in moveToCodepointOffset."""
716+
return self.text
717+
714718
def moveToCodepointOffset(
715719
self,
716720
codepointOffset: int,
@@ -803,7 +807,7 @@ def moveToCodepointOffset(
803807
we reduce the count of characters in order to make sure
804808
the algorithm makes some progress on each iteration.
805809
"""
806-
text = self.text
810+
text = self._getTextForCodepointMovement()
807811
if codepointOffset < 0 or codepointOffset > len(text):
808812
raise ValueError
809813
if codepointOffset == 0 or codepointOffset == len(text):
@@ -845,7 +849,7 @@ def moveToCodepointOffset(
845849
moveCharacters = codepointOffsetLeft
846850
code = tmpInfo.move(UNIT_CHARACTER, moveCharacters, endPoint="end")
847851
lastMove = moveCharacters
848-
tmpText = tmpInfo.text
852+
tmpText = tmpInfo._getTextForCodepointMovement()
849853
actualCodepointOffset = len(tmpText)
850854
if not text.startswith(tmpText):
851855
raise RuntimeError(
@@ -865,7 +869,7 @@ def moveToCodepointOffset(
865869
moveCharacters = -codepointOffsetRight
866870
code = tmpInfo.move(UNIT_CHARACTER, moveCharacters, endPoint="start")
867871
lastMove = moveCharacters
868-
tmpText = tmpInfo.text
872+
tmpText = tmpInfo._getTextForCodepointMovement()
869873
actualCodepointOffset = totalCodepointOffset - len(tmpText)
870874
if not text.endswith(tmpText):
871875
raise RuntimeError(

tests/unit/test_braille/test_routing.py

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
# A part of NonVisual Desktop Access (NVDA)
22
# This file is covered by the GNU General Public License.
33
# See the file COPYING for more details.
4-
# Copyright (C) 2023 NV Access Limited, Leonard de Ruijter
4+
# Copyright (C) 2023-2024 NV Access Limited, Leonard de Ruijter
55

6-
"""Unit tests for the move system caret when routing review cursor braille setting."""
6+
"""Unit tests for braille cursor routing."""
77

88
import config
99
import braille
1010
import textInfos
1111
import api
1212
import controlTypes
13-
from ..textProvider import CursorManager
13+
from ..textProvider import CursorManager, BasicTextProvider
1414
import unittest
1515
import time
1616
from config.featureFlagEnums import ReviewRoutingMovesSystemCaretFlag
@@ -147,3 +147,58 @@ def test_moveCaret_always_instantActivate(self):
147147
self.assertGreaterEqual(self.cm.lastActivateTime, curTime)
148148
caret = self.cm.makeTextInfo(textInfos.POSITION_CARET)
149149
self.assertEquals(caret, review)
150+
151+
152+
class TestTextInfoRegionRouting(unittest.TestCase):
153+
"""A test for TextInfoRegion.getTextInfoForBraillePos, which is used in braille cursor routing.
154+
This test ensures that braille routes to the expected character when dealing with emoji
155+
or other composites.
156+
These glyphs are threated as one character by uniscribe, however they span multiple characters
157+
on a braille display.
158+
Note that due to the nature of this test, it relies on uniscribe to be available.
159+
"""
160+
161+
def test_routeToEmoji(self):
162+
testText = "⚠️test"
163+
obj = BasicTextProvider(text=testText)
164+
ti = obj.makeTextInfo(textInfos.POSITION_CARET)
165+
ti.expand(textInfos.UNIT_CHARACTER)
166+
self.assertEqual(ti.text, testText[:2])
167+
ti.collapse(end=True)
168+
ti.expand(textInfos.UNIT_CHARACTER)
169+
self.assertEqual(ti.text, testText[2])
170+
region = braille.TextInfoRegion(obj)
171+
region.update()
172+
index = 3 # Position of e
173+
pos = region.rawToBraillePos[index]
174+
region.routeTo(pos)
175+
ti = obj.makeTextInfo(textInfos.POSITION_CARET)
176+
ti.expand(textInfos.UNIT_CHARACTER)
177+
self.assertEqual(ti.text, testText[index])
178+
179+
def test_routeToComposite(self):
180+
testText = "רבְּר"
181+
obj = BasicTextProvider(text=testText)
182+
ti = obj.makeTextInfo(textInfos.POSITION_CARET)
183+
ti.expand(textInfos.UNIT_CHARACTER)
184+
self.assertEqual(ti.text, testText[0])
185+
ti.collapse(end=True)
186+
ti.expand(textInfos.UNIT_CHARACTER)
187+
self.assertEqual(ti.text, testText[1:4])
188+
ti.collapse(end=True)
189+
ti.expand(textInfos.UNIT_CHARACTER)
190+
self.assertEqual(ti.text, testText[4])
191+
region = braille.TextInfoRegion(obj)
192+
region.update()
193+
index = 1 # Position of ב
194+
pos = region.rawToBraillePos[index]
195+
region.routeTo(pos)
196+
ti = obj.makeTextInfo(textInfos.POSITION_CARET)
197+
ti.expand(textInfos.UNIT_CHARACTER)
198+
self.assertEqual(ti.text, testText[1:4])
199+
index = 3 # Position of ּ (\u5bc)
200+
pos = region.rawToBraillePos[index]
201+
region.routeTo(pos)
202+
ti = obj.makeTextInfo(textInfos.POSITION_CARET)
203+
ti.expand(textInfos.UNIT_CHARACTER)
204+
self.assertEqual(ti.text, testText[1:4])

tests/unit/textProvider.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@
1818

1919

2020
class BasicTextInfo(NVDAObjectTextInfo):
21-
# NVDAHelper is not initialized, so we can't use Uniscribe.
22-
useUniscribe = False
2321
# Most of our code use UTF-16 as internal encoding.
2422
# Mimic this behavior, so we can also implicitly test textUtils module code
2523
encoding = textUtils.WCHAR_ENCODING

user_docs/en/changes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ The available options are:
2323
* UIA in Windows Terminal. (#16873, @codeofdusk)
2424
* When accessing Microsoft Word without UI Automation, NVDA no longer outputs garbage characters in braille in table headers defined with the set row and column header commands. (#7212)
2525
* The Seika Notetaker driver now correctly generates braille input for space, backspace and dots with space/backspace gestures. (#16642, @school510587)
26+
* Braille cursor routing is now much more reliable when a line contains one or more Unicode variation selectors or decomposed characters. (#10960, @mltony, @LeonarddeR)
2627
* In on-demand speech mode, NVDA does not talk anymore when a message is opened in Outlook, when a new page is loaded in a browser or during the slideshow in PowerPoint. (#16825, @CyrilleB79)
2728
* In Mozilla Firefox, moving the mouse over text before or after a link now reliably reports the text. (#15990, @jcsteh)
2829

0 commit comments

Comments
 (0)