Skip to content

Commit cf90f52

Browse files
authored
Merge e6ed323 into bf96860
2 parents bf96860 + e6ed323 commit cf90f52

File tree

12 files changed

+720
-28
lines changed

12 files changed

+720
-28
lines changed

source/NVDAObjects/UIA/__init__.py

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
import ui
6666
import winVersion
6767
import NVDAObjects
68+
from documentNavigation import sentenceNavigation
6869

6970

7071
paragraphIndentIDs = {
@@ -899,26 +900,66 @@ def _get_boundingRects(self):
899900
return self._getBoundingRectsFromUIARange(self._rangeObj)
900901

901902
def expand(self, unit: str) -> None:
902-
UIAUnit=UIAHandler.NVDAUnitsToUIAUnits[unit]
903-
self._rangeObj.ExpandToEnclosingUnit(UIAUnit)
903+
if unit == textInfos.UNIT_SENTENCE:
904+
context = sentenceNavigation.SentenceContext(self)
905+
sentence = context.moveSentence(0)
906+
self._rangeObj = sentence._rangeObj
907+
else:
908+
UIAUnit = UIAHandler.NVDAUnitsToUIAUnits[unit]
909+
self._rangeObj.ExpandToEnclosingUnit(UIAUnit)
904910

905911
def move(
906912
self,
907913
unit: str,
908914
direction: int,
909915
endPoint: Optional[str] = None,
910916
):
911-
UIAUnit=UIAHandler.NVDAUnitsToUIAUnits[unit]
912-
if endPoint=="start":
913-
res=self._rangeObj.MoveEndpointByUnit(UIAHandler.TextPatternRangeEndpoint_Start,UIAUnit,direction)
914-
elif endPoint=="end":
915-
res=self._rangeObj.MoveEndpointByUnit(UIAHandler.TextPatternRangeEndpoint_End,UIAUnit,direction)
917+
if unit == textInfos.UNIT_SENTENCE:
918+
if direction == 0:
919+
return 0
920+
endPointInfo = self.copy()
921+
if endPoint == "start":
922+
endPointInfo.collapse()
923+
elif endPoint == "end":
924+
endPointInfo.collapse(end=True)
925+
if direction > 0:
926+
iteration = range(direction)
927+
direction = 1
928+
else:
929+
iteration = range(-direction)
930+
direction = -1
931+
result = 0
932+
for i in iteration:
933+
context = sentenceNavigation.SentenceContext(endPointInfo)
934+
sentence = context.moveSentence(direction)
935+
if sentence is not None:
936+
result += direction
937+
endPointInfo = sentence
938+
else:
939+
break
940+
if result == 0:
941+
return 0
942+
endPointInfo.collapse()
943+
if endPoint == "start":
944+
self.setEndPoint(endPointInfo, "startToStart")
945+
elif endPoint == "end":
946+
self.setEndPoint(endPointInfo, "endToEnd")
947+
else:
948+
self._rangeObj = endPointInfo._rangeObj
949+
return result
916950
else:
917-
res=self._rangeObj.Move(UIAUnit,direction)
918-
#Some Implementations of Move and moveEndpointByUnit return a positive number even if the direction is negative
919-
if direction<0 and res>0:
920-
res=0-res
921-
return res
951+
UIAUnit = UIAHandler.NVDAUnitsToUIAUnits[unit]
952+
if endPoint == "start":
953+
res = self._rangeObj.MoveEndpointByUnit(UIAHandler.TextPatternRangeEndpoint_Start, UIAUnit, direction)
954+
elif endPoint == "end":
955+
res = self._rangeObj.MoveEndpointByUnit(UIAHandler.TextPatternRangeEndpoint_End, UIAUnit, direction)
956+
else:
957+
res = self._rangeObj.Move(UIAUnit, direction)
958+
# Some Implementations of Move and moveEndpointByUnit return a positive number even if the direction is
959+
# negative
960+
if direction < 0 and res > 0:
961+
res = 0 - res
962+
return res
922963

923964
def copy(self):
924965
return self.__class__(self.obj,None,_rangeObj=self._rangeObj)

source/browseMode.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
from abc import ABCMeta, abstractmethod
5757
import globalVars
5858
from typing import Optional
59+
from documentNavigation.sentenceNavigation import getSentenceStopRegex
5960

6061

6162
def reportPassThrough(treeInterceptor,onlyIfChanged=True):
@@ -473,12 +474,14 @@ def _quickNavScript(self,gesture, itemType, direction, errorMessage, readUnit):
473474
if itemType=="notLinkBlock":
474475
iterFactory=self._iterNotLinkBlock
475476
elif itemType == "textParagraph":
476-
punctuationMarksRegex = re.compile(
477-
config.conf["virtualBuffers"]["textParagraphRegex"],
478-
)
477+
punctuationMarksRegex = getSentenceStopRegex()
479478

480479
def paragraphFunc(info: textInfos.TextInfo) -> bool:
481-
return punctuationMarksRegex.search(info.text) is not None
480+
# sentence regex always matches beginning and end of string. We add some words at the end to test
481+
# whether there is third match in the middle, that would be suggestive of a complete sentence.
482+
text = info.text + " traiiling word"
483+
matches = punctuationMarksRegex.findall(text)
484+
return len(matches) >= 3
482485

483486
def iterFactory(direction: str, pos: textInfos.TextInfo) -> Generator[TextInfoQuickNavItem, None, None]:
484487
return self._iterSimilarParagraph(
@@ -1680,6 +1683,10 @@ def event_caretMovementFailed(self, obj, nextHandler, gesture=None):
16801683
currentExpandedControl=None #: an NVDAObject representing the control that has just been expanded with the collapseOrExpandControl script.
16811684

16821685
def script_collapseOrExpandControl(self, gesture: inputCore.InputGesture):
1686+
states = self.currentNVDAObject.states
1687+
if controlTypes.State.COLLAPSED not in states and controlTypes.State.EXPANDED not in states:
1688+
direction = 1 if gesture.mainKeyName == "downArrow" else -1
1689+
return self._caretMovementScriptHelper(gesture, textInfos.UNIT_SENTENCE, direction)
16831690
if not config.conf["virtualBuffers"]["autoFocusFocusableElements"]:
16841691
self._focusLastFocusableObject()
16851692
# Give the application time to focus the control.

source/config/configDefaults.py

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,46 @@
22
# Copyright (C) 2024 NV Access Limited
33
# This file is covered by the GNU General Public License.
44
# See the file COPYING for more details.
5+
import json
6+
57

68
DEFAULT_TEXT_PARAGRAPH_REGEX = (
7-
r"({lookBehind}{optQuote}{punc}{optQuote}{optWiki}{lookAhead}|{punc2}|{cjk})".format(
9+
r"^|({plb}{nlb}{optQuote}{dot}{optQuote}|{punc2}{optQuote}){optWiki}{spaces}|{cjk}|{n2}|\Z".format(
810
# Look behind clause ensures that we have a text character before text punctuation mark.
911
# We have a positive lookBehind \w that resolves to a text character in any language,
1012
# coupled with negative lookBehind \d that excludes digits.
11-
lookBehind=r'(?<=\w)(?<!\d)',
13+
plb=r"(?<=\w)(?<!\d)",
14+
# Language-specific exceptions: characters suggesting that the following dot is not indicative
15+
# of a sentence stop.
16+
# This is a negative look-behind and will be inserted later when language is specified.
17+
nlb="{nonBreakingRegex}",
1218
# In some cases quote or closing parenthesis might appear right before or right after text punctuation.
1319
# For example:
1420
# > He replied, "That's wonderful."
1521
optQuote=r'["”»)]?',
1622
# Actual punctuation marks that suggest end of sentence.
1723
# We don't include symbols like comma and colon, because of too many false positives.
1824
# We include question mark and exclamation mark below in punc2.
19-
punc=r'[.…]{1,3}',
25+
dot=r"[.]{{1,3}}",
2026
# On Wikipedia references appear right after period in sentences, the following clause takes this
2127
# into account. For example:
2228
# > On his father's side, he was a direct descendant of John Churchill.[3]
23-
optWiki=r'(\[\d+\])*',
24-
# LookAhead clause checks that punctuation mark must be followed by either space,
29+
optWiki=r"(\[\d+\])*",
30+
# spaces clause checks that punctuation mark must be followed by either space,
2531
# or newLine symbol or end of string.
26-
lookAhead=r'(?=[\r\n  ]|$)',
32+
spaces=r"([  ]+|([  \t]*\n)+|$)",
2733
# Include question mark and exclamation mark with no extra conditions,
2834
# since they don't trigger as many false positives.
29-
punc2=r'[?!]',
35+
punc2=r"[?!…]",
3036
# We also check for CJK full-width punctuation marks without any extra rules.
31-
cjk=r'[.!?:;]',
37+
cjk=r"[。.!?]",
38+
# Double newline means end of sentence too.
39+
n2=r"([  \t]*\n){{2,}}",
3240
)
3341
)
42+
43+
44+
nonBreakingPrefix = json.dumps({
45+
"en": r"\b[A-Z]|\bMr|\bMs|\bMrs|\bDr|\bProf|\bSt|\be.g|\bi.e",
46+
"ru": r"\b[A-ZА-Я]|\b[Тт]ов|\b[Уу]л|\bт.[ке]|\bт. [ке]",
47+
})

source/config/configSpec.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,8 @@
244244
245245
[documentNavigation]
246246
paragraphStyle = featureFlag(optionsEnum="ParagraphNavigationFlag", behaviorOfDefault="application")
247+
sentenceReconstruction = featureFlag(optionsEnum="SentenceReconstructionFlag", behaviorOfDefault="same_style_paragraphs") # noqa: E501 Breaking this line makes invalid config
248+
nonBreakingPrefixRegex = string(default='{configDefaults.nonBreakingPrefix}')
247249
248250
[reviewCursor]
249251
simpleReviewMode = boolean(default=True)

source/config/featureFlagEnums.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,25 @@ def _displayStringLabels(self):
100100
ALWAYS = enum.auto()
101101

102102

103+
class SentenceReconstructionFlag(DisplayStringEnum):
104+
@property
105+
def _displayStringLabels(self):
106+
return {
107+
# Translators: Sentence reconstruction mode
108+
SentenceReconstructionFlag.NEVER: _("Never reconstruct sentences across paragraphs"),
109+
# Translators: Sentence reconstruction mode
110+
SentenceReconstructionFlag.SAME_STYLE_PARAGRAPHS: _("Reconstruct sentences across same style paragraphs"),
111+
# Translators: Sentence reconstruction mode
112+
SentenceReconstructionFlag.ANY_PARAGRAPHS: _("Reconstruct sentences across any paragraphs"),
113+
}
114+
115+
DEFAULT = enum.auto()
116+
NEVER = enum.auto()
117+
SAME_STYLE_PARAGRAPHS = enum.auto()
118+
ANY_PARAGRAPHS = enum.auto()
119+
120+
121+
103122
class WindowsTerminalStrategyFlag(DisplayStringEnum):
104123
"""
105124
A feature flag for defining how new text is calculated in Windows Terminal

0 commit comments

Comments
 (0)