Bundle Example: Add a Qt-based Tool

This example describes how to create a ChimeraX bundle that defines a graphical interface showing a text-input field that logs text typed by the user via the appropriate log command.

The ChimeraX user interface is built using PyQt5, which is a Python wrapping of the Qt5 C++ windowing toolkit. Bundle writers can themselves use PyQt5 to provide a graphical interface to their bundle functionality. This example shows how to build a simple graphical interface, and is not meant to cover all the capabilities of Qt in detail (and there are many!). To learn more you should explore PyQt5 tutorials and/or look at the code of other tools that do things similar to what you want your tool to do.

The steps in implementing the bundle are:

  1. Create a bundle_info.xml file 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_qt. 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_qt folder are:

  • tut_tool_qt - 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 (Qt) 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-TutorialToolQt"
 9	    version="0.1" package="chimerax.tut_tool_qt"
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 (Qt)" to log typed user input.
23  </Description>
24
25  <!-- Categories is a list where this bundle should appear -->
26  <Categories>
27    <Category name="General"/>
28  </Categories>
29
30  <!-- Dependencies on other ChimeraX/Python packages -->
31  <!-- This example uses functionality from the Tutorial_Command bundle -->
32  <Dependencies>
33    <Dependency name="ChimeraX-Core" version="~=1.1"/>
34    <Dependency name="ChimeraX-UI" version="~=1.0"/>
35  </Dependencies>
36
37  <!-- Non-Python files that are part of package -->
38  <DataFiles>
39    <DataFile>docs/user/tools/tutorial.html</DataFile>
40  </DataFiles>
41
42  <Classifiers>
43    <!-- Development Status should be compatible with bundle version number -->
44    <PythonClassifier>Development Status :: 3 - Alpha</PythonClassifier>
45    <PythonClassifier>License :: Freeware</PythonClassifier>
46    <!-- ChimeraX classifiers describe supplied functionality -->
47    <!-- Register a graphical interface tool -->
48    <ChimeraXClassifier>ChimeraX :: Tool :: Tutorial (Qt) ::
49      General :: Graphical interface example</ChimeraXClassifier>
50  </Classifiers>
51
52</BundleInfo>

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

  1. this bundle depends on the ChimeraX-UI bundle (line 34),

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

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

The Dependency tag on line 34 informs ChimeraX that the ChimeraX-UI bundle must be present when this bundle is installed. If it is not, it is installed first. The ChimeraX-UI bundle is needed for the chimerax.ui.MainToolWindow class that provides the tool window that will contain our interface elements (see tool.py` below).

The DataFiles tag on lines 38-40 informs ChimeraX to include non-Python files as part of the bundle when building. In this case, docs/user/tools/tutorial.html (implicitly in the src folder) which provides the help documentation for our tool.

The ChimeraXClassifier tag on lines 48-49 informs ChimeraX that there is one graphical interface tool named Tutorial (Qt) in the bundle. The last two fields (separated by ::) are the tool category and the tool description. ChimeraX will add a Tutorial (Qt) 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     # start_tool called with BundleInfo and
12                        # ToolInfo instance (vs. BundleInfo and
13                        # tool name when api_version==0 [the default])
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 ``tool`` module.
28        if ti.name == "Tutorial (Qt)":
29            from . import tool
30            return tool.TutorialTool(session, ti.name)
31        raise ValueError("trying to start unknown tool: %s" % ti.name)
32
33    @staticmethod
34    def get_class(class_name):
35        # class_name will be a string
36        if class_name == "TutorialTool":
37            from . import tool
38            return tool.TutorialTool
39        raise ValueError("Unknown class name '%s'" % class_name)
40
41# Create the ``bundle_api`` object that ChimeraX expects.
42bundle_api = _MyAPI()

In this example, the start_tool() method is overridden to invoke a bundle function, tool.TutorialTool(), when the user selects the Tutorial (Qt) menu item from the General submenu of the Tools menu. (The Tutorial (Qt) 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.

The get_class() method is used by the ChimeraX session-saving mechanism to find needed class objects in the bundle and is discussed in more detail in the Sessions section below.

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 (Qt) menu item from the Tools menu. We will discuss tool.py in sections.

Class Initialization

 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.core.tools import ToolInstance
15
16
17class TutorialTool(ToolInstance):
18
19    # Inheriting from ToolInstance makes us known to the ChimeraX tool mangager,
20    # so we can be notified and take appropriate action when sessions are closed,
21    # saved, or restored, and we will be listed among running tools and so on.
22    #
23    # If cleaning up is needed on finish, override the 'delete' method
24    # but be sure to call 'delete' from the superclass at the end.
25
26    SESSION_ENDURING = False    # Does this instance persist when session closes
27    SESSION_SAVE = True         # We do save/restore in sessions
28    help = "help:user/tools/tutorial.html"
29                                # Let ChimeraX know about our help page
30

Our TutorialTool class inherits from chimerax.core.tools.ToolInstance, which makes it known to the ChimeraX tool manager, and it will thereby work correctly in all the generic ways that tools work, such as being displayed by the command tool show.

By declaring SESSION_ENDURING as False, we are telling ChimeraX’s session handling that this tool should be closed/destroyed when a session is closed. SESSION_SAVE = True tells session handling that this tool will save state into sessions and should be restored by sessions. This is discussed further in the Sessions section below. Lastly, setting the class variable help informs the ChimeraX help system where the help documentation for this tool can be found, and is discussed in more detail in the Help Documentation section.

Instance Initialization

31    def __init__(self, session, tool_name):
32        # 'session'   - chimerax.core.session.Session instance
33        # 'tool_name' - string
34
35        # Initialize base class.
36        super().__init__(session, tool_name)
37
38        # Set name displayed on title bar (defaults to tool_name)
39        # Must be after the superclass init, which would override it.
40        self.display_name = "Tutorial — Qt-based"
41
42        # Create the main window for our tool.  The window object will have
43        # a 'ui_area' where we place the widgets composing our interface.
44        # The window isn't shown until we call its 'manage' method.
45        #
46        # Note that by default, tool windows are only hidden rather than
47        # destroyed when the user clicks the window's close button.  To change
48        # this behavior, specify 'close_destroys=True' in the MainToolWindow
49        # constructor.
50        from chimerax.ui import MainToolWindow
51        self.tool_window = MainToolWindow(self)
52
53        # We will be adding an item to the tool's context menu, so override
54        # the default MainToolWindow fill_context_menu method
55        self.tool_window.fill_context_menu = self.fill_context_menu
56
57        # Our user interface is simple enough that we could probably inline
58        # the code right here, but for any kind of even moderately complex
59        # interface, it is probably better to put the code in a method so
60        # that this __init__ method remains readable.
61        self._build_ui()
62

Our TutorialTool class constructor is called with session and tool_name arguments, because that is how we called it from the start_tool() method of our _MyAPI class:

            from . import tool
            return tool.TutorialTool(session, ti.name)

On line 36, we call our superclass (chimerax.core.tools.ToolInstance) constructor. It also takes a session and tool name as arguments, which is one of the principal reasons we passed those arguments to our own constructor. The chimerax.core.tools.ToolInstance constructor sets its session attribute to be the same as the passed-in session, so in other parts of our code we can refer to the session with self.session.

On lines 50 and 51, we create our MainToolWindow instance, which will contain our user interface. The window will not actually be shown until we call its manage() method, as discussed in the following Interface Construction section.

Every tool in ChimeraX has a context menu, which will at least contain some generically useful tool actions (e.g. Hide Tool). To add additional tool-specific items to the context menu, we must override MainToolWindow’s fill_context_menu() method (by default a no-op) with our own routine to add our custom menu items, as discussed in more detail in the Context Menu section. On line 55 we override that default fill_context_menu() with self.fill_context_menu().

Lastly, on line 61 we call a routine to fill out our user interface, discussed in the next section.

Interface Construction

63    def _build_ui(self):
64        # Put our widgets in the tool window
65
66        # We will use an editable single-line text input field (QLineEdit)
67        # with a descriptive text label to the left of it (QLabel).  To
68        # arrange them horizontally side by side we use QHBoxLayout
69        from Qt.QtWidgets import QLabel, QLineEdit, QHBoxLayout
70        layout = QHBoxLayout()
71        layout.addWidget(QLabel("Log this text:"))
72        self.line_edit = QLineEdit()
73
74        # Arrange for our 'return_pressed' method to be called when the
75        # user presses the Return key
76        self.line_edit.returnPressed.connect(self.return_pressed)
77        layout.addWidget(self.line_edit)
78
79        # Set the layout as the contents of our window
80        self.tool_window.ui_area.setLayout(layout)
81
82        # Show the window on the user-preferred side of the ChimeraX
83        # main window
84        self.tool_window.manage('side')
85
86    def return_pressed(self):
87        # The use has pressed the Return key; log the current text as HTML
88        from chimerax.core.commands import run
89        # ToolInstance has a 'session' attribute...
90        run(self.session, "log html %s" % self.line_edit.text())
91

The _build_ui() method adds our user interface widgets to the tool window and causes the tool window to be shown. PyQt5 is the windowing toolkit used by ChimeraX. It is a Python wrapping of the (C++) Qt5 toolkit. This tutorial is in no way meant to also be a PyQt5/Qt5 tutorial (since those toolkits are very extensive) but merely shows how to use those toolkits in the context of ChimeraX. To gain additional familarity with those toolkits, there are PyQt5 tutorials and Qt5 tutorials available on the web.

On line 69 we import the widgets will need for our interface from the PyQt5 toolkit:

  • A text-label widget (QLabel)

  • An editable single-line text entry field (QLineEdit)

  • A “metawidget” for laying out the above two widgets side by side (QHBoxLayout; “HBox” == “horizontal box”)

Line 70 creates our horizontal layout metawidget, and line 71 creates and adds the label we want next to our entry field to it. Note that by default widgets added to an QHBoxLayout will be ordered left to right. Line 72 creates our text-entry field and line 77 adds it to out layout.

Changes in widgets that the containing interface may care about cause the widget to emit what Qt refers to as a “signal”. returnPressed is the signal that QLineEdit emits when the users presses the Return key. A signal’s connect() method is the way to get a particular routine to be called when the signal is emitted, which we have done on line 76 to get our return_pressed() method called when the returnPressed signal is emitted.

Lines 86-90 is our handler for the returnPressed signal. Some signals also have arguments (detailed in each widget’s signal documentation), but the returnPressed signal has no arguments, so therefore our handler has no non-self arguments. The handler imports the run() utility command that runs a text string as a ChimeraX command, and then calls that routine with the session and the appropriate log command, formed based on the current text in the line editor (i.e. self.line_edit.text()).

We have created both our widgets and added them to the layout. Line 80 installs our layout as the layout for the user-interface area of our tool window (the user-interface area is in fact an instance of QWidget).

Line 84 calls our tool window’s manage() method to cause the tool window to be displayed. The argument to manage() specifies the general position of the tool window, with possible values of:

“side”

The user’s preferred side of the main window for tools (specified in Window preferences)

“left” / “right” / “top” / “bottom”

A specific side of the main window. Normally, honoring the user’s preference with “side” is preferred, but some tools may work best at the top or bottom of the main window for example.

None

The window should start out “floating”, not docked into the ChimeraX main window.

Some tools may use multiple windows (created via the MainToolWindow’s create_child_window() method), and for those tools another possible value for manage() is another tool window (typically the tool’s main window), in which case the tool window will start out tabbed with the other window.

Context Menu

 92    def fill_context_menu(self, menu, x, y):
 93        # Add any tool-specific items to the given context menu (a QMenu instance).
 94        # The menu will then be automatically filled out with generic tool-related actions
 95        # (e.g. Hide Tool, Help, Dockable Tool, etc.) 
 96        #
 97        # The x,y args are the x() and y() values of QContextMenuEvent, in the rare case
 98        # where the items put in the menu depends on where in the tool interface the menu
 99        # was raised.
100        from Qt.QtGui import QAction
101        clear_action = QAction("Clear", menu)
102        clear_action.triggered.connect(lambda *args: self.line_edit.clear())
103        menu.addAction(clear_action)
104

ChimeraX will create a context menu for every tool, populated with generically useful tool actions, such as Dockable Tool (to control whether the tool can be docked into the main window). To add custom items to this context we have to override the MainToolWindow’s default implementation of fill_context_menu() (which does nothing) with our own implementation, which we did on line 55:

53        # We will be adding an item to the tool's context menu, so override
54        # the default MainToolWindow fill_context_menu method
55        self.tool_window.fill_context_menu = self.fill_context_menu

Our overriding routine is shown on lines 92-103. The routine is invoked with three arguments:

menu

A QMenu instance that we will add our custom menu items to. It is not yet populated with the generic menu items.

x and y

The x and y position of the click that is bringing up the context menu, relative to the entire user-interface area (self.toolwindow.ui_area). These arguments are only used in the rare case where the contents of the context menu depend on exactly where in the tool the user clicked. These values are the x() and y() methods of the QContextMenuEvent that is bringing up this menu.

Qt abstracts actions on widgets (such as button clicks and menu selections) with its QAction class. In order to add a Clear item to the menu which will clear the text in the input field, we import the QAction class on line 100 and create an instance of it with the text “Clear”, and associated with the context menu, on line 101.

When the action encapsulated by a QAction occurs, its triggered signal is emitted (in a similar fashion to the returnPressed signal in the Interface Construction section above). We arrange for our text-input field to be cleared by connecting an anonymous lambda function (that calls self.line_edit.clear()) to the triggered signal, shown on line 102. The triggered signal does provide an argument (which the lambda uses *args to ignore) indicating whether the item is checked on or off. That isn’t relevant in our case because we haven’t made our menu item “checkable”. But you may want to add “checkable” menu items in some cases. To do so, use QAction’s setCheckable method with a value of True to make it checkable and then set its initial checked/unchecked state with the setChecked method, with the appropriate boolean argument.

We actually add the action/item to the menu on line 103.

Help Documentation

All tools will have a Help context-menu entry, one of the “generic” context-menu items that ChimeraX adds to all tool context menus. The Help menu item will be disabled unless the tool specifies that it provides help by setting the help attribute of the ToolInstance instance that it creates. We did do this on line 28:

26    SESSION_ENDURING = False    # Does this instance persist when session closes
27    SESSION_SAVE = True         # We do save/restore in sessions
28    help = "help:user/tools/tutorial.html"
29                                # Let ChimeraX know about our help page
30
31    def __init__(self, session, tool_name):

The string we set the help attribute to is an URL. The “help:” prefix tells ChimeraX to use its built-in help system to locate the help page. It could instead have been “http:” to have the help page found on web, but this is typically not recommended since it is best to have the help documentation match the actual installed version of the tool, and also to allow help to be accessed even if the user doesn’t currently have Internet connectiity.

The remainder of the string after “help:” is the actual location of the help page, relative to the package’s src/docs folder. 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.

As for the actual contents of the help file…

src/docs/user/tools/tutorial.html
<html>

<!--
=== UCSF ChimeraX Copyright ===
Copyright 2018 Regents of the University of California.
All rights reserved.  This software provided pursuant to a
license agreement containing restrictions on its disclosure,
duplication and use.  For details see:
http://www.rbvi.ucsf.edu/chimerax/docs/licensing.html
This notice must be embedded in or attached to all copies,
including partial copies, of the software or any revisions
or derivations thereof.
=== UCSF ChimeraX Copyright ===
-->

<head>
<link rel="stylesheet" type="text/css" href="../userdocs.css" />
<title>Tool: Tutorial (Qt)</title>
</head><body>

<a name="top"></a>
<a href="../index.html">
<img width="60px" src="../ChimeraX-docs-icon.svg" alt="ChimeraX docs icon"
class="clRight" title="User Guide Index"/></a>

<h3><a href="../index.html#tools">Tool</a>: Tutorial (Qt)</h3>
<p>
The <b>Tutorial (Qt)</b> tool demonstrates how to
create a graphical tool interface using the
<a href="https://wiki.qt.io/Qt_for_Python">Qt for Python</a> (PySide2)
toolkit.
</p>
<p>
The tool displays a type-in field.  When the user hits the Return key,
any text in the field will be shown in the log as its HTML equivalent.
That means that the text:
<pre>
	&lt;a href="http://www.cgl.ucsf.edu/chimerax"&gt;ChimeraX home page&lt;/a&gt;
</pre>
will be logged as:
<br><br>
	<a href="http://www.cgl.ucsf.edu/chimerax">ChimeraX home page</a>
</p>
<p>
Choosing <b>Clear</b> from the tool context menu will clear the text field
</p>

<hr>
<address>UCSF Resource for Biocomputing, Visualization, and Informatics / 
February 2019</address>
</body></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.

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.

Sessions

As mentioned briefly earlier, the behavior of our tool when sessions are closed, saved, or restored is control by the boolean attributes SESSION_ENDURING and SESSION_SAVE, which we set on lines 26 and 27:

25
26    SESSION_ENDURING = False    # Does this instance persist when session closes
27    SESSION_SAVE = True         # We do save/restore in sessions
28    help = "help:user/tools/tutorial.html"

Tools that set SESSION_ENDURING to True will not be closed when a session is closed (restoring a session implicitly closes the existing session). This behavior can be appropriate for widely used tools with no particular state to save — such as the Model Panel, which treats the models closed and opened by the session restore in the same fashion as other model closures and openings. Our tool does save state (the current text of the input field), so we set SESSION_ENDURING to False.

Tools that set SESSION_SAVE to True will have their state saved in sessions and need to implement a couple of additional methods in the ToolInstance class and one in the BundleAPI 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.

Our implementation of the take_snapshot() and restore_snapshot() methods are on lines 105 to 119:

105    def take_snapshot(self, session, flags):
106        return {
107            'version': 1,
108            'current text': self.line_edit.text()
109        }
110
111    @classmethod
112    def restore_snapshot(class_obj, session, data):
113        # Instead of using a fixed string when calling the constructor below, we could
114        # have saved the tool name during take_snapshot() (from self.tool_name, inherited
115        # from ToolInstance) and used that saved tool name.  There are pros and cons to
116        # both approaches.
117        inst = class_obj(session, "Tutorial (Qt)")
118        inst.line_edit.setText(data['current text'])
119        return inst

The take_snapshot() method forms and returns a dictionary encapsulating the tool state. It has two keys:

version

An integer indicating what “version” of the state dictionary it is. This key is not used currently during the restore, but if the format of the state dictionary is ever changed, it may be useful to use the version key to distinguish between the different formats and restore appropriately.

current text

The text in the input field as the session is saved.

Note that the take_snapshot() method could return any type of data, but a dictionary is very flexible, in case additional state needs to be stored in later versions of the tool. In the rare event that you have to make a change to the data that is not backwards compatible with previous versions of the bundle, you will need to bump the maxSessionVersion number in the BundleInfo tag of your bundle’s bundle_info.xml file. Similarly, if your bundle can no longer handle session data from old versions of your bundle, you would need to increase the minSessionVersion.

The flags argument of take_snapshot() can be ignored. It is intended for use in the future to distinguish between snapshots saved for sessions vs. those saved for scenes.

The restore_snapshot() class method constructs an instance of TutorialTool, and then sets the text in the instance’s input field to what was saved in the session, and then returns the instance.

Note that restore_snapshot() could have been coded as a static method (and therefore would not receive a class_obj argument), in which case you would have to use the actual class name in the constructor call.

Lastly, 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:

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

get_class() is passed the needed class name as a string, and finds and returns the corresponding class object. get_class() only needs to handle classes that will be saved in sessions, not other bundle classes, and should throw an error if it gets a string that doesn’t match a class name it expects to be involved in session saving/restoring.

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