Skip to content

Commit 3297106

Browse files
authored
Merge 9c50e89 into 3c7504e
2 parents 3c7504e + 9c50e89 commit 3297106

6 files changed

Lines changed: 271 additions & 0 deletions

File tree

source/browseMode.py

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from typing import (
88
Any,
99
Callable,
10+
Generator,
1011
Union,
1112
cast,
1213
)
@@ -439,10 +440,24 @@ def _iterNodesByType(self,itemType,direction="next",pos=None):
439440

440441
def _iterNotLinkBlock(self, direction="next", pos=None):
441442
raise NotImplementedError
443+
444+
def _iterTextStyle(
445+
self,
446+
kind: str,
447+
direction: str = "next",
448+
pos: textInfos.TextInfo = None,
449+
) -> Generator[TextInfoQuickNavItem, None, None]:
450+
raise NotImplementedError
442451

443452
def _quickNavScript(self,gesture, itemType, direction, errorMessage, readUnit):
444453
if itemType=="notLinkBlock":
445454
iterFactory=self._iterNotLinkBlock
455+
elif itemType in ["sameStyle", "differentStyle"]:
456+
def iterFactory(
457+
direction: str,
458+
info: textInfos.TextInfo,
459+
) -> Generator[TextInfoQuickNavItem, None, None]:
460+
return self._iterTextStyle(itemType, direction, info)
446461
else:
447462
iterFactory=lambda direction,info: self._iterNodesByType(itemType,direction,info)
448463
info=self.selection
@@ -949,6 +964,30 @@ def _get_disableAutoPassThrough(self):
949964
# Translators: Message presented when the browse mode element is not found.
950965
prevError=_("no previous tab")
951966
)
967+
qn(
968+
"sameStyle",
969+
key=None,
970+
# Translators: Input help message for a quick navigation command in browse mode.
971+
nextDoc=_("moves to the next same style text"),
972+
# Translators: Message presented when the browse mode element is not found.
973+
nextError=_("No next same style text"),
974+
# Translators: Input help message for a quick navigation command in browse mode.
975+
prevDoc=_("moves to the previous same style text"),
976+
# Translators: Message presented when the browse mode element is not found.
977+
prevError=_("No previous same style text")
978+
)
979+
qn(
980+
"differentStyle",
981+
key=None,
982+
# Translators: Input help message for a quick navigation command in browse mode.
983+
nextDoc=_("moves to the next different style text"),
984+
# Translators: Message presented when the browse mode element is not found.
985+
nextError=_("No next different style text"),
986+
# Translators: Input help message for a quick navigation command in browse mode.
987+
prevDoc=_("moves to the previous different style text"),
988+
# Translators: Message presented when the browse mode element is not found.
989+
prevError=_("No previous different style text")
990+
)
952991
del qn
953992

954993

@@ -2001,6 +2040,129 @@ def _iterNotLinkBlock(self, direction="next", pos=None):
20012040
yield TextInfoQuickNavItem("notLinkBlock", self, textRange)
20022041
item1=item2
20032042

2043+
STYLE_ATTRIBUTES = frozenset([
2044+
'background-color',
2045+
'color',
2046+
'font-family,',
2047+
'font-size',
2048+
'bold',
2049+
'italic',
2050+
'marked',
2051+
'strikethrough',
2052+
'text-line-through-style',
2053+
'underline',
2054+
'text-underline-style',
2055+
])
2056+
2057+
def _extractStyles(
2058+
self,
2059+
info: textInfos.TextInfo,
2060+
) -> "textInfos.TextInfo.TextWithFieldsT":
2061+
"""
2062+
This function calls TextInfo.getTextWithFields(), and then processes fiields in the following way:
2063+
1. Highlighted(marked) text is currently reported as Role.MARKED_CONTENT, and not formatChange.
2064+
For ease of further handling we create a new boolean format field "marked"
2065+
and set its value according to presence of Role.MARKED_CONTENT.
2066+
2. Then we drop all control fields, leaving only formatChange fields and text.
2067+
"""
2068+
stack = [{}]
2069+
result = []
2070+
for field in info.getTextWithFields():
2071+
if isinstance(field, textInfos.FieldCommand):
2072+
if field.command == 'controlStart':
2073+
style = {**stack[-1]}
2074+
if field.field['role'] == controlTypes.Role.MARKED_CONTENT:
2075+
style['marked'] = True
2076+
stack.append(style)
2077+
elif field.command == 'controlEnd':
2078+
del stack[-1]
2079+
elif field.command == 'formatChange':
2080+
field.field = {
2081+
k: v
2082+
for k, v in {**field.field, **stack[-1]}.items()
2083+
if k in self.STYLE_ATTRIBUTES
2084+
}
2085+
result.append(field)
2086+
else:
2087+
raise RuntimeError()
2088+
elif isinstance(field, str):
2089+
result.append(field)
2090+
else:
2091+
raise RuntimeError
2092+
return result
2093+
2094+
def _iterTextStyle(
2095+
self,
2096+
kind: str,
2097+
direction: str = "next",
2098+
pos: textInfos.TextInfo = None
2099+
) -> Generator[TextInfoQuickNavItem, None, None]:
2100+
sameStyle = kind == 'sameStyle'
2101+
initialTextInfo = pos.copy()
2102+
initialTextInfo.collapse()
2103+
result = initialTextInfo.move(textInfos.UNIT_CHARACTER, 1, endPoint='end')
2104+
if result == 0:
2105+
result = initialTextInfo.move(textInfos.UNIT_CHARACTER, -1, endPoint='start')
2106+
if result == 0:
2107+
# Translators: Error message for same/different style QuickNav command
2108+
ui.message(_("Cannot determine current style"))
2109+
raise RuntimeError
2110+
styles = self._extractStyles(initialTextInfo)
2111+
if (
2112+
len(styles) == 0
2113+
or not isinstance(styles[0], textInfos.FieldCommand)
2114+
or styles[0].command != 'formatChange'
2115+
):
2116+
# Translators: Error message for same/different style QuickNav command
2117+
ui.message(_("Cannot determine current style"))
2118+
raise RuntimeError
2119+
initialStyle = styles[0]
2120+
2121+
firstParagraph = True
2122+
paragraph = pos.copy()
2123+
tmpInfo = pos.copy()
2124+
tmpInfo.expand(textInfos.UNIT_PARAGRAPH)
2125+
paragraph.setEndPoint(tmpInfo, which='endToEnd' if direction == 'next' else 'startToStart')
2126+
while True:
2127+
if not paragraph.isCollapsed:
2128+
styles = self._extractStyles(paragraph)
2129+
firstStyleWithinParagraph = True
2130+
iterationRange = range(len(styles)) if direction == 'next' else range(len(styles) - 1, -1, -1)
2131+
for i in iterationRange:
2132+
if not isinstance(styles[i], textInfos.FieldCommand):
2133+
continue
2134+
if (
2135+
(styles[i].field == initialStyle.field) == sameStyle
2136+
and (
2137+
not firstStyleWithinParagraph
2138+
or not firstParagraph
2139+
)
2140+
):
2141+
# Found text that matches desired style!
2142+
startOffset = sum([
2143+
len(s)
2144+
for s in styles[:i]
2145+
if isinstance(s, str)
2146+
])
2147+
endOffset = len(styles[i + 1])
2148+
textRange = paragraph.copy()
2149+
textRange.collapse()
2150+
textRange.move(textInfos.UNIT_CHARACTER, startOffset)
2151+
textRange.move(textInfos.UNIT_CHARACTER, endOffset, endPoint='end')
2152+
yield TextInfoQuickNavItem(kind, self, textRange)
2153+
firstStyleWithinParagraph = False
2154+
firstParagraph = False
2155+
if direction == 'next':
2156+
paragraph.collapse(end=True)
2157+
else:
2158+
paragraph.collapse(end=False)
2159+
result = paragraph.move(textInfos.UNIT_CHARACTER, -1)
2160+
if result == 0:
2161+
return
2162+
paragraph.expand(textInfos.UNIT_PARAGRAPH)
2163+
if paragraph.isCollapsed:
2164+
return
2165+
20042166
__gestures={
20052167
"kb:alt+upArrow": "collapseOrExpandControl",
20062168
"kb:alt+downArrow": "collapseOrExpandControl",

tests/system/libraries/SystemTestSpy/speechSpyGlobalPlugin.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,23 @@ def set_configValue(self, keyPath: ConfKeyPath, val: ConfKeyVal):
106106
ultimateKey = keyPath[-1]
107107
penultimateConf[ultimateKey] = val
108108

109+
def assignGesture(
110+
self,
111+
gesture: str,
112+
module: str,
113+
className: str,
114+
script: Optional[str],
115+
replace: bool = False
116+
):
117+
import inputCore
118+
inputCore.manager.userGestureMap.add(
119+
gesture,
120+
module,
121+
className,
122+
script,
123+
replace,
124+
)
125+
109126
fakeTranslations: typing.Optional[gettext.NullTranslations] = None
110127

111128
def override_translationString(self, invariantString: str, replacementString: str):

tests/system/robot/chromeTests.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2473,3 +2473,89 @@ def test_i13307():
24732473
]),
24742474
message="jumping into region with aria-labelledby should speak label",
24752475
)
2476+
2477+
2478+
def test_styleNav():
2479+
""" Tests that same style and different style navigation work correctly in browse mode.
2480+
Bydefault these commands don't have assigned gestures,
2481+
so we will assign temporary gestures just for testing.
2482+
"""
2483+
spy: "NVDASpyLib" = _NvdaLib.getSpyLib()
2484+
spy.assignGesture(
2485+
"kb:s",
2486+
"browseMode",
2487+
'BrowseModeTreeInterceptor',
2488+
"nextSameStyle",
2489+
)
2490+
2491+
spy.assignGesture(
2492+
"kb:shift+s",
2493+
"browseMode",
2494+
'BrowseModeTreeInterceptor',
2495+
"previousSameStyle",
2496+
)
2497+
spy.assignGesture(
2498+
"kb:d",
2499+
"browseMode",
2500+
'BrowseModeTreeInterceptor',
2501+
"nextDifferentStyle",
2502+
)
2503+
2504+
spy.assignGesture(
2505+
"kb:shift+d",
2506+
"browseMode",
2507+
'BrowseModeTreeInterceptor',
2508+
"previousDifferentStyle",
2509+
)
2510+
2511+
_chrome.prepareChrome("""
2512+
<p>Hello world!</p>
2513+
<p>This text is <b>bold</b></p>
2514+
<p>Second line is <font size="15pt">large</font></p>
2515+
<p>Third line is <mark>highlighted</mark></p>
2516+
<p>Fourth line is <b>bold again</b></p>
2517+
<p>End of document.</p>
2518+
""")
2519+
# For some reason we need to send Control+RightArrow;
2520+
# otherwise getting "Test page load complete" as actual speech
2521+
actualSpeech, actualBraille = _NvdaLib.getSpeechAndBrailleAfterKey("control+rightArrow")
2522+
actualSpeech, actualBraille = _NvdaLib.getSpeechAndBrailleAfterKey("s")
2523+
_asserts.strings_match(actualSpeech, "Hello world!")
2524+
actualSpeech, actualBraille = _NvdaLib.getSpeechAndBrailleAfterKey("shift+d")
2525+
_asserts.strings_match(actualSpeech, "No previous different style text")
2526+
actualSpeech, actualBraille = _NvdaLib.getSpeechAndBrailleAfterKey("s")
2527+
_asserts.strings_match(actualSpeech, "This text is")
2528+
actualSpeech, actualBraille = _NvdaLib.getSpeechAndBrailleAfterKey("s")
2529+
_asserts.strings_match(actualSpeech, "Second line is")
2530+
actualSpeech, actualBraille = _NvdaLib.getSpeechAndBrailleAfterKey("shift+d")
2531+
_asserts.strings_match(actualSpeech, "bold")
2532+
actualSpeech, actualBraille = _NvdaLib.getSpeechAndBrailleAfterKey("s")
2533+
_asserts.strings_match(actualSpeech, "bold again")
2534+
actualSpeech, actualBraille = _NvdaLib.getSpeechAndBrailleAfterKey("s")
2535+
_asserts.strings_match(actualSpeech, "No next same style text")
2536+
actualSpeech, actualBraille = _NvdaLib.getSpeechAndBrailleAfterKey("d")
2537+
_asserts.strings_match(actualSpeech, "End of document.")
2538+
actualSpeech, actualBraille = _NvdaLib.getSpeechAndBrailleAfterKey("d")
2539+
_asserts.strings_match(actualSpeech, "No next different style text")
2540+
for s in [
2541+
"Second line is",
2542+
"Third line is",
2543+
"Fourth line is",
2544+
][::-1]:
2545+
actualSpeech, actualBraille = _NvdaLib.getSpeechAndBrailleAfterKey("shift+s")
2546+
_asserts.strings_match(actualSpeech, s)
2547+
actualSpeech, actualBraille = _NvdaLib.getSpeechAndBrailleAfterKey("d")
2548+
_asserts.strings_match(actualSpeech, "large")
2549+
actualSpeech, actualBraille = _NvdaLib.getSpeechAndBrailleAfterKey("shift+s")
2550+
_asserts.strings_match(actualSpeech, "No previous same style text")
2551+
actualSpeech, actualBraille = _NvdaLib.getSpeechAndBrailleAfterKey("s")
2552+
_asserts.strings_match(actualSpeech, "No next same style text")
2553+
2554+
actualSpeech, actualBraille = _NvdaLib.getSpeechAndBrailleAfterKey("d")
2555+
_asserts.strings_match(actualSpeech, "Third line is")
2556+
actualSpeech, actualBraille = _NvdaLib.getSpeechAndBrailleAfterKey("d")
2557+
_asserts.strings_match(actualSpeech, "highlighted highlighted")
2558+
actualSpeech, actualBraille = _NvdaLib.getSpeechAndBrailleAfterKey("shift+s")
2559+
_asserts.strings_match(actualSpeech, "No previous same style text")
2560+
actualSpeech, actualBraille = _NvdaLib.getSpeechAndBrailleAfterKey("s")
2561+
_asserts.strings_match(actualSpeech, "No next same style text")

tests/system/robot/chromeTests.robot

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,3 +156,6 @@ ARIA switch role
156156
i13307
157157
[Documentation] ensure aria-labelledby on a landmark or region is automatically spoken when jumping inside from outside using focus in browse mode
158158
test_i13307
159+
styleNav
160+
[Documentation] Same style navigation
161+
test_styleNav

user_docs/en/changes.t2t

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ What's New in NVDA
77

88
== New Features ==
99
- In Windows 11, NVDA will announce alerts from voice typing and suggested actions including the top suggestion when copying data such as phone numbers to the clipboard (Windows 11 2022 Update and later). (#16009, @josephsl)
10+
- Added same style and different style navigation QuickNav commands, not assigned to any gestures.
1011
-
1112

1213

user_docs/en/userGuide.t2t

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -850,6 +850,8 @@ Here is a list of available commands:
850850
- Article
851851
- Grouping
852852
- Tab
853+
- Same style text
854+
- Different style text
853855
-
854856

855857
Keep in mind that there are two commands for each type of element, for moving forward in the document and backward in the document, and you must assign gestures to both commands in order to be able to quickly navigate in both directions.

0 commit comments

Comments
 (0)