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.
78Every 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
432450decide_executeGesture = extensionPoints .Decider ()
433451"""
0 commit comments