Skip to content

Commit 8f39873

Browse files
Merge 2004b28 into 5f9750d
2 parents 5f9750d + 2004b28 commit 8f39873

7 files changed

Lines changed: 166 additions & 4 deletions

File tree

nvdaHelper/local/UIAUtils.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,13 @@ PROPERTYID registerUIAProperty(GUID* guid, LPCWSTR programmaticName, UIAutomatio
1818
registrar->Release();
1919
return propertyId;
2020
}
21+
22+
int registerUIAAnnotationType(GUID* guid) {
23+
if(!guid) {
24+
LOG_DEBUGWARNING(L"NULL GUID given");
25+
return 0;
26+
}
27+
winrt::Windows::UI::UIAutomation::Core::CoreAutomationRegistrar registrar {};
28+
auto res = registrar.RegisterAnnotationType(*guid);
29+
return res.LocalId;
30+
}

nvdaHelper/local/UIAUtils.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
#ifndef NVDAHELPERLOCAL_UIAUTILS_H
22
#define NVDAHELPERLOCAL_UIAUTILS_H
33

4+
// The following header included to allow winrt::guid to be converted to GUID
5+
#include <unknwn.h>
6+
7+
#include <winrt/windows.ui.uiautomation.core.h>
48
#include <uiAutomationCore.h>
59

610
PROPERTYID registerUIAProperty(GUID* guid, LPCWSTR programmaticName, UIAutomationType propertyType);
11+
int registerUIAAnnotationType(GUID* guid);
12+
713

814
#endif
915

nvdaHelper/local/nvdaHelperLocal.def

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ EXPORTS
5555
calculateCharacterOffsets
5656
findWindowWithClassInThread
5757
registerUIAProperty
58+
registerUIAAnnotationType
5859
dllImportTableHooks_hookSingle
5960
dllImportTableHooks_unhookSingle
6061
audioDucking_shouldDelay

source/NVDAObjects/UIA/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import languageHandler
1818
import UIAHandler
1919
import _UIACustomProps
20+
import _UIACustomAnnotations
2021
import globalVars
2122
import eventHandler
2223
import controlTypes
@@ -818,6 +819,7 @@ def updateSelection(self):
818819

819820
class UIA(Window):
820821
_UIACustomProps = _UIACustomProps.CustomPropertiesCommon.get()
822+
_UIACustomAnnotationTypes = _UIACustomAnnotations.CustomAnnotationTypesCommon.get()
821823

822824
shouldAllowDuplicateUIAFocusEvent = False
823825

source/NVDAObjects/UIA/excel.py

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
# Copyright (C) 2018-2021 NV Access Limited, Leonard de Ruijter
55

66
from typing import Optional, Tuple
7+
from comtypes import COMError
8+
import winVersion
79
import UIAHandler
810
import _UIAHandler
911
import _UIAConstants
@@ -16,6 +18,9 @@
1618
from _UIACustomProps import (
1719
CustomPropertyInfo,
1820
)
21+
from _UIACustomAnnotations import (
22+
CustomAnnotationTypeInfo,
23+
)
1924
from comtypes import GUID
2025
from scriptHandler import script
2126
import ui
@@ -90,10 +95,39 @@ def __init__(self):
9095
)
9196

9297

98+
class ExcelCustomAnnotationTypes:
99+
""" UIA 'custom annotation types' specific to Excel.
100+
Once registered, all subsequent registrations will return the same ID value.
101+
This class should be used as a singleton via ExcelCustomAnnotationTypes.get()
102+
to prevent unnecessary work by repeatedly interacting with UIA.
103+
"""
104+
#: Singleton instance
105+
_instance: "Optional[ExcelCustomAnnotationTypes]" = None
106+
107+
@classmethod
108+
def get(cls) -> "ExcelCustomAnnotationTypes":
109+
"""Get the singleton instance or initialise it.
110+
"""
111+
if cls._instance is None:
112+
cls._instance = cls()
113+
return cls._instance
114+
115+
def __init__(self):
116+
# Available custom Annotations list at https://docs.microsoft.com/en-us/office/uia/excel/excelannotations
117+
# Note annotation:
118+
# Represents an old-style comment (now known as a note)
119+
# which contains non-threaded plain text content.
120+
self.note = CustomAnnotationTypeInfo(
121+
guid=GUID("{4E863D9A-F502-4A67-808F-9E711702D05E}"),
122+
)
123+
124+
93125
class ExcelObject(UIA):
94126
"""Common base class for all Excel UIA objects
95127
"""
96128
_UIAExcelCustomProps = ExcelCustomProperties.get()
129+
_UIAExcelCustomAnnotationTypes = ExcelCustomAnnotationTypes.get()
130+
97131

98132

99133
class ExcelCell(ExcelObject):
@@ -367,6 +401,15 @@ def _get_states(self):
367401
states.add(controlTypes.State.HASFORMULA)
368402
if self._getUIACacheablePropertyValue(self._UIAExcelCustomProps.hasDataValidationDropdown.id):
369403
states.add(controlTypes.State.HASPOPUP)
404+
if winVersion.getWinVer() >= winVersion.WIN11:
405+
try:
406+
annotationTypes = self._getUIACacheablePropertyValue(UIAHandler.UIA_AnnotationTypesPropertyId)
407+
except COMError:
408+
# annotationTypes cannot be fetched on older Operating Systems such as Windows 7.
409+
annotationTypes = None
410+
if annotationTypes:
411+
if self._UIAExcelCustomAnnotationTypes.note.id in annotationTypes:
412+
states.add(controlTypes.State.HASNOTE)
370413
return states
371414

372415
def _get_cellCoordsText(self):
@@ -421,28 +464,39 @@ def _get_cellCoordsText(self):
421464
description=_("Reports the note or comment thread on the current cell"),
422465
gesture="kb:NVDA+alt+c")
423466
def script_reportComment(self, gesture):
467+
if winVersion.getWinVer() >= winVersion.WIN11:
468+
noteElement = self.UIAAnnotationObjects.get(self._UIAExcelCustomAnnotationTypes.note.id)
469+
if noteElement:
470+
name = noteElement.CurrentName
471+
desc = noteElement.GetCurrentPropertyValue(UIAHandler.UIA_FullDescriptionPropertyId)
472+
# Translators: a note on a cell in Microsoft excel.
473+
text = _("{name}: {desc}").format(name=name, desc=desc)
474+
ui.message(text)
475+
else:
476+
# Translators: message when a cell in Excel contains no note
477+
ui.message(_("No note on this cell"))
424478
commentsElement = self.UIAAnnotationObjects.get(UIAHandler.AnnotationType_Comment)
425479
if commentsElement:
426480
comment = commentsElement.GetCurrentPropertyValue(UIAHandler.UIA_FullDescriptionPropertyId)
427481
author = commentsElement.GetCurrentPropertyValue(UIAHandler.UIA_AnnotationAuthorPropertyId)
428482
numReplies = commentsElement.GetCurrentPropertyValue(self._UIAExcelCustomProps.commentReplyCount.id)
429483
if numReplies == 0:
430484
# Translators: a comment on a cell in Microsoft excel.
431-
text = _("{comment} by {author}").format(
485+
text = _("Comment thread: {comment} by {author}").format(
432486
comment=comment,
433487
author=author
434488
)
435489
else:
436490
# Translators: a comment on a cell in Microsoft excel.
437-
text = _("{comment} by {author} with {numReplies} replies").format(
491+
text = _("Comment thread: {comment} by {author} with {numReplies} replies").format(
438492
comment=comment,
439493
author=author,
440494
numReplies=numReplies
441495
)
442496
ui.message(text)
443497
else:
444-
# Translators: A message in Excel when there is no note
445-
ui.message(_("No note or comment thread on this cell"))
498+
# Translators: A message in Excel when there is no comment thread
499+
ui.message(_("No comment thread on this cell"))
446500

447501

448502
class ExcelWorksheet(ExcelObject):

source/_UIACustomAnnotations.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# A part of NonVisual Desktop Access (NVDA)
2+
# Copyright (C) 2021 NV Access Limited
3+
# This file is covered by the GNU General Public License.
4+
# See the file COPYING for more details.
5+
6+
from dataclasses import (
7+
dataclass,
8+
field,
9+
)
10+
from typing import Optional
11+
12+
from comtypes import (
13+
GUID,
14+
byref,
15+
)
16+
import winVersion
17+
18+
19+
"""
20+
This module provides helpers and a common format to define UIA custom annotation types.
21+
The common custom annotation types are defined here.
22+
Custom annotation types specific to an application should be defined within a NVDAObjects/UIA
23+
submodule specific to that application, E.G. 'NVDAObjects/UIA/excel.py'
24+
25+
UIA originally had hard coded 'static' ID's for annotation types.
26+
For an example see 'AnnotationType_SpellingError' in
27+
`source/comInterfaces/_944DE083_8FB8_45CF_BCB7_C477ACB2F897_0_1_0.py`
28+
imported via `UIAutomationClient.py`.
29+
When a new annotation type was added the UIA spec had to be updated.
30+
Now a mechanism is in place to allow applications to register "custom annotation types".
31+
This relies on both the UIA server application and the UIA client application sharing a known
32+
GUID for the annotation type.
33+
"""
34+
35+
36+
@dataclass
37+
class CustomAnnotationTypeInfo:
38+
"""Holds information about a CustomAnnotationType
39+
This makes it easy to define custom annotation types to be loaded.
40+
"""
41+
guid: GUID
42+
id: int = field(init=False)
43+
44+
def __post_init__(self) -> None:
45+
""" The id field must be initialised at runtime.
46+
A GUID uniquely identifies a custom annotation, but the UIA system relies on integer IDs.
47+
Any application (clients or providers) can register a custom annotation type, subsequent applications
48+
will get the same id for a given GUID.
49+
Registering custom annotations is only supported on Windows 11 and above.
50+
For any lesser version, id will be 0.
51+
52+
"""
53+
if winVersion.getWinVer() >= winVersion.WIN11:
54+
import NVDAHelper
55+
self.id = NVDAHelper.localLib.registerUIAAnnotationType(
56+
byref(self.guid),
57+
)
58+
else:
59+
self.id = 0
60+
61+
62+
class CustomAnnotationTypesCommon:
63+
"""UIA 'custom annotation types' common to all applications.
64+
Once registered, all subsequent registrations will return the same ID value.
65+
This class should be used as a singleton via CustomAnnotationTypesCommon.get()
66+
to prevent unnecessary work by repeatedly interacting with UIA.
67+
"""
68+
#: Singleton instance
69+
_instance: "Optional[CustomAnnotationTypesCommon]" = None
70+
71+
@classmethod
72+
def get(cls) -> "CustomAnnotationTypesCommon":
73+
"""Get the singleton instance or initialise it.
74+
"""
75+
if cls._instance is None:
76+
cls._instance = cls()
77+
return cls._instance
78+
79+
def __init__(self):
80+
# Registration of Custom annotation types used across multiple applications or frameworks should go here.
81+
# Currently there are none known.
82+
# An example registration would look like the following:
83+
# self.note = CustomAnnotationTypeInfo(
84+
# guid=GUID("{4E863D9A-F502-4A67-808F-9E711702D05E}"),
85+
# )
86+
pass

source/controlTypes/state.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ def negativeDisplayString(self) -> str:
7171
OVERFLOWING = 0x10000000000
7272
UNLOCKED = 0x20000000000
7373
HAS_ARIA_DETAILS = 0x40000000000
74+
HASNOTE = 0x80000000000
7475

7576

7677
STATES_SORTED = frozenset([State.SORTED, State.SORTED_ASCENDING, State.SORTED_DESCENDING])
@@ -160,6 +161,8 @@ def negativeDisplayString(self) -> str:
160161
# Translators: a state that denotes that the object is unlocked (such as an unlocked cell in a protected
161162
# Excel spreadsheet).
162163
State.UNLOCKED: _("unlocked"),
164+
# Translators: a state that denotes the existance of a note.
165+
State.HASNOTE: _("has note"),
163166
}
164167

165168

0 commit comments

Comments
 (0)