Skip to content
87 changes: 56 additions & 31 deletions source/NVDAObjects/UIA/wordDocument.py
Original file line number Diff line number Diff line change
Expand Up @@ -716,40 +716,65 @@ def event_UIA_notification(self, activityId=None, **kwargs):
return
super(WordDocument, self).event_UIA_notification(**kwargs)

# The following overide of the EditableText._caretMoveBySentenceHelper private method
# Falls back to the MS Word object model if available.
# This override should be removed as soon as UI Automation in MS Word has the ability to move by sentence.
def _caretMoveBySentenceHelper(self, gesture, direction):
# The following override of the EditableText._caretMoveBySentenceHelper private method
# First tries to use UI Automation remote operations to move by sentence when available,
# falling back to the MS Word object model otherwise.
def _caretMoveBySentenceHelper(self, gesture: inputCore.InputGesture, direction: int):
if isScriptWaiting():
return
if not self.WinwordSelectionObject:
# Legacy object model not available.
# Translators: a message when navigating by sentence is unavailable in MS Word
ui.message(_("Navigating by sentence not supported in this document"))
gesture.send()
return
# Using the legacy object model,
# Move the caret to the next sentence in the requested direction.
legacyInfo = LegacyWordDocumentTextInfo(self, textInfos.POSITION_CARET)
legacyInfo.move(textInfos.UNIT_SENTENCE, direction)
# Save the start of the sentence for future use
legacyStart = legacyInfo.copy()
# With the legacy object model,
# Move the caret to the end of the new sentence.
legacyInfo.move(textInfos.UNIT_SENTENCE, 1)
legacyInfo.updateCaret()
# Fetch the caret position (end of the next sentence) with UI automation.
endInfo = self.makeTextInfo(textInfos.POSITION_CARET)
# Move the caret back to the start of the next sentence,
# where it should be left for the user.
legacyStart.updateCaret()
# Fetch the new caret position (start of the next sentence) with UI Automation.
startInfo = self.makeTextInfo(textInfos.POSITION_CARET)
# Make a UI automation text range spanning the entire next sentence.
info = startInfo.copy()
info.end = endInfo.end

info = None

# Prefer UIA remote sentence navigation when available.
if UIARemote.isSupported():
try:
caretInfo = self.makeTextInfo(textInfos.POSITION_CARET)
sentenceRange = UIARemote.msWord_moveTextRangeBySentence(
self.UIAElement,
caretInfo._rangeObj,
direction,
)
except Exception:
log.debugWarning(
"Failed to fetch caret text range for remote sentence navigation",
exc_info=True,
)
else:
if sentenceRange is not None:
info = WordDocumentTextInfo(self, textInfos.POSITION_CARET, _rangeObj=sentenceRange)
info.updateCaret()

if info is None:
if not self.WinwordSelectionObject:
# Legacy object model not available.
# Translators: a message when navigating by sentence is unavailable in MS Word
ui.message(_("Navigating by sentence not supported in this document"))
gesture.send()
return
# Using the legacy object model,
# Move the caret to the next sentence in the requested direction.
legacyInfo = LegacyWordDocumentTextInfo(self, textInfos.POSITION_CARET)
legacyInfo.move(textInfos.UNIT_SENTENCE, direction)
# Save the start of the sentence for future use
legacyStart = legacyInfo.copy()
legacyInfo.move(textInfos.UNIT_SENTENCE, 1)
# Fetch the caret position (end of the next sentence) with UI automation.
endInfo = self.makeTextInfo(textInfos.POSITION_CARET)
# Move the caret back to the start of the next sentence,
# where it should be left for the user.
legacyStart.updateCaret()
# Fetch the new caret position (start of the next sentence) with UI Automation.
startInfo = self.makeTextInfo(textInfos.POSITION_CARET)
# Make a UI automation text range spanning the entire next sentence.
info = startInfo.copy()
info.end = endInfo.end
Comment on lines +754 to +770
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we move this to a helper function? e.g. getLegacyTextInfo(self, direction: int) -> LegacyWordDocumentTextInfo


# Speak the sentence moved to
speech.speakTextInfo(info, unit=textInfos.UNIT_SENTENCE, reason=controlTypes.OutputReason.CARET)
speech.speakTextInfo(
info,
unit=textInfos.UNIT_SENTENCE,
reason=controlTypes.OutputReason.CARET,
)
# Forget the word currently being typed as the user has moved the caret somewhere else.
speech.clearTypedWordBuffer()
# Alert review and braille the caret has moved to its new position
Expand Down
112 changes: 92 additions & 20 deletions source/UIAHandler/remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@


from typing import (
Optional,
Any,
Generator,
cast,
Expand All @@ -16,6 +15,7 @@
from logHandler import log
from ._remoteOps import remoteAlgorithms
from ._remoteOps.remoteTypes import (
RemoteElement,
RemoteExtensionTarget,
RemoteInt,
)
Expand Down Expand Up @@ -56,12 +56,36 @@ def terminate():
_isSupported = False


def _msWord_remote_getExtendedTextRangePattern(
ra: remoteAPI.RemoteAPI,
remoteDocElement: RemoteElement,
) -> RemoteExtensionTarget:
guid_msWord_extendedTextRangePattern = GUID("{93514122-FF04-4B2C-A4AD-4AB04587C129}")
remoteResult = ra.newVariant()
extendedTextRangeIsSupported = remoteDocElement.isExtensionSupported(
guid_msWord_extendedTextRangePattern,
)
with ra.ifBlock(extendedTextRangeIsSupported.inverse()):
ra.logRuntimeMessage("docElement does not support extendedTextRangePattern")
ra.Return(None)
ra.logRuntimeMessage("docElement supports extendedTextRangePattern")
ra.logRuntimeMessage("doing callExtension for extendedTextRangePattern")
remoteDocElement.callExtension(
guid_msWord_extendedTextRangePattern,
remoteResult,
)
with ra.ifBlock(remoteResult.isNull()):
ra.logRuntimeMessage("extendedTextRangePattern is null")
ra.Return(None)
ra.logRuntimeMessage("got extendedTextRangePattern")
return remoteResult.asType(RemoteExtensionTarget)


def msWord_getCustomAttributeValue(
docElement: UIA.IUIAutomationElement,
textRange: UIA.IUIAutomationTextRange,
customAttribID: int,
) -> Optional[Any]:
guid_msWord_extendedTextRangePattern = GUID("{93514122-FF04-4B2C-A4AD-4AB04587C129}")
) -> Any | None:
guid_msWord_getCustomAttributeValue = GUID("{081ACA91-32F2-46F0-9FB9-017038BC45F8}")
op = operation.Operation()

Expand All @@ -70,24 +94,10 @@ def code(ra: remoteAPI.RemoteAPI):
remoteDocElement = ra.newElement(docElement)
remoteTextRange = ra.newTextRange(textRange)
remoteCustomAttribValue = ra.newVariant()
extendedTextRangeIsSupported = remoteDocElement.isExtensionSupported(
guid_msWord_extendedTextRangePattern,
)
with ra.ifBlock(extendedTextRangeIsSupported.inverse()):
ra.logRuntimeMessage("docElement does not support extendedTextRangePattern")
ra.Return(None)
ra.logRuntimeMessage("docElement supports extendedTextRangePattern")
remoteResult = ra.newVariant()
ra.logRuntimeMessage("doing callExtension for extendedTextRangePattern")
remoteDocElement.callExtension(
guid_msWord_extendedTextRangePattern,
remoteResult,
remoteExtendedTextRangePattern = _msWord_remote_getExtendedTextRangePattern(
ra,
remoteDocElement,
)
with ra.ifBlock(remoteResult.isNull()):
ra.logRuntimeMessage("extendedTextRangePattern is null")
ra.Return(None)
ra.logRuntimeMessage("got extendedTextRangePattern")
remoteExtendedTextRangePattern = remoteResult.asType(RemoteExtensionTarget)
customAttributeValueIsSupported = remoteExtendedTextRangePattern.isExtensionSupported(
guid_msWord_getCustomAttributeValue,
)
Expand All @@ -112,6 +122,68 @@ def code(ra: remoteAPI.RemoteAPI):
return customAttribValue


def msWord_moveTextRangeBySentence(
docElement: UIA.IUIAutomationElement,
textRange: UIA.IUIAutomationTextRange,
unitCount: int,
) -> UIA.IUIAutomationTextRange | None:
"""
Move a UI Automation text range by sentence using the Word-specific UIA
extended text range pattern, if available.
Returns None if the operation fails or the extensions are not supported.
"""
if not isSupported():
return None

guid_msWord_moveBySentence = GUID("{F39655AC-133A-435B-A318-C197F0D3D203}")
guid_msWord_expandToEnclosingSentence = GUID("{98FE8B34-F317-459A-9627-21123EA95BEA}")
op = operation.Operation()

@op.buildFunction
def code(ra: remoteAPI.RemoteAPI):
remoteDocElement = ra.newElement(docElement)
remoteTextRange = ra.newTextRange(textRange)

remoteExtendedTextRangePattern = _msWord_remote_getExtendedTextRangePattern(
ra,
remoteDocElement,
)

moveBySentenceSupported = remoteExtendedTextRangePattern.isExtensionSupported(
guid_msWord_moveBySentence,
)
with ra.ifBlock(moveBySentenceSupported.inverse()):
ra.logRuntimeMessage("extendedTextRangePattern does not support MoveBySentence")
ra.Return(None)
expandSupported = remoteExtendedTextRangePattern.isExtensionSupported(
guid_msWord_expandToEnclosingSentence,
)
with ra.ifBlock(expandSupported.inverse()):
ra.logRuntimeMessage(
"extendedTextRangePattern does not support ExpandToEnclosingSentence",
)
ra.Return(None)

moveCount = ra.newInt(unitCount)
actualMoved = ra.newInt(0)
ra.logRuntimeMessage("doing callExtension for MoveBySentence")
remoteExtendedTextRangePattern.callExtension(
guid_msWord_moveBySentence,
remoteTextRange,
moveCount,
actualMoved,
)
ra.logRuntimeMessage("doing callExtension for ExpandToEnclosingSentence")
remoteExtendedTextRangePattern.callExtension(
guid_msWord_expandToEnclosingSentence,
remoteTextRange,
)

ra.Return(remoteTextRange)

return op.execute()


def collectAllHeadingsInTextRange(
textRange: UIA.IUIAutomationTextRange,
) -> Generator[tuple[int, str, UIA.IUIAutomationElement], None, None]:
Expand Down
Loading