Skip to content

Commit ea1c418

Browse files
committed
Sentence navigation
1 parent 2238cd9 commit ea1c418

File tree

9 files changed

+513
-26
lines changed

9 files changed

+513
-26
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
@@ -55,6 +55,7 @@
5555
from abc import ABCMeta, abstractmethod
5656
import globalVars
5757
from typing import Optional
58+
from documentNavigation.sentenceNavigation import getSentenceStopRegex
5859

5960

6061
def reportPassThrough(treeInterceptor,onlyIfChanged=True):
@@ -485,12 +486,14 @@ def _quickNavScript(self,gesture, itemType, direction, errorMessage, readUnit):
485486
if itemType=="notLinkBlock":
486487
iterFactory=self._iterNotLinkBlock
487488
elif itemType == "textParagraph":
488-
punctuationMarksRegex = re.compile(
489-
config.conf["virtualBuffers"]["textParagraphRegex"],
490-
)
489+
punctuationMarksRegex = getSentenceStopRegex()
491490

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

495498
def iterFactory(direction: str, pos: textInfos.TextInfo) -> Generator[TextInfoQuickNavItem, None, None]:
496499
return self._iterSimilarParagraph(
@@ -1662,6 +1665,10 @@ def event_caretMovementFailed(self, obj, nextHandler, gesture=None):
16621665
currentExpandedControl=None #: an NVDAObject representing the control that has just been expanded with the collapseOrExpandControl script.
16631666

16641667
def script_collapseOrExpandControl(self, gesture: inputCore.InputGesture):
1668+
states = self.currentNVDAObject.states
1669+
if controlTypes.State.COLLAPSED not in states and controlTypes.State.EXPANDED not in states:
1670+
direction = 1 if gesture.mainKeyName == "downArrow" else -1
1671+
return self._caretMovementScriptHelper(gesture, textInfos.UNIT_SENTENCE, direction)
16651672
if not config.conf["virtualBuffers"]["autoFocusFocusableElements"]:
16661673
self._focusLastFocusableObject()
16671674
# Give the application time to focus the control.

source/config/configDefaults.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,40 @@
44
# See the file COPYING for more details.
55

66
DEFAULT_TEXT_PARAGRAPH_REGEX = (
7-
r"({lookBehind}{optQuote}{punc}{optQuote}{optWiki}{lookAhead}|{punc2}|{cjk})".format(
7+
r"^|({plb}{nlb}{optQuote}{dot}{optQuote}|{punc2}{optQuote}|{cjk}){optWiki}{spaces}|{n2}|\Z".format(
88
# Look behind clause ensures that we have a text character before text punctuation mark.
99
# We have a positive lookBehind \w that resolves to a text character in any language,
1010
# coupled with negative lookBehind \d that excludes digits.
11-
lookBehind=r'(?<=\w)(?<!\d)',
11+
plb=r"(?<=\w)(?<!\d)",
12+
# Language-specific exceptions: characters suggesting that the following dot is not indicative
13+
# of a sentence stop.
14+
# This is a negative look-behind and will be inserted later when language is specified.
15+
nlb="{nonBreakingRegex}",
1216
# In some cases quote or closing parenthesis might appear right before or right after text punctuation.
1317
# For example:
1418
# > He replied, "That's wonderful."
1519
optQuote=r'["”»)]?',
1620
# Actual punctuation marks that suggest end of sentence.
1721
# We don't include symbols like comma and colon, because of too many false positives.
1822
# We include question mark and exclamation mark below in punc2.
19-
punc=r'[.…]{1,3}',
23+
dot=r"[.]{{1,3}}",
2024
# On Wikipedia references appear right after period in sentences, the following clause takes this
2125
# into account. For example:
2226
# > 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,
27+
optWiki=r"(\[\d+\])*",
28+
# spaces clause checks that punctuation mark must be followed by either space,
2529
# or newLine symbol or end of string.
26-
lookAhead=r'(?=[\r\n  ]|$)',
30+
spaces=r"([  ]+|([  \t]*\n)+|$)",
2731
# Include question mark and exclamation mark with no extra conditions,
2832
# since they don't trigger as many false positives.
29-
punc2=r'[?!]',
33+
punc2=r"[?!…]",
3034
# We also check for CJK full-width punctuation marks without any extra rules.
31-
cjk=r'[.!?:;]',
35+
cjk=r"[.!?:;]",
36+
# Double newline means end of sentence too.
37+
n2=r"([  \t]*\n){{2,}}",
3238
)
3339
)
40+
41+
DEFAULT_ENGLISH_NON_BREAKING_PREFIXES_NLBS = (
42+
r"\b[A-Z]|\bMr|\bMs|\bMrs|\bDr|\bProf|\bSt|\be.g|\bi.e"
43+
)

source/config/configSpec.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,8 @@
241241
242242
[documentNavigation]
243243
paragraphStyle = featureFlag(optionsEnum="ParagraphNavigationFlag", behaviorOfDefault="application")
244+
sentenceReconstruction = featureFlag(optionsEnum="SentenceReconstructionFlag", behaviorOfDefault="same_style_paragraphs")
245+
nonBreakingPrefixRegex = string(default="{configDefaults.DEFAULT_ENGLISH_NON_BREAKING_PREFIXES_NLBS}")
244246
245247
[reviewCursor]
246248
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)