CCP4i2 Developer Notes - Graphics classes

Introduction

Useful links Qt documentation, PyQt documentation

The i2 graphics is based on Qt4.8 accessed via a Python interface provided by PyQt. Qt class names begin with a 'Q' and i2 class names begin with a 'C'. In notes below I will list the base classes in brackets after the classname and attempt to cover main gui classes top-down.

Qt uses a model-view concept - separating the model data from the viewer. I have tried to apply this to i2 implementation. Signals and slots are vital for communication.

Overall application

utils.startup.py/startBrowser() is responsible for bootstrapping the i2 gui.

utils/QApp.py: CGuiApplication(QtGui.QApplication) - an instance of QApplication is created at startup, it is vital for Qt to work.

CCP4WebBrowser.exitBrowser() handles shutdown. Note the functions CCP4WebBrowser.saveStatus() and CCP4WebBrowser.restoreStatus() are responsible for saving and restoring the open windows and projects at shutdown and startup. Other aspects of status are saved by the appropriate modules.

Main windows

CCP4WebBrowser.CMainWindow(QtGui.QMainWindow) is the superclass for CCP4WebBrowser.CBrowserWindow (a basic web viewer used to show documentation etc.) and CCP4ProjectViewer.CProjectViewer (the project window). CMainWindow mostly provides the menu bar and handles the actions on the menus.

CBrowserWindow is responsible for displaying html and other files. CBrowserWindow.openFile() is expected to display any file that is thrown at it. It uses the file type definitions in CCP4CustomMimeTypes.py to decide how to display the file and may use:

CProjectViewer

This is responsible for a Project window and handling many of the project-releated actions including those initiated in the 'Job list'.

Several other classes are responsible for the various elements on the project window:

Besides handling signals from the CProjectWidget (Job list) widget this class also provides the functionality for the actions displayed on the toolbar.

CProjectWidget - the Job list

This is a complex example of a Qt QTreeView using Qt's model-view architecture. See the Simple tree model example - note that link is Qt5 and C++ but I originally copied from something like this.

CProjectWidget is just a QFrame containing a CProjectView (a QTreeView) and a CProjectModel (a QAbstractItemModel). CProjectModel holds all the data that is displayed in the Job list in a hierachy of CTreeItem objects reflect the hierachy of the Job list. CTreeItem is sub-classed to CTreeItemJob and CTreeItemFile. (It also has CTreeItemFolder and CTreeItemProject sub-classes that are used in the interface to i1 projects, CCP4I1Projects.) When CProjectModel is instantiated CProjectModel.setupModelData() pulls the required information from the database and creates the CTreeItem hierarchy. CProjectModel responds to any 'data changed' signals from the database by updating the hierarchy appropriately and for hard changes has to resort to complete redo by resetAll(). Each CTreeItem class holds the appropriate data and lists of its child objects. The CTreeItem.data() method returns the appropriate data for each column and role and CProjectView/QTreeView use this toget the data to draw. One special case for CTreeItem.data() is that column 0, and role UserRole returns the database jobId or fileId which enables searching the data structure for these.

The use of CModelIndex (see Qt docs) always seems like black magic to me but CProjectModel.index() does as required in the Qt documentation: it creates and returns a QModelIndex with an internalPointer to the appropriate CTreeItem for a given node, row and column in the data hierarchy. In some places we need to get the CModelIndex appropriate for a given jobId or fileId (the i2 database ids). CProjectModel.modelIndexFromJob() and CProjectModel.modelIndexFromFile() do this using the CAbstractItemModel.search() method to find the jobId/fileId in the data structure.

CProjectWidget.showJobListPopup() creates a popup menu when user clicks on a job or file in the Job list and most menu selections result in a signal that is recognised by CProjectViewer which performs the required operation. Many operations involve changes to jobs and files require the database to be updated. The database update usually results in a signal that is recognised by CProjectModel prompting it to update the model hierarchy and so update the appearance of the Job list.

CTaskWidget - the task input gui

See task developer docs.

CCP4TaskWidget.CTaskWidget class provides the task input gui that goes into the CTaskInputFrame. Each task provides a gui definition class that is a sub-class of CTaskWidget and the subclass draw() method defines the task gui by using many of the base class methods such as openFolder(),createLine().

Since many tasks have a lot of input it needs to be organised into groups. The CTaskWidget class is complicated by the fact that it supports two different ways of displaying groups of inputs - either as tabs (as seen in i2 by default) or as folders (similar to i1). The folders option is now disabled (and likely not working) but the code remains. CTaskWidget creates either a CTabTaskWidget or CFolderTaskWidget to support the two modes. Both of these classes inherit CTaskWidgetDrawMethods which provides a lot of the methods (openFolder(),createLine() etc) that the draw() uses to define the user interface.

When CProjectViewer (or actually CTaskInputFrame.createTaskWidget()) draws a new task input frame it calls the task manager, CTaskManager.getTaskWidgetClass(), which returns a reference to the appropriate CTaskWidget sub-class.

Widgets for data objects

See developer docs for overview of data classes.

When drawing the task input gui CTaskWidget.createLine() is passed drawing commands that are passed on to CTaskLine.draw(). The drawing commands include 'widget' followed by the name of a parameter. CTaskLine.draw() will find the data object with that parameter name in the CContainer that holds all the job parameters and will call the Data manager (CCP4DataManager.DATAMANAGER().widget()) to return an appropriate widget object.

The CCP4Widgts.py file has some simple widget classes such as CLineEdit and CComboBox which are derived from Qt widget classes adding some i2 specific functionality. These classes are not 'viewer' classes for i2 data objects but are used in assembling the viewer classes.

In i2 there are some 'simple' data classes to represent strings (CString), integers (CInt), floats (CFloat) and Booleans (CBoolean) and the appropriate viewer widgets: CStringView, CIntView, CFloatView and CBooleanView. But most of the code handles the complex data classes that contain two or more elements of these basic data types.

CViewWidget

CViewWidget is the base class for view widgets - the main methods are:

CViewWidget.__init__() draws the widget and (if it receives a valid model object as input argument) loads the current data from the model so it is visible in the widget. The CViewWidget contains one data entry widget for each element in the data object. So the CViewWidget for a simple (one element) data object has one data entry widget saved as CViewWidget.widget. Multiple data entry widgets are saved in a dict CViewWidget.widgets with the key for each item being the same as the name of the element in the data object. The CViewWidget may also contain labels and widgets to control the appearance of the CViewWidget.

CViewWidget.__init__() initialises CViewWidget.editable to True that can be overriden by the input qualifiers including an 'editable' key with value False. This is the case for widgets in a task window for a task that is already running or finished. All classes derived from CViewWidget should handle the case of CViewWidget.editable being False by using non-editable sub-widgets - for example a label rather than a text entry.

CViewWidget.setModel(model)set a data object on CViewWidget.model and call CViewWidget.updateViewFromModel() and CViewWidget.validate().

CViewWidget.getModel()return a reference to the data object CViewWidget.model.

CViewWidget.unsetModel() unset CViewWidget.model

CViewWidget.setValue(value) expects a dict (with keys that are the data object elements/widgets names) as input and sets the values on the CViewWidget.widgets.

CViewWidget.getValue() returns a dict containing the values from CViewWidget.widgets.

CViewWidget.updateViewFromModel() copies the data values from CViewWidget.model (a reference to the data object) to the CViewWidget.widgets (the data entry widgets).

CViewWidget.updateModelFromView() copies the data values from CViewWidget.widgets (the data entry widgets) to CViewWidget.model (a reference to the data object).

CViewWidget.connectUpdateViewFromModel() A major problem with syncing the model and the data entry widgets is that changes in either of these will send a signal that trips further cycles of updates. This cycle is broken by calling CViewWidget.connectUpdateViewFromModel(False) which creates a block on CViewWidget.updateViewFromModel(). This is applied before applying changes in CViewWidget.updateModelFromView() and must be called again with value True to end the block.

CViewWidget.validate(isValid=None,excludeWidgets=[],report=None,reportMessage=True) This uses CViewWidget.model.validity() to test if loaded data is valid (or uses the input isValid if it is True or False). it then calls QObject.setProperty() to set isValid and hasWarning properties that are interpreted by the Qt style (CCP4StyleSheet.py) to highlight invalid widgets. It also puts the input report or the report from CViewWidget.model.validity() in CViewWidget.validityMessage to be displayed if the cursor is over this widget - controlled by reimplementation of QWidget methods: CViewWidget.enterEvent() and CViewWidget.leaveEvent().

CViewWidget.setToolTip(tip) sets a toolTip on all widgets in the CViewWidget.

CComplexLineWidget

This sub-classes CViewWidget and adds an icon in top-left corner of the view widget. The icon has an attached pop-up menu and supports drag-n-drop of data. By default if the view widget is for the data object CWhatever:

The icon is loaded from qticons/Whatever.png or qticons/Whatever.svg
CComplexLineWidget._dragType - a text string specifying a drag type- is set to 'Whatever'
CComplexLineWidget._dropTypes - a list of text strings that specify acceptable drop types - is set to ['Whatever']
The view widget is given a QHBoxLayout suitable for a single line layout.

A widget that sub-classes CComplexLineWidget can call CComplexLineWidget.__init__() can customise this behaviour with an argument qualifiers - a dict that can have items:
iconName - alternative name for icon file in qticons
gridLayout - value true for alternative QGridLayout
vboxLayout - value true for alternative QVBoxLayout
dragType - alternative text string name for drag type
dropTypes - alternative list of text string names for drop types

Drag-n-drop (see Qt docs) works by parcelling an xml text representation of the data (from CComplexLineWidget.model.xmlText()) into the QDrag object - if you need to change the representation reimplement CComplexLineWidget.dragData() and CComplexLineWidget.acceptDropData(). Beware the most useful drag-n-drop is from the Job list and CCP4ProjectWidget CTreeItemFile.mimeData() should use the same representation.

The items on the icon menu are specified by a list of action labels returned by CComplexLineWidget.getMenuDef(). The actions are defined in CComplexLineWidget.getActionDef(). The definition for each action includes a slot - a reference to the function that will perform the required action. The most useful actions are:
'view' which has a default slot CComplexLineWidget.openViewer() - subclasses may need a reimplementation of this to display their data.
'copy' and 'paste' which use a text xml representation of the data similar to drag-n-drop.

CDataFileView

This is the most used view widget - representing data objects that are references to files. The view widget consists of a combo box (CDataFileView.jobCombo) listing files of appropriate type from recent jobs in the project, a button to open a database search of other i2 projects, a button to download files from web servers (only if using native file browser which does not have built in web server download) and a button to open a file browser.

The jobCombo (and the rest of the i2 gui) displays the CDataFile.annotation text if it exists or the filename otherwise. The CDataFile.fileId (the i2 database id) is saved in QComboBox user data for reference if the file is selected.

This class is reimplemented in:
CCP4ModelWidgets:CPdbDataFileView to provide an atom selection tool and alternative viewers
CCP4XtalWidgets.CMiniMtzDataFileView for CMiniMtzDataFileView.handleBrowserOpenFile() to handle importing 'monster' MTZ files.

CFileDialog

A reimplementation of QFileDialog to add extra tools to enable downloading from web servers and shortcuts to the project directory. QFileDialog is normally a dialog box (i.e. a window) but this implementation forces it to be a widget within a window. The odd coding with CFileDialog(QtGui.QDialog) contains CFileDialog1(QtGui.QWidget) contains CFileDialog0(QtGui.QFileDialog) was the only way to get this to work. There is a risk this will not work in an upgraded Qt but the PREFERENCES().NATIVEBROWSER could be set permanently True to use the native browser and a separate web server download accessed from CDataFileView.

CListView

A list view widget has two main components:
a list or table showing all of the items in the list
an editor enabling editing of one item from the list - this will be the standard view widget for the data objects in the list.
additionally it will have:
the top row of the widget has the usual icon (a 'list' icon by default), a button with 'Hide list'/'Show list' to toggle display of the list and a title explaining the data.
'+'/'-' buttons below the list to enable adding and deleting items
When the input is likely to be a single item the list element is sometimes initially not visible and the user must click the 'Show list' to enter more than one item.

The classes CListViewListWidget and CListViewTableWidget provide the alternative ways of showing the list. Both support a context (right mouse click) menu to enable Edit, Insert and Delete rows.

To use CListView it is always necessary to sub-class it and to use the qualifiers argument to CListView.__init__() to specify the appearance of the list view. For example the unmerged data list that appears in the Aimless gui:

class CImportUnmergedListView(CCP4Widgets.CListView):
  MODEL_CLASS = CCP4XtalData.CImportUnmergedList
  def __init__(self,parent=None,model=None,qualifiers={}):
    qualis =  { 'mode' : 'table',
                'tableItems' : ['file','crystalName','dataset','excludeSelection'],
                'columnHeaders':['Filename','Crystal','Dataset','Exclude batches'],
               }
    qualis.update(qualifiers)
    CCP4Widgets.CListView.__init__(self,parent,model=model,qualifiers=qualis)

Here, although the data object CImportUnmerged and it's editor widget CImportUnmergedView are quite complex the list view specification is simple enough. The qualifiers input to CListView.__init__() are:
title - title to appear in view widget
mode - either 'list' or 'table'
tableItems - in 'table' mode only - a list of the data object components to be displayed in the columns
columnHeaders - in table or list mode - a list of column headers
editorClassName - a text string with the name of the editor class to override the default view widget for the data object
listVisible - True/False - is the list initially displayed by default

For the list element to display the correct information for each item in the list the data object (i.e. the CWhatever in CWhaterListView) must provide either:
CWhatever.getTextItem() - for a 'list' mode return a text string to appear in the list
CWhatever.getTableTextItems() - for a 'table' mode return a list of text items - one for each column in the table

CTreeView

This is similar to CListView but the 'list' is a tree with different types of data represented at each level in the tree. This has only been implemented for a two-deep heirarchy - an example is the Ensemble widget in Phaser MR task. The code for this is:

class CEnsembleListView(CCP4Widgets.CTreeView):
  MODEL_CLASS = CCP4ModelData.CEnsembleList
  def __init__(self,parent=None,model=None,qualifiers={}):
    displayRole = QtCore.Qt.DisplayRole
    qualis = { 'editors' : [ { 'modelClass' : CCP4ModelData.CEnsemble, 'name' : 'ensemble', 'label':'ensemble' } ,
                             { 'modelClass' : CCP4ModelData.CPdbEnsembleItem , 'name' : 'pdbEnsembleItem','label':'structure in ensemble' } ],

               'columnHeaders':[  {displayRole:'Ensemble/Filename','width':240},
                                  {displayRole:'Selection','width':240},
                                  {displayRole:'Identity','width':50},
                                  {displayRole:'RMS','width':50}  ]
               }
    qualis.update(qualifiers)
    super(CEnsembleListView,self).__init__(parent,model=model,qualifiers=qualis)

In this case the possible items in the qualifiers input to CTreeView.__init__() are:
title - title to appear in view widget
editors - a list of dict - the list should have two items and each item should have 'modelClass', 'name' and 'label' items.
columnHeaders - a list of dict - each item in the list defines one column header and include 'width' and then for the possible role inputs to CTreeItemWidget.setData() (see Qt documentation).

The tree view of the data uses the Qt model-view paradigm where CTreeViewAbstractItemModel(QtCore.QAbstractItemModel) is the model and CTreeViewTreeView(QtGui.QTreeView) is the view. For these to work the 'top' data object (CCP4ModelData.CEnsemble in the example) must provide some methods: data(), columnCount(), rowCount(), child() and row()

Note that attempt to implement the 'tree' mode in CListView got too complicated - hence a separate class.

General programming notes

On instantiating a Qt object it usually passed a reference to a parent object - for graphics objects this will be the graphics object that it is in or the window it is a child of. It is important to get this right to ensure child objects get deleted.

Beware closing a Qt object - there is both the C++ Qt object to delete and the Python reference. To delete self.myGuiObject:

    self.myGuiObject.close()  # If appropraite for this object class
    self.myGuiObject.deleteLater()
    del self.myGuiObject