Please read the Data Classes documentation first.
If you have implemented a wrapper or pipeline in the ccp4i2 wrappers or pipelines directories then a basic gui can be autogenerated from the def file and will be accessible from the 'Wrappers' section at the bottom of the list of jobs. Note that if the wrapper uses some data object that does not have a corresponding widget implelemented then that data will not appear in the gui and you need to ask Liz to fix that. To create a better gui you need to implement a Python script called tasks/mytask/CTaskmytask.py as a subclass of CTaskWidget from qtgui/CCP4TaskWidget. See < a href="../../tasks/buccaneer_build_refine/CTaskbuccaneer_build_refine.py">tasks/buccaneer_build_refine/CTaskbuccaneer_build_refine.py
for an example.Also look at < a href="../../tasks/demo/CTaskdemo.py">tasks/demo/CTaskdemo.py does not interface to any real wrapper but is used to test and demo gui features.Note that if there is an error in the Python file which prevents it being imported when CCP4i2 starts then this is reported to the terminal and the Program print log (under Utilities pull-down menu) and the task will not appear on the task menu. Any other errors will be reported to the same places when you try to open the task widget.
The task gui is split into 'folders' or 'tabs'; these two modes depend on the option under Preferences but do not require any alternative coding by the application programmer. The top folder is Input Data which should contain all of the required input files or data. Ideally a user should be able to run the program without looking at any other folder. There are currently a couple of features in the Input Data folder that are added by the core system:
The 'follow from' features should be switched off ( CTaskWidget.AUTOPOPULATEINPUT = False ) if the the task is only importing data. Note that the application should not have any jobTitle parameter that would be redundant with the jobTitle parameter handled by the core. The parameters managed by the core are recorded in the task container in a sub-container named guiParams and appear at the bottom of the params files.
The 'follow from' feature is implemented by the core system but, for the appropriate input files, the qualifier fromPreviousJob must be set True. There is no 'Output Data' folder - the output file names are set automatically and the user can 'export' files after the job has completed. Also there is no 'Protocol' folder (as ther was in CCP4i) so some options that may have gone in the 'Protocol' folder may now be in the 'Input data' folder. There is usually an Options folder for basic options and then there may be one of more specific advanced options folders.
Note that the initial opening of a complex task in the gui can take significant time and a way to speed this up is to only draw the 'hidden' task frames when the user opens them - see speedup below.
Minimal code to create a new task widget:
import CCP4TaskWidget class CTaskBuccaneer(CCP4TaskWidget.CTaskWidget): TASKNAME = 'buccaneer' TASKVERSION = 0.0 TASKMODULE='model_building' TASKTITLE='Buccaneer automated model building' SHORTTASKTITLE='Buccaneer' WHATNEXT = ['coot_rebuild','refmac_martin','buccaneer_build_refine'] MGDISPLAYFILES = ['XYZOUT'] DESCRIPTION = '''Do several cycles of alternate model building with Buccaneer and refinement with Refmac''' def __init__(self,parent): CCP4TaskWidget.CTaskWidget.__init__(self,parent) def drawContents(self): pass
The essential attributes for the sub-class
Other possible attributes:
The drawContents() method should specify the layout of the gui using the library of methods documented below.
CTaskWidget method | Argument | Description |
---|---|---|
openFolder | Start definition of a tab or folder | |
folderFunction | Default is 'general' - should be 'inputData' for first folder | |
title | A short title to appear on folder tab or title bar | |
toggle | A list of parameters for controlling visibility of the folder - see below | |
toggleFunction | A list of parameters for controlling visibility of the folder - see below | |
closeFolder | End definition of a tab or folder | |
createLine | Define content on one line of the gui - returns a Qt QFrame | |
defintion | list of strings that are keywords and parameters to define line content (see below) | |
toggle | A list of parameters for controlling visibility of the line - see below | |
toggleFunction | A list of parameters for controlling visibility of the line - see below | |
appendLine | A preceeding line (as returned by createLine()) to which this 'line' is appended | |
openSubFrame | Start definition of lines that are grouped for purpose of toggling visibility or creating a frame. Returns a Qt QFrame. | |
frame | default False - if true draw a frame around the sub-frame | |
toggle | A list of parameters for controlling visibility of the sub-frame - see below | |
toggleFunction | A list of parameters for controlling visibility of the folder - see below | |
closeSubFrame | End definition of group pf lines | |
openStack | Open definition of a stack of two or more lines of which only one will be displayed | |
controlVar | Name of variable controlling which line in the stack is visible. This variable is probable presented as radio buttons in the preceeding line of the gui. | |
closeStack | End definition of a stack | |
createRadioGroup | Create set of radio buttons aranged one per line | |
label | A string label | |
itemList | A list of string labels for each radio button | |
objectName | The name of the parameter that is controlled by the radio buttons - expected to have enumerators qualifer that is list same length as itemList | |
setHelpFile | Name of help file linked to by all widgets defined after a call to this method | |
helpFile | The name of a file that is expected to be found in $CCP4_TOP/docs/tasks | |
whatNext | This is a module level function that returns a list of suggested next tasks. | |
jobId | The id of a finished job that is an instance of this task | |
The following methods may be needed to define some more dynamic behaviour.
CTaskWidget method | Argument | Description |
---|---|---|
visibleFolder | Return the index of the curently visible 'folder' - only relevant in tab mode. | |
setVisibleFolder | Set which 'folder' is visible (only relevant in tab mode) | |
title | title of 'folder' | |
index | index of folder (only one of these arments should be used) | |
getContainer | Return the instance of CContainer which holds the task parameters | |
getWidget | Return the widget for the named parameter | |
name | Parameter name | |
project | Return the name of the project of the job open in the task window | |
jobId | Return the id of the job (an integer) of the job open in the task window | |
getParams | Return python dict of values of parameters currently set in the gui | |
paramsValues | A Python dict whose keys are the names of the parameters whose values are required. | |
setParams | Set gui widget values to reflect the input parameters | |
paramsValues | A Python dict of values to be set in the gui | |
updateViewFromModel | refresh the gui from the parameters in the task container | |
validate | validate and mark any invalid parameters in the gui | |
isValid | Return a list of any invalid parameters in the gui | |
createWidget | Return an instance of the appropriate widget for a named parameter in the task container | |
name | Parameter name | |
widgetQualifiers | a Python dict of qualifying parameters for the widget |
The definition argument to createLine() is a list of strings which consist of keywords followed by a fixed number of arguments. Note that a 'line' may be a very fat line containing a complex widget - in this case there should probably be no other labels or widgets specified in the line.
keyword | Argument | Description |
---|---|---|
help | A help link applied to all following widgets in the line | |
target | The target part of the link - complements the file specified by setHelpFile() | |
tip | Tooltip to apply to all following widgets in the line | |
tip | A short text string | |
message | Alternative keyword for 'tip' | |
label | A text label to appear in the line. Can be styled using HTML tags. | |
label | a text string | |
advice | A text label to appear in italics in the line. Can be styled using HTML tags. There should not be anything else in the line | |
label | a text string | |
subtitle | A differently-coloured label that can be used to define sections. Adds a vertical spacer before it and shows a tooltip that should be used to explainthe forthcoming section. Can be styled using HTML tags. There should not be anything else in the line | |
label | a text string for the label | |
tooltip | a text string for the tooltip | |
spacing | Add a non-stretchable space | |
spacing | Number of pixels | |
stretch | Add stretchable space (usually end of line) | |
stretch factor | integer (default =1 for widgets and labels) larger value means more stretchable | |
widget | Add widget to the line | |
-qualifier | Note '-' first character. Qualifier passed as argument to the widget initialisation method | |
name | Name of parameter |
These are arguments to createLine(), openFolder() and openSubFrame() control whether the line or frame is visible. The visibility is usually dependent on a parameter in the task container (for the toggle argument) or, more complex cases (handled by toggleFunction> have a dependency on multiple parameters and require a function to be implemented to evaluate and return a logical value. If controlling the gui appearance requires the definition of some parameters that are not subsequently used in program wrappers then they should be put in the guiParameters container in the task definition file.
The toggle argument is a list of 3 parameters:
The toggleFunction argument is a list of 2 parameters:
It is sometimes necessary to access the individual widgets defined by createLine() - this can be done by callying getWidget(name) where name is the name of the paramter. This returns a Qt widget that can be customised using the standard Qt methods for that widget type (see the Qt documentation).
These are arguments to createLine() that are inserted after the keyword widget and before the name of the parameter that the widget will represent. For each qualifier the name of the qualifier and the qualifier value are inserted in the argument list - for example:
self.createLine( [ 'widget', '-browseDb', True, 'ABCD' ] )
The most common qualifiers are:
Qualifier | Widget for data type | Description |
---|---|---|
-guiLabel | CDataFile | A short text label to appear on the widget |
-browseDb | CDataFile | A boolean value. Control display of icon to browse the database for files. |
-guiMode | CString,CInt,CFloat which have enumerator | Values 'radio','multiLineRadio' or 'combo' determine presentation of alternative parameter values |
-title | CList | A short title to appear at top of a list/tree widget |
The main reason to use signals and slots is to cause some code to be run when the user provides some input. An example of such code might be resetting defaults dependent on the user's choice of input file. Qt signals and slots are explained in detail here but you only need to know that any Qt object (and, in the context of the gui, both the data objects and the widgets are Qt objects) emits a signal when something happens (e.g. a button is clicked or the value of a data object is changed). Other objects can 'connect' to the signal i.e. request that whenever a particular signal is emited by a particular object then a particular function is run. The most relevant signal in CCP4i2 is the dataChanged signal from any data object whenever it is changed.
The code that you require to run on receiving a signal must be a separate function, almost certainly a method of your CTaskWidget subclass. The request to call that function is via the connect() method and should probably go at the end of task widget drawContents() method. For example to call the analyseXyzin() method every time the XYZIN parameter is changed:
from PyQt4 import QtCore ... def drawContents(self): self.createLine( [ 'label','Input model ','widget','XYZIN' ] ) ... self.connect(self.container.inputData.XYZIN,QtCore.SIGNAL('dataChanged'),self.analyseXyzin) self.analyseXyzin() def analyseXyzin(self): if not self.container.inputData.XYZIN.isSet() or not self.container.inputData.XYZIN.exists(): return ...
Beware that the dataChanged signal is also emited when the parameter has been unset so you need to test carefully (as in the example above) that the value is valid and may need to unset defaults (or whatever your function does). Beware also that you may need to call your function explicitly once when you draw the gui to get things setup initially with whatever data is initially set.
Note that there is an alternative to calling connect - instead use CPluginScript.connectDataChanged() :
self.connectDataChanged('XYZIN',self.analyseXyzin)
which is simpler but only applies to dataChanged signals.
Text input widgets can be tricky because the dataChanged signal will be emitted for every character that the user types and the unfinished input is probably invalid. An alternative approach for text input is to use the editingFinished() signal from the actual text input widget - this is emitted when th user clicks return or moves the focus out of the widget so suggesting that the input is complete. An example of this calling setDefaultParameters() every time the user finishes editing the TEXT parameter:
def drawContents(self): self.createLine( [ 'label','Input some text ','widget','TEXT' ] ) ''' self.connect( self.getWidget('TEXT').widget, QtCore.SIGNAL('editingFinished()'), self.setDefaultParameters ) def setDefaultParameters(self): ....
The getWidget() method is used to get the widget for the TEXT parameter but note that the actual object connected to is the member widget. Also note the brackets that are essential in the editingFinished() signal definition.
The gui code may need to do significant analysis in order to check
for appropriate input or set appropriate default parameters
etc.. There is an example of this in the Matthews task
where the results of an analysis are presented in a text widget. It is possible to use wrapper scripts from the gui as demonstrated
in the Crank2 task.
The significant code there is:
First this code calls PROJECTSMANAGER().jobDirectory() to
get the path of the TMP sub-directory of the appropriate job
directory so all files will be written here. (The TMP
sub-directory may be deleted later by Cleanup.)Then an instance of the
CPluginScript base class is created and the
makePluginObject() method used to create an instance of the
required 'crank2' wrapper. The CPluginScript is instantiated
with dummy=True that prevents jobs and files being recorded in
the database and the workDirectory is set to the TMP
sub-directory. Sub-tasks of this instance of CPluginScript, such as the crank2
plugin, are then given job-directories in the TMP
directory. The crank2.process() method has an optional
container argument that is passed the current gui container
with the current state of the gui that can be queried by the
crank2.process() method. The validation of gui input to highlight missing/invalid input and to check before running a job is done by the widget's validate() method. The particular requirements for each data object are specified by qualifiers of the object that are usually defined in the def file. The requirements for some parameter may be dependent on other parameters such that its qualifiers mat need changing by using the CData.setQualifiers() method. There is an example of this in the dumMee demo task ( tasks/dummy/CTaskDummy.py can be seen if the Demo folders on the task menu are open). The behaviour of the PDBIN file parameter is dependent on the PDBIN_COMPULSARY boolean parameter.
Here the two widgets for PDBIN_COMPULSARY and PDBIN
are drawn and then a connectDataChanged call ensures that
handlePDBIN_COMPULSARY() is called whenever
PDBIN_COMPULSARY is changed. There is also an call to
handlePDBIN_COMPULSARY() to initialise the status
correctly. The code in handlePDBIN_COMPULSARY() checks the
state of PDBIN_COMPULSARY and sets the
allowUndefined qualifier of PDBIN appropriately. There is also a call to the validate() method of the PDBIN widget to ensure the highlightling of the widget is updated. The behaviour seen in the gui is that if the
PDBIN_COMPULSARY checkbox is checked but there is no valid
file selected in the PDBIN widget then that widget is
highlighted. When the user clicks the Run button the input is validated
and if the validation fails there is an error report and the job
run is aborted. By default the tests are the same validation
checks of individual widgets that lead to widgets being
highlighted in red. If the task requires additional tests, perhaps
cross-checking two or more separate widgets, then you
should implement a CTaskWidget.taskValidation() method
that returns a CErrorReport. There is an example of this at the
bottom of
dumMee demo task ( tasks/dummy/CTaskDummy.py
can be seen if the Demo folders on the task menu are open). The example tests if the
space group in PDB and MTZ file are the same and returns an error
if they are not.
This is a work-in-progress (June 2014) being developed particularly for generating a gui for MR Phaser. Other interfaces might require additional features - please contact Liz. 'Autogenerating' means creating a GUI from the information in the
def file without any Python coding. When CCP4i2 is run in DEVELOPER
mode it will autogenerate guis from def files for any wrappers which
do not have a specified gui. The same mechanism, generating gui
elements from information in the def file, can be used for some parts
of a gui, this is probably most useful for the more obscure (and
perhaps more changeable) control parameters. We expect that most
control parameters are simple data types (booleans,floats etc) that
have widgets defined in CCP4i2 - more complex data types might need
custom written widgets and guis. An example of including some
autogenerated gui is in the drawContents() method of
dumMee demo task ( tasks/dummy/CTaskDummy.py
can be seen if the Demo folders on the task
menu are open). The Autogenerated folder is generated by the code: After opening a folder the autoGenerate() method is called
with the arguments: container that is the def file container
that is the source of specifications of parameters and
selection that is a Python dict with several possible keys that
specify which parameters in the container are to be drawn.The
includeParameters item lists parameters to be included in the gui or
excludeParameters lists parameters to be excluded when putting
all other parameters from the container in the gui.Data analysis in the task gui
import CCP4Modules,CCP4PluginScript
try:
workDir = CCP4Modules.PROJECTSMANAGER().jobDirectory(self.jobId(),subDir='TMP')
except:
workDir = None
try:
defaults = CCP4PluginScript.CPluginScript(dummy=True,workDirectory=workDir).makePluginObject(pluginName='crank2',reportToDatabase=False).process(self.container)
except:
pass
Validation and changing parameter qualifiers
self.createLine ( [ 'widget' , 'PDBIN_COMPULSARY' , 'label' ,'Model data input is compulsary'])
self.createLine ( [ 'widget','PDBIN' ] )
self.connectDataChanged('PDBIN_COMPULSARY',self.handlePDBIN_COMPULSARY)
self.handlePDBIN_COMPULSARY()
def handlePDBIN_COMPULSARY(self):
mode = bool(self.container.inputData.PDBIN_COMPULSARY)
self.container.inputData.PDBIN.setQualifiers({'allowUndefined' : (not mode) } )
self.getWidget('PDBIN').validate()
Auto-generating GUIs
self.openFolder(title='Autogenerated')
self.autoGenerate(container=self.container.controlParameters,selection={'includeParameters' : ['OBSCURE_MODE','OBSCURE_CUTOFF']})
Qualifier | Description |
---|---|
-guiLabel | A short text label to appear by the widget |
-toolTip | A longer text description |
-guiDefinition | The xml element contains sub-elements that will be converted to a Python dict. There is no restriction on key values but currently used key values are given below |
guiDefinition keys | Description |
---|---|
toggleParameter | Name of a parameter that controls visibility of the widget in the GUI |
toggleState | The state (open/closed) for the listed toggleValues (default is open) |
toggleValues | The values of toggleParameter for which toggleState applies |
expertLevel | (Or any arbitary name) can be used in the selection argument to autoGenerate() |
Related parameters can be grouped into a container and they will then be displayed in a frame probably with a surrounding border. The visiblity of the frame will also be toggleable. The container can have the same qualifiers as listed above for the parameters.
Some programs provide multiple functionalities that are used to solve different problems at different points in the structure solution process and these are best presented to the user in different task interfaces. To implement this the developer could provide multiple wrappers to the one program but one wrapper will be easier to maintain. Typically the different modes of the program may require different data file inputs and have different defaults. The input data, defaults etc. are set in def.xml files and again, to ease maintenance, it would be better to avoid replication between def.xml files. The solution to this is to have one 'base' def.xml file that specifies all the input, output and control parameters for the wrapper. A 'derived' def.xml file 'includes' the 'base' def.xml file and then specifies any altered parameter definitions that will overwrite the 'base' definitions. An example of this is in ccp4i2/tasks/molrep_den/molrep_den.def.xml
<ccp4i2_header> ... ... <pluginName>molrep_den</pluginName> <pluginTitle>Fitting to a map using Molrep</pluginTitle> <jobId/> </ccp4i2_header> <ccp4i2_body id="molrep_den"> <file> <CI2XmlDataFile> <project>CCP4I2_TOP</project> <relPath>wrappers/molrep_mr/script</relPath> <baseName>molrep_mr.def.xml</baseName> </CI2XmlDataFile> </file> <container id="inputData"> <content id="F_SIGF"> <className>CObsDataFile </className> <qualifiers> ... ...
Here the molrep_den def file is 'derived' from the molrep_mr def file. The file/CI2XmlDataFile element specifies the file path for a 'base' def.xml file and the rest of this file contains specification of only a few parameters that are different between molrep_den and molrep_mr.
The molrep_den GUI definition in ccp4i2/tasks/molrep_den/Cmolrep_den.py must also contain an additional class parameter GUINAME:
class Cmolrep_den(CCP4TaskWidget.CTaskWidget): TASKTITLE='Search density using Molrep' TASKNAME = 'molrep_mr' GUINAME = 'molrep_den' TASKMODULE='model_building' ...
Note that the TASKNAME is the name of the script that this GUI will run and GUINAME is the unique name for this GUI which should matchup with the pluginName in the header of the def.xml file. (In case you are wondering, for most GUIs the GUINAME is assumed to be the same as the TASKNAME.)
The task menu in the gui is populated automatically at startup by taking information from the CTaskWidget subclasses. The modules presented in the menu are listed at the top of ccp4i2/core/CCP4TaskManager.py. The CTaskWidget class attributes that determine the menu are:
CTaskWidget Class Attributes | Description |
TASKNAME | The obligatory name of the script essential for cross-refererence to other files |
TASKVERSION | A version number for the script. Please give one! |
TASKMODULE | Refers to where the task should appear in the task menu, can be list of multiple modules |
TASKTITLE | The name for the task to appear in the GUI |
DESCRIPTION | More details to appear in the GUI |
Note that tasks under developement should be put in the wrapper or test module and the developer can make these modules visible in the gui by setting the SHOW_WRAPPERS parameter at the top of the ccp4i2/core/CCP4TaskManager.py file to True. Note that the demo task dumMee is only visible if SHOW_WRAPPERS is True.
The preferred style for title and description can be inferred from the existing menu but:
The title should be short - remember it will appear in the left-hand job list as well. The names of programs should not appear unless really necessary to distinguish similar tasks.
The description should amplify on the functionality and include names of key programs run.
The icon on the menu is taken from a file ccp4i2/qticons/TASKNAME.png.These image files should have resolution at least 32x32 and are currently also displayed at 24x24 in the 'Compact' style - see Preferences.
This is a facility to put a button in the task window that creates another task gui in a pop-out window. The parent CTaskWidget class should also reimplement the CTaskWidget.handleLaunchedJob() method which is called when the child job is created and when it has finished running. There is an example in tasks/dummy/CTaskDummy.py which creates a launch button for the Gesamt task. The result can be seen in the Create New Job folder of the dumMee task (only visble if the Demo folders on the task menu are open). The hypothetical scenario is that the user needs to reposition the input coordinate file by superposing on another model before continuing with this task. The button to lauch and alternative task is defined by the launchButton keyword input to createLine() and it should be followed by the name of the task to be opened. In the example code below the XYZIN input widget and the lauch button are drawn. A connect() call is used to ensure that updateLauchButton() is called whenever the value of XYZIN is changed and this is also called immediately in order to disable the launch button if XYZIN is unset.
self.createLine(['label','Enter a coordinate file to test launch job button']) self.createLine(['widget','XYZIN']) self.createLine(['label','Superpose the coordinates on another model by','launchButton','gesamt']) # Update the status of the lauch button dependent on selection of XYZIN self.connect(self.container.inputData.XYZIN,QtCore.SIGNAL('dataChanged'),self.updateLauchButton) # .. and initialise its status self.updateLauchButton()
The code to enable/disable the launch button:
def updateLauchButton(self): # Enable the launch button dependent on whether XYZIN is set self.findChild(QtGui.QWidget,'gesamt').setEnabled(self.container.inputData.XYZIN.isSet())
Note that the findChild() method is used to get a reference to the launch button. The launch button has been created with its object name set to the name of the task to be launched and this is the second argument to findChild().
You should also implement an updateLauchButton() method which will be called twice: firstly when the child task window is created and the input status value is 1 (Pending - see top of dbapi/CCP4DbApi.py file) and then when the job has finished so status is 6 (Finished) or 5 (Failed). Both calls also pass the jobId but only the first call passes the a reference to the new task widget; beware the task widget may have been closed by the time the job finishes. The example code shows how to copy data from the current task wigdet into the newly launched task widget and then how to get information on job output files from the database in order to use it in the current gui.
def handleLaunchedJob(self,jobId=None,status=None,taskWidget=None): # If this is called with status=1=Pending the taskWidget (gesamt window) has just been opened # and we can set a value in it if status == 1 and taskWidget is not None: taskWidget.container.inputData.XYZIN_QUERY.set(self.container.inputData.XYZIN) # Status is 6 (Finished) elif status == 6: # Can not assume that the gesamt widget is still there - must instead query the database for output file # Use CDbApi.getJobFilesInfo() which returns a list of dicts containing description of files output by the job # The best way to set the file object ot a new value is by setDbFileId() import CCP4Modules gesamtFileList = CCP4Modules.PROJECTSMANAGER().db().getJobFilesInfo(jobId=jobId,jobParamName='XYZOUT') if len(gesamtFileList)>0: self.getWidget('XYZIN').model.setDbFileId(gesamtFileList[0]['fileId'])
There are some possible problems that are not trapped here - what if the user changes the XYZIN in the current gui but still runs the gesamt task? An exercise for the reader perhaps?