Skip to content

Commit 7c13aec

Browse files
LeonarddeROzancanKaratas
authored andcommitted
Report multi select on list boxes that support it (nvaccess#18409)
Link to issue number: Closes nvaccess#18365 Summary of the issue: NVDA does not report whether multiple selection is supported on a list control. Description of user facing changes: NVDA now reports multi select for a list that supports multiple selection. Description of developer facing changes: None Description of development approach: Add a new MULTISELECTABLE state, analogous to oleacc Map that state to the oleacc state for MSAA Map the UIA Selection Can Select Multiple property to the new state.
1 parent 9e8b6c2 commit 7c13aec

File tree

11 files changed

+49
-3
lines changed

11 files changed

+49
-3
lines changed

source/IAccessibleHandler/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@
228228
oleacc.STATE_SYSTEM_PROTECTED: controlTypes.State.PROTECTED,
229229
oleacc.STATE_SYSTEM_SELECTABLE: controlTypes.State.SELECTABLE,
230230
oleacc.STATE_SYSTEM_FOCUSABLE: controlTypes.State.FOCUSABLE,
231+
oleacc.STATE_SYSTEM_MULTISELECTABLE: controlTypes.State.MULTISELECTABLE,
231232
}
232233

233234
IAccessible2StatesToNVDAStates = {

source/NVDAObjects/IAccessible/__init__.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1080,15 +1080,15 @@ def _get_IAccessibleStates(self) -> int:
10801080
return 0
10811081
return res if isinstance(res, int) else 0
10821082

1083-
states: typing.Set[controlTypes.State]
1083+
states: set[controlTypes.State]
10841084
"""Type info for auto property: _get_states
10851085
"""
10861086

10871087
# C901 '_get_states' is too complex. Look for opportunities to break this method down.
1088-
def _get_states(self) -> typing.Set[controlTypes.State]: # noqa: C901
1088+
def _get_states(self) -> set[controlTypes.State]: # noqa: C901
10891089
states = set()
10901090
if self.event_objectID in (winUser.OBJID_CLIENT, winUser.OBJID_WINDOW) and self.event_childID == 0:
1091-
states.update(super(IAccessible, self).states)
1091+
states.update(super().states)
10921092
try:
10931093
IAccessibleStates = self.IAccessibleStates
10941094
except COMError:
@@ -2434,6 +2434,12 @@ class List(IAccessible):
24342434
def _get_role(self):
24352435
return controlTypes.Role.LIST
24362436

2437+
def _get_states(self) -> set[controlTypes.State]:
2438+
states = super().states
2439+
if self.windowStyle & winUser.LBS_EXTENDEDSEL:
2440+
states.add(controlTypes.State.MULTISELECTABLE)
2441+
return states
2442+
24372443

24382444
class SysLinkClient(IAccessible):
24392445
def reportFocus(self):

source/NVDAObjects/UIA/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1891,6 +1891,7 @@ def _get_keyboardShortcut(self):
18911891

18921892
_UIAStatesPropertyIDs = {
18931893
UIAHandler.UIA_HasKeyboardFocusPropertyId,
1894+
UIAHandler.UIA.UIA_SelectionCanSelectMultiplePropertyId,
18941895
UIAHandler.UIA_SelectionItemIsSelectedPropertyId,
18951896
UIAHandler.UIA_IsDataValidForFormPropertyId,
18961897
UIAHandler.UIA_IsRequiredForFormPropertyId,
@@ -1934,6 +1935,8 @@ def _get_states(self):
19341935
if role == controlTypes.Role.RADIOBUTTON
19351936
else controlTypes.State.SELECTED,
19361937
)
1938+
if self._getUIACacheablePropertyValue(UIAHandler.UIA.UIA_SelectionCanSelectMultiplePropertyId):
1939+
states.add(controlTypes.State.MULTISELECTABLE)
19371940
if not self._getUIACacheablePropertyValue(UIAHandler.UIA_IsEnabledPropertyId, True):
19381941
states.add(controlTypes.State.UNAVAILABLE)
19391942
try:

source/braille.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,8 @@
279279
controlTypes.State.ON: "⣏⣿⣹",
280280
# Translators: Displayed in braille when a link destination points to the same page
281281
controlTypes.State.INTERNAL_LINK: _("smp"),
282+
# Translators: Displayed in braille when an object supports multiple selected items.
283+
controlTypes.State.MULTISELECTABLE: _("msel"),
282284
}
283285
negativeStateLabels = {
284286
# Translators: Displayed in braille when an object is not selected.
@@ -706,6 +708,11 @@ def getPropertiesBraille(**propertyValues) -> str: # noqa: C901
706708
states.discard(controlTypes.State.VISITED)
707709
# Translators: Displayed in braille for a link which has been visited.
708710
roleText = _("vlnk")
711+
elif role == controlTypes.Role.LIST and states and controlTypes.State.MULTISELECTABLE in states:
712+
states = states.copy()
713+
states.discard(controlTypes.State.MULTISELECTABLE)
714+
# Translators: Displayed in braille for a multi select list.
715+
roleText = _("mslst")
709716
elif (
710717
name or cellCoordsText or rowNumber or columnNumber
711718
) and role in controlTypes.silentRolesOnFocus:

source/config/configSpec.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@
116116
reportKeyboardShortcuts = boolean(default=true)
117117
reportObjectPositionInformation = boolean(default=true)
118118
guessObjectPositionInformationWhenUnavailable = boolean(default=false)
119+
reportMultiSelect = boolean(default=false)
119120
reportTooltips = boolean(default=false)
120121
reportHelpBalloons = boolean(default=true)
121122
reportObjectDescriptions = boolean(default=True)

source/controlTypes/processAndLabelStates.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ def _processPositiveStates(
7373
and State.SELECTABLE in states
7474
):
7575
positiveStates.discard(State.SELECTED)
76+
positiveStates.discard(State.MULTISELECTABLE)
77+
elif not config.conf["presentation"]["reportMultiSelect"]:
78+
positiveStates.discard(State.MULTISELECTABLE)
7679
if role not in (Role.EDITABLETEXT, Role.CHECKBOX):
7780
positiveStates.discard(State.READONLY)
7881
if role == Role.CHECKBOX:

source/controlTypes/state.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ def negativeDisplayString(self) -> str:
103103
HASPOPUP_LIST = setBit(49)
104104
HASPOPUP_TREE = setBit(50)
105105
INTERNAL_LINK = setBit(51)
106+
MULTISELECTABLE = setBit(52)
106107

107108

108109
STATES_SORTED = frozenset([State.SORTED, State.SORTED_ASCENDING, State.SORTED_DESCENDING])
@@ -210,6 +211,9 @@ def negativeDisplayString(self) -> str:
210211
# Translators: Presented when a link destination points to the page containing the link.
211212
# For example, links of a table of contents of a document with different sections.
212213
State.INTERNAL_LINK: _("same page"),
214+
# Translators: Presented when the control allows multiple selected objects.
215+
# For example, a list box that allows selecting multiple items.
216+
State.MULTISELECTABLE: _("multi-select"),
213217
}
214218

215219

source/gui/settingsDialogs.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2478,6 +2478,15 @@ def makeSettings(self, settingsSizer):
24782478
config.conf["presentation"]["guessObjectPositionInformationWhenUnavailable"],
24792479
)
24802480

2481+
# Translators: This is the label for a checkbox in the
2482+
# object presentation settings panel.
2483+
reportMultiSelectText = _("Report when lists support &multiple selection")
2484+
self.reportMultiSelectCheckBox = sHelper.addItem(wx.CheckBox(self, label=reportMultiSelectText))
2485+
self.bindHelpEvent("ReportMultiSelect", self.reportMultiSelectCheckBox)
2486+
self.reportMultiSelectCheckBox.SetValue(
2487+
config.conf["presentation"]["reportMultiSelect"],
2488+
)
2489+
24812490
# Translators: This is the label for a checkbox in the
24822491
# object presentation settings panel.
24832492
descriptionText = _("Report object &descriptions")
@@ -2542,6 +2551,7 @@ def onSave(self):
25422551
config.conf["presentation"]["guessObjectPositionInformationWhenUnavailable"] = (
25432552
self.guessPositionInfoCheckBox.IsChecked()
25442553
)
2554+
config.conf["presentation"]["reportMultiSelect"] = self.reportMultiSelectCheckBox.IsChecked()
25452555
config.conf["presentation"]["reportObjectDescriptions"] = self.descriptionCheckBox.IsChecked()
25462556
config.conf["presentation"]["progressBarUpdates"]["progressBarOutputMode"] = self.progressLabels[
25472557
self.progressList.GetSelection()

source/winUser.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ class GUITHREADINFO(Structure):
154154
LBS_OWNERDRAWFIXED = 0x0010
155155
LBS_OWNERDRAWVARIABLE = 0x0020
156156
LBS_HASSTRINGS = 0x0040
157+
LBS_EXTENDEDSEL = 0x0800
157158
CBS_OWNERDRAWFIXED = 0x0010
158159
CBS_OWNERDRAWVARIABLE = 0x0020
159160
CBS_HASSTRINGS = 0x00200

user_docs/en/changes.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ Windows 10 is the minimum Windows version supported.
1111

1212
### New Features
1313

14+
* Added the possibility to report when multiple items can be selected in a list control.
15+
This can be enabled using the "Report when lists support multiple selection" setting in NVDA's object presentation settings. (#18365 @LeonarddeR)
16+
1417
### Changes
1518

1619
* Added a button to the About dialog to copy the NVDA version number to the clipboard. (#18667)

0 commit comments

Comments
 (0)