Skip to content

Commit

Permalink
Merge branch 'master' into TypedWords
Browse files Browse the repository at this point in the history
  • Loading branch information
cary-rowen committed Dec 12, 2024
2 parents 146dd24 + 8a82bd5 commit f7a48f0
Show file tree
Hide file tree
Showing 11 changed files with 182 additions and 94 deletions.
10 changes: 10 additions & 0 deletions source/NVDAObjects/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from typing import (
Dict,
Optional,
TYPE_CHECKING,
)
import weakref
import textUtils
Expand Down Expand Up @@ -45,6 +46,9 @@
import aria
from winAPI.sessionTracking import isLockScreenModeActive

if TYPE_CHECKING:
from utils.urlUtils import _LinkData


class NVDAObjectTextInfo(textInfos.offsets.OffsetsTextInfo):
"""A default TextInfo which is used to enable text review of information about widgets that don't support text content.
Expand Down Expand Up @@ -1642,3 +1646,9 @@ def _get_linkType(self) -> controlTypes.State | None:
if not isinstance(ti, BrowseModeDocumentTreeInterceptor):
return None
return ti.getLinkTypeInDocument(self.value)

linkData: "_LinkData | None"

def _get_linkData(self) -> "_LinkData | None":
"""If the object has an associated link, returns the link's data (target and text)."""
raise NotImplementedError
31 changes: 16 additions & 15 deletions source/NVDAObjects/window/excel.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import config
from config.configFlags import ReportCellBorders
import textInfos
from utils.urlUtils import _LinkData
import colors
import eventHandler
import api
Expand Down Expand Up @@ -1368,21 +1369,6 @@ def _getFormatFieldAndOffsets(self, offset, formatConfig, calculateOffsets=True)
def _get_locationText(self):
return self.obj.getCellPosition()

def _getLinkDataAtCaretPosition(self) -> textInfos._Link | None:
links = self.obj.excelCellObject.Hyperlinks
if links.count == 0:
return None
link = links(1)
if link.Type == MsoHyperlink.RANGE:
text = link.TextToDisplay
else:
log.debugWarning(f"No text to display for link type {link.Type}")
text = None
return textInfos._Link(
displayText=text,
destination=link.Address,
)


NVCELLINFOFLAG_ADDRESS = 0x1
NVCELLINFOFLAG_TEXT = 0x2
Expand Down Expand Up @@ -1710,6 +1696,21 @@ def _get_role(self):
return controlTypes.Role.LINK
return controlTypes.Role.TABLECELL

def _get_linkData(self) -> _LinkData | None:
links = self.excelCellObject.Hyperlinks
if links.count == 0:
return None
link = links(1)
if link.Type == MsoHyperlink.RANGE:
text = link.TextToDisplay
else:
log.debugWarning(f"No text to display for link type {link.Type}")
text = None
return _LinkData(
displayText=text,
destination=link.Address,
)

TextInfo = ExcelCellTextInfo

def _isEqual(self, other):
Expand Down
5 changes: 3 additions & 2 deletions source/NVDAObjects/window/winword.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
from enum import IntEnum
import documentBase
from utils.displayString import DisplayStringIntEnum
from utils.urlUtils import _LinkData

if TYPE_CHECKING:
import inputCore
Expand Down Expand Up @@ -915,7 +916,7 @@ def _getShapeAtCaretPosition(self) -> comtypes.client.lazybind.Dispatch | None:
return shapes[1]
return None

def _getLinkDataAtCaretPosition(self) -> textInfos._Link | None:
def _getLinkDataAtCaretPosition(self) -> _LinkData | None:
link = self._getLinkAtCaretPosition()
if not link:
return None
Expand All @@ -928,7 +929,7 @@ def _getLinkDataAtCaretPosition(self) -> textInfos._Link | None:
case _:
log.debugWarning(f"No text to display for link type {link.Type}")
text = None
return textInfos._Link(
return _LinkData(
displayText=text,
destination=link.Address,
)
Expand Down
82 changes: 62 additions & 20 deletions source/appModules/powerpnt.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import ctypes

import comtypes.client.lazybind
import config
import oleacc
import comHelper
import ui
Expand All @@ -27,6 +28,7 @@
from treeInterceptorHandler import DocumentTreeInterceptor
from NVDAObjects import NVDAObjectTextInfo
from displayModel import DisplayModelTextInfo, EditableTextDisplayModelTextInfo
import textInfos
import textInfos.offsets
import eventHandler
import appModuleHandler
Expand All @@ -41,6 +43,7 @@
import scriptHandler
from locationHelper import RectLTRB
from NVDAObjects.window._msOfficeChart import OfficeChart
from utils.urlUtils import _LinkData

# Translators: The name of a category of NVDA commands.
SCRCAT_POWERPOINT = _("PowerPoint")
Expand Down Expand Up @@ -994,6 +997,19 @@ def _get_mathMl(self):
except: # noqa: E722
raise LookupError("Couldn't get MathML from MathType")

def _get_linkData(self) -> _LinkData | None:
mouseClickSetting = self.ppObject.ActionSettings(ppMouseClick)
if mouseClickSetting.action == ppActionHyperlink:
if self.value:
text = f"{self.roleText} {self.value}"
else:
text = self.roleText
return _LinkData(
displayText=text,
destination=mouseClickSetting.Hyperlink.Address,
)
return None

__gestures = {
"kb:leftArrow": "moveHorizontal",
"kb:rightArrow": "moveHorizontal",
Expand Down Expand Up @@ -1160,6 +1176,23 @@ def _getBoundingRectFromOffset(self, offset: int) -> RectLTRB:
bottom = self.obj.documentWindow.ppObjectModel.pointsToScreenPixelsY(rangeTop + rangeHeight)
return RectLTRB(left, top, right, bottom)

def _getCurrentRun(
self,
offset: int,
) -> tuple[comtypes.client.lazybind.Dispatch | None, int, int]:
runs = self.obj.ppObject.textRange.runs()
for run in runs:
start = run.start - 1
end = start + run.length
if start <= offset < end:
startOffset = start
endOffset = end
curRun = run
break
else:
curRun, startOffset, endOffset = None, 0, 0
return curRun, startOffset, endOffset

def _getFormatFieldAndOffsets(
self,
offset: int,
Expand All @@ -1169,15 +1202,7 @@ def _getFormatFieldAndOffsets(
formatField = textInfos.FormatField()
curRun = None
if calculateOffsets:
runs = self.obj.ppObject.textRange.runs()
for run in runs:
start = run.start - 1
end = start + run.length
if start <= offset < end:
startOffset = start
endOffset = end
curRun = run
break
curRun, startOffset, endOffset = self._getCurrentRun(self)
if not curRun:
curRun = self.obj.ppObject.textRange.characters(offset + 1)
startOffset, endOffset = offset, self._endOffset
Expand Down Expand Up @@ -1213,6 +1238,17 @@ def _getFormatFieldAndOffsets(
formatField["link"] = True
return formatField, (startOffset, endOffset)

def _getLinkDataAtCaretPosition(self) -> _LinkData | None:
offset = self._getCaretOffset()
curRun, _startOffset, _endOffset = self._getCurrentRun(offset)
mouseClickSetting = curRun.actionSettings(ppMouseClick)
if mouseClickSetting.action == ppActionHyperlink:
return textInfos._LinkData(
displayText=mouseClickSetting.Hyperlink.TextToDisplay,
destination=mouseClickSetting.Hyperlink.Address,
)
return None

def _setCaretOffset(self, offset: int) -> None:
return self._setSelectionOffsets(offset, offset)

Expand Down Expand Up @@ -1386,16 +1422,9 @@ def __contains__(self, obj):
def event_treeInterceptor_gainFocus(self):
braille.handler.handleGainFocus(self)
self.rootNVDAObject.reportFocus()
self.reportNewSlide(self.hadFocusOnce)
if not self.hadFocusOnce:
self.hadFocusOnce = True
self.reportNewSlide()
else:
info = self.selection
if not info.isCollapsed:
speech.speakPreselectedText(info.text)
else:
info.expand(textInfos.UNIT_LINE)
speech.speakTextInfo(info, reason=controlTypes.OutputReason.CARET, unit=textInfos.UNIT_LINE)

def event_gainFocus(self, obj, nextHandler):
pass
Expand All @@ -1405,9 +1434,22 @@ def event_gainFocus(self, obj, nextHandler):
def makeTextInfo(self, position):
return self.TextInfo(self, position)

def reportNewSlide(self):
self.makeTextInfo(textInfos.POSITION_FIRST).updateCaret()
sayAll.SayAllHandler.readText(sayAll.CURSOR.CARET)
def reportNewSlide(self, suppressSayAll: bool = False):
"""Reports a new slide, activating say all when appropriate.
:param suppressSayAll: When say all should be suppressed always, e.g.
because tree interceptor gets focus multiple times.
"""
doSayAll = not suppressSayAll and config.conf["virtualBuffers"]["autoSayAllOnPageLoad"]
if doSayAll:
self.makeTextInfo(textInfos.POSITION_FIRST).updateCaret()
sayAll.SayAllHandler.readText(sayAll.CURSOR.CARET)
else:
info = self.selection
if not info.isCollapsed:
speech.speakPreselectedText(info.text)
else:
info.expand(textInfos.UNIT_LINE)
speech.speakTextInfo(info, reason=controlTypes.OutputReason.CARET, unit=textInfos.UNIT_LINE)

@scriptHandler.script(
description=_(
Expand Down
6 changes: 6 additions & 0 deletions source/browseMode.py
Original file line number Diff line number Diff line change
Expand Up @@ -1999,6 +1999,12 @@ def event_gainFocus(self, obj, nextHandler):
# and this was the last non-root node with focus, so ignore this focus event.
# Otherwise, if the user switches away and back to this document, the cursor will jump to this node.
# This is not ideal if the user was positioned over a node which cannot receive focus.
# #17501: Even though we're ignoring this event, we still need to call
# _postGainFocus. This does things such as initialize auto select detection
# for editable text controls. Without this, the focus object might not
# behave correctly (e.g. text selection changes might not be reported) if the
# user switches to focus mode with this object still focused.
self._postGainFocus(obj)
return
if obj == self.rootNVDAObject:
if self.passThrough:
Expand Down
27 changes: 16 additions & 11 deletions source/globalCommands.py
Original file line number Diff line number Diff line change
Expand Up @@ -4277,16 +4277,20 @@ def script_reportLinkDestination(
positioned on a link, or an element with an included link such as a graphic.
:param forceBrowseable: skips the press once check, and displays the browseableMessage version.
"""
focus = api.getFocusObject()
try:
ti: textInfos.TextInfo = api.getCaretPosition()
except RuntimeError:
log.debugWarning("Unable to get the caret position.", exc_info=True)
ti: textInfos.TextInfo = api.getFocusObject().makeTextInfo(textInfos.POSITION_FIRST)
link = ti._getLinkDataAtCaretPosition()
try:
link = focus.linkData
except NotImplementedError:
link = None
else:
link = ti._getLinkDataAtCaretPosition()
presses = scriptHandler.getLastScriptRepeatCount()
if link:
if link.destination is None:
# Translators: Informs the user that the link has no destination
if not link.destination: # May be None or ""
# Translators: Reported when using the command to report the destination of a link.
ui.message(_("Link has no apparent destination"))
return
if (
Expand All @@ -4301,18 +4305,19 @@ def script_reportLinkDestination(
link.destination,
# Translators: Informs the user that the window contains the destination of the
# link with given title
title=_("Destination of: {name}").format(
name=text,
closeButton=True,
copyButton=True,
),
title=_("Destination of: {name}").format(name=text),
closeButton=True,
copyButton=True,
)
elif presses == 0: # One press
ui.message(link.destination) # Speak the link
else: # Some other number of presses
return # Do nothing
elif focus.role == controlTypes.Role.LINK or controlTypes.State.LINKED in focus.states:
# Translators: Reported when using the command to report the destination of a link.
ui.message(_("Unable to get the destination of this link."))
else:
# Translators: Tell user that the command has been run on something that is not a link
# Translators: Reported when using the command to report the destination of a link.
ui.message(_("Not a link."))

@script(
Expand Down
Loading

0 comments on commit f7a48f0

Please sign in to comment.