Skip to content

Commit

Permalink
added an @script decorator to scriptHandler for setting script metada…
Browse files Browse the repository at this point in the history
…ta (#8236)

* added an @script decorator for setting script metadata to the scriptHandler module
added a metaclass for collecting gestures on ScriptableObjects, this now supports getting from .gesture and .gestures set on script_ functions by the @script decorator.
Fixes #6266

* Removed gesture attribute from script functions
Now passing gesture to the ```@script``` decorator will append the gesture to the list of gestures, rather than setting both gesture and gestures attributes on the function.

* Removed unused getScripts method from BaseObject

* Address review comment and rename GestureCollector to ScriptableType

* Make the script decorator gestures work both for normal and dynamic NVDAObjects. Provide a working example for Excel

* First baceObject tests

* Fix decorated scripts not properly being added when subclassing objects

* Finish unit tests

* Add bypasInputHelp and resumeSayAllMode to the script decorator

* Review actions

* Add unit tests for the script decorator itself

* Update developer guide

* Review actions

* Review actions regarding devGuide and comments

* Only use the per class __gestures dictionary, also for decorated scripts

* Last review action

* Revert NVDAObjects/__init__.py to master, as in the end, this hasn't been touched by this change

* Also revert changes to test suite objectProvider

* Split of lines in devGuide

* Rename the scripts in test_baseObject to follow the phonetic alphabet, to fix a bug in baseObject that used only the first character of a script name.

* Log warnings when using the script decorator in a wrong way, either by decorating a non method or a method which name doesn't start with script_
  • Loading branch information
LeonarddeR authored and michaelDCurran committed Jun 13, 2018
1 parent f996ee4 commit 9d35a2f
Show file tree
Hide file tree
Showing 6 changed files with 331 additions and 62 deletions.
107 changes: 75 additions & 32 deletions developerGuide.t2t
Expand Up @@ -257,33 +257,33 @@ From anywhere, you can now press NVDA+shift+v to have NVDA's version spoken and
# Developer guide example 2

import globalPluginHandler
from scriptHandler import script
import ui
import versionInfo

class GlobalPlugin(globalPluginHandler.GlobalPlugin):

@scriptHandler.script(gesture="kb:NVDA+shift+v")
def script_announceNVDAVersion(self, gesture):
ui.message(versionInfo.version)

__gestures={
"kb:NVDA+shift+v": "announceNVDAVersion",
}

--- end ---
```

This Global Plugin file starts with two comment lines, which describe what the file is for.

It then imports the globalPluginHandler module, so that the Global Plugin has access to the base GlobalPlugin class.

It also imports a few other modules, namely ui and versionInfo, which this specific plugin needs in order for it to perform the necessary actions to announce the version.
It also imports a few other modules, namely ui, versionInfo and scriptHandler, which this specific plugin needs in order for it to perform the necessary actions to announce the version.

Next, it defines a class called GlobalPlugin, which is inherited from globalPluginHandler.GlobalPlugin.

Inside this class, it defines 1 or more events, scripts or gesture bindings.
In this example, it defines a script method that performs the version announcement, and provides a binding from NVDA+shift+v to this script.
In this example, it defines a script method that performs the version announcement.
The script decorator from the scriptHandler module is used to assign the NVDA+shift+v shortcut to this script.
However, the details of the script and its binding are not important for the purposes of this example.
The most important part is the class itself.
More information about scripts and the script decorator can be found in the [Defining script properties #DefiningScriptProperties] section of this guide.

As with other examples in this guide, remember to delete the created Global Plugin when finished testing and then restart NVDA or reload plugins, so that original functionality is restored.

Expand Down Expand Up @@ -349,17 +349,13 @@ A script method takes two arguments:

As well as the actual script method, some form of gesture binding must be defined, so that NVDA knows what input should execute the script.

To bind a gesture to a script, a special "__gestures" Python dictionary can be defined as a class variable on the App Module, Global Plugin or NVDA Object.
These dictionaries should contain gesture identifier strings pointing to the name of the requested script, without the "script_" prefix.

There are more advanced ways of binding gestures in a more dynamic fashion, though the __gestures dictionary is the simplest.

A gesture identifier string is a simple string representation of a piece of input.
It consists of a two leter character code denoting the source of the input, an optional device in brackets, a colon (:) and one or more names separated by a plus (+) denoting the actual keys or input values.
It consists of a two letter character code denoting the source of the input, an optional device in brackets, a colon (:) and one or more names separated by a plus (+) denoting the actual keys or input values.

Some examples of gesture string identifiers are:
- "kb:NVDA+shift+v"
- "br(freedomScientific):leftWizWheelUp"
- "br(alva.BC640):t3"
- "kb(laptop):NVDA+t"
-

Expand All @@ -374,25 +370,70 @@ When NVDA receives input, it looks for a matching gesture binding in a particula
Once a gesture binding is found, the script is executed and no further bindings are used, nore is that particular gesture passed on automatically to the Operating System.

The order for gesture binding lookup is:
- The user specific gesture map
- The locale specific gesture map
- The braille display driver specific gesture map
- Loaded Global Plugins
- App Module of the active application
- Tree Interceptor of the NVDA Object with focus if any; e.g. a virtualBuffer
- NVDA Object with focus
- Global Commands (built in commands like quitting NVDA, object navigation commands, etc.)
-

You should specify a description of the script in the function's docstring which describes the command for users.
For example, this is reported to users when in Input Help mode and shown in the Input Gestures dialog.
You specify the docstring by setting a "__doc__" attribute on the script function.
The script will not appear in the Input Gestures dialog unless this is specified.
+++ Defining script properties +++[DefiningScriptProperties]
For NVDA 2018.3 and above, the recommended way to set script properties is by means of the so called script decorator.
In short, a decorator is a function that modifies the behavior of a particular function.
The script decorator modifies the script in such a way that it will be properly bound to the desired gestures.
Furthermore, it ensures that the script is listed with the description you specify, and that it is categorised under the desired category in the input gestures dialog.

You can also specify a category for a script so that it can be grouped with other similar scripts.
For example, a script in a global plugin which adds browse mode quick navigation keys may be categorized under the "Browse mode" category.
For individual scripts, this is done by setting a "category" attribute on the script function to a string containing the name of the category.
You can also set the "scriptCategory" attribute on the plugin class, which will be used for scripts which do not specify a category.
There are constants for common categories prefixed with SCRCAT_ in the inputCore and globalCommands modules.
The script will be listed under the specified category in the Input Gestures dialog.
If no category is specified, the script will be categorized under "Miscellaneous".
In order for you to use the script decorator, you will have to import it from the scriptHandler module.
```
from scriptHandler import script
```

After that, just above your script definition, add the script decorator, providing it the desired arguments.
For example:
```
--- start ---
@script(
description=_("Speaks the date and time"),
category=inputCore.SCRCAT_MISC,
gestures=["kb:NVDA+shift+t", "kb:NVDA+alt+r"]
)
def script_sayDateTime(self, gesture):

--- end ---
```

In this example, your script will be listed in the input gestures dialog under the "Miscellaneous" category.
It will have the description "Speaks the date and time", and will be bound to the "NVDA+shift+t" and "NVDA+alt+r" key combinations on the keyboard.

The following keyword arguments can be used when applying the script decorator:
- description: A short, translatable string which describes the command for users.
This is reported to users when in Input Help mode and shown in the input gestures dialog.
The script will not appear in the Input Gestures dialog unless you specify a description.
- category: The category of the script in order for it to be grouped with other similar scripts.
For example, a script in a global plugin which adds browse mode quick navigation keys may be categorized under the "Browse mode" category.
The category can be set for individual scripts, but you can also set the "scriptCategory" attribute on the plugin class, which will be used for scripts which do not specify a category.
There are constants for common categories prefixed with SCRCAT_ in the inputCore and globalCommands modules, which can also be specified.
The script will be listed under the specified category in the Input Gestures dialog.
If no category is specified, the script will be categorized under "Miscellaneous".
- gesture: A string containing a single gesture associated with this script, e.g. "kb:NVDA+shift+r".
- gestures: A string list of multiple gestures associated with this script, e.g. ["kb:NVDA+shift+r", "kb:NVDA+alt+t"].
When both gesture and gestures are specified, they are combined.
Either gesture, or any item in gestures can be used to trigger the script.
- canPropagate: A boolean indicating whether this script should also apply when it belongs to a focus ancestor object.
For example, this can be used when you want to specify a script on a particular foreground object, or another object in the focus ancestry which is not the current focus object.
This option defaults to False.
- bypassInputHelp: A boolean indicating whether this script should run when input help is active.
This option defaults to False.
-

Though the script decorator makes the script definition process a lot easier, there are more ways of binding gestures and setting script properties.
For example, a special "__gestures" Python dictionary can be defined as a class variable on an App Module, Global Plugin or NVDA Object.
This dictionary should contain gesture identifier strings pointing to the name of the requested script, without the "script_" prefix.
You can also specify a description of the script in the function's docstring.
Furthermore, an alternative way of specifying the script's category is by means of setting a "category" attribute on the script function to a string containing the name of the category.

++ Example 3: A Global Plugin to Find out Window Class and Control ID ++
The following Global Plugin allows you to press NVDA+leftArrow to have the window class of the current focus announced, and NVDA+rightArrow to have the window control ID of the current focus announced.
Expand All @@ -409,28 +450,32 @@ Once saved in the right place, either restart NVDA or choose Reload Plugins foun
#Developer guide example 3

import globalPluginHandler
from scriptHandler import script
import ui
import api

class GlobalPlugin(globalPluginHandler.GlobalPlugin):

@script(
description=_("Announces the window class name of the current focus object"),
gesture="kb:NVDA+leftArrow"
)
def script_announceWindowClassName(self, gesture):
focusObj = api.getFocusObject()
name = focusObj.name
windowClassName = focusObj.windowClassName
ui.message("class for %s window: %s" % (name, windowClassName))

@script(
description=_("Announces the window control ID of the current focus object"),
gesture="kb:NVDA+rightArrow"
)
def script_announceWindowControlID(self, gesture):
focusObj = api.getFocusObject()
name = focusObj.name
windowControlID = focusObj.windowControlID
ui.message("Control ID for %s window: %d" % (name, windowControlID))

__gestures = {
"kb:NVDA+leftArrow": "announceWindowClassName",
"kb:NVDA+rightArrow": "announceWindowControlID",
}

--- end ---
```

Expand Down Expand Up @@ -538,6 +583,7 @@ The following code can be copied and pasted in to a text file, then saved in the
```
--- start ---
import appModuleHandler
from scriptHandler import script
from NVDAObjects.IAccessible import IAccessible
import controlTypes
import ui
Expand All @@ -550,13 +596,10 @@ class AppModule(appModuleHandler.AppModule):

class EnhancedEditField(IAccessible):

@script(gesture="kb:NVDA+l")
def script_reportLength(self, gesture):
ui.message("%d" % len(self.value))

__gestures = {
"kb:NVDA+l": "reportLength",
}

--- end ---
```

Expand Down
54 changes: 25 additions & 29 deletions source/NVDAObjects/window/excel.py
Expand Up @@ -857,8 +857,6 @@ def __init__(self,windowHandle=None,excelWindowObject=None,excelWorksheetObject=
self.excelWindowObject=excelWindowObject
self.excelWorksheetObject=excelWorksheetObject
super(ExcelWorksheet,self).__init__(windowHandle=windowHandle)
for gesture in self.__changeSelectionGestures:
self.bindGesture(gesture, "changeSelection")

def _get_name(self):
return self.excelWorksheetObject.name
Expand All @@ -878,32 +876,7 @@ def _get_states(self):
states.add(controlTypes.STATE_PROTECTED)
return states

def script_changeSelection(self,gesture):
oldSelection=api.getFocusObject()
gesture.send()
import eventHandler
import time
newSelection=None
curTime=startTime=time.time()
while (curTime-startTime)<=0.15:
if scriptHandler.isScriptWaiting():
# Prevent lag if keys are pressed rapidly
return
if eventHandler.isPendingEvents('gainFocus'):
return
newSelection=self._getSelection()
if newSelection and newSelection!=oldSelection:
break
api.processPendingEvents(processEventQueue=False)
time.sleep(0.015)
curTime=time.time()
if newSelection:
if oldSelection.parent==newSelection.parent:
newSelection.parent=oldSelection.parent
eventHandler.executeEvent('gainFocus',newSelection)
script_changeSelection.canPropagate=True

__changeSelectionGestures = (
@scriptHandler.script(gestures=(
"kb:tab",
"kb:shift+tab",
"kb:enter",
Expand Down Expand Up @@ -948,7 +921,30 @@ def script_changeSelection(self,gesture):
"kb:control+a",
"kb:control+v",
"kb:shift+f11",
)
), canPropagate=True)
def script_changeSelection(self,gesture):
oldSelection=api.getFocusObject()
gesture.send()
import eventHandler
import time
newSelection=None
curTime=startTime=time.time()
while (curTime-startTime)<=0.15:
if scriptHandler.isScriptWaiting():
# Prevent lag if keys are pressed rapidly
return
if eventHandler.isPendingEvents('gainFocus'):
return
newSelection=self._getSelection()
if newSelection and newSelection!=oldSelection:
break
api.processPendingEvents(processEventQueue=False)
time.sleep(0.015)
curTime=time.time()
if newSelection:
if oldSelection.parent==newSelection.parent:
newSelection.parent=oldSelection.parent
eventHandler.executeEvent('gainFocus',newSelection)

class ExcelCellTextInfo(NVDAObjectTextInfo):

Expand Down
27 changes: 26 additions & 1 deletion source/baseObject.py
@@ -1,6 +1,6 @@
#baseObject.py
#A part of NonVisual Desktop Access (NVDA)
#Copyright (C) 2007-2017 NV Access Limited
#Copyright (C) 2007-2018 NV Access Limited, Christopher Toth, Babbage B.V.
#This file is covered by the GNU General Public License.
#See the file COPYING for more details.

Expand Down Expand Up @@ -128,6 +128,23 @@ def invalidateCaches(cls):
for instance in cls.__instances.keys():
instance.invalidateCache()

class ScriptableType(AutoPropertyType):
"""A metaclass used for collecting and caching gestures on a ScriptableObject"""

def __new__(meta, name, bases, dict):
cls = super(ScriptableType, meta).__new__(meta, name, bases, dict)
gestures = getattr(cls, "_%s__gestures" % cls.__name__, {})
# Python 3 incompatible.
for name, script in dict.iteritems():
if not name.startswith('script_'):
continue
scriptName = name[len("script_"):]
if hasattr(script, 'gestures'):
for gesture in script.gestures:
gestures[gesture] = scriptName
setattr(cls, "_%s__gestures" % cls.__name__, gestures)
return cls

class ScriptableObject(AutoPropertyObject):
"""A class that implements NVDA's scripting interface.
Input gestures are bound to scripts such that the script will be executed when the appropriate input gesture is received.
Expand All @@ -143,16 +160,24 @@ class ScriptableObject(AutoPropertyObject):
@type scriptCategory: basestring
"""

__metaclass__ = ScriptableType

def __init__(self):
#: Maps input gestures to script functions.
#: @type: dict
self._gestureMap = {}
# Bind gestures specified on the class.
# This includes gestures specified on decorated scripts.
# This does not include the gestures that are added when creating a DynamicNVDAObjectType.
for cls in reversed(self.__class__.__mro__):
try:
self.bindGestures(getattr(cls, "_%s__gestures" % cls.__name__))
except AttributeError:
pass
try:
self.bindGestures(cls._scriptDecoratorGestures)
except AttributeError:
pass
super(ScriptableObject, self).__init__()

def bindGesture(self, gestureIdentifier, scriptName):
Expand Down
59 changes: 59 additions & 0 deletions source/scriptHandler.py
Expand Up @@ -231,3 +231,62 @@ def isCurrentScript(scriptFunc):
return False
return givenFunc==realFunc

def script(
description="",
category=None,
gesture=None,
gestures=None,
canPropagate=False,
bypassInputHelp=False,
resumeSayAllMode=None
):
"""Define metadata for a script.
This function is to be used as a decorator to set metadata used by the scripting system and gesture editor.
It can only decorate methods which name start swith "script_"
@param description: A short translatable description of the script to be used in the gesture editor, etc.
@type description: string
@param category: The category of the script displayed in the gesture editor.
@type category: string
@param gesture: A gesture associated with this script.
@type gesture: string
@param gestures: A list of gestures associated with this script
@type gestures: list(string)
@param canPropagate: Whether this script should also apply when it belongs to a focus ancestor object.
@type canPropagate: bool
@param bypassInputHelp: Whether this script should run when input help is active.
@type bypassInputHelp: bool
@param resumeSayAllMode: The say all mode that should be resumed when active before executing this script.
One of the C{sayAllHandler.CURSOR_*} constants.
@type resumeSayAllMode: int
"""
if gestures is None:
gestures = []
def script_decorator(decoratedScript):
# Scripts are unbound instance methods in python 2 and functions in python 3.
# Therefore, we use inspect.isroutine to check whether a script is either a function or instance method.
if not inspect.isroutine(decoratedScript):
log.warning(
"Using the script decorator is unsupported for %r" % decoratedScript,
stack_info=True
)
return decoratedScript
if not decoratedScript.__name__.startswith("script_"):
log.warning(
"Can't apply script decorator to %r which name does not start with 'script_'" % decoratedScript.__name__,
stack_info=True
)
return decoratedScript
decoratedScript.__doc__ = description
if category is not None:
decoratedScript.category = category
if gesture is not None:
gestures.append(gesture)
if gestures:
decoratedScript.gestures = gestures
decoratedScript.canPropagate = canPropagate
decoratedScript.bypassInputHelp = bypassInputHelp
if resumeSayAllMode is not None:
decoratedScript.resumeSayAllMode = resumeSayAllMode
return decoratedScript
return script_decorator

0 comments on commit 9d35a2f

Please sign in to comment.