Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PowerPoint: Report link destination #17435

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open
8 changes: 8 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,7 @@ def _get_linkType(self) -> controlTypes.State | None:
if not isinstance(ti, BrowseModeDocumentTreeInterceptor):
return None
return ti.getLinkTypeInDocument(self.value)

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
53 changes: 44 additions & 9 deletions source/appModules/powerpnt.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,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 +42,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 +996,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 +1175,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 +1201,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 +1237,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
24 changes: 14 additions & 10 deletions source/globalCommands.py
Original file line number Diff line number Diff line change
Expand Up @@ -4280,16 +4280,19 @@ 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()
link = ti._getLinkDataAtCaretPosition()
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
presses = scriptHandler.getLastScriptRepeatCount()
if link:
if link.destination is None:
# Translators: Informs the user that the link has no destination
# Translators: Reported when using the command to report the destination of a link.
ui.message(_("Link has no apparent destination"))
return
if (
Expand All @@ -4304,18 +4307,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
14 changes: 3 additions & 11 deletions source/textInfos/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@

from abc import abstractmethod
from enum import Enum
from dataclasses import dataclass
import weakref
import re
import typing
Expand All @@ -31,6 +30,7 @@
from controlTypes import OutputReason
import locationHelper
from logHandler import log
from utils.urlUtils import _LinkData

if typing.TYPE_CHECKING:
import documentBase # noqa: F401 used for type checking only
Expand Down Expand Up @@ -339,14 +339,6 @@ def _logBadSequenceTypes(sequence: SpeechSequence, shouldRaise: bool = True):
return speech.types.logBadSequenceTypes(sequence, raiseExceptionOnError=shouldRaise)


@dataclass
class _Link:
"""Class to store information on a link in text."""

displayText: str | None
destination: str


class TextInfo(baseObject.AutoPropertyObject):
"""Provides information about a range of text in an object and facilitates access to all text in the widget.
A TextInfo represents a specific range of text, providing access to the text itself, as well as information about the text such as its formatting and any associated controls.
Expand Down Expand Up @@ -713,7 +705,7 @@ def activate(self):
mouseHandler.doPrimaryClick()
winUser.setCursorPos(oldX, oldY)

def _getLinkDataAtCaretPosition(self) -> _Link | None:
def _getLinkDataAtCaretPosition(self) -> _LinkData | None:
self.expand(UNIT_CHARACTER)
obj: NVDAObjects.NVDAObject = self.NVDAObjectAtStart
if obj.role == controlTypes.role.Role.GRAPHIC and (
Expand All @@ -726,7 +718,7 @@ def _getLinkDataAtCaretPosition(self) -> _Link | None:
obj.role == controlTypes.role.Role.LINK # If it's a link, or
or controlTypes.state.State.LINKED in obj.states # if it isn't a link but contains one
):
return _Link(
return _LinkData(
displayText=obj.name,
destination=obj.value,
)
Expand Down
3 changes: 2 additions & 1 deletion source/treeInterceptorHandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

if TYPE_CHECKING:
import NVDAObjects
from utils.urlUtils import _LinkData

post_browseModeStateChange = extensionPoints.Action()
"""
Expand Down Expand Up @@ -228,7 +229,7 @@ def find(self, text, caseSensitive=False, reverse=False):
def activate(self):
return self.innerTextInfo.activate()

def _getLinkDataAtCaretPosition(self) -> textInfos._Link | None:
def _getLinkDataAtCaretPosition(self) -> "_LinkData | None":
return self.innerTextInfo._getLinkDataAtCaretPosition()

def compareEndPoints(self, other, which):
Expand Down
9 changes: 9 additions & 0 deletions source/utils/urlUtils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import controlTypes
from urllib.parse import ParseResult, urlparse, urlunparse
from dataclasses import dataclass
from logHandler import log


Expand Down Expand Up @@ -52,3 +53,11 @@ def isSamePageURL(targetURLOnPage: str, rootURL: str) -> bool:
return targetURLOnPageWithoutFragments == rootURLWithoutFragments and not any(
char in parsedTargetURLOnPage.fragment for char in fragmentInvalidChars
)


@dataclass
class _LinkData:
"""Class to store information on a link."""

displayText: str | None
destination: str
2 changes: 1 addition & 1 deletion user_docs/en/changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ Specifically, MathML inside of span and other elements that have the attribute `
* When spelling, unicode normalization now works more appropriately:
* After reporting a normalized character, NVDA no longer incorrectly reports subsequent characters as normalized. (#17286, @LeonarddeR)
* Composite characters (such as é) are now reported correctly. (#17295, @LeonarddeR)
* The command to Report the destination URL of a link now works as expected when using the legacy object model in Microsoft Word, Outlook and Excel. (#17292, #17362, @CyrilleB79)
* The command to Report the destination URL of a link now works as expected when using the legacy object model in Microsoft Word, Outlook, Excel and PowerPoint. (#17292, #17362, #17435, @CyrilleB79)
* NVDA will no longer announce Windows 11 clipboard history entries when closing the window while items are present. (#17308, @josephsl)
* If the plugins are reloaded while a browseable message is opened, NVDA will no longer fail to report subsequent focus moves. (#17323, @CyrilleB79)
* When using applications such as Skype, Discord, Signal and Phone Link for audio communication, NVDA speech and sounds no longer decrease in volume. (#17349, @jcsteh)
Expand Down