In the previous installment, you learned what StationPlaylist Studio is and a brief history behind this add-on. Starting with this installment, we'll tour the real internals of this add-on, starting with overall design and a few important notes. But before we get into that, there are some things we need to go over such as programming background, user experience and a few definitions.
A place to start: reader questions and definitions
I'm sure some readers might ask, "doesn't writing a series on internals require programming knowledge?" Yes and no. Yes, as you may need some basic exposure to programming such as what a variable is, conditional execution and so forth. On the flip side, you don't have to be a programmer to write about internal workings of an add-on (the basic requirement is passion for teaching and a hope for users to learn something new). Same could be said about reading this series: you may need some exposure to programming, but you don't have to be a programmer to follow along.
Another question might be, "will this series teach me all there is to it when writing an add-on of my own?" Yes and no. Yes, as you'll learn how add-on writers think when it comes to taking care of their add-ons and get a glimpse into add-on development processes. On the other side of the coin is scope of this series - this series does not serve as a definitive guide on add-on writing (there are documentation, linked at the end of this article that'll give you some basic overview). If you are familiar with add-on development and/or NVDA screen reader development and source code, you'll have slightly easier time understanding this series of articles. I tried my best to make it easy for users to understand (although I do have to include some technical details).
* Add-on: An add-on is a module for a program that adds additional features or changes the behavior of a program (1).
* API: Application Programming Interface, a set of specifications for programmers for using services offered by a program such as modules, functions and documentation (2). One of the most well-known API's is Python and its documentation (3).
With some basics out of the way, let's dive into SPL add-on internals (you should download the add-on source code, which can be found at http://bitbucket.org/nvdaaddonteam/stationplaylist).
Overall design and source code layout
StationPlaylist Studio add-on for NVDA consists of two app modules and a global plugin. Because Studio comes with Track Tool for managing tracks, the add-on includes an app module for Track Tool in addition to the main app module for Studio.
The overall design is that of a partnership between the main Studio app module and the Studio Utilities (SPLStudioUtils) global plugin. Studio app module performs things expected from scripts such as responding to key presses, announcing status information, configuration management and so forth, while the global plugin is responsible for running Studio commands from anywhere and for encoder support (the add-on supports SAM and SPL encoders). In reality, the global plugin is subordinate to the app module, as the app module controls overall functionality of the add-on and because the global plugin requires Studio to be running to unlock some features (here, unlock means using layer commands and encoder support).
The source code consists of:
* appModules: This folder contains the main splstudio (app module) package and the app module for Track Tool.
* The SPL Studio package consists of various modules, which include __init__ (main app module and track item classes), configuration manager (splconfig) and miscellaneous services (splmisc) as well as support modules and various wave files used by the add-on.
* The main app module file is divided into sections. First, the overlay classes for track items are defined, then comes the app module, further divided into four sections: fundamental methods (constructor, events and others), time commands (end of track, broadcaster time, etc.), other commands (track Finder and others) and SPL Assistant layer. This allows me to identify where a bug is coming from and to add features in appropriate sections.
* globalPlugins: This folder contains SPLStudioUtils package, which consists of __init__ (main plugin and SPL Controller layer) and encoder support module.
When I set out to write the add-on in 2013, I put forth certain things the add-on should adhere to, including:
* Consistency: The add-on should have a consistent interface and command structure. Interface includes various GUI's such as add-on configuration dialog. For layer commands, I tried using native Studio command assignments.
* Extensibility: The add-on should be organized and written in such a way that permits easy extensibility, hence the app module and the global plugin were divided into submodules, with each of them being a specialist of some kind (such as configuration management).
* Separation of concerns: Coupled with extensibility, this allowed me to provide just needed commands at the right time, which resulted in two layer command sets (explained below).
* Easy to follow source code: Although some may say excessive documentation is a noise, I believe it is important for a developer to understand how a function or a module came about. Also, I have used and read user guides for other screen reader scripts to better understand how a feature worked and come up with some enhancements to a point where I found some major bugs with JAWS scripts (one of them, which I hope Brian patched by now is microphone alarm where the alarm would go off despite the fact that microphone was turned off before alarm timeout has expired).
* Unique feature labels: One way to stand out was to give features interesting names. For instance, during add-on 3.0 development, I decided to give cart learn mode a name that better reflects what the feature does: Cart Explorer to explore cart assignments. Same could be set about NvDA's implementation of enhanced arrow keys (called Track Dial, as the feature is similar to flipping a dial on a remote control).
* Extensive collaboration and feedback cycle between users and developers: I believed that the real stars of the show were not the add-on code files, but broadcasters who'll use various add-on features. Because of this, I worked with users early on, and their continued feedback shapes future add-on releases. This collaboration and feedback cycle also helped me (the add-on author) understand how the add-on was used and to plan future features to meet the needs of broadcasters who may use this add-on in various scenarios (a good example is broadcast profiles, as you'll see in add-on configuration article).
Why two layer sets?
When I first sat down to design the add-on, I knew I had to write both an app module and a global plugin (to perform Studio commands from anywhere), which led to defining two layer command sets for specific purposes:
* SPL Assistant: This layer command set is available in the app module and is intended to obtain status information and to manage app module features. I called this Assistant because this layer serves as an assistant to a broadcaster in reading various status information. More details can be found in a future installment on SPL Assistant layer commands.
* SPL Controller: This layer is for the global plugin and performs Studio commands from anywhere. I called this "controller" because it controls various functions of Studio from other programs. More details will be provided in a future installment.
In the early days, I enforced this separation, but in add-on 6.0, it will be possible to invoke SPL Assistant layer by pressing the command used to invoke SPL Controller.
The "magic" behind layer commands
In order for layer commands to work, I borrowed code from another add-on: Toggle and ToggleX by Tyler Spivey. Toggle/ToggleX allows one to toggle various formatting announcement settings via a layer command set. It works like this:
* Dynamic Command:script binding and removal: It is possible to bind gestures dynamically via bindGesture/bindGestures method for an app module or a global plugin (bindGesture binds a single command to a script, whereas bindGestures binds commands to scripts from a gestures map or another container). To remove gesture map dynamically, the main/layer gestures combo was cleared, then the main gestures were bound.
* Use of two gesture maps in the app module/global plugin: Normally, an app module or a global plugin that accepts keyboard input uses a single gestures map (called __gestures; a map is another term for dictionaries or associative array where there is a value tied to a key). But in order for layers to work, a second gestures map was provided to store layer commands (command and the bound script of the form "command":"script").
* Wrapped functions: Tyler used "wraps" decorator from functools to wrap how "finally" function is called from within the layer set (this was needed to remove bindings for layer commands after they are done). Also, a custom implementation of getScript function (app module/global plugin) was used to return either the main script of the layer version depending on context.
A typical layer command execution is as follows:
1. First, assign a command to a layer (entry) command (add-on 2.0 and later; add-on 1.x used NvDA+Grave for SPL Controller and Control+NVDA+Grave for the Assistant layer; removed in 2.0 to prevent conflicts with language-specific gestures).
2. You press the layer entry command. This causes the app module/global plugin to perform the following:
A. Layer conditions are checked. For the app module, making sure that you are in the Playlist Viewer, and for the global plugin, checks if Studio is running.
B. Sets a flag telling NvDA that the Assistant/Controller layer is active.
C. Adds gestures for the layer set to the main gestures map via bindGestures function.
3. You press a command in the layer set (such as A from Assistant to hear automation status, or press A to turn automation on if using SPL Controller layer). Depending on how the layer script is implemented, it either calls Studio API (for SPL Controller layer and for some Assistant commands) or simulates object navigation to fetch needed information (Assistant layer). In the app module, for performance reasons, the object is cached. More details on mechanics of this procedure in subsequent articles.
4. After the layer command is done, it calls "finish" function (app module/global plugin) to perform clean up actions such as:
A. Clears layer flags.
B. Removes the "current" gestures (main gestures and layer commands) and reassigns it to the main gestures map (this is dynamic binding removal).
C. Performs additional actions depending on context (for example, if Cart Explorer was in use).
The importance of Studio window handle and Studio API
In order to use services offered by Studio, one has to use Studio API, which in turn requires one to keep an eye on window handle to Studio (in Windows API, a window handle (just called handle) is a reference to something, such as a window, a file, connection routines and so on). This is important if one wishes to perform Studio commands from other programs (Studio uses messages to communicate with the outside program in question via user32.dll's SendMessage function).
In add-on 6.0, (Git master branch in the add-on source code) one of the activities the app module performs when started (besides announcing the version of Studio you are using) is to look for the handle to Studio's main window until it is found (this is done via a thread which calls user32.dll's FindWindowA (not FindWindowW) function every second), and once found, the app module caches this information for later use. A similar check is performed by SPL Controller command, as without this, SPL Controller is useless (as noted earlier). Because of the prominence of the Studio API and the window handle, one of the first things I do when a new version of Studio is released is to ask for the latest Studio API and modify the app module and/or global plugin accordingly.
I hope this provided a good overview of how the add-on is organized and let you learn at least some basics of how the add-on operates. Starting with the next installment, we'll dive deeper into the add-on internals, starting with the app module's lifecycle (from birth to death and all activities in between). Along the way we'll learn more about how various commands, alarms and dialogs are implemented, culminating in a tour of the global plugin and encoder support.
1. Plug-in (Wikipedia): https://en.wikipedia.org/wiki/Plug-in_(computing)
2. Application Programming Interface (Wikipedia): https://en.wikipedia.org/wiki/Application_programming_interface
3. Python 2.7.10 documentation overview (Python Software Foundation): https://docs.python.org/2/
4. Handle (Wikipedia): https://en.wikipedia.org/wiki/Handle_(computing)
5. What is a Windows handle (Stack Overflow): http://stackoverflow.com/questions/902967/what-is-a-windows-handle
6. FindWindow (user32.dll) reference (Windows API): https://msdn.microsoft.com/en-us/library/windows/desktop/ms633499(v=vs.85).aspx
7. SendMessage (user32.dll) reference (Windows API): https://msdn.microsoft.com/en-us/library/windows/desktop/ms644950(v=vs.85).aspx
8. NVDA Developer Guide (NV Access): http://www.nvaccess.org/files/nvda/documentation/developerGuide.html