Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
NVDA no longer automatically loads custom code from package directori…
…es in the NVDA user configuration directory. Rather it will load tem from subdirectories in a new 'scratchpad' directory in the NVDA user configuration directory, but only if the open in the advanced category is enabled.
  • Loading branch information
michaelDCurran committed Feb 4, 2019
commit b993b072ac3b254ab788f9087299ee58d5a739b2
16 changes: 11 additions & 5 deletions developerGuide.t2t
Original file line number Diff line number Diff line change
Expand Up @@ -167,16 +167,23 @@ Both App Modules and Global Plugins share a common look and feel.
They are both Python source files (with a .py extension), they both define a special class containing all events, scripts and bindings, and they both may define custom classes to access controls, text content and complex documents.
However, they do differ in some ways.

Custom appModules and globalPlugins can be packaged into NVDA add-ons.
This allows easy distribution, and provides a safe way for the user to install and uninstall the custom code.
Please refer to the Add-ons section later on in this document.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that the concept of an Addon should be introduced first (at least, before appModules and globalPlugins). I think it would paint a clearer picture to start by describing the end goal, an addon, which may contain several components AppModules, GlobalPlugins, synth drivers, braille drives. Make it clear what is optional, and what is not. And describe other files required in the addon (eg the manifest). Then mention as a note, that it can be helpful to develop these components using the scratchpad to avoid having to repackage the addon.

At the moment (and I expect this is due to historical reasons) it the dev guide describes the internal components first, then almost says "by the way this is all packaged as an addon". This coupled with the use of the word plugin, I think makes it hard to get a clear picture of how everything fits together.

Perhaps this is not the right PR to address this in. It would be great if someone who really understood the addon development workflow could think about how to explain this top down.


In order to test the code while developing, you can place it in a special 'scratchpad' directory in your NVDA user configuration directory.
You will also need to configure NVDA to enable loading of custom code from the Developer Scratchpad Directory, by enabling this in the Advanced category of NVDA's Settings dialog.
The Advanced category also contains a button to easily open the Developer Scratchpad directory if enabled.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Space at start of line.


The following few sections will talk separately about App Modules and Global Plugins.
After this point, discussion is again more general.

++ Basics of an App Module ++
App Module files have a .py extension, and are named the same as the main executable of the application for which you wish them to be used.
For example, an App Module for notepad would be called notepad.py, as notepad's main executable is called notepad.exe.

App Module files must be placed in the appModules subdirectory of the user's NVDA user configuration directory.
For more information on where to find the user configuration directory, please see the NVDA user guide.

App Module files must be placed in the appModules subdirectory of an add-on, or of the scratchpad directory of the NVDA user configuration directory.

App Modules must define a class called AppModule, which inherits from appModuleHandler.AppModule.
This class can then define event and script methods, gesture bindings and other code.
This will all be covered in depth later.
Expand Down Expand Up @@ -231,8 +238,7 @@ As with other examples in this guide, remember to delete the created app module
++ Basics of a Global Plugin ++
Global Plugin files have a .py extension, and should have a short unique name which identifies what they do.

Global Plugin files must be placed in the globalPlugins subdirectory of the user's NVDA user configuration directory.
For more information on where to find the user configuration directory, please see the NVDA user guide.
Global plugin files must be placed in the globalPlugins subdirectory of an add-on, or of the scratchpad directory of the NVDA user configuration directory.

Global Plugins must define a class called GlobalPlugin, which inherits from globalPluginHandler.GlobalPlugin.
This class can then define event and script methods, gesture bindings and other code.
Expand Down
33 changes: 26 additions & 7 deletions source/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,18 @@ def getSystemConfigPath():
pass
return None

def getScratchpadDir(ensureExists=False):
""" Returns the path where custom appModules, globalPlugins and drivers can be placed while being developed."""
path=os.path.join(globalVars.appArgs.configPath,'scratchpad')
if ensureExists:
if not os.path.isdir(path):
os.makedirs(path)
for subdir in ('appModules','brailleDisplayDrivers','globalPlugins','synthDrivers'):
subpath=os.path.join(path,subdir)
if not os.path.isdir(subpath):
os.makedirs(subpath)
return path

def initConfigPath(configPath=None):
"""
Creates the current configuration path if it doesn't exist. Also makes sure that various sub directories also exist.
Expand All @@ -149,7 +161,7 @@ def initConfigPath(configPath=None):
os.makedirs(configPath)
subdirs=["speechDicts","profiles"]
if not isAppX:
subdirs.extend(["addons", "appModules","brailleDisplayDrivers","synthDrivers","globalPlugins"])
subdirs.append("addons")
for subdir in subdirs:
subdir=os.path.join(configPath,subdir)
if not os.path.isdir(subdir):
Expand Down Expand Up @@ -295,6 +307,7 @@ def getConfigDirs(subpath=None):
@return: The configuration directories in the order in which they should be searched.
@rtype: list of str
"""
log.warning("getConfigDirs is deprecated. Use globalVars.appArgs.configPath instead")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd use warnings.warn here, which is the pythonic way to raise a warning. See also the validate module in the source repository (not the configobj one). Using warnings.warn also eases in cleanup when we would like to remove those deprecated functions at some point.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Err... where do these warnings show up? I thought they got mapped to debugWarning in the log, but I can't seem to make them appear.

return [os.path.join(dir, subpath) if subpath else dir
for dir in (globalVars.appArgs.configPath,)
]
Expand All @@ -309,16 +322,22 @@ def addConfigDirsToPythonPackagePath(module, subdir=None):
"""
if isAppX or globalVars.appArgs.disableAddons:
return
if not subdir:
subdir = module.__name__
# Python 2.x doesn't properly handle unicode import paths, so convert them.
dirs = [dir.encode("mbcs") for dir in getConfigDirs(subdir)]
dirs.extend(module.__path__ )
module.__path__ = dirs
# FIXME: this should not be coupled to the config module....

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this something to fix as part of this pr, may be? I came up with an alternative in #9151, though I can't yet find the particular comment.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As there is still code that is unrelated to add-ons (I.e. the scratchpad directories) I don't think there is a problem keeping this in config, called from the various modules. Perhaps something we can clean up in future, but I'm not sure I quite see the advantage.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the main goal of cleaning this up would be to simplify / clarify the pathway. At the moment its fairly hard to follow. The mechanism certainly isn't obvious.

That said, I only recommend refactoring it if we can actually make it easier to understand, and it does not require large changes. It would be very easy to change the order of initialization, we would want to think very carefully about whether that might break things.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think there's a real advantage, apart from that it might be much more obvious if the addonHandler adds the add-ons to the module paths rather than the config module. I really do not insist on having this changed. It's just the fix me comment that somehow calls us to do something about it while we're at it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not convinced it's necessary for this PR, unless there was an easy and safe way to do it.

From a code readability standpoint, I think there is quite an advantage. I expect having a more obvious "enable" mechanism on addons would have saved quite a lot of time for me when working on the addon compatibility checks. I looked into it and gave up a few times, when I eventually decided it was important to know, it probably took a couple of hours of jumping around the code to realize this was essentially the core of it.

import addonHandler
for addon in addonHandler.getRunningAddons():
addon.addToPackagePath(module)
if not conf['development']['enableScratchpadDir']:
return
if not subdir:
subdir = module.__name__
fullPath=os.path.join(getScratchpadDir(),subdir)
# Python 2.x doesn't properly handle unicode import paths, so convert them.
fullPath=fullPath.encode("mbcs")
# Insert this path at the beginning of the module's search paths.
# The module's search paths may not be a mutable list, so replace it with a new one
pathList=[fullPath]
pathList.extend(module.__path__)
module.__path__=pathList

class ConfigManager(object):
"""Manages and provides access to configuration.
Expand Down
3 changes: 3 additions & 0 deletions source/config/configSpec.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,9 @@

[editableText]
caretMoveTimeoutMs = integer(min=0, max=2000, default=100)

[development]
enableScratchpadDir = boolean(default=false)
""").format(latestSchemaVersion=latestSchemaVersion)

#: The configuration specification
Expand Down
2 changes: 2 additions & 0 deletions source/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,8 @@ def main():
log.debug("loading config")
import config
config.initialize()
if config.conf['development']['enableScratchpadDir']:
log.info("Developer Scratchpad mode enabled")
if not globalVars.appArgs.minimal and config.conf["general"]["playStartAndExitSounds"]:
try:
nvwave.playWaveFile("waves\\start.wav")
Expand Down
30 changes: 29 additions & 1 deletion source/gui/settingsDialogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -1939,6 +1939,25 @@ def makeSettings(self, settingsSizer):
panelText =_("Warning! The following settings are for advanced users. Changing them may cause NVDA to function incorrectly. Please only change these if you know what you are doing or have been specifically instructed by NVDA developers.")
sHelper.addItem(wx.StaticText(self, label=panelText))

# Translators: This is the label for a group of Advanced options in the
# Advanced settings panel
groupText = _("NVDA Development")
devGroup = guiHelper.BoxSizerHelper(self, sizer=wx.StaticBoxSizer(wx.StaticBox(self, label=groupText), wx.VERTICAL))
sHelper.addItem(devGroup)

# Translators: This is the label for a checkbox in the
# Advanced settings panel.
label = _("Enable loading custom code from Developer Scratchpad directory")
self.scratchpadCheckBox=devGroup.addItem(wx.CheckBox(self, label=label))
self.scratchpadCheckBox.SetValue(config.conf["development"]["enableScratchpadDir"])
self.scratchpadCheckBox.Bind(wx.EVT_CHECKBOX,self.onToggleScratchpadCheckBox)

# Translators: the label for a button in the Advanced settings category
label=_("Open developer scratchpad directory")
self.openScratchpadButton=devGroup.addItem(wx.Button(self, label=label))
self.openScratchpadButton.Bind(wx.EVT_BUTTON,self.onOpenScratchpadDir)
self.openScratchpadButton.Enable(config.conf["development"]["enableScratchpadDir"])

# Translators: This is the label for a group of Advanced options in the
# Advanced settings panel
groupText = _("Microsoft UI Automation")
Expand Down Expand Up @@ -1985,7 +2004,15 @@ def makeSettings(self, settingsSizer):
self.logCategoriesList.CheckedItems=[index for index,x in enumerate(self.logCategories) if config.conf['debugLog'][x]]
self.logCategoriesList.Select(0)

def onToggleScratchpadCheckBox(self,evt):
self.openScratchpadButton.Enable(evt.IsChecked())

def onOpenScratchpadDir(self,evt):
path=config.getScratchpadDir(ensureExists=True)
os.startfile(path)

def onSave(self):
config.conf["development"]["enableScratchpadDir"]=self.scratchpadCheckBox.IsChecked()
config.conf["UIA"]["useInMSWordWhenAvailable"]=self.UIAInMSWordCheckBox.IsChecked()
config.conf["editableText"]["caretMoveTimeoutMs"]=self.caretMoveTimeoutSpinControl.GetValue()
for index,key in enumerate(self.logCategories):
Expand Down Expand Up @@ -2545,7 +2572,8 @@ class NVDASettingsDialog(MultiCategorySettingsDialog):
if winVersion.isUwpOcrAvailable():
categoryClasses.append(UwpOcrPanel)
# And finally the Advanced panel which should always be last.
categoryClasses.append(AdvancedPanel)
if not globalVars.appArgs.secure:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch!

categoryClasses.append(AdvancedPanel)

def makeSettings(self, settingsSizer):
# Ensure that after the settings dialog is created the name is set correctly
Expand Down
15 changes: 11 additions & 4 deletions user_docs/en/userGuide.t2t
Original file line number Diff line number Diff line change
Expand Up @@ -1607,6 +1607,17 @@ This combo box allows you to choose the language to be used for text recognition
Warning! The settings in this category are for advanced users and may cause NVDA to not function correctly if configured in the wrong way.
Only make changes to these settings if you are sure you know what you are doing or if you have been specifically instructed to by an NVDA developer.

==== Enable loading custom code from Developer Scratchpad Directory ====
When developing add-ons for NVDA, it is useful to be able to test code as you are writing it.
This option when enabled, allows NVDA to load custom appModules, globalPlugins, brailleDisplayDrivers and synthDrivers, from a special developer scratchpad directory in your NVDA user configuration directory.
Previously NVDA would load custom code directly from the user configuration directory, with no way of disabling this.
This option is off by default, ensuring that no untested code is ever run in NVDA with out the user's explicit knowledge.
If you wish to distribute custom code to others, you should package it as an NVDA add-on.

==== Open Developer Scratchpad Directory ====
This button opens the directory where you can place custom code while developing it.
This button is only enabled if NVDA is configured to enable loading custom code from the Developer Scratchpad Directory.

==== Use UI automation to access Microsoft Word document controls when available ====
When this option is enabled, NVDA will try to use the Microsoft UI Automation accessibility api in order to fetch information from Microsoft Word document controls.
This includes in Microsoft Word itself, and also the Microsoft Outlook message viewer and composer.
Expand Down Expand Up @@ -1876,10 +1887,6 @@ When using an older version of NVDA, some new add-ons may not be compatible eith
Attempting to install an incompatible add-on will result in an error explaining why the add-on is considered incompatible.
To inspect these incompatible add-ons, you can use the "view incompatible add-ons" button to launch the incompatible add-ons manager.

In the past, it was possible to extend NVDA's functionality by copying individual plugins and drivers into your NVDA user Configuration directory.
Although this version of NVDA may still load them, they will not be shown in the Add-on Manager.
It is best to remove these files from your configuration and install the appropriate add-on if one is available.

To access the Add-ons Manager from anywhere, please assign a custom gesture using the [Input Gestures dialog #InputGestures].

++ Incompatible Add-ons Manager ++[incompatibleAddonsManager]
Expand Down