Skip to content

Commit

Permalink
PowerPoint: Report link destination (#17435)
Browse files Browse the repository at this point in the history
When in PowerPoint, NVDA+K does not work to report a link.

Description of user facing changes
In PowerPoint slides (edition mode), pressing NVDA+K will now report the link destination. This works in text areas as well as on shapes / charts / images that are links.
Also 2 small fixes:
Restored the Copy and Close buttons of the browseable message of NVDA+K that were removed by mistake in Report destination of link in Word / Outlook legacy #17292
In Windows UI, e.g. settings, pressing NVDA+K on a link does not report anymore that there is no link; it rather reports that the destination cannot be reported.
Description of development approach
Use PowerPoint object model
Changed the global strategy to retrieve a link: first try from text infos; if not supported, try from the focused object.
  • Loading branch information
CyrilleB79 authored Dec 11, 2024
1 parent cef4abb commit bdd3153
Show file tree
Hide file tree
Showing 9 changed files with 104 additions and 50 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
53 changes: 44 additions & 9 deletions source/appModules/powerpnt.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,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 @@ -42,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 @@ -995,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 @@ -1161,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 @@ -1170,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 @@ -1214,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
27 changes: 16 additions & 11 deletions source/globalCommands.py
Original file line number Diff line number Diff line change
Expand Up @@ -4282,16 +4282,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 @@ -4306,18 +4310,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 @@ -68,7 +68,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

0 comments on commit bdd3153

Please sign in to comment.