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

Add ability to recognize links to the same page #16994

Merged
merged 76 commits into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from 73 commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
4e1f7cd
Add ability to recognize links to the same page
nvdaes Aug 12, 2024
60dd50a
Add IAccessible::value to vbuf
LeonarddeR Aug 13, 2024
9d13634
Merge pull request #4 from LeonarddeR/accValue
nvdaes Aug 13, 2024
445be6c
Add internal link state for some links in virtualBuffers
nvdaes Aug 13, 2024
82d34be
Cleanup code
nvdaes Aug 14, 2024
e0ac8a8
Merge branch 'master' into links
nvdaes Aug 14, 2024
253e576
Make _valueToSamePage function private
nvdaes Aug 14, 2024
eb01b6f
Add configuration options and unassigned command to cycle between rep…
nvdaes Aug 14, 2024
5cf585c
Don't check if object is internal link if the option is disabled
nvdaes Aug 14, 2024
ba36a24
dd configuration to format document settings panel
nvdaes Aug 14, 2024
4c4d8e3
Add urlUtils module
nvdaes Aug 14, 2024
a965d2d
Updated user guide nentioning this option, without a specific section
nvdaes Aug 14, 2024
fae812b
Update documentation for changes
nvdaes Aug 14, 2024
cedfc69
Enable reporting of link type by default, addressing reviewsuggestion
nvdaes Aug 15, 2024
362e583
Apply suggestions from code review
nvdaes Aug 15, 2024
549a40c
Revert some changes
nvdaes Aug 15, 2024
5e35313
Add braille label for INTERNAL_LINK state, addressing review suggestion
nvdaes Aug 15, 2024
13bf11d
Use urlparse for function to check same page links, suggested by reviewr
nvdaes Aug 15, 2024
bf290bb
Merge branch 'master' into links
nvdaes Aug 15, 2024
b032b2e
Several changes as proposed in review, working
LeonarddeR Aug 15, 2024
c156c71
Merge pull request #5 from LeonarddeR/linksFollowUp
nvdaes Aug 15, 2024
456facb
Pre-commit auto-fix
pre-commit-ci[bot] Aug 15, 2024
b483be6
Update changes for developers
nvdaes Aug 15, 2024
a18de14
Don't consider same page if fragment contains a path like in Gmail la…
nvdaes Aug 15, 2024
5efea77
Add support for Edge
LeonarddeR Aug 16, 2024
f84d23e
Add LINKED state check
LeonarddeR Aug 16, 2024
ffad846
Merge pull request #6 from LeonarddeR/linksEdge
nvdaes Aug 16, 2024
37c9bf4
Apply suggestions from code review
nvdaes Aug 16, 2024
d4b9ea2
Mention developer in changelog
nvdaes Aug 16, 2024
69bd7e7
Update changes for developers
nvdaes Aug 16, 2024
e207ca5
Applysuggestions from coderabbitai
nvdaes Aug 16, 2024
07ddb09
Ensure that fragments of internal links are alnum
nvdaes Aug 16, 2024
984a59e
Accept more characters in fragments
nvdaes Aug 16, 2024
90bbd12
Use link type property, instead of isInternalLink
nvdaes Aug 17, 2024
4b21bb3
Fix documentation for function
nvdaes Aug 17, 2024
d602cbe
Apply suggestions from coderabbitai
nvdaes Aug 17, 2024
4fbe4cb
Add STETE_LINK_TYPE
nvdaes Aug 17, 2024
cd347d2
Merge branch 'master' into links
nvdaes Aug 31, 2024
df22719
Apply suggestions from code review
nvdaes Sep 2, 2024
a0af6ee
Address review
nvdaes Sep 2, 2024
d1c6286
Fix system tests
nvdaes Sep 2, 2024
af32760
Apply suggestions from code review
nvdaes Sep 3, 2024
65d82db
Pre-commit auto-fix
pre-commit-ci[bot] Sep 3, 2024
161b2fa
Fux urlUtils
nvdaes Sep 3, 2024
a2b1a88
Add type hint for gesture
nvdaes Sep 3, 2024
f0cbcbe
Improve unit tests
nvdaes Sep 3, 2024
9e767d2
Improve urlUtils
nvdaes Sep 3, 2024
48f1b61
Fix urlUtils
nvdaes Sep 3, 2024
db0c8cd
Fix variable
nvdaes Sep 3, 2024
fab26cf
Add toggleBooleanValue function
nvdaes Sep 3, 2024
b91c8d2
Pre-commit auto-fix
pre-commit-ci[bot] Sep 3, 2024
0bcc928
Fix changes for developers
nvdaes Sep 3, 2024
4849e6b
Merge branch 'links' of https://github.com/nvdaes/nvda into links
nvdaes Sep 3, 2024
bbce160
Update changes for developers
nvdaes Sep 3, 2024
8fb71ca
Remove % from invalidChars
nvdaes Sep 3, 2024
d6d59f8
Fix script and function to toggle boolean values
nvdaes Sep 3, 2024
1fc8382
Pre-commit auto-fix
pre-commit-ci[bot] Sep 3, 2024
4e331d5
Apply suggestions from code review
nvdaes Sep 4, 2024
e526e25
Merge remote-tracking branch 'nvaccess/master' into links
nvdaes Sep 4, 2024
0cb9b6b
Remove accidentally included info
nvdaes Sep 4, 2024
19bcae9
Apply the same page link concept, not anchor approach
nvdaes Sep 4, 2024
ce7396b
Pre-commit auto-fix
pre-commit-ci[bot] Sep 4, 2024
1dab305
Fix script
nvdaes Sep 4, 2024
64cbf92
Uppercase URL word
nvdaes Sep 4, 2024
da60228
Pre-commit auto-fix
pre-commit-ci[bot] Sep 4, 2024
8eddd33
Fix grammar in vaariable names
nvdaes Sep 4, 2024
1529e2c
Merge branch 'links' of https://github.com/nvdaes/nvda into links
nvdaes Sep 4, 2024
9dff709
Add missing translator comments
nvdaes Sep 4, 2024
30381ed
Add missing import
nvdaes Sep 4, 2024
8620913
Fix variable name
nvdaes Sep 4, 2024
957d541
Fix variable name
nvdaes Sep 4, 2024
96d8462
Improve function name
nvdaes Sep 4, 2024
aa83277
Don't toggle report link type if report link is disabled
nvdaes Sep 4, 2024
b8eb917
Update tests/unit/test_util/test_urlUtils.py
seanbudd Sep 5, 2024
2ca89c0
Merge branch 'master' into links
seanbudd Sep 5, 2024
18b0034
Merge branch 'master' into links
seanbudd Sep 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions nvdaHelper/vbufBackends/gecko_ia2/gecko_ia2.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1000,10 +1000,16 @@ VBufStorage_fieldNode_t* GeckoVBufBackend_t::fillVBuf(
}

BSTR value=NULL;
if(pacc->get_accValue(varChild,&value)==S_OK) {
if(value&&SysStringLen(value)==0) {
SysFreeString(value);
value=NULL;
if (pacc->get_accValue(varChild, &value) == S_OK) {
if (value) {
if (role == ROLE_SYSTEM_LINK) {
// For links, store the IAccessible value to handle same page link detection.
parentNode->addAttribute(L"IAccessible::value", value);
}
if (SysStringLen(value) == 0) {
SysFreeString(value);
value = NULL;
}
}
}

Expand Down
2 changes: 2 additions & 0 deletions source/NVDAObjects/IAccessible/ia2Web.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,8 @@ def _get_states(self):
if popupState:
states.discard(controlTypes.State.HASPOPUP)
states.add(popupState)
if self.role == controlTypes.Role.LINK and controlTypes.State.LINKED in states and self.linkType:
states.add(self.linkType)
return states

def _get_landmark(self):
Expand Down
10 changes: 10 additions & 0 deletions source/NVDAObjects/UIA/chromium.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# See the file COPYING for more details.
# Copyright (C) 2020-2021 NV Access limited, Leonard de Ruijter


import UIAHandler
from . import web
import controlTypes
Expand Down Expand Up @@ -66,11 +67,20 @@ def _getControlFieldForUIAObject(self, obj, isEmbedded=False, startOfNode=False,
class ChromiumUIA(web.UIAWeb):
_TextInfo = ChromiumUIATextInfo

def _get_states(self) -> set[controlTypes.State]:
states = super().states
if self.role == controlTypes.Role.LINK and self.linkType:
states.add(self.linkType)
return states


class ChromiumUIATreeInterceptor(web.UIAWebTreeInterceptor):
def _get_documentConstantIdentifier(self):
return self.rootNVDAObject.parent._getUIACacheablePropertyValue(UIAHandler.UIA_AutomationIdPropertyId)

def _get_documentURL(self) -> str | None:
return self.rootNVDAObject.value


class ChromiumUIADocument(ChromiumUIA):
treeInterceptorClass = ChromiumUIATreeInterceptor
Expand Down
15 changes: 15 additions & 0 deletions source/NVDAObjects/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1627,3 +1627,18 @@ def _get_isBelowLockScreen(self) -> bool:
if not isLockScreenModeActive():
return False
return _isObjectBelowLockScreen(self)

linkType: controlTypes.State | None
"""Typing information for auto property _get_linkType
Determines the link type based on the link and document URLs.
"""

def _get_linkType(self) -> controlTypes.State | None:
if self.role != controlTypes.Role.LINK:
return None
from browseMode import BrowseModeDocumentTreeInterceptor

ti = getattr(self, "treeInterceptor", None)
if not isinstance(ti, BrowseModeDocumentTreeInterceptor):
return None
return ti.getLinkTypeInDocument(self.value)
2 changes: 2 additions & 0 deletions source/braille.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,8 @@
controlTypes.State.HASCOMMENT: _("cmnt"),
# Translators: Displayed in braille when a control is switched on
controlTypes.State.ON: "⣏⣿⣹",
# Translators: Displayed in braille when a link destination points to the same page
controlTypes.State.INTERNAL_LINK: _("smp"),
}
negativeStateLabels = {
# Translators: Displayed in braille when an object is not selected.
Expand Down
10 changes: 10 additions & 0 deletions source/browseMode.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
import gui.contextHelp
from abc import ABCMeta, abstractmethod
import globalVars
from utils import urlUtils
from typing import Optional


Expand Down Expand Up @@ -304,6 +305,15 @@ class BrowseModeTreeInterceptor(treeInterceptorHandler.TreeInterceptor):
scriptCategory = inputCore.SCRCAT_BROWSEMODE
_disableAutoPassThrough = False
APPLICATION_ROLES = (controlTypes.Role.APPLICATION, controlTypes.Role.DIALOG)
documentURL: str | None = None
"""The URL of the current browse mode document.
C{None} when there is no URL or it is unknown.
Used to determine the type of a link in the document.
"""

def getLinkTypeInDocument(self, url: str) -> controlTypes.State | None:
"""Returns the type of a link in the document, or C{None} if the link type cannot be determined."""
return urlUtils.getLinkType(url, self.documentURL)

def _get_currentNVDAObject(self):
raise NotImplementedError
Expand Down
1 change: 1 addition & 0 deletions source/config/configSpec.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@
# 0: Off, 1: style, 2: color and style
reportCellBorders = integer(0, 2, default=0)
reportLinks = boolean(default=true)
reportLinkType = boolean(default=true)
reportGraphics = boolean(default=True)
reportComments = boolean(default=true)
reportBookmarks = boolean(default=true)
Expand Down
6 changes: 5 additions & 1 deletion source/controlTypes/processAndLabelStates.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from typing import Dict, List, Optional, Set

from .role import Role, clickableRoles
from .state import State, STATES_SORTED
from .state import State, STATES_SORTED, STATES_LINK_TYPE
from .outputReason import OutputReason


Expand All @@ -31,6 +31,7 @@ def _processPositiveStates(
positiveStates.discard(State.EDITABLE)
if role != Role.LINK:
positiveStates.discard(State.VISITED)
positiveStates.discard(State.INTERNAL_LINK)
positiveStates.discard(State.SELECTABLE)
positiveStates.discard(State.FOCUSABLE)
positiveStates.discard(State.CHECKABLE)
Expand All @@ -47,6 +48,9 @@ def _processPositiveStates(
# or reporting clickable just isn't useful,
# or the user has explicitly requested no reporting clickable
positiveStates.discard(State.CLICKABLE)
if not config.conf["documentFormatting"]["reportLinkType"]:
for state in STATES_LINK_TYPE:
positiveStates.discard(state)
if reason == OutputReason.QUERY:
return positiveStates
positiveStates.discard(State.DEFUNCT)
Expand Down
6 changes: 6 additions & 0 deletions source/controlTypes/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,13 @@ def negativeDisplayString(self) -> str:
HASPOPUP_GRID = setBit(48)
HASPOPUP_LIST = setBit(49)
HASPOPUP_TREE = setBit(50)
INTERNAL_LINK = setBit(51)


STATES_SORTED = frozenset([State.SORTED, State.SORTED_ASCENDING, State.SORTED_DESCENDING])

STATES_LINK_TYPE = frozenset([State.INTERNAL_LINK])


_stateLabels: Dict[State, str] = {
# Translators: This is presented when a control or document is unavailable.
Expand Down Expand Up @@ -204,6 +207,9 @@ def negativeDisplayString(self) -> str:
State.HASPOPUP_LIST: _("opens list"),
# Translators: Presented when a control has a pop-up tree.
State.HASPOPUP_TREE: _("opens tree"),
# Translators: Presented when a link destination points to the page containing the link.
# For example, links of a table of contents of a document with different sections.
State.INTERNAL_LINK: _("same page"),
nvdaes marked this conversation as resolved.
Show resolved Hide resolved
}


Expand Down
43 changes: 43 additions & 0 deletions source/globalCommands.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,29 @@
NO_SETTINGS_MSG = _("No settings")


def toggleBooleanValue(
configSection: str,
configKey: str,
enabledMsg: str,
disabledMsg: str,
) -> None:
"""
Toggles a boolean value in the configuration and returns the appropriate message.

:param configSection: The configuration section containing the boolean key.
:param configKey: The configuration key associated with the boolean value.
:param enabledMsg: The message for the enabled state.
:param disabledMsg: The message for the disabled state.
:return: None.
"""
currentValue = config.conf[configSection][configKey]
newValue = not currentValue
config.conf[configSection][configKey] = newValue

msg = enabledMsg if newValue else disabledMsg
ui.message(msg)


class GlobalCommands(ScriptableObject):
"""Commands that are available at all times, regardless of the current focus."""

Expand Down Expand Up @@ -902,6 +925,26 @@ def script_toggleReportLinks(self, gesture):
config.conf["documentFormatting"]["reportLinks"] = True
ui.message(state)

@script(
# Translators: Input help mode message for toggle report link type command.
description=_("Toggles on and off the reporting of link type"),
category=SCRCAT_DOCUMENTFORMATTING,
)
def script_toggleReportLinkType(self, gesture: inputCore.InputGesture):
if config.conf["documentFormatting"]["reportLinks"]:
toggleBooleanValue(
configSection="documentFormatting",
configKey="reportLinkType",
# Translators: The message announced when toggling the report link type document formatting setting.
enabledMsg=_("Report link type on"),
# Translators: The message announced when toggling the report link type document formatting setting.
disabledMsg=_("Report link type off"),
)
else:
# Translators: The message announced when reporting links is disabled,
# and the user tries to toggle the report link type document formatting setting.
ui.message(_("The report links setting must be enabled to toggle report link type"))

coderabbitai[bot] marked this conversation as resolved.
Show resolved Hide resolved
@script(
# Translators: Input help mode message for toggle report graphics command.
description=_("Toggles on and off the reporting of graphics"),
Expand Down
11 changes: 11 additions & 0 deletions source/gui/settingsDialogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -2799,8 +2799,15 @@ def makeSettings(self, settingsSizer):
# Translators: This is the label for a checkbox in the
# document formatting settings panel.
self.linksCheckBox = elementsGroup.addItem(wx.CheckBox(elementsGroupBox, label=_("Lin&ks")))
self.linksCheckBox.Bind(wx.EVT_CHECKBOX, self._onLinksChange)
self.linksCheckBox.SetValue(config.conf["documentFormatting"]["reportLinks"])

# Translators: This is the label for a checkbox in the
# document formatting settings panel.
self.linkTypeCheckBox = elementsGroup.addItem(wx.CheckBox(elementsGroupBox, label=_("Link type")))
self.linkTypeCheckBox.SetValue(config.conf["documentFormatting"]["reportLinkType"])
self.linkTypeCheckBox.Enable(self.linksCheckBox.IsChecked())

# Translators: This is the label for a checkbox in the
# document formatting settings panel.
self.graphicsCheckBox = elementsGroup.addItem(wx.CheckBox(elementsGroupBox, label=_("&Graphics")))
Expand Down Expand Up @@ -2867,6 +2874,9 @@ def makeSettings(self, settingsSizer):
def _onLineIndentationChange(self, evt: wx.CommandEvent) -> None:
self.ignoreBlankLinesRLICheckbox.Enable(evt.GetSelection() != 0)

def _onLinksChange(self, evt: wx.CommandEvent):
self.linkTypeCheckBox.Enable(evt.IsChecked())

def onSave(self):
config.conf["documentFormatting"]["detectFormatAfterCursor"] = (
self.detectFormatAfterCursorCheckBox.IsChecked()
Expand Down Expand Up @@ -2901,6 +2911,7 @@ def onSave(self):
config.conf["documentFormatting"]["reportTableCellCoords"] = self.tableCellCoordsCheckBox.IsChecked()
config.conf["documentFormatting"]["reportCellBorders"] = self.borderComboBox.GetSelection()
config.conf["documentFormatting"]["reportLinks"] = self.linksCheckBox.IsChecked()
config.conf["documentFormatting"]["reportLinkType"] = self.linkTypeCheckBox.IsChecked()
config.conf["documentFormatting"]["reportGraphics"] = self.graphicsCheckBox.IsChecked()
config.conf["documentFormatting"]["reportHeadings"] = self.headingsCheckBox.IsChecked()
config.conf["documentFormatting"]["reportLists"] = self.listsCheckBox.IsChecked()
Expand Down
54 changes: 54 additions & 0 deletions source/utils/urlUtils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# A part of NonVisual Desktop Access (NVDA)
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.
# Copyright (C) 2024 NV Access Limited, Noelia Ruiz Martínez, Leonard de Ruijter

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


def getLinkType(targetURL: str, rootURL: str) -> controlTypes.State | None:
"""Returns the link type corresponding to a given URL.

:param targetURL: The URL of the link destination
:param rootURL: The root URL of the page
:return: A controlTypes.State corresponding to the link type, or C{None} if the state cannot be determined
"""
if not targetURL or not rootURL:
log.debug(f"getLinkType: Either targetUrl {targetURL} or rootUrl {rootURL} is empty.")
return None
if isSamePageURL(targetURL, rootURL):
log.debug(f"getLinkType: {targetURL} is an internal link.")
return controlTypes.State.INTERNAL_LINK
log.debug(f"getLinkType: {targetURL} type is unknown.")
return None


def isSamePageURL(targetURLOnPage: str, rootURL: str) -> bool:
"""Returns whether a given URL belongs to the same page as another URL.

:param targetURLOnPage: The URL that should be on the same page as `rootURL`
:param rootURL: The root URL of the page
:return: Whether `targetURLOnPage` belongs to the same page as `rootURL`
"""
if not targetURLOnPage or not rootURL:
return False

validSchemes = ("http", "https")
# Parse the URLs
parsedTargetURLOnPage: ParseResult = urlparse(targetURLOnPage)
if parsedTargetURLOnPage.scheme not in validSchemes:
return False
parsedRootURL: ParseResult = urlparse(rootURL)
if parsedRootURL.scheme not in validSchemes:
return False

# Reconstruct URLs without schemes and without fragments for comparison
targetURLOnPageWithoutFragments = urlunparse(parsedTargetURLOnPage._replace(scheme="", fragment=""))
rootURLWithoutFragments = urlunparse(parsedRootURL._replace(scheme="", fragment=""))

fragmentInvalidChars: str = "/" # Characters not considered valid in fragments
return targetURLOnPageWithoutFragments == rootURLWithoutFragments and not any(
char in parsedTargetURLOnPage.fragment for char in fragmentInvalidChars
)
17 changes: 13 additions & 4 deletions source/virtualBuffers/gecko_ia2.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# A part of NonVisual Desktop Access (NVDA)
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.
# Copyright (C) 2008-2023 NV Access Limited, Babbage B.V., Mozilla Corporation, Accessolutions, Julien Cochuyt
# Copyright (C) 2008-2024 NV Access Limited, Babbage B.V., Mozilla Corporation, Accessolutions,
# Julien Cochuyt, Noelia Ruiz Martínez, Leonard de Ruijter

from dataclasses import dataclass
from typing import (
Expand Down Expand Up @@ -167,9 +168,14 @@ def _normalizeControlField(self, attrs): # noqa: C901
attrs["roleTextBraille"] = roleTextBraille
if attrs.get("IAccessible2::attribute_dropeffect", "none") != "none":
states.add(controlTypes.State.DROPTARGET)
if role == controlTypes.Role.LINK and controlTypes.State.LINKED not in states:
# This is a named link destination, not a link which can be activated. The user doesn't care about these.
role = controlTypes.Role.TEXTFRAME
if role == controlTypes.Role.LINK:
if controlTypes.State.LINKED not in states:
# This is a named link destination, not a link which can be activated. The user doesn't care about these.
role = controlTypes.Role.TEXTFRAME
elif (value := attrs.get("IAccessible::value")) is not None and (
linkType := self.obj.getLinkTypeInDocument(value)
) is not None:
seanbudd marked this conversation as resolved.
Show resolved Hide resolved
states.add(linkType)
level = attrs.get("IAccessible2::attribute_level", "")
xmlRoles = attrs.get("IAccessible2::attribute_xml-roles", "").split(" ")
landmark = next((xr for xr in xmlRoles if xr in aria.landmarkRoles), None)
Expand Down Expand Up @@ -322,6 +328,9 @@ def _get_isAlive(self):
isDefunct = True
return not isDefunct

def _get_documentURL(self) -> str:
return self.documentConstantIdentifier

def getNVDAObjectFromIdentifier(
self,
docHandle: int,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,5 @@ schemaVersion = 2
highlightFocus = True
highlightNavigator = True
highlightBrowseMode = True
[documentFormatting]
reportLinkType = False
Loading