Repository navigation
Provide configurable developer scratchpad dir rather than automatic loading of custom code #9238
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
b993b07
e003bdd
dc24c29
d472224
2190dd8
06270d3
5dab22a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
…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
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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. | ||
|
|
||
| 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. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
|
@@ -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. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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. | ||
|
|
@@ -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): | ||
|
|
@@ -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") | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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,) | ||
| ] | ||
|
|
@@ -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.... | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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") | ||
|
|
@@ -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): | ||
|
|
@@ -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: | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
|
||
There was a problem hiding this comment.
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.