diff --git a/nvdaHelper/vbufBackends/gecko_ia2/gecko_ia2.cpp b/nvdaHelper/vbufBackends/gecko_ia2/gecko_ia2.cpp index f8ca0120eea..8a1491c08f3 100755 --- a/nvdaHelper/vbufBackends/gecko_ia2/gecko_ia2.cpp +++ b/nvdaHelper/vbufBackends/gecko_ia2/gecko_ia2.cpp @@ -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; + } } } diff --git a/source/NVDAObjects/IAccessible/ia2Web.py b/source/NVDAObjects/IAccessible/ia2Web.py index 50a7103a819..84abe4fde0c 100644 --- a/source/NVDAObjects/IAccessible/ia2Web.py +++ b/source/NVDAObjects/IAccessible/ia2Web.py @@ -213,8 +213,30 @@ def _get_states(self): if popupState: states.discard(controlTypes.State.HASPOPUP) states.add(popupState) + if self.isInternalLink: + states.add(controlTypes.State.INTERNAL_LINK) return states + @property + def isInternalLink(self) -> bool: + if self.role != controlTypes.Role.LINK: + return False + if ( + not hasattr(self, "treeInterceptor") + or self.treeInterceptor is None + or not self.treeInterceptor.documentConstantIdentifier + ): + return False + documentConstantIdentifier = self.treeInterceptor.documentConstantIdentifier + if documentConstantIdentifier.endswith("/"): + documentConstantIdentifier = documentConstantIdentifier[:-1] + queryParamCharPos = documentConstantIdentifier.find("?") + if queryParamCharPos > 0: + documentConstantIdentifier = documentConstantIdentifier[:queryParamCharPos] + if self.value.startswith(f"{documentConstantIdentifier}#"): + return True + return False + def _get_landmark(self): xmlRoles = self.IA2Attributes.get("xml-roles", "").split(" ") landmark = next((xr for xr in xmlRoles if xr in aria.landmarkRoles), None) diff --git a/source/controlTypes/state.py b/source/controlTypes/state.py index affc16b3550..ef26f0ef653 100644 --- a/source/controlTypes/state.py +++ b/source/controlTypes/state.py @@ -102,6 +102,7 @@ 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]) @@ -204,6 +205,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"), } diff --git a/source/virtualBuffers/gecko_ia2.py b/source/virtualBuffers/gecko_ia2.py index ec9a6d8b4df..628f3c5c672 100755 --- a/source/virtualBuffers/gecko_ia2.py +++ b/source/virtualBuffers/gecko_ia2.py @@ -167,9 +167,20 @@ 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 + else: + attrs["value"] = self.NVDAObjectAtStart.value + documentConstantIdentifier = self.obj.documentConstantIdentifier + value = attrs.get("value", "") + if ( + value + and documentConstantIdentifier + and value.startswith(f"{documentConstantIdentifier}#") + ): + states.add(controlTypes.State.INTERNAL_LINK) 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) @@ -210,6 +221,7 @@ def _normalizeControlField(self, attrs): # noqa: C901 attrs["detailsRoles"] = set(self._normalizeDetailsRole(detailsRoles)) if config.conf["debugLog"]["annotations"]: log.debug(f"detailsRoles: {attrs['detailsRoles']}") + log.info(attrs) return super()._normalizeControlField(attrs) def _normalizeDetailsRole(self, detailsRoles: str) -> Iterable[Optional[controlTypes.Role]]: