Repository: nvaccess/nvda

Issue:
Handle checkable list view controls as list views with columns
### Steps to reproduce:
1. Install the "Add-on Updater" add-on, since as far as I know, for the time being that is the only component for NVDA that has the new checkable list view controls with multiple columns.
2. Make sure that you have at least 2 add-ons that require updating.
3. Check for updates for your installed add-ons, by going to NVDA menu -> Tools -> Check for add-on updates.
4. When the dialog with the available add-on updates appears, focus the checkable list view control listing the available add-on updates.
5. Use NVDA's table navigation commands (Control+Alt+Arrows by default) to navigate between rows and columns in the checkable list view control.

### Actual behavior:
NVDA's table navigation commands appear not to work for this type of checkable list view control, as they do for standard list view controls with multiple columns. NVDA stays silent when the commands are executed.

### Expected behavior:
NVDA's table navigation commands should work for this type of checkable list view control, as they do for standard list view controls with multiple columns. NVDA should be able to read information column by column in this type of checkable list view control, as it does in standard list view controls.

### System configuration:
- Operating system: Windows 10 Pro version 1809 (build 17763.55), 64-bit, in Bulgarian with all locale settings set to "Bulgarian".
- NVDA version: alpha-16164,8a7da1d4.
- "Add-on Updater" add-on version: 20181018.
- Processor: Intel Core i5-4670 at 3.40GHz.
- RAM Memory: 8.00GB.
- Graphics: MSI GeForce GTX-1050TI Gaming X 4G, 4096MB dedicated memory + 4065MB shared system memory, desktop resolution set to 1920x1080.
- Sound Card: Realtek ALC887 at Intel Lynx Point PCH - High Definition Audio Controller.

### Other questions:
#### Does the issue still occur after restarting your PC?
Yes, it does.

#### Have you tried any other versions of NVDA?
No.


Discussion:
This is probably a one liner to accomplish and could best be combined with #8554. However, it will change the look and feel for these lists a bit. I.e. instead of
- Downloaded Program Files check box checked 0 bytes 1 of 10
It would be
- Downloaded Program Files; 0 bytes check box checked 1 of 10

This could get more complex for lists with more columns. I'd be an advocate of this change, though.

Resolving patch:
diff --git a/source/NVDAObjects/IAccessible/__init__.py b/source/NVDAObjects/IAccessible/__init__.py
--- a/source/NVDAObjects/IAccessible/__init__.py
+++ b/source/NVDAObjects/IAccessible/__init__.py
@@ -1996,6 +1996,7 @@ def _get_value(self):
 	("SysListView32",oleacc.ROLE_SYSTEM_LISTITEM):"sysListView32.ListItem",
 	("DirectUIHWND", oleacc.ROLE_SYSTEM_LISTITEM): "UIItem",
 	("SysListView32",oleacc.ROLE_SYSTEM_MENUITEM):"sysListView32.ListItemWithoutColumnSupport",
+	("SysListView32", oleacc.ROLE_SYSTEM_CHECKBUTTON): "sysListView32.ListItem",
 	("SysTreeView32",oleacc.ROLE_SYSTEM_OUTLINE):"sysTreeView32.TreeView",
 	("SysTreeView32",oleacc.ROLE_SYSTEM_OUTLINEITEM):"sysTreeView32.TreeViewItem",
 	("SysTreeView32",oleacc.ROLE_SYSTEM_MENUITEM):"sysTreeView32.TreeViewItem",
diff --git a/source/NVDAObjects/behaviors.py b/source/NVDAObjects/behaviors.py
--- a/source/NVDAObjects/behaviors.py
+++ b/source/NVDAObjects/behaviors.py
@@ -733,8 +733,10 @@ def _get_states(self):
 		states = self.parent.states.copy()
 		if self.location and self.location.width == 0:
 			states.add(controlTypes.STATE_INVISIBLE)
+		states.discard(controlTypes.STATE_CHECKED)
 		return states
 
+
 class FocusableUnfocusableContainer(NVDAObject):
 	"""Makes an unfocusable container focusable using its first focusable descendant.
 	One instance where this is useful is ARIA applications on the web where the author hasn't set a tabIndex.
diff --git a/source/controlTypes.py b/source/controlTypes.py
--- a/source/controlTypes.py
+++ b/source/controlTypes.py
@@ -723,7 +723,13 @@ def processPositiveStates(role, states, reason: OutputReason, positiveStates=Non
 	positiveStates.discard(STATE_INVISIBLE)
 	if reason != REASON_CHANGE:
 		positiveStates.discard(STATE_LINKED)
-		if role in (ROLE_LISTITEM, ROLE_TREEVIEWITEM, ROLE_MENUITEM, ROLE_TABLEROW) and STATE_SELECTABLE in states:
+		if role in (
+			ROLE_LISTITEM,
+			ROLE_TREEVIEWITEM,
+			ROLE_MENUITEM,
+			ROLE_TABLEROW,
+			ROLE_CHECKBOX,
+		) and STATE_SELECTABLE in states:
 			positiveStates.discard(STATE_SELECTED)
 	if role not in (ROLE_EDITABLETEXT, ROLE_CHECKBOX):
 		positiveStates.discard(STATE_READONLY)
@@ -772,7 +778,8 @@ def processNegativeStates(role, states, reason: OutputReason, negativeStates=Non
 			ROLE_TABLEROW,
 			ROLE_TABLECELL,
 			ROLE_TABLECOLUMNHEADER,
-			ROLE_TABLEROWHEADER
+			ROLE_TABLEROWHEADER,
+			ROLE_CHECKBOX,
 		)
 	):
 		speakNegatives.add(STATE_SELECTED)


Test patch:
diff --git a/tests/unit/test_controlTypes.py b/tests/unit/test_controlTypes.py
--- a/tests/unit/test_controlTypes.py
+++ b/tests/unit/test_controlTypes.py
@@ -30,24 +30,74 @@ class TestProcessStates(unittest.TestCase):
 	def setUp(self):
 		self.obj = PlaceholderNVDAObject()
 		self.obj.role = controlTypes.ROLE_CHECKBOX
-		self.obj.states = set((controlTypes.STATE_FOCUSABLE, controlTypes.STATE_INVALID_ENTRY, controlTypes.STATE_FOCUSED, controlTypes.STATE_REQUIRED))
+		self.obj.states = {
+			controlTypes.STATE_FOCUSABLE,
+			controlTypes.STATE_INVALID_ENTRY,
+			controlTypes.STATE_FOCUSED,
+			controlTypes.STATE_REQUIRED
+		}
 
 	def test_positiveStates(self):
-		self.assertSetEqual(controlTypes.processPositiveStates(self.obj.role, self.obj.states, controlTypes.REASON_FOCUS, self.obj.states), set([controlTypes.STATE_INVALID_ENTRY, controlTypes.STATE_REQUIRED]))
+		self.assertSetEqual(
+			controlTypes.processPositiveStates(
+				self.obj.role,
+				self.obj.states,
+				controlTypes.REASON_FOCUS,
+				self.obj.states
+			),
+			{controlTypes.STATE_INVALID_ENTRY, controlTypes.STATE_REQUIRED}
+		)
 
 	def test_negativeStates(self):
-		self.assertSetEqual(controlTypes.processNegativeStates(self.obj.role, self.obj.states, controlTypes.REASON_FOCUS, None), set([controlTypes.STATE_CHECKED]))
+		self.assertSetEqual(
+			controlTypes.processNegativeStates(
+				self.obj.role,
+				self.obj.states,
+				controlTypes.REASON_FOCUS,
+				None
+			),
+			{controlTypes.STATE_CHECKED}
+		)
 
 class TestStateOrder(unittest.TestCase):
 
 	def test_positiveMergedStatesOutput(self):
 		obj = PlaceholderNVDAObject()
 		obj.role = controlTypes.ROLE_CHECKBOX
-		obj.states = set((controlTypes.STATE_CHECKED, controlTypes.STATE_FOCUSABLE, controlTypes.STATE_FOCUSED, controlTypes.STATE_SELECTED, controlTypes.STATE_SELECTABLE))
-		self.assertEqual(controlTypes.processAndLabelStates(obj.role, obj.states, controlTypes.REASON_FOCUS, obj.states, None), [controlTypes.stateLabels[controlTypes.STATE_SELECTED],controlTypes.stateLabels[controlTypes.STATE_CHECKED]])
+		obj.states = {
+			controlTypes.STATE_CHECKED,
+			controlTypes.STATE_FOCUSABLE,
+			controlTypes.STATE_FOCUSED,
+			controlTypes.STATE_SELECTED,
+			controlTypes.STATE_SELECTABLE
+		}
+		self.assertEqual(
+			controlTypes.processAndLabelStates(
+				obj.role,
+				obj.states,
+				controlTypes.REASON_FOCUS,
+				obj.states,
+				None
+			),
+			[controlTypes.stateLabels[controlTypes.STATE_CHECKED]]
+		)
 
 	def test_negativeMergedStatesOutput(self):
 		obj = PlaceholderNVDAObject()
 		obj.role = controlTypes.ROLE_CHECKBOX
-		obj.states = set((controlTypes.STATE_FOCUSABLE, controlTypes.STATE_FOCUSED, controlTypes.STATE_SELECTED, controlTypes.STATE_SELECTABLE))
-		self.assertEqual(controlTypes.processAndLabelStates(obj.role, obj.states, controlTypes.REASON_FOCUS, obj.states, None), [controlTypes.stateLabels[controlTypes.STATE_SELECTED],controlTypes.negativeStateLabels[controlTypes.STATE_CHECKED]])
+		obj.states = {
+			controlTypes.STATE_FOCUSABLE,
+			controlTypes.STATE_FOCUSED,
+			controlTypes.STATE_SELECTED,
+			controlTypes.STATE_SELECTABLE
+		}
+		self.assertEqual(
+			controlTypes.processAndLabelStates(
+				obj.role,
+				obj.states,
+				controlTypes.REASON_FOCUS,
+				obj.states,
+				None
+			),
+			[controlTypes.negativeStateLabels[controlTypes.STATE_CHECKED]]
+		)