Repository: nvaccess/nvda

Issue:
ALVA: SP2 isn't allocated with the emulated ALT key
NVDA-Version: 2018.2 RC2, Portable, Win7x64

The emulated ALT key isn't allocated with SP2 on a ALVA BC680. This bug should be easy to fix. (I know that all the other bugs (PR #8230, #8240 and #8245) aren't fixed in 2018.2 RC2. I guess that this will be done in 2018.3.)

Quote from PR #8230:
> File: brailleDisplayDrivers/alva.py
> • Line 417: "kb:alt": ("br(alva):sp2",),
> • Line 435: "kb:alt": ("br(alva):alt",),
> This maybe could fail. Please change it to the following:
> • Line 417: "kb:alt": ("br(alva):sp2","br(alva):alt",),
> • Delete line 435.
Combining emulated keys via a braille display ignores "gestures.ini"
NVDA Version: nvda_snapshot_try-i8106-14994,fcd1ac68
Run as portable version. (It doesn't contain my huge "gestures.ini" file from my installed NVDA version. Well, it doesn't contain anything from the installed version at all.)
Braille display: Optelec ALVA BC680 (firmware: 3.0.2, connected via USB2 to the PC)

Default key mapping ("gestures.ini" doesn't exist):
Current Result: SP2 = ALT; SPLeft = Left Arrow; SP2+SPLeft = ALT+Left Arrow

Then open the input gestures window and delete the SP2 entry allocated with the ALT key (emulated hotkeys).

The "gestures.ini" now exists and contains the following line: "None = br(alva):sp2" (line 2).

Current Result: SP2 = [none]; SPLeft = Left Arrow; SP2+SPLeft = ALT+Left Arrow
Expected Result: SP2 = [none]; SPLeft = Left Arrow; SP2+SPLeft = [none]

then open the input gestures window again an allocate BC680-SP2 with the command to say the current time and date.

The following line was added in the "gestures.ini" below line 2: "dateTime = br(alva.bc680):sp2"

Current Result: SP2 = dateTime; SPLeft = Left Arrow; SP2+SPLeft = ALT+Left Arrow
Expected Result: SP2 = dateTime; SPLeft = Left Arrow; SP2+SPLeft = [none]

Then delete the "dateTime" via the input gesture window and allocate this command with SP2. The 2nd line with the "None" entry in the "gestures.ini" was deleted now.

The 2nd line contains the following: "dateTime = br(alva):sp2". The 3rd line was of course deleted too.

Current Result: SP2 = dateTime; SPLeft = Left Arrow; SP2+SPLeft = ALT+Left Arrow
Expected Result: SP2 = dateTime; SPLeft = Left Arrow; SP2+SPLeft = [none]

Then open the "gestures.ini" directly in Notepad++ (or any other source code editor) and add the following line followed by an empty line: "None = br(alva):sp2+spleft". After saving this file restart NVDA.

Current Result: SP2 = dateTime; SPLeft = Left Arrow; SP2+SPLeft = ALT+Left Arrow
Expected Result: SP2 = dateTime; SPLeft = Left Arrow; SP2+SPLeft = [none]

And the last step: Open the input gestures window and add SP2+SPLeft to the "dateTime" entry. The "None" line in the "gestures.ini" was deleted after that. The 2nd line contains the following: "dateTime = br(alva):sp2+spleft, br(alva):sp2".

Current Result: SP2 = dateTime; SPLeft = Left Arrow; SP2+SPLeft = dateTime
Expected Result: SP2 = dateTime; SPLeft = Left Arrow; SP2+SPLeft = dateTime

This new bug in NVDA 2018.1 might be the accidently result of #7783. It can be solved if NVDA considers the "gestures.ini" as well as checks if all pressed keys on the braille display are allocated with an emulated key. If only one of those pressed keys on the braille display is not allocated with an emulated key NVDA shouldn't combine emulated keys.

I just now tested the whole thing with two keys on my ALVA BC680 via the USB2 port and within changing the allocations of only two commands to keep it simple. There are thousands of combinations which can be pressed alone with the ALVA BC680. And by the way I don't think that this is an ALVA specific issue. For me it looks more like a general issue in this new function.


Discussion:

Resolving patch:
diff --git a/source/braille.py b/source/braille.py
--- a/source/braille.py
+++ b/source/braille.py
@@ -2446,7 +2446,7 @@ def _get_script(self):
 			globalMaps = [inputCore.manager.userGestureMap, handler.display.gestureMap]
 			for globalMap in globalMaps:
 				for fakeGestureId in fakeGestureIds:
-					scriptNames.extend(scriptName for cls, scriptName in globalMap.getScriptsForGesture(fakeGestureId.lower()) if scriptName.startswith("kb"))
+					scriptNames.extend(scriptName for cls, scriptName in globalMap.getScriptsForGesture(fakeGestureId.lower()) if scriptName and scriptName.startswith("kb"))
 			if not scriptNames:
 				# Gesture contains modifiers, but no keyboard emulate script exists for the gesture without modifiers
 				return None
diff --git a/source/brailleDisplayDrivers/alva.py b/source/brailleDisplayDrivers/alva.py
--- a/source/brailleDisplayDrivers/alva.py
+++ b/source/brailleDisplayDrivers/alva.py
@@ -27,6 +27,7 @@
 ALVA_KEY_REPORT_KEY_GROUP_POS = 2
 ALVA_KEY_RAW_INPUT_MASK = 0x30
 ALVA_DISPLAY_SETTINGS_REPORT = b"\x05"
+ALVA_DISPLAY_SETTINGS_STATUS_CELL_SIDE_POS = 2
 ALVA_DISPLAY_SETTINGS_CELL_COUNT_POS = 6
 ALVA_KEY_SETTINGS_REPORT = b"\x06"
 ALVA_KEY_SETTINGS_POS = 1 # key settings are stored as bits in 1 byte
@@ -35,11 +36,14 @@
 ALVA_RTC_MAX_DRIFT = 5
 ALVA_RTC_MIN_YEAR = 1900
 ALVA_RTC_MAX_YEAR = 3000
+ALVA_MODEL_BC640 = 0x40
+ALVA_MODEL_BC680 = 0x80
+ALVA_MODEL_CONVERTER = 0x99
 
 ALVA_MODEL_IDS = {
-	0x40: "BC640",
-	0x80: "BC680",
-	0x99: "ProtocolConverter",
+	ALVA_MODEL_BC640: "BC640",
+	ALVA_MODEL_BC680: "BC680",
+	ALVA_MODEL_CONVERTER: "ProtocolConverter",
 }
 
 ALVA_SER_CMD_LENGTHS = {
@@ -66,20 +70,28 @@
 	b"K": 2, # Keys message
 }
 
+ALVA_THUMB_GROUP = 0x71
+ALVA_ETOUCH_GROUP = 0x72
+ALVA_SP_GROUP = 0x73
 ALVA_CR_GROUP = 0x74
 ALVA_FEATURE_PACK_GROUP = 0x78
 ALVA_SPECIAL_KEYS_GROUP = 0x01
 ALVA_SPECIAL_SETTINGS_CHANGED = 0x01
 
+DOUBLED_KEY_COUNTS = {
+	ALVA_THUMB_GROUP: 5,
+	ALVA_SP_GROUP: 9
+}
+
 ALVA_KEYS = {
 	# Thumb keys (FRONT_GROUP)
-	0x71: ("t1", "t2", "t3", "t4", "t5",
+	ALVA_THUMB_GROUP: ("t1", "t2", "t3", "t4", "t5",
 		# Only for BC680
 		"t1", "t2", "t3", "t4", "t5"),
 	# eTouch keys (ETOUCH_GROUP)
-	0x72: ("etouch1", "etouch2", "etouch3", "etouch4"),
+	ALVA_ETOUCH_GROUP: ("etouch1", "etouch2", "etouch3", "etouch4"),
 	# Smartpad keys (PDA_GROUP)
-	0x73: ("sp1", "sp2", "spLeft", "spEnter", "spUp", "spDown", "spRight", "sp3", "sp4",
+	ALVA_SP_GROUP: ("sp1", "sp2", "spLeft", "spEnter", "spUp", "spDown", "spRight", "sp3", "sp4",
 		# Only for BC680
 		"sp1", "sp2", "spLeft", "spEnter", "spUp", "spDown", "spRight", "sp3", "sp4"),
 	# Feature pack keys.
@@ -109,6 +121,10 @@ def _updateSettings(self):
 		oldNumCells = self.numCells
 		if self.isHid:
 			displaySettings = self._dev.getFeature(ALVA_DISPLAY_SETTINGS_REPORT)
+			if ord(displaySettings[ALVA_DISPLAY_SETTINGS_STATUS_CELL_SIDE_POS]) > 1:
+				# #8106: The ALVA BC680 is known to return a malformed feature report for the first issued request.
+				# Therefore, request another display settings report
+				displaySettings = self._dev.getFeature(ALVA_DISPLAY_SETTINGS_REPORT)
 			self.numCells = ord(displaySettings[ALVA_DISPLAY_SETTINGS_CELL_COUNT_POS])
 			timeStr = self._dev.getFeature(ALVA_RTC_REPORT)[1:ALVA_RTC_STR_LENGTH+1]
 			try:
@@ -364,7 +380,7 @@ def script_toggleHidKeyboardInput(self, gesture):
 			"title": ("br(alva):etouch2",),
 			"reportStatusLine": ("br(alva):etouch4",),
 			"kb:shift+tab": ("br(alva):sp1",),
-			"kb:alt": ("br(alva):sp2",),
+			"kb:alt": ("br(alva):sp2","br(alva):alt",),
 			"kb:escape": ("br(alva):sp3",),
 			"kb:tab": ("br(alva):sp4",),
 			"kb:upArrow": ("br(alva):spUp",),
@@ -372,17 +388,16 @@ def script_toggleHidKeyboardInput(self, gesture):
 			"kb:leftArrow": ("br(alva):spLeft",),
 			"kb:rightArrow": ("br(alva):spRight",),
 			"kb:enter": ("br(alva):spEnter","br(alva):enter",),
-			"dateTime": ("br(alva):sp1+sp2",),
+			"dateTime": ("br(alva):sp2+sp3",),
 			"showGui": ("br(alva):sp1+sp3",),
 			"kb:windows+d": ("br(alva):sp1+sp4",),
 			"kb:windows+b": ("br(alva):sp3+sp4",),
-			"kb:windows": ("br(alva):sp2+sp3","br(alva):windows",),
+			"kb:windows": ("br(alva):sp1+sp2","br(alva):windows",),
 			"kb:alt+tab": ("br(alva):sp2+sp4",),
 			"kb:control+home": ("br(alva):t3+spUp",),
 			"kb:control+end": ("br(alva):t3+spDown",),
 			"kb:home": ("br(alva):t3+spLeft",),
 			"kb:end": ("br(alva):t3+spRight",),
-			"kb:alt": ("br(alva):alt",),
 			"kb:control": ("br(alva):control",),
 		}
 	})
@@ -392,27 +407,39 @@ class InputGesture(braille.BrailleDisplayGesture, brailleInput.BrailleInputGestu
 
 	def __init__(self, model, keys, brailleInput=False):
 		super(InputGesture, self).__init__()
+		isNoBC640 = model != ALVA_MODEL_IDS[ALVA_MODEL_BC640]
 		# Model identifiers should not contain spaces.
 		self.model = model.replace(" ", "")
 		assert(self.model.isalnum())
 		self.keyCodes = set(keys)
 		self.keyNames = names = []
+		if isNoBC640:
+			secondaryNames = []
 		dots = 0
 		space = False
 		for group, number in self.keyCodes:
 			if group == ALVA_CR_GROUP:
 				if number & ALVA_2ND_CR_MASK:
-					names.append("secondRouting")
+					keyName = "secondRouting"
 					self.routingIndex = number & ~ALVA_2ND_CR_MASK
 				else:
-					names.append("routing")
+					keyName = "routing"
 					self.routingIndex = number
+				names.append(keyName)
+				if isNoBC640:
+					secondaryNames.append(keyName)
 			else:
 				try:
-					names.append(ALVA_KEYS[group][number])
+					keyName = ALVA_KEYS[group][number]
 				except (KeyError, IndexError):
 					log.debugWarning("Unknown key with group %d and number %d" % (group, number))
-
+					return
+				names.append(keyName)
+				if isNoBC640:
+					doubledKeyCount = DOUBLED_KEY_COUNTS.get(group)
+					if doubledKeyCount:
+						keyName = ("r" if number >= doubledKeyCount else "l") + keyName
+					secondaryNames.append(keyName)
 			# Braille input
 			if brailleInput:
 				if group == ALVA_FEATURE_PACK_GROUP:
@@ -426,6 +453,15 @@ def __init__(self, model, keys, brailleInput=False):
 					brailleInput = False
 
 		self.id = "+".join(names)
+		self.secondaryId = "+".join(secondaryNames) if isNoBC640 else self.id
 		if brailleInput:
 			self.dots = dots
 			self.space = space
+
+	def _get_identifiers(self):
+		ids = [
+			u"br({source}.{model}):{id}".format(source=self.source, model=self.model, id=self.secondaryId),
+			u"br({source}):{id}".format(source=self.source, id=self.id),
+		]
+		ids.extend(brailleInput.BrailleInputGesture._get_identifiers(self))
+		return ids


Test patch: