Skip to content

Commit dd3480c

Browse files
Merge a7608d6 into 746e8fa
2 parents 746e8fa + a7608d6 commit dd3480c

7 files changed

Lines changed: 203 additions & 10 deletions

File tree

.gitmodules

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
ignore = untracked
1515
[submodule "include/ia2"]
1616
path = include/ia2
17-
url = https://github.com/LinuxA11y/IAccessible2.git
17+
url = https://github.com/nvAccess/IAccessible2.git
1818
[submodule "include/javaAccessBridge32"]
1919
path = include/javaAccessBridge32
2020
url = https://github.com/nvaccess/javaAccessBridge32-bin.git

nvdaHelper/ia2_sconscript

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ idlFiles = [
4444
'AccessibleStates.idl',
4545
'Accessible2.idl',
4646
'Accessible2_2.idl',
47-
'Accessible2_3.idl',
4847
'AccessibleComponent.idl',
4948
'AccessibleValue.idl',
5049
'AccessibleText.idl',
@@ -60,6 +59,7 @@ idlFiles = [
6059
'AccessibleEventID.idl',
6160
'AccessibleApplication.idl',
6261
'AccessibleDocument.idl',
62+
'AccessibleTextSelectionContainer.idl',
6363
'IA2TypeLibrary.idl',
6464
]
6565
idlFile = env.Command('ia2.idl',

nvdaHelper/vbufBackends/gecko_ia2/gecko_ia2.cpp

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1016,10 +1016,18 @@ VBufStorage_fieldNode_t* GeckoVBufBackend_t::fillVBuf(
10161016
// Add the chunk to the buffer.
10171017
if(tempNode=buffer->addTextFieldNode(parentNode,previousNode,wstring(IA2Text+chunkStart,i-chunkStart))) {
10181018
previousNode=tempNode;
1019-
// Add the IA2Text start offset as an attribute on the node.
1019+
// Add IA2Text start offset as an attribute on the node.
10201020
s << chunkStart;
10211021
previousNode->addAttribute(L"ia2TextStartOffset", s.str());
10221022
s.str(L"");
1023+
// Also add IA2 windowHandle and ID on the text node
1024+
// To make fetching IA2Ranges for selecting much easier.
1025+
s << docHandle;
1026+
previousNode->addAttribute(L"ia2TextWindowHandle", s.str());
1027+
s.str(L"");
1028+
s << ID;
1029+
previousNode->addAttribute(L"ia2TextUniqueID", s.str());
1030+
s.str(L"");
10231031
// Add text attributes.
10241032
for (map<wstring, wstring>::const_iterator it = textAttribs.begin(); it != textAttribs.end(); ++it) {
10251033
previousNode->addAttribute(it->first, it->second);
@@ -1071,6 +1079,18 @@ VBufStorage_fieldNode_t* GeckoVBufBackend_t::fillVBuf(
10711079
);
10721080
if (tempNode) {
10731081
previousNode=tempNode;
1082+
// Add IA2Text start offset as an attribute on the node.
1083+
s << i;
1084+
previousNode->addAttribute(L"ia2TextStartOffset", s.str());
1085+
s.str(L"");
1086+
// Also add IA2 windowHandle and ID on the text node
1087+
// To make fetching IA2Ranges for selecting much easier.
1088+
s << docHandle;
1089+
previousNode->addAttribute(L"ia2TextWindowHandle", s.str());
1090+
s.str(L"");
1091+
s << ID;
1092+
previousNode->addAttribute(L"ia2TextUniqueID", s.str());
1093+
s.str(L"");
10741094
} else {
10751095
LOG_DEBUG(L"Error in fillVBuf");
10761096
}

nvdaHelper/vbufBase/storage.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,8 @@ void VBufStorage_fieldNode_t::generateAttributesForMarkupOpeningTag(std::wstring
226226
wostringstream s;
227227
s<<L"_startOfNode=\""<<(startOffset==0?1:0)<<L"\" ";
228228
s<<L"_endOfNode=\""<<(endOffset>=this->length?1:0)<<L"\" ";
229+
s<<L"_offsetFromStartOfNode=\""<<startOffset<<L"\" ";
230+
s<<L"_offsetFromEndOfNode=\""<<max(0, this->length - endOffset)<<L"\" ";
229231
s<<L"isBlock=\""<<this->isBlock<<L"\" ";
230232
s<<L"isHidden=\""<<this->isHidden<<L"\" ";
231233
int childCount=0;

source/NVDAObjects/IAccessible/chromium.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import typing
99
from typing import Dict, Optional
1010
from comtypes import COMError
11-
1211
import config
1312
import controlTypes
1413
from NVDAObjects.IAccessible import IAccessible

source/XMLFormatting.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616

1717
class XMLTextParser(object):
1818

19+
def __init__(self) -> None:
20+
self._controlFieldStack: typing.List[textInfos.ControlField] = []
21+
1922
def _startElementHandler(self,tagName,attrs):
2023
if tagName=='unich':
2124
data=attrs.get('value',None)
@@ -28,6 +31,7 @@ def _startElementHandler(self,tagName,attrs):
2831
return
2932
elif tagName=='control':
3033
newAttrs=textInfos.ControlField(attrs)
34+
self._controlFieldStack.append(newAttrs)
3135
self._commandList.append(textInfos.FieldCommand("controlStart",newAttrs))
3236
elif tagName=='text':
3337
newAttrs=textInfos.FormatField(attrs)
@@ -44,10 +48,19 @@ def _startElementHandler(self,tagName,attrs):
4448
newAttrs["_endOfNode"] = newAttrs["_endOfNode"] == "1"
4549
except KeyError:
4650
pass
51+
try:
52+
newAttrs["_offsetFromStartOfNode"] = int(newAttrs["_offsetFromStartOfNode"])
53+
except KeyError:
54+
pass
55+
try:
56+
newAttrs["_offsetFromEndOfNode"] = int(newAttrs["_offsetFromEndOfNode"])
57+
except KeyError:
58+
pass
4759

4860
def _EndElementHandler(self,tagName):
4961
if tagName=="control":
50-
self._commandList.append(textInfos.FieldCommand("controlEnd",None))
62+
attrs = self._controlFieldStack.pop()
63+
self._commandList.append(textInfos.FieldCommand("controlEnd", attrs))
5164
elif tagName in ("text","unich"):
5265
pass
5366
else:

source/virtualBuffers/gecko_ia2.py

Lines changed: 164 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,26 @@
66
from typing import (
77
Iterable,
88
Optional,
9+
Tuple,
910
)
1011
import typing
1112
import weakref
13+
from ctypes import byref
1214
from . import VirtualBuffer, VirtualBufferTextInfo, VBufStorage_findMatch_word, VBufStorage_findMatch_notEmpty
1315
import treeInterceptorHandler
1416
import controlTypes
1517
import NVDAObjects.IAccessible.mozilla
1618
import NVDAObjects.behaviors
1719
import winUser
1820
import IAccessibleHandler
19-
21+
from scriptHandler import script
22+
import ui
2023
import oleacc
2124
from logHandler import log
2225
import textInfos
2326
from comtypes.gen.IAccessible2Lib import IAccessible2
2427
from comInterfaces import IAccessible2Lib as IA2
28+
from comInterfaces.IAccessible2Lib import IAccessibleTextSelectionContainer, IA2TextSelection, IAccessibleText
2529
from comtypes import COMError
2630
import aria
2731
import config
@@ -45,6 +49,14 @@ def _getNormalizedCurrentAttrs(attrs: textInfos.ControlField) -> typing.Dict[str
4549

4650
class Gecko_ia2_TextInfo(VirtualBufferTextInfo):
4751

52+
def _setSelectionOffsets(self, start, end):
53+
super()._setSelectionOffsets(start, end)
54+
if self.obj._nativeAppSelectionMode:
55+
if start != end:
56+
self.obj.updateAppSelection()
57+
else:
58+
self.obj.clearAppSelection()
59+
4860
def _getBoundingRectFromOffset(self,offset):
4961
formatFieldStart, formatFieldEnd = self._getUnitOffsets(textInfos.UNIT_FORMATFIELD, offset)
5062
# The format field starts at the first character.
@@ -89,6 +101,15 @@ def _calculateDescriptionFrom(self, attrs: textInfos.ControlField) -> controlTyp
89101
# Note: when working on _normalizeControlField, look for opportunities to simplify
90102
# and move logic out into smaller helper functions.
91103
def _normalizeControlField(self, attrs): # noqa: C901
104+
# convert some IAccessible2 text values to integers
105+
for name in (
106+
"ia2TextWindowHandle",
107+
"ia2TextUniqueID",
108+
"ia2TextStartOffset",
109+
):
110+
val = attrs.get(name, None)
111+
if val is not None:
112+
attrs[name] = int(val)
92113
for attr in (
93114
"table-rownumber-presentational",
94115
"table-columnnumber-presentational",
@@ -223,12 +244,18 @@ def _normalizeDetailsRole(self, detailsRoles: str) -> Iterable[Optional[controlT
223244

224245
def _normalizeFormatField(self, attrs):
225246
normalizeIA2TextFormatField(attrs)
226-
ia2TextStartOffset = attrs.get("ia2TextStartOffset")
227-
if ia2TextStartOffset is not None:
228-
assert ia2TextStartOffset.isdigit(), "ia2TextStartOffset isn't a digit, %r" % ia2TextStartOffset
229-
attrs["ia2TextStartOffset"] = int(ia2TextStartOffset)
247+
# convert some IAccessible2 values to integers
248+
for name in (
249+
"ia2TextWindowHandle",
250+
"ia2TextUniqueID",
251+
"ia2TextStartOffset",
252+
):
253+
val = attrs.get(name, None)
254+
if val is not None:
255+
attrs[name] = int(val)
230256
return super(Gecko_ia2_TextInfo,self)._normalizeFormatField(attrs)
231257

258+
232259
class Gecko_ia2(VirtualBuffer):
233260

234261
TextInfo=Gecko_ia2_TextInfo
@@ -238,6 +265,35 @@ class Gecko_ia2(VirtualBuffer):
238265
#: frame/iframe in the lists is a tuple of (IAccessible2_2, uniqueId). This
239266
#: cache is used across instances.
240267
_framesCache = weakref.WeakKeyDictionary()
268+
_nativeAppSelectionMode = False
269+
270+
@script(
271+
gesture="kb:NVDA+shift+f10"
272+
)
273+
def script_toggleNativeAppSelectionMode(self, gesture):
274+
self._nativeAppSelectionMode = not self._nativeAppSelectionMode
275+
if self._nativeAppSelectionMode:
276+
ui.message(_("Native app selection mode enabled."))
277+
try:
278+
self.updateAppSelection()
279+
except NotImplementedError:
280+
pass
281+
else:
282+
ui.message(_("Native app selection mode disabled."))
283+
try:
284+
self.clearAppSelection()
285+
except NotImplementedError:
286+
pass
287+
288+
@script(
289+
gesture="kb:control+c"
290+
)
291+
def script_copyToClipboard(self, gesture):
292+
if self._nativeAppSelectionMode:
293+
ui.message(_("native copy"))
294+
gesture.send()
295+
else:
296+
super().script_copyToClipboard(gesture)
241297

242298
def __init__(self,rootNVDAObject):
243299
super(Gecko_ia2,self).__init__(rootNVDAObject,backendName="gecko_ia2")
@@ -576,3 +632,106 @@ def _getInitialCaretPos(self):
576632
if initialPos:
577633
return initialPos
578634
return self._initialScrollObj
635+
636+
# C901 'updateAppSelection' is too complex
637+
# Note: when working on updateAppSelection, look for opportunities to simplify
638+
# and move logic out into smaller helper functions.
639+
def updateAppSelection(self): # noqa: C901
640+
"""Update the native selection in the application to match the browse mode selection in NVDA."""
641+
try:
642+
paccTextSelectionContainer = self.rootNVDAObject.IAccessibleObject.QueryInterface(
643+
IAccessibleTextSelectionContainer
644+
)
645+
except COMError as e:
646+
raise NotImplementedError from e
647+
selInfo = self.makeTextInfo(textInfos.POSITION_SELECTION)
648+
selFields = selInfo.getTextWithFields()
649+
ia2StartWindow = None
650+
ia2StartID = None
651+
ia2StartOffset = None
652+
ia2EndWindow = None
653+
ia2EndID = None
654+
ia2EndOffset = None
655+
log.debug("checking fields...")
656+
# Locate the start of the selection by walking through the fields.
657+
# Until we find the deepest field with IAccessibleText information.
658+
# It may be on a formatChange which represents a text attribute run,
659+
# or on a controlStart which represents an embeded object within text,
660+
# Where we have not included its inner text attribute run
661+
# as the content was overridden by an ARIA label or similar.
662+
for field in selFields:
663+
if isinstance(field, textInfos.FieldCommand):
664+
if field.command in ("controlStart", "formatChange"):
665+
hwnd = field.field.get('ia2TextWindowHandle')
666+
if hwnd is not None:
667+
ia2StartWindow = hwnd
668+
ia2StartID = field.field['ia2TextUniqueID']
669+
ia2StartOffset = field.field['ia2TextStartOffset']
670+
if field.command == "formatChange":
671+
ia2StartOffset += field.field.get('strippedCharsFromStart', 0)
672+
ia2StartOffset += field.field['_offsetFromStartOfNode']
673+
if field.command == "controlStart":
674+
continue
675+
break
676+
if ia2StartOffset is None:
677+
raise NotImplementedError
678+
log.debug(f"ia2StartWindow: {ia2StartWindow}")
679+
log.debug(f"ia2StartID: {ia2StartID}")
680+
log.debug(f"ia2StartOffset: {ia2StartOffset}")
681+
ia2StartObj, childID = IAccessibleHandler.accessibleObjectFromEvent(
682+
ia2StartWindow, winUser.OBJID_CLIENT, ia2StartID
683+
)
684+
assert (childID == 0), f"childID should be 0"
685+
ia2StartObj = ia2StartObj.QueryInterface(IAccessibleText)
686+
log.debug(f"ia2StartObj {ia2StartObj}")
687+
textLen = 0
688+
# Locate the end of the selection by walking through the fields in reverse,
689+
# similar to how we located the start of the selection.
690+
for field in reversed(selFields):
691+
if isinstance(field, str):
692+
textLen = len(field)
693+
continue
694+
elif isinstance(field, textInfos.FieldCommand):
695+
if field.command in ("controlEnd", "formatChange"):
696+
hwnd = field.field.get('ia2TextWindowHandle')
697+
if hwnd is not None:
698+
ia2EndWindow = hwnd
699+
ia2EndID = field.field['ia2TextUniqueID']
700+
ia2EndOffset = field.field['ia2TextStartOffset']
701+
if field.command == "controlEnd":
702+
ia2EndOffset += 1
703+
elif field.command == "formatChange":
704+
ia2EndOffset += field.field.get('strippedCharsFromStart', 0)
705+
ia2EndOffset += field.field['_offsetFromStartOfNode']
706+
ia2EndOffset += textLen
707+
if field.command == "controlEnd":
708+
continue
709+
break
710+
if ia2EndOffset is None:
711+
raise NotImplementedError
712+
log.debug(f"ia2EndWindow: {repr(ia2EndWindow)}")
713+
log.debug(f"ia2EndID: {repr(ia2EndID)}")
714+
log.debug(f"ia2EndOffset: {ia2EndOffset}")
715+
if ia2EndID == ia2StartID:
716+
ia2EndObj = ia2StartObj
717+
log.debug("Reusing ia2StartObj for ia2EndObj")
718+
else:
719+
ia2EndObj, childID = IAccessibleHandler.accessibleObjectFromEvent(
720+
ia2EndWindow, winUser.OBJID_CLIENT, ia2EndID
721+
)
722+
assert (childID == 0), f"childID should be 0"
723+
ia2EndObj = ia2EndObj.QueryInterface(IAccessibleText)
724+
log.debug(f"ia2EndObj {ia2EndObj}")
725+
r = IA2TextSelection(ia2StartObj, ia2StartOffset, ia2EndObj, ia2EndOffset, False)
726+
paccTextSelectionContainer.SetSelections(1, byref(r))
727+
728+
def clearAppSelection(self):
729+
"""Clear the native selection in the application."""
730+
try:
731+
paccTextSelectionContainer = self.rootNVDAObject.IAccessibleObject.QueryInterface(
732+
IAccessibleTextSelectionContainer
733+
)
734+
except COMError as e:
735+
raise NotImplementedError from e
736+
r = IA2TextSelection(None, 0, None, 0, False)
737+
paccTextSelectionContainer.SetSelections(0, byref(r))

0 commit comments

Comments
 (0)