Skip to content

Commit 1880042

Browse files
authored
Merge 4e37197 into 8b1b943
2 parents 8b1b943 + 4e37197 commit 1880042

2 files changed

Lines changed: 72 additions & 42 deletions

File tree

source/inputCore.py

Lines changed: 60 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
# A part of NonVisual Desktop Access (NVDA)
22
# This file is covered by the GNU General Public License.
33
# See the file COPYING for more details.
4-
# Copyright (C) 2010-2023 NV Access Limited, Babbage B.V., Mozilla Corporation, Cyrille Bougot
4+
# Copyright (C) 2010-2023 NV Access Limited, Babbage B.V., Mozilla Corporation, Cyrille Bougot,
5+
# Leonard de Ruijter
56

67
"""Core framework for handling input from the user.
78
Every piece of input from the user (e.g. a key press) is represented by an L{InputGesture}.
@@ -219,32 +220,43 @@ def executeScript(self, script):
219220
"""
220221
return scriptHandler.executeScript(script, self)
221222

222-
class GlobalGestureMap(object):
223+
224+
FlattenedGestureMapT = Dict[
225+
str, # moduleName.className
226+
Dict[
227+
Optional[ScriptNameT], # Script name
228+
Optional[Union[str, List[str]]], # Normalized gestures
229+
],
230+
]
231+
_InternalGestureMapT = Dict[
232+
str, # Normalized gesture
233+
List[
234+
Tuple[
235+
str, # module
236+
str, # class name
237+
Optional[ScriptNameT], # script
238+
],
239+
],
240+
]
241+
242+
243+
class GlobalGestureMap:
223244
"""Maps gestures to scripts anywhere in NVDA.
224-
This is used to allow users and locales to bind gestures in addition to those bound by individual scriptable objects.
245+
This is used to allow users and locales to bind gestures in addition to those bound by
246+
individual scriptable objects.
225247
Map entries will most often be loaded from a file using the L{load} method.
226248
See that method for details of the file format.
227249
"""
228250

229-
def __init__(self, entries=None):
251+
def __init__(self, entries: Optional[FlattenedGestureMapT] = None):
230252
"""Constructor.
231253
@param entries: Initial entries to add; see L{update} for the format.
232-
@type entries: mapping of str to mapping
233254
"""
234-
self._map: Dict[
235-
str, # Normalized gesture
236-
List[
237-
Tuple[
238-
str, # module
239-
str, # class name
240-
str, # script
241-
]]] = {}
255+
self._map: _InternalGestureMapT = {}
242256
#: Indicates that the last load or update contained an error.
243-
#: @type: bool
244-
self.lastUpdateContainedError = False
257+
self.lastUpdateContainedError: bool = False
245258
#: The file name for this gesture map, if any.
246-
#: @type: str
247-
self.fileName = None
259+
self.fileName: Optional[str] = None
248260
if entries:
249261
self.update(entries)
250262

@@ -259,7 +271,7 @@ def add(
259271
gesture: str,
260272
module: str,
261273
className: str,
262-
script: Optional[str],
274+
script: Optional[ScriptNameT],
263275
replace: bool = False
264276
):
265277
"""Add a gesture mapping.
@@ -268,7 +280,8 @@ def add(
268280
@param className: The name of the class in L{module} containing the target script.
269281
@param script: The name of the target script
270282
or C{None} to unbind the gesture for this class.
271-
@param replace: if true replaces all existing bindings for this gesture with the given script, otherwise only appends this binding.
283+
@param replace: if true replaces all existing bindings for this gesture with the given script,
284+
otherwise only appends this binding.
272285
"""
273286
gesture = normalizeGestureIdentifier(gesture)
274287
try:
@@ -279,7 +292,7 @@ def add(
279292
del scripts[:]
280293
scripts.append((module, className, script))
281294

282-
def load(self, filename):
295+
def load(self, filename: str):
283296
"""Load map entries from a file.
284297
The file is an ini file.
285298
Each section contains entries for a particular scriptable object class.
@@ -292,34 +305,33 @@ def load(self, filename):
292305
nextHeading = kb:a
293306
None = kb:h
294307
@param filename: The name of the file to load.
295-
@type: str
296308
"""
297309
self.fileName = filename
298310
try:
299311
conf = configobj.ConfigObj(filename, file_error=True, encoding="UTF-8")
300-
except (configobj.ConfigObjError,UnicodeDecodeError) as e:
312+
except (configobj.ConfigObjError, UnicodeDecodeError) as e:
301313
log.warning("Error in gesture map '%s': %s"%(filename, e))
302314
self.lastUpdateContainedError = True
303315
return
304316
self.update(conf)
305317

306-
def update(self, entries):
318+
def update(self, entries: FlattenedGestureMapT):
307319
"""Add multiple map entries.
308320
C{entries} must be a mapping of mappings.
309321
Each inner mapping contains entries for a particular scriptable object class.
310322
The key in the outer mapping must be the full Python module and class name.
311-
The key of each entry in the inner mappings is the script name and the value is a list of one or more gestures.
323+
The key of each entry in the inner mappings is the script name
324+
and the value is one gesture string or a list of one or more gesture strings.
312325
If the script name is C{None}, the gesture will be unbound for this class.
313326
For example, the following binds the "a" key to move to the next heading in virtual buffers
314-
and removes the default "h" binding::
327+
and removes the default "h" binding:
315328
{
316329
"virtualBuffers.VirtualBuffer": {
317330
"nextHeading": "kb:a",
318331
None: "kb:h",
319332
}
320333
}
321334
@param entries: The items to add.
322-
@type entries: mapping of str to mapping
323335
"""
324336
self.lastUpdateContainedError = False
325337
for locationName, location in entries.items():
@@ -332,7 +344,7 @@ def update(self, entries):
332344
for script, gestures in location.items():
333345
if script == "None":
334346
script = None
335-
if gestures == "":
347+
if gestures in ("", None):
336348
gestures = ()
337349
elif isinstance(gestures, str):
338350
gestures = [gestures]
@@ -375,16 +387,12 @@ def getScriptsForAllGestures(self):
375387
for cls, scriptName in self.getScriptsForGesture(gesture):
376388
yield cls, gesture, scriptName
377389

378-
def remove(self, gesture, module, className, script):
390+
def remove(self, gesture: str, module: str, className: str, script: ScriptNameT):
379391
"""Remove a gesture mapping.
380392
@param gesture: The gesture identifier.
381-
@type gesture: str
382393
@param module: The name of the Python module containing the target script.
383-
@type module: str
384394
@param className: The name of the class in L{module} containing the target script.
385-
@type className: str
386395
@param script: The name of the target script.
387-
@type script: str
388396
@raise ValueError: If the requested mapping does not exist.
389397
"""
390398
gesture = normalizeGestureIdentifier(gesture)
@@ -394,19 +402,13 @@ def remove(self, gesture, module, className, script):
394402
raise ValueError("Mapping not found")
395403
scripts.remove((module, className, script))
396404

397-
@blockAction.when(blockAction.Context.SECURE_MODE)
398-
def save(self):
399-
"""Save this gesture map to disk.
400-
@precondition: L{load} must have been called.
405+
def export(self) -> FlattenedGestureMapT:
406+
"""Exports this gesture map to a dictionary that can be saved to disk or imported into another gesture map.
401407
"""
402-
if not self.fileName:
403-
raise ValueError("No file name")
404-
out = configobj.ConfigObj(encoding="UTF-8")
405-
out.filename = self.fileName
406-
408+
out: FlattenedGestureMapT = {}
407409
for gesture, scripts in self._map.items():
408410
for module, className, script in scripts:
409-
key = "%s.%s" % (module, className)
411+
key = f"{module}.{className}"
410412
try:
411413
outSect = out[key]
412414
except KeyError:
@@ -424,10 +426,26 @@ def save(self):
424426
outVal.append(gesture)
425427
else:
426428
outSect[script] = [outVal, gesture]
429+
return out
430+
431+
@blockAction.when(blockAction.Context.SECURE_MODE)
432+
def save(self):
433+
"""Save this gesture map to disk.
434+
@precondition: L{load} must have been called.
435+
"""
436+
if not self.fileName:
437+
raise ValueError("No file name")
438+
out = configobj.ConfigObj(self.export(), encoding="UTF-8")
439+
out.filename = self.fileName
427440

428441
with FaultTolerantFile(out.filename) as f:
429442
out.write(f)
430443

444+
def __eq__(self, other):
445+
if isinstance(other, GlobalGestureMap):
446+
return self._map == other._map
447+
return NotImplemented
448+
431449

432450
decide_executeGesture = extensionPoints.Decider()
433451
"""

tests/unit/test_inputCore.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,15 @@ def test_decide_executeGesture(self):
3030
gesture=gesture
3131
):
3232
inputCore.manager.executeGesture(gesture)
33+
34+
35+
class TestGlobalGestureMap(unittest.TestCase):
36+
"""Test to ensure that importing an exported global gesture map results in the same map.
37+
The gesture map for the Hims braille display driver is used for reference."""
38+
39+
def test_exportAndUpdateAreEqual(self):
40+
from brailleDisplayDrivers.hims import BrailleDisplayDriver as HimsDriver
41+
exported = HimsDriver.gestureMap.export()
42+
newMap = inputCore.GlobalGestureMap(exported)
43+
self.assertEqual(HimsDriver.gestureMap, newMap)
44+
self.assertDictEqual(HimsDriver.gestureMap._map, newMap._map)

0 commit comments

Comments
 (0)