Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion source/NVDAObjects/UIA/wordDocument.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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,
Expand Down
33 changes: 29 additions & 4 deletions source/braille.py
Original file line number Diff line number Diff line change
Expand Up @@ -1426,14 +1426,39 @@ 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.
# Therefore, get the start of the reading unit...
maxIterations = 10
startTime = time.time()
for i, curPos in enumerate(range(pos, max(-1, pos - maxIterations), -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(curPos)
except RuntimeError:
msg = f"Error in moveToCodepointOffset in iteration {i + 1} (position {curPos}"
if i + 1 >= maxIterations or (exceeded := time.time() - startTime > 0.5):
logFunc = log.exception
curPos = pos
if exceeded:
msg += ", exceeded time limit of 0.5 seconds"
else:
logFunc = log.debug
logFunc(msg)
dest = self._readingInfo.copy()
dest.collapse()
# and move pos characters from there.
dest.move(textInfos.UNIT_CHARACTER, pos)
if curPos > 0:
dest.move(textInfos.UNIT_CHARACTER, curPos)
return dest

def routeTo(self, braillePos: int):
Expand Down
10 changes: 7 additions & 3 deletions source/textInfos/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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()
Comment thread
LeonarddeR marked this conversation as resolved.
if codepointOffset < 0 or codepointOffset > len(text):
raise ValueError
if codepointOffset == 0 or codepointOffset == len(text):
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand Down
61 changes: 58 additions & 3 deletions tests/unit/test_braille/test_routing.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -147,3 +147,58 @@ 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
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.
"""

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])

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 ּ (\u5bc)
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])
2 changes: 0 additions & 2 deletions tests/unit/textProvider.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions user_docs/en/changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
* UIA in Windows Terminal. (#16873, @codeofdusk)
* 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)
* The Seika Notetaker driver now correctly generates braille input for space, backspace and dots with space/backspace gestures. (#16642, @school510587)
* Braille cursor routing is now much more reliable when a line contains one or more Unicode variation selectors or decomposed characters. (#10960, @mltony, @LeonarddeR)
* 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)


Expand Down