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:
Create a
bundle_info.xml
containing information about the bundle,Create a Python package that interfaces with ChimeraX and implements the command functionality, and
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 folderbundle_info.xml
- bundle information read by ChimeraXsrc
- source code to Python package for bundle__init__.py
- package initializer and interface to ChimeraXtool.py
- source code to implement theTutorial (HTML)
tooldocs/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:
this bundle depends on the
ChimeraX-UI
andChimeraX-Tutorial_Command
bundles (lines 35-36),non-Python files need to be included in the bundle (lines 40-43), and
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# https://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:
build a list of 2-tuples of (display text, atom_specifier), one for each open model.
convert the list into HTML strings of
option
elements.concatenate them into a single HTML text string.
set a string to “true” or “false” depending on whether there are any models open.
combine the HTML text string and the boolean string with a JavaScript template to generate a JavaScript script.
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:
9https://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 theirtake_snapshot()
method and stows the result. One of the state managers in the session is the tool manager. The tool manager will in turn calltake_snapshot()
on all running tools that inherit fromchimerax.core.state.State
. (which should be all of them sinceToolInstance
inherits fromState
) and stow the result. On restore, the class static methodrestore_snapshot()
is called with the data thattake_snapshot()
produced, andrestore_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 fromState
.
restore_snapshot(session, data)
usesdata
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. passingNone
to an otherwise mandatory argument) and then calling some method (frequently calledset_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’sget_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¶
Bundle Example: Add a Command (previous topic)
Bundle Example: Add a Tool (current topic)
Bundle Example: Read a New File Format (next topic)