Bundle Example: Add an HTML-based Tool

This tutorial builds on the material from Bundle Example: Add a Command.

This example describes how to create a ChimeraX bundle that defines a graphical interface to the two commands, tutorial cofm and tutorial highlight, defined in the Bundle Example: Add a Command example.

The ChimeraX user interface is built using PyQt5, which has a significant learning curve. However, PyQt5 has very good support for displaying HTML 5 with JavaScript in a window, which provides a simpler avenue for implementing graphical interfaces. This example shows how to combine a static HTML page with dynamically generated JavaScript to create an interface with only a small amount of code.

The steps in implementing the bundle are:

  1. Create a bundle_info.xml containing information about the bundle,

  2. Create a Python package that interfaces with ChimeraX and implements the command functionality, and

  3. Install and test the bundle in ChimeraX.

The final step builds a Python wheel that ChimeraX uses to install the bundle. So if the bundle passes testing, it is immediately available for sharing with other users.

Source Code Organization

The source code for this example may be downloaded as a zip-format file containing a folder named tut_tool_html. Alternatively, one can start with an empty folder and create source files based on the samples below. The source folder may be arbitrarily named, as it is only used during installation; however, avoiding whitespace characters in the folder name bypasses the need to type quote characters in some steps.

Sample Files

The files in the tut_tool_html folder are:

  • tut_tool_html - bundle folder
    • bundle_info.xml - bundle information read by ChimeraX

    • src - source code to Python package for bundle
      • __init__.py - package initializer and interface to ChimeraX

      • tool.py - source code to implement the Tutorial (HTML) tool

      • docs/user/commands/tutorial.html - help file describing the graphical tool

The file contents are shown below.

bundle_info.xml

bundle_info.xml is an eXtensible Markup Language format file whose tags are listed in Bundle Information XML Tags. While there are many tags defined, only a few are needed for bundles written completely in Python. The bundle_info.xml in this example is similar to the one from the Bundle Example: Add a Command example with changes highlighted. For explanations of the unhighlighted sections, please see Bundle Example: Hello World and Bundle Example: Add a Command.

 1<!--
 2ChimeraX bundle names must start with "ChimeraX-"
 3to avoid clashes with package names in pypi.python.org.
 4When uploaded to the ChimeraX toolshed, the bundle
 5will be displayed without the ChimeraX- prefix.
 6-->
 7
 8<BundleInfo name="ChimeraX-TutorialToolHTML"
 9	    version="0.1" package="chimerax.tut_tool_html"
10  	    minSessionVersion="1" maxSessionVersion="1">
11
12  <!-- Additional information about bundle source -->
13  <Author>UCSF RBVI</Author>
14  <Email>chimerax@cgl.ucsf.edu</Email>
15  <URL>https://www.rbvi.ucsf.edu/chimerax/</URL>
16
17  <!-- Synopsis is a one-line description
18       Description is a full multi-line description -->
19  <Synopsis>Example for adding a graphical interface tool</Synopsis>
20  <Description>Example code for implementing ChimeraX bundle.
21
22Implements tool "Tutorial (HTML)" to access "tutorial cofm"
23and "tutorial highlight" commands from a graphical interface.
24  </Description>
25
26  <!-- Categories is a list where this bundle should appear -->
27  <Categories>
28    <Category name="General"/>
29  </Categories>
30
31  <!-- Dependencies on other ChimeraX/Python packages -->
32  <!-- This example uses functionality from the TutorialCommand bundle -->
33  <Dependencies>
34    <Dependency name="ChimeraX-Core" version="~=1.1"/>
35    <Dependency name="ChimeraX-UI" version="~=1.0"/>
36    <Dependency name="ChimeraX-TutorialCommand" version="~=0.1"/>
37  </Dependencies>
38
39  <!-- Non-Python files that are part of package -->
40  <DataFiles>
41    <DataFile>tool.html</DataFile>
42    <DataFile>docs/user/tools/tutorial.html</DataFile>
43  </DataFiles>
44
45  <Classifiers>
46    <!-- Development Status should be compatible with bundle version number -->
47    <PythonClassifier>Development Status :: 3 - Alpha</PythonClassifier>
48    <PythonClassifier>License :: Freeware</PythonClassifier>
49    <!-- ChimeraX classifiers describe supplied functionality -->
50    <!-- Register a graphical interface tool -->
51    <ChimeraXClassifier>ChimeraX :: Tool :: Tutorial (HTML) ::
52      General :: Graphical interface to "tutorial" commands</ChimeraXClassifier>
53  </Classifiers>
54
55</BundleInfo>

The BundleInfo, Synopsis and Description tags are changed to reflect the new bundle name and documentation (lines 8-10 and 19-24). Three other changes are needed for this bundle to declare that:

  1. this bundle depends on the ChimeraX-UI and ChimeraX-Tutorial_Command bundles (lines 35-36),

  2. non-Python files need to be included in the bundle (lines 40-43), and

  3. a single graphical interface tool is provided in this bundle (lines 51-52).

The Dependency tags on lines 35 and 36 inform ChimeraX that the ChimeraX-UI and ChimeraX-Tutorial_Command bundles must be present when this bundle is installed. If they are not, they are installed first. The ChimeraX-UI bundle is needed to provide the chimerax.ui.HtmlToolInstance class used for building the user interface (see tool.py` below) and the ChimeraX-Tutorial_Command bundle is needed to provide the ChimeraX commands that will be used for actually performing user actions.

The DataFiles tag on lines 40-43 informs ChimeraX to include non-Python files as part of the bundle when building. In this case, tool.html (implicitly in the src folder) which provides the HTML component of our interface should be included. Also, the help documentation for our tool, tutorial.html.

The ChimeraXClassifier tag on lines 51-52 informs ChimeraX that there is one graphical interface tool named Tutorial (HTML) in the bundle. The last two fields (separated by ::) are the tool category and the tool description. ChimeraX will add a Tutorial (HTML) menu entry in its Tool submenu that matches the tool category, General; if the submenu does not exist, it will be created.

src

src is the folder containing the source code for the Python package that implements the bundle functionality. The ChimeraX devel command, used for building and installing bundles, automatically includes all .py files in src as part of the bundle. (Additional files may also be included using bundle information tags such as DataFiles as shown in Bundle Example: Add a Tool.) The only required file in src is __init__.py. Other .py files are typically arranged to implement different types of functionality. For example, cmd.py is used for command-line commands; tool.py or gui.py for graphical interfaces; io.py for reading and saving files, etc.

src/__init__.py

As described in Bundle Example: Hello World, __init__.py contains the initialization code that defines the bundle_api object that ChimeraX needs in order to invoke bundle functionality. ChimeraX expects bundle_api class to be derived from chimerax.core.toolshed.BundleAPI with methods overridden for registering commands, tools, etc.

 1# vim: set expandtab shiftwidth=4 softtabstop=4:
 2
 3from chimerax.core.toolshed import BundleAPI
 4
 5
 6# Subclass from chimerax.core.toolshed.BundleAPI and
 7# override the method for registering commands,
 8# inheriting all other methods from the base class.
 9class _MyAPI(BundleAPI):
10
11    api_version = 1     # register_command called with BundleInfo and
12                        # CommandInfo instance instead of command name
13                        # (when api_version==0)
14
15    # Override method
16    @staticmethod
17    def start_tool(session, bi, ti):
18        # session is an instance of chimerax.core.session.Session
19        # bi is an instance of chimerax.core.toolshed.BundleInfo
20        # ti is an instance of chimerax.core.toolshed.ToolInfo
21
22        # This method is called once for each time the tool is invoked.
23
24        # We check the name of the tool, which should match one of the
25        # ones listed in bundle_info.xml (without the leading and
26        # trailing whitespace), and create and return an instance of the
27        # appropriate class from the ``gui`` module.
28        from . import tool
29        if ti.name == "Tutorial (HTML)":
30            return tool.TutorialTool(session, ti.name)
31        raise ValueError("trying to start unknown tool: %s" % ti.name)
32
33
34# Create the ``bundle_api`` object that ChimeraX expects.
35bundle_api = _MyAPI()

In this example, the start_tool() method is overridden to invoke a bundle function, tool.TutorialTool, when the user selects the Tutorial (HTML) menu item from the General submenu of the Tools menu. (The Tutorial (HTML) and General names are from the ChimeraXClassifier tag in bundle_info.xml as described above.)

The arguments to start_tool(), in bundle API version 1, are session, a chimerax.core.session.Session instance, bi, a chimerax.core.toolshed.BundleInfo instance, and ti, a chimerax.core.toolshed.ToolInfo instance. session is used to access other available data such as open models, running tasks and the logger for displaying messages, warnings and errors. bi contains the bundle information and is not used in this example. ti contains the tool information; in this case, it is used to make sure the name of the tool being invoked is the expected one. If it is, tool.TutorialTool is called; if not, an exception is thrown, which ChimeraX will turn into an error message displayed to the user.

src/tool.py

tool.py defines the TutorialTool class that is invoked by ChimeraX (via the start_tool() method of bundle_api in __init__.py) when the user selects the Tutorial (HTML) menu item from the Tools menu.

  1# vim: set expandtab shiftwidth=4 softtabstop=4:
  2
  3# === UCSF ChimeraX Copyright ===
  4# Copyright 2016 Regents of the University of California.
  5# All rights reserved.  This software provided pursuant to a
  6# license agreement containing restrictions on its disclosure,
  7# duplication and use.  For details see:
  8# http://www.rbvi.ucsf.edu/chimerax/docs/licensing.html
  9# This notice must be embedded in or attached to all copies,
 10# including partial copies, of the software or any revisions
 11# or derivations thereof.
 12# === UCSF ChimeraX Copyright ===
 13
 14from chimerax.ui import HtmlToolInstance
 15
 16class TutorialTool(HtmlToolInstance):
 17
 18    # Inheriting from HtmlToolInstance gets us the following attributes
 19    # after initialization:
 20    #   self.tool_window: instance of chimerax.ui.MainToolWindow
 21    #   self.html_view: instance of chimerax.ui.widgets.HtmlView
 22    # Defining methods in this subclass also trigger some automated callbacks:
 23    #   handle_scheme: called when custom-scheme link is visited
 24    #   update_models: called when models are opened or closed
 25    # If cleaning up is needed on finish, override the ``delete`` method
 26    # but be sure to call ``delete`` from the superclass at the end.
 27
 28    SESSION_ENDURING = False    # Does this instance persist when session closes
 29    SESSION_SAVE = False        # No session saving for now
 30    CUSTOM_SCHEME = "tutorial"  # Scheme used in HTML for callback into Python
 31    help = "help:user/tools/tutorial.html"
 32                                # Let ChimeraX know about our help page
 33
 34    def __init__(self, session, tool_name):
 35        # ``session`` - ``chimerax.core.session.Session`` instance
 36        # ``tool_name``      - string
 37
 38        # Initialize base class.  ``size_hint`` is the suggested
 39        # initial tool size in pixels.  For debugging, add
 40        # "log_errors=True" to get Javascript errors logged
 41        # to the ChimeraX log window.
 42        super().__init__(session, tool_name, size_hint=(575, 400))
 43
 44        # Set name displayed on title bar (defaults to tool_name)
 45        # Must be after the superclass initialization in order
 46        # to override the default
 47        self.display_name = "Tutorial — HTML-based"
 48
 49        self._build_ui()
 50
 51    def _build_ui(self):
 52        # Fill in html viewer with initial page in the module
 53        import os.path
 54        html_file = os.path.join(os.path.dirname(__file__), "tool.html")
 55        import pathlib
 56        self.html_view.setUrl(pathlib.Path(html_file).as_uri())
 57
 58    def handle_scheme(self, url):
 59        # ``url`` - ``Qt.QtCore.QUrl`` instance
 60
 61        # This method is called when the user clicks a link on the HTML
 62        # page with our custom scheme.  The URL path and query parameters
 63        # are controlled on the HTML side via Javascript.  Obviously,
 64        # we still do security checks in case the user somehow was
 65        # diverted to a malicious page specially crafted with links
 66        # with our custom scheme.  (Unlikely, but not impossible.)
 67        # URLs should look like: tutorial:cofm?weighted=1
 68
 69        # First check that the path is a real command
 70        command = url.path()
 71        if command == "update_models":
 72            self.update_models()
 73            return
 74        elif command in ["cofm", "highlight"]:
 75            # Collect the optional parameters from URL query parameters
 76            # and construct a command to execute
 77            from urllib.parse import parse_qs
 78            query = parse_qs(url.query())
 79
 80            # First the command
 81            cmd_text = ["tutorial", command]
 82
 83            # Next the atom specifier
 84            target = query["target"][0]
 85            models = query["model"]
 86            if target == "sel":
 87                cmd_text.append("sel")
 88            elif target == "model":
 89                cmd_text.append(''.join(models))
 90            # else target must be "all":
 91            #   for which we leave off atom specifier completely
 92
 93            # Then "highlight" specific parameters
 94            if command == "highlight":
 95                color = query["color"][0]
 96                cmd_text.append(color)
 97                count = query["count"][0]
 98                cmd_text.extend(["count", count])
 99
100            # Add remaining global options
101            weighted = "weighted" in query
102            cmd_text.extend(["weighted", "true" if weighted else "false"])
103            transformed = "transformed" in query
104            cmd_text.extend(["transformed", "true" if transformed else "false"])
105
106            # Run the command
107            cmd = ' '.join(cmd_text)
108            from chimerax.core.commands import run
109            run(self.session, cmd)
110        else:
111            from chimerax.core.errors import UserError
112            raise UserError("unknown tutorial command: %s" % command)
113
114    def update_models(self, trigger=None, trigger_data=None):
115        # Update the <select> options in the web form with current
116        # list of atomic structures.  Also enable/disable submit
117        # buttons depending on whether there are any structures open.
118
119        # Get the list of atomic structures
120        from chimerax.atomic import AtomicStructure
121        options = []
122        for m in self.session.models:
123            if not isinstance(m, AtomicStructure):
124                continue
125            options.append((m, m.atomspec))
126
127        # Construct Javascript for updating <select> and submit buttons
128        if not options:
129            options_text = ""
130            disabled_text = "true";
131        else:
132            options_text = ''.join(['<option value="%s">%s</option>' % (v, t)
133                                    for t, v in options])
134            disabled_text = "false";
135        import json
136        js = self.JSUpdate % (json.dumps(options_text), disabled_text)
137        self.html_view.runJavaScript(js)
138
139    JSUpdate = """
140document.getElementById("model").innerHTML = %s;
141var buttons = document.getElementsByClassName("submit");
142for (var i = 0; i != buttons.length; ++i) {
143    buttons[i].disabled = %s;
144}
145"""

chimerax.ui.HtmlToolInstance is the base class for simplifying construction of tools with HTML-based graphical interface. When an instance of a subclass of HtmlToolInstance is created, its constructor must call the HtmlToolInstance constructor to set up the graphical interface framework. The arguments to the HtmlToolInstance constructor is the session and the tool name. An optional argument, size_hint, may be supplied to guide the tool layout, but, as the name suggests, it is only a hint and may not be honored. The superclass constructor creates a ChimeraX tool which contains a single widget for displaying an HTML page. The widget is accessible using the html_view attribute, an instance of chimerax.ui.widgets.HtmlView. In this example, the TutorialTool constructor calls its superclass constructor and then its own _build_ui method, which simply constructs the URL to a static HTML file in the bundle Python package and displays it in the widget using self.html_view's setUrl() method.

The HtmlToolInstance class also helps manage threading issues that arise from the way HTML is displayed using PyQt5. The underlying Qt WebEngine machinery uses a separate thread for rendering HTML, so developers need to make sure that code is run in the proper thread. In particular, access to shared data must be synchronized between the Qt main and WebEngine threads. HtmlToolInstance simplifies the issues by calling subclass methods in the main thread when an interesting event occurs in the WebEngine thread.

The HtmlToolInstance constructor checks the derived class for the presence of an attribute, CUSTOM_SCHEME and a method, handle_scheme(). If both are defined, then the base class will arrange for handle_scheme(). to be called (in the main thread) whenever a link matching CUSTOM_SCHEME is followed. In this example, the custom scheme is tutorial (line 31), so when the user clicks on links such as tutorial:cofm and tutorial:highlight (see tool.html below), handle_scheme(). is called with the clicked URL as its lone argument. Currently, the argument is an instance of PyQt5.QtCore.QUrl but that may change later to remove explicit dependency on PyQt. handle_scheme(). is expected to parse the URL and take appropriate action depending on the data. In this example, the URL path is a command name and the query contains data for command arguments. Three command names are supported: update_models(), cofm, and highlight. update_models() is invoked when the page is loaded (see tool.html below) and is handled as special case (see below). For the other commands, known query fields are target, model, color, count, weighted and transformed. The command names and query fields are combined to generate a ChimeraX command string, which is then executed using chimerax.core.commands.run(). The main benefit of executing a command string is automatic display of command and replies in the ChimeraX log.

The HtmlToolInstance class also helps with monitoring the opening and closing of models. If the derived class defines a method named update_models(), the method will be called whenever a new model is opened or an existing model is closed. Note that this is not when a model instance is created or deleted, because transient models that are not shown to the user (opened) do not trigger calls to update_models(). update_models() is typically called with two arguments: the name of the triggering event (either “add models” or “remove models”) and the list of models added or removed. In this example, update_models() is used for updating the HTML drop-down list of models, so only the currently opened models are important, and neither the trigger name nor the models added or removed is relevant. In fact, its arguments are given default values so that update_models() can be called with no arguments when the HTML page is first loaded. Whether called in response to model addition/removal or HTML events, update_models() does the following:

  1. build a list of 2-tuples of (display text, atom_specifier), one for each open model.

  2. convert the list into HTML strings of option elements.

  3. concatenate them into a single HTML text string.

  4. set a string to “true” or “false” depending on whether there are any models open.

  5. combine the HTML text string and the boolean string with a JavaScript template to generate a JavaScript script.

  6. execute the JavaScript script in the HTML widget using self.html_view's runJavaScript() method.

Note the conversion from Python string to JavaScript string is accomplished using json.dumps(), which properly handles special characters such as quotes. The JavaScript template uses standard JavaScript HTML DOM functionality to manipulate the HTML page contents. If executing JavaScript results in errors, the messages should appear in the ChimeraX log.

src/tool.html

tool.html is an HTML 5 file containing the skeleton of the graphical user interface, consisting of a form with multiple elements such as check boxes for boolean options and radio buttons for multiple-choice options. Even more exotic inputs like color selection or date and time are supported in HTML 5 forms.

 1<html>
 2<head>
 3<title>Tutorial: Tool Example</title>
 4</head>
 5<body onload="window.location = 'tutorial:update_models';">
 6<h2>Tutorial: Tool Example</h2>
 7<form method="get">
 8<h4>Global Options</h4>
 9<ul>
10<li><input type="checkbox" name="weighted">Weighted by atomic mass</input></li>
11<li><input type="checkbox" name="transformed" checked="checked">
12    Use scene (transformed) coordinates</input></li>
13<li>
14    <input type="radio" name="target" value="sel" checked="checked">Selected</input>
15    <input type="radio" name="target" value="all">All</input>
16    <input type="radio" name="target" value="model">Structure</input><br/>
17    <select name="model" id="model">
18        <option value="none" selected="selected">No atomic structures open</option>
19    </select>
20</li>
21</ul>
22<input class="submit" type="submit" formaction="tutorial:cofm" disabled="true"
23    value="Report Center of Mass"/>
24<h4>Highlight Options</h4>
25<ul>
26<li><input type="color" name="color" value="#ff0000">Color</input></li>
27<li>Number of atoms (1-5):
28    <input type="number" name="count" value="1" min="1" max="5"/></li>
29</ul>
30<input class="submit" type="submit" formaction="tutorial:highlight" disabled="true"
31    value="Highlight Center of Mass"/>
32</form>
33</body>
34</html>

The name attributes in the HTML form elements correspond to the query field names, and are exactly the same set of query field names expected by handle_scheme() in tool.py.

The select element is the drop-down list that is modified when update_models() runs its generated JavaScript script. To make the element easier to find, it not only has a name attribute, which does not have to be unique among all elements, but also an id attribute, which is (or should be) unique. The JavaScript getElementById function returns a single element, whereas getElementsByName function returns a list of elements.

The two submit buttons are tagged with class name submit so that they can be found using getElementsByClassName. The buttons are enabled or disabled in the same JavaScript script that updates the drop-down list of models.

src/docs/user/commands/tutorial.html

The documentation for the graphical tool should be written in HTML 5 and saved in a file with a suffix of .html. For our example, we named the help file tutorial.html. The location of the help file (relative to src/docs) is expicitly indicated by setting the help attribute of the HtmlToolInstance, as shown on line 31 of tool.py:

28    SESSION_ENDURING = False    # Does this instance persist when session closes
29    SESSION_SAVE = False        # No session saving for now
30    CUSTOM_SCHEME = "tutorial"  # Scheme used in HTML for callback into Python
31    help = "help:user/tools/tutorial.html"
32                                # Let ChimeraX know about our help page
33
34    def __init__(self, session, tool_name):

When help files are included in bundles, documentation for the tools may be displayed using the Help entry of the tool’s context menu, the same as built-in ChimeraX tools. The directory structure is chosen to allow for multiple types of documentation for a bundle. For example, developer documentation such as the bundle API are saved in a devel directory instead of user; documentation for typed commands are saved in user/commands instead of user/tools.

 1<html>
 2
 3<!--
 4=== UCSF ChimeraX Copyright ===
 5Copyright 2018 Regents of the University of California.
 6All rights reserved.  This software provided pursuant to a
 7license agreement containing restrictions on its disclosure,
 8duplication and use.  For details see:
 9http://www.rbvi.ucsf.edu/chimerax/docs/licensing.html
10This notice must be embedded in or attached to all copies,
11including partial copies, of the software or any revisions
12or derivations thereof.
13=== UCSF ChimeraX Copyright ===
14-->
15
16<head>
17<link rel="stylesheet" type="text/css" href="../userdocs.css" />
18<title>Tool: Log</title>
19</head><body>
20
21<a name="top"></a>
22<a href="../index.html">
23<img width="60px" src="../ChimeraX-docs-icon.svg" alt="ChimeraX docs icon"
24class="clRight" title="User Guide Index"/></a>
25
26<h3><a href="../index.html#tools">Tool</a>: Tutorial (HTML)</h3>
27<p>
28The <b>Tutorial (HTML)</b> tool is the graphical interface to
29the <a href="../commands/tutorial.html"><b>tutorial</b></a> commands.
30</p>
31<p>
32The <b>Global Options</b> section is used to
33select which atoms are used for computing the center,
34whether the center calculation is weighted by atomic mass,
35and whether transformed or untransformed coordinates
36are used.  Clicking the <b>Report Center of Mass</b>
37button will report the center coordinates in the Reply Log.
38</p>
39<p>
40The <b>Highlight Options</b> section is used to additionally
41select the number of atoms to color, and the color to apply.
42Clicking the <b>Highlight Center of Mass</b> button
43will set the color of the atoms nearest the center of mass.
44</p>
45
46<hr>
47<address>UCSF Resource for Biocomputing, Visualization, and Informatics / 
48April 2018</address>
49</body></html>

While the only requirement for documentation is that it be written as HTML, it is recommended that developers write tool help files following the above template, with:

  • a banner linking to the documentation index,

  • text describing the tool, and

  • an address for contacting the bundle author.

Note that the target links used in the HTML file are all relative to ... Even though the tool documentation HTML file is stored with the bundle, ChimeraX treats the links as if the file were located in the tools directory in the developer documentation tree. This creates a virtual HTML documentation tree where tool HTML files can reference each other without having to be collected together.

Optional: Session Saving

The current session behavior of our example tool (disappearing/closing) may be fine for some tools, particularly simpler ones. However, some tools may prefer to either stay in existence across a session restore, or to save state in sessions and restore appropriately.

The Models tool is an example of the former behavior. It does not close when a session restores, but instead simply displays the new model information. It saves no state in the session. To achieve this behavior, it just sets the chimerax.core.tools.ToolInstance class attribute SESSION_ENDURING to True. In the above example, changing the SESSION_ENDURING HtmlToolInstance class attribute would have the same effect, since HtmlToolInstance inherits from ToolInstance.

To achieve the latter behavior, you would instead change the SESSION_SAVE class variable to True, and in addition you would implement a couple of additional methods in the TutorialTool class and one in the _MyAPI class. Before we get to the details of that, it would be good to go over how the ChimeraX session-saving mechanism works, so you can have a better understanding of how these new methods are used and should be implemented…

When a session is saved, ChimeraX looks through the session object for attributes that inherit from chimerax.core.state.StateManager. For such attributes it calls their take_snapshot() method and stows the result. One of the state managers in the session is the tool manager. The tool manager will in turn call take_snapshot() on all running tools that inherit from chimerax.core.state.State. (which should be all of them since ToolInstance inherits from State) and stow the result. On restore, the class static method restore_snapshot() is called with the data that take_snapshot() produced, and restore_snapshot() needs to return a restored object.

In practice, take_snapshot() typically returns a dictionary with descriptive key names and associated values of various information that would be needed during restore. Frequently one of the keys is ‘version’ so that restore_snapshot can do the right thing if the format of various session data items changes. The values can be regular Python data (including numpy/tinyarray) or class instances that themselves inherit from State.

restore_snapshot(session, data) uses data to instantiate an object of that class and return it. If it is difficult to form the constructor arguments for the class from the session data, or to completely set the object state via those arguments then you will have to use “two pass” initialization, where you call the constructor in a way that indicates that it is being restored from a session (e.g. passing None to an otherwise mandatory argument) and then calling some method (frequently called set_state_from_snapshot()) to fully initialize the minimally initialized object.

Session restore knows what bundles various classes came from, but not how to get those classes from the bundle so therefore the bundle’s BundleAPI object needs to implement it’s get_class(class_name) static method to return the class object that corresponds to a string containing the class name.

Now, the TutorialTool class doesn’t really have any state that would need to be saved into a session. For the purpose of example, let’s suppose that the tool’s behavior somehow depended on the last command it had issued, and that command was saved in an attribute of TutorialTool named prev_command.

To save/restore the tool and its prev_command attribute, we add take_snapshot() and restore_snapshot() methods to the TutorialTool class, as per the below:

class TutorialTool(HtmlToolInstance):

    # previously implemented parts of the class here...

    def take_snapshot(self, session, flags):
        # For now, the 'flags' argument can be ignored.  In the
        # future, it will be used to distnguish between saving
        # for inclusion in a session vs. inclusion in a scene
        #
        # take_snapshot can actually return any type of data
        # it wants, but a dictionary is usually preferred because
        # it is easy to add to if the tool is later enhanced or
        # modified.  Also, the data returned has to consist of
        # builtin Python types (including numpy/tinyarray
        # types) and/or class instances that derive from State.

        return {
            # The 'version' key not strictly necessary here,
            # but will simplify coding the restore_snapshot
            # method in the future if the format of the
            # data dictionary is changed
            'version': 1,
            'prev_command': self.prev_command
        }

    @classmethod
    def restore_snapshot(class_obj, session, data):
        # This could also be coded as an @staticmethod, in which
        # case you would have to use the actual class name in 
        # lieu of 'class_obj' below
        #
        # 'data' is what take_snaphot returned.  At this time,
        # we have no need for the 'version' key of 'data'
        inst = class_obj(session, "Tutorial (HTML)")
        inst.prev_command = data['prev_command']
        return inst

Finally, for the session-restore code to be able to find the TutorialTool class, we must implement the get_class() static method in our _MyAPI class, like so:

class _MyAPI(BundleAPI):

    # previously implemented parts of the class here...

    @staticmethod
    def get_class(class_name):
        # class_name will be a string
        if class_name == "TutorialTool":
            from . import tool
            return tool.TutorialTool
        raise ValueError("Unknown class name '%s'" % class_name)

Building and Testing Bundles

To build a bundle, start ChimeraX and execute the command:

devel build PATH_TO_SOURCE_CODE_FOLDER

Python source code and other resource files are copied into a build sub-folder below the source code folder. C/C++ source files, if any, are compiled and also copied into the build folder. The files in build are then assembled into a Python wheel in the dist sub-folder. The file with the .whl extension in the dist folder is the ChimeraX bundle.

To test the bundle, execute the ChimeraX command:

devel install PATH_TO_SOURCE_CODE_FOLDER

This will build the bundle, if necessary, and install the bundle in ChimeraX. Bundle functionality should be available immediately.

To remove temporary files created while building the bundle, execute the ChimeraX command:

devel clean PATH_TO_SOURCE_CODE_FOLDER

Some files, such as the bundle itself, may still remain and need to be removed manually.

Building bundles as part of a batch process is straightforward, as these ChimeraX commands may be invoked directly by using commands such as:

ChimeraX --nogui --exit --cmd 'devel install PATH_TO_SOURCE_CODE_FOLDER exit true'

This example executes the devel install command without displaying a graphics window (--nogui) and exits immediately after installation (exit true). The initial --exit flag guarantees that ChimeraX will exit even if installation fails for some reason.

Distributing Bundles

With ChimeraX bundles being packaged as standard Python wheel-format files, they can be distributed as plain files and installed using the ChimeraX toolshed install command. Thus, electronic mail, web sites and file sharing services can all be used to distribute ChimeraX bundles.

Private distributions are most useful during bundle development, when circulation may be limited to testers. When bundles are ready for public release, they can be published on the ChimeraX Toolshed, which is designed to help developers by eliminating the need for custom distribution channels, and to aid users by providing a central repository where bundles with a variety of different functionality may be found.

Customizable information for each bundle on the toolshed includes its description, screen captures, authors, citation instructions and license terms. Automatically maintained information includes release history and download statistics.

To submit a bundle for publication on the toolshed, you must first sign in. Currently, only Google sign in is supported. Once signed in, use the Submit a Bundle link at the top of the page to initiate submission, and follow the instructions. The first time a bundle is submitted to the toolshed, it is held for inspection by the ChimeraX team, which may contact the authors for more information. Once approved, all subsequent submissions of new versions of the bundle are posted immediately on the site.

What’s Next