Difference between revisions of "Plugins Tutorial"

From PyMOLWiki
Jump to: navigation, search
(global dialog)
 
(8 intermediate revisions by 2 users not shown)
Line 1: Line 1:
==Installing Plugins==
+
This tutorial is about writing your own plugin for PyMOL 2.x.
To install a plugin, simply save the plugin file and load it into PyMol with the ''Plugin -> Install Plugin...'' menu item. Alternatively, plugins can be installed by copying the file into the PYMOLPATH/module/pmg_tk/startup/ folder.
 
  
 +
See the [[plugins]] page for how to install and use exisiting plugins.
  
 
==Writing Plugins: Learn By Example==
 
==Writing Plugins: Learn By Example==
This tutorial is a more sophisticated version of the [[PDB Loader Service]] plugin that is bundled with PyMol. It is a relatively simple plugin, but should prove to be a good starting point for developing new plugins.
+
 
 +
This tutorial shows how to write a PyMOL plugin with PyQt. '''The full source of the demo plugin is [https://github.com/Pymol-Scripts/pymol2-demo-plugin available on github]'''.
 +
 
 +
The demo plugin adds a dialog to render images at a custom resolution.
 +
 
 +
=== Plugin Files ===
 +
 
 +
We will create a plugin which consists of [[PluginArchitecture#More than one file per plugin|multiple files inside a directory]]:
 +
 
 +
<source lang="Python">
 +
pymol2-demo-plugin/
 +
├── demowidget.ui
 +
└── __init__.py (defines "__init_plugin__(app=None)" function)
 +
</source>
  
 
===Registering your Plugin===
 
===Registering your Plugin===
First you must add your plugin to the ''Plugins'' menu. This is most easily done in the ''__init__'' method of your plugin. A callback, fetchPDBDialog, is added.  It is passed a reference to the main Tk app.  It will need this in order to create new content in the main Tkinter loop.  It is not strictly required in this example, but is needed in more complex interfaces.
+
 
 +
First you must add your plugin to the ''Plugins'' menu.
 +
This is done in the <code>__init_plugin__</code> function of your plugin.
 +
A callback (here: <code>run_plugin_gui</code>) is added.
  
 
<source lang="python">
 
<source lang="python">
def __init__(self):
+
# file pymol2-demo-plugin/__init__.py
     self.menuBar.addmenuitem('Plugin', 'command',
+
def __init_plugin__(app=None):
                            'PDB Loader Service',
+
     from pymol.plugins import addmenuitemqt
                            label = 'PDB Loader Service',
+
    addmenuitemqt('Demo "Render" Plugin', run_plugin_gui)
                            command = lambda s=self : fetchPDBDialog(s))
 
 
</source>
 
</source>
  
===Adding Functionality===
+
''Legacy note'': The <code>__init_plugin__</code> function takes one argument, a reference to the main Tk app to [[PluginArchitecture#init_plugin|support legacy plugins witten with Tkinter]] (unused with PyQt plugins).
For this example we are going to dynamically download and load a pdb structure into the pymol interface.  Here is a simple method which does that given a pdbCode.
+
 
 +
=== Creating a GUI ===
 +
 
 +
The callback may do arbitrary stuff. Here we're going to create a dialog and show it to the user.
  
 
<source lang="python">
 
<source lang="python">
def remote(pdbCode):
+
# global reference to avoid garbage collection of our dialog
     pdbCode = pdbCode.upper()
+
dialog = None
     try:
+
 
      pdbFile = urllib2.urlopen('http://www.rcsb.org/pdb/cgi/export.cgi/' +
+
def run_plugin_gui():
                                pdbCode + '.pdb.gz?format=PDB&pdbId=' +
+
     # pymol.Qt provides the PyQt5 interface, but may support PyQt4 and/or PySide as well
                                pdbCode + '&compression=gz')
+
     from pymol.Qt import QtWidgets
      cmd.read_pdbstr(zlib.decompress(pdbFile.read()[22:], -zlib.MAX_WBITS), pdbCode)
+
 
     except:
+
    global dialog
      print "Unexpected error:", sys.exc_info()[0]
+
 
      tkMessageBox.showerror('Invalid Code',
+
     if dialog is None:
                              'You entered an invalid pdb code:' + pdbCode)
+
        # create a new (empty) Window
 +
        dialog = QtWidgets.QDialog()
 +
 
 +
        # TODO: FILL DIALOG WITH WIDGETS HERE
 +
 
 +
    dialog.show()
 
</source>
 
</source>
  
===Creating the Interface===
+
=== Filling the GUI with Widgets ===
Now we need to write the callback method, ''fetchPDBDialog'', which creates the interface.
+
 
 +
[[Image:designer-demo-plugin.png|thumb|right|400px|Screenshot of Qt Designer]]
 +
 
 +
The most convenient and maintainable way to create user interfaces with Qt is by using the [http://doc.qt.io/qt-5/qtdesigner-manual.html Qt Designer].
 +
Follow these steps to get started:
 +
 
 +
# Create a new "Widget" form (New Form &gt; templates/forms &gt; Widget &gt; Create)
 +
# Drag widgets like "Push Button" or "Line Edit" into your form
 +
# Name your widgets ("objectName" in the "Property Editor"), this name will become a Python variable in your code
 +
# Save as a UI file (<code>*.ui</code>) inside the <code>pymol2-demo-plugin</code> directory
 +
 
 +
PyMOL provides a utility function to load a UI file into a parent widget: <code>pymol.Qt.utils.loadUi(filename, widget)</code>
 +
 
 +
<syntaxhighlight lang="python">
 +
    # filename of our UI file
 +
    uifile = os.path.join(os.path.dirname(__file__), 'demowidget.ui')
 +
 
 +
    # load the UI file into our dialog
 +
    from pymol.Qt.utils import loadUi
 +
    form = loadUi(uifile, dialog)
 +
</syntaxhighlight>
 +
 
 +
=== Make Buttons do something ===
 +
 
 +
We need to connect the [http://doc.qt.io/qt-5/signalsandslots.html signals] of those widgets we created with the Qt Designer, like our buttons' <code>clicked</code> signal.
 +
Our example has a "Ray" button (''objectName=button_ray'') that we'll connect to the <code>run()</code> function.
  
<source lang="python">
+
<syntaxhighlight lang="python">
def fetchPDBDialog(app):
+
    def run():
    pdbCode = tkSimpleDialog.askstring('PDB Loader Service',
+
        # get form data
                                      'Please enter a 4-digit pdb code:',
+
        height = form.input_height.value()
                                      parent=app.root)
 
    remote(pdbCode)
 
</source>
 
  
Pretty simple!  As of now you have a fully functional plugin that will prompt the user for a pdb code, download it from [http://www.rcsb.org/pdb/ RCSB], and load it into the PyMol interface.
+
        # some debugging feedback
 +
        print('User entered height', height)
  
===Extending Plugins to the Command Line===
+
        # TODO: DO SOMETHING WITH FORM DATA
Opening the dialog can be tedious, so let's add a command line callback to make use of this new functionality.
 
  
<source lang="python">
+
    form.button_ray.clicked.connect(run)
cmd.extend('remote', remote)
+
</syntaxhighlight>
</source>
 
  
Now you can type ''remote 1di9'', for example, to load the the corresponding pdb.
+
=== Deploy the final plugin ===
  
==Full Source==
+
The <code>pymol2-demo-plugin</code> directory can be zipped for deployment (see [[PluginArchitecture#More than one file per plugin]]).
Here is the complete example.  You can save this file and load it into PyMol with the ''Plugin -> Install Plugin...'' menu item.  I also include the sample license that Warren provides for plugin developers. It is optional, of course.
 
  
<source lang="python">
+
=== Full Source ===
# Copyright Notice
 
# ================
 
#
 
# The PyMOL Plugin source code in this file is copyrighted, but you can
 
# freely use and copy it as long as you don't change or remove any of
 
# the copyright notices.
 
#
 
# ----------------------------------------------------------------------
 
# This PyMOL Plugin is Copyright (C) 2004 by Charles Moad <cmoad@indiana.edu>
 
#
 
#                        All Rights Reserved
 
#
 
# Permission to use, copy, modify, distribute, and distribute modified
 
# versions of this software and its documentation for any purpose and
 
# without fee is hereby granted, provided that the above copyright
 
# notice appear in all copies and that both the copyright notice and
 
# this permission notice appear in supporting documentation, and that
 
# the name(s) of the author(s) not be used in advertising or publicity
 
# pertaining to distribution of the software without specific, written
 
# prior permission.
 
#
 
# THE AUTHOR(S) DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
 
# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS.  IN
 
# NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY SPECIAL, INDIRECT OR
 
# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF
 
# USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
 
# OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
 
# PERFORMANCE OF THIS SOFTWARE.
 
# ----------------------------------------------------------------------
 
  
import tkSimpleDialog
+
The full source of the demo plugin is [https://github.com/Pymol-Scripts/pymol2-demo-plugin available on github]'''.
import tkMessageBox
 
from pymol import cmd
 
import sys, urllib2, zlib
 
  
def __init__(self):
+
== Extending Plugins to the Command Line ==
  self.menuBar.addmenuitem('Plugin', 'command',
 
                            'PDB Loader Service',
 
                            label = 'PDB Loader Service',
 
                            command = lambda s=self : fetchPDBDialog(s))
 
  
def remote(pdbCode):
+
While not really applicable to our simple "Render" plugin (it only wraps existing <code>ray</code> and <code>png</code> commands), most plugins should also provide their functionality as new PyMOL commands using [[extend|cmd.extend()]].
  pdbCode = pdbCode.upper()
 
  try:
 
      pdbFile = urllib2.urlopen('http://www.rcsb.org/pdb/cgi/export.cgi/' +
 
                                pdbCode + '.pdb.gz?format=PDB&pdbId=' +
 
                                pdbCode + '&compression=gz')
 
      cmd.read_pdbstr(zlib.decompress(pdbFile.read()[22:], -zlib.MAX_WBITS), pdbCode)
 
  except:
 
      print "Unexpected error:", sys.exc_info()[0]
 
      tkMessageBox.showerror('Invalid Code',
 
                            'You entered an invalid pdb code:' + pdbCode)
 
  
def fetchPDBDialog(app):
+
== See Also ==
  pdbCode = tkSimpleDialog.askstring('PDB Loader Service',
 
                                      'Please enter a 4-digit pdb code:',
 
                                      parent=app.root)
 
 
 
  remote(pdbCode)
 
  
cmd.extend('remote', remote)
+
* [[PluginArchitecture]]
</source>
+
* [[Plugin Manager]]
 +
* [[Plugins]]
 +
* [[extend|cmd.extend()]]
  
 
[[Category:Developers|Plugins_Tutorial]]
 
[[Category:Developers|Plugins_Tutorial]]
 
[[Category:Plugins|Plugins_Tutorial]]
 
[[Category:Plugins|Plugins_Tutorial]]
 
[[Category:Tutorials]]
 
[[Category:Tutorials]]

Latest revision as of 04:09, 14 June 2019

This tutorial is about writing your own plugin for PyMOL 2.x.

See the plugins page for how to install and use exisiting plugins.

Writing Plugins: Learn By Example

This tutorial shows how to write a PyMOL plugin with PyQt. The full source of the demo plugin is available on github.

The demo plugin adds a dialog to render images at a custom resolution.

Plugin Files

We will create a plugin which consists of multiple files inside a directory:

pymol2-demo-plugin/
├── demowidget.ui
└── __init__.py (defines "__init_plugin__(app=None)" function)

Registering your Plugin

First you must add your plugin to the Plugins menu. This is done in the __init_plugin__ function of your plugin. A callback (here: run_plugin_gui) is added.

# file pymol2-demo-plugin/__init__.py
def __init_plugin__(app=None):
    from pymol.plugins import addmenuitemqt
    addmenuitemqt('Demo "Render" Plugin', run_plugin_gui)

Legacy note: The __init_plugin__ function takes one argument, a reference to the main Tk app to support legacy plugins witten with Tkinter (unused with PyQt plugins).

Creating a GUI

The callback may do arbitrary stuff. Here we're going to create a dialog and show it to the user.

# global reference to avoid garbage collection of our dialog
dialog = None

def run_plugin_gui():
    # pymol.Qt provides the PyQt5 interface, but may support PyQt4 and/or PySide as well
    from pymol.Qt import QtWidgets

    global dialog

    if dialog is None:
        # create a new (empty) Window
        dialog = QtWidgets.QDialog()

        # TODO: FILL DIALOG WITH WIDGETS HERE

    dialog.show()

Filling the GUI with Widgets

Screenshot of Qt Designer

The most convenient and maintainable way to create user interfaces with Qt is by using the Qt Designer. Follow these steps to get started:

  1. Create a new "Widget" form (New Form > templates/forms > Widget > Create)
  2. Drag widgets like "Push Button" or "Line Edit" into your form
  3. Name your widgets ("objectName" in the "Property Editor"), this name will become a Python variable in your code
  4. Save as a UI file (*.ui) inside the pymol2-demo-plugin directory

PyMOL provides a utility function to load a UI file into a parent widget: pymol.Qt.utils.loadUi(filename, widget)

    # filename of our UI file
    uifile = os.path.join(os.path.dirname(__file__), 'demowidget.ui')

    # load the UI file into our dialog
    from pymol.Qt.utils import loadUi
    form = loadUi(uifile, dialog)

Make Buttons do something

We need to connect the signals of those widgets we created with the Qt Designer, like our buttons' clicked signal. Our example has a "Ray" button (objectName=button_ray) that we'll connect to the run() function.

    def run():
        # get form data
        height = form.input_height.value()

        # some debugging feedback
        print('User entered height', height)

        # TODO: DO SOMETHING WITH FORM DATA

    form.button_ray.clicked.connect(run)

Deploy the final plugin

The pymol2-demo-plugin directory can be zipped for deployment (see PluginArchitecture#More than one file per plugin).

Full Source

The full source of the demo plugin is available on github.

Extending Plugins to the Command Line

While not really applicable to our simple "Render" plugin (it only wraps existing ray and png commands), most plugins should also provide their functionality as new PyMOL commands using cmd.extend().

See Also