Module Configuration Example

From VistrailsWiki
Jump to navigation Jump to search

A module configuration widget is a widget that is shown in the module configuration panel (of the Tools window) when a user selects a module. For many modules, there is no specific configuration widget, and the panel informs users to set parameters via the Module Information panel (on the right side of the main window). However, other built-in modules like PythonSource have custom widgets. Any module may specify its own configuration widget via the configureWidgetType setting.

The current StandardConfigurationWidget requires only a couple of methods (saveTriggered and resetTriggered), but it is not very clear what needs to be implemented and where. Below, we have written a ModuleConfigurationWidgetBase that attempts to abstract much of the common code that developers may wish to use. Then, the MyConfigurationWidget focuses on the specifics for a particular configuration widget. Note that one of the tricky pieces that ModuleConfigurationWidgetBase takes care of is locating and transforming parameter values. Any "function" in VisTrails may store multiple parameter values; for example, "SetCameraPosition" might have three parameters (x,y,z). Thus, we use lists of values throughout the code. In addition, the parameter values are serialized in order to ensure their storage, but each Constant subclass must provide translate_to_python and translate_to_string methods. We use these methods to serialize and deserialize values. The heavy lifting is done by the self.controller.update_functions which persists all of the changes to the vistrail. We use refresh_module to make sure the changes are reflected in the GUI.

Also, note that the example shows how the fruitport's visibility can be programmatically set using Module.visible_input_ports. However, such changes are not persisted across sessions. The state_was_changed and state_was_reset provide some cleanup code to make sure that VisTrails is aware when there are unsaved changes; you should link any widget state changes to this method.

In future versions of VisTrails, we expect to streamline the process and make the API cleaner. Expect a per-module _settings field that allows settings like the configuration widget to be specified with the module definition as well as support for named arguments so that port and settings definitions are more understandable.

Custom module config.png

Example Code for VisTrails 2.0+

init.py
from core.modules.vistrails_module import Module
from widgets import MyConfigurationWidget

class MyConfigureModule(Module):
    _input_ports = [("order", "(edu.utah.sci.vistrails.basic:Integer)"),
                    ("rating", "(edu.utah.sci.vistrails.basic:Integer)"),
                    ("fruit", "(edu.utah.sci.vistrails.basic:String)",
                     {"optional": True}),]
    _output_ports = [("value", "(edu.utah.sci.vistrails.basic:String)")]

_modules = [(MyConfigureModule, {"configureWidgetType": MyConfigurationWidget})]
widgets.py
from PyQt4 import QtCore, QtGui
from itertools import izip

from gui.modules.module_configure import StandardModuleConfigurationWidget
from core import debug

class ModuleConfigurationWidgetBase(StandardModuleConfigurationWidget):
    """ModuleConfigurationWidgetBase provides more scaffolding over
    StandardModuleConfigurationWidget so developers do not have to
    comb through the source code to determine what needs to be implemented

    """
    def __init__(self, module, controller, parent=None):
        StandardModuleConfigurationWidget.__init__(self, module, controller, 
                                                   parent)
        self.has_button_layout = False
        self.create_widget()
        self.set_gui_values()

    def create_widget(self):
        raise NotImplementedError('Subclass needs to implement'
                                  '"create_widget"')

    def set_vistrails_values(self):
        raise NotImplementedError('Subclass needs to implement '
                                  '"set_vistrails_values"')

    def set_gui_values(self):
        raise NotImplementedError('Subclass needs to implement '
                                  '"set_gui_values"')

    def create_button_layout(self):
        self.has_button_layout = True

        button_layout = QtGui.QHBoxLayout()
        self.reset_button = QtGui.QPushButton('&Reset')
        self.reset_button.setEnabled(False)
        self.save_button = QtGui.QPushButton('&Save')
        self.save_button.setDefault(True)
        self.save_button.setEnabled(False)
        button_layout.addStretch(1)
        button_layout.addWidget(self.reset_button)
        button_layout.addWidget(self.save_button)

        self.connect(self.reset_button, QtCore.SIGNAL("clicked()"),
                     self.resetTriggered)
        self.connect(self.save_button, QtCore.SIGNAL("clicked()"),
                     self.saveTriggered)

        return button_layout

    def state_was_changed(self, *args, **kwargs):
        if self.has_button_layout:
            self.save_button.setEnabled(True)
            self.reset_button.setEnabled(True)
        self.state_changed = True

    def state_was_reset(self):
        if self.has_button_layout:
            self.save_button.setEnabled(False)
            self.reset_button.setEnabled(False)
        self.state_changed = False        

    def get_function_by_name(self, function_name):
        """Given its name, returns a ModuleFunction object if it exists
        otherwise None.  If two or more functions exist for the same
        port, it writes a warning and returns the first.

        """
        found = []
        for function in self.module.functions:
            if function.name == function_name:
                found.append(function)

        if len(found) > 1:
            debug.warning("Found more than one function named '%s'" % \
                          function_name)
        if len(found) < 1:
            return None
        return found[0]

    def get_function_values(self, function_name):
        """Takes a function name and returns a list of (python) values on that
        function.  Note that this is a list because there can be
        multiple parameter values per function if we have a compound
        port.

        """
        f = self.get_function_by_name(function_name)
        if f is None:
            return None

        str_values = [p.strValue for p in f.params]

        ps = self.module.get_port_spec(function_name, 'input')
        descriptors = ps.descriptors()
        if len(str_values) != len(descriptors):
            debug.critical("Parameters for '%s' do not match specification" % \
                           function_name)
            return None

        values = []
        for str_value, desc in izip(str_values, descriptors):
            value = desc.module.translate_to_python(str_value)
            values.append(value)

        return values

    def get_function_str_values(self, function_name, values):
        """Takes a function name and a list of (python) values and serializes
        them, returning a list of string values.  Note that this is a
        list because there can be multiple parameter values per
        function if we have a compound port.

        """
        ps = self.module.get_port_spec(function_name, 'input')
        descriptors = ps.descriptors()
        if len(values) != len(descriptors):
            debug.critical("Values for '%s' do not match specification" % \
                           function_name)
            return None
        
        str_values = []
        for value, desc in izip(values, descriptors):
            str_value = desc.module.translate_to_string(value)
            str_values.append(str_value)

        return str_values

    def refresh_module(self):
        self.controller.flush_delayed_actions()

        # need to check this for 2.0 versus 2.1:
        if hasattr(self.controller, 'current_pipeline_scene'):
            scene = self.controller.current_pipeline_scene
        else:
            scene = self.controller.current_pipeline_view
        scene.recreate_module(self.controller.current_pipeline, self.module.id)
        
    def saveTriggered(self):
        self.set_vistrail_values()

    def resetTriggered(self):
        self.set_gui_values()


class MyConfigurationWidget(ModuleConfigurationWidgetBase):
    def create_widget(self):
        grid_layout = QtGui.QGridLayout()
        slider_label = QtGui.QLabel("Order:")
        self.slider_widget = QtGui.QSlider()
        self.slider_widget.setTracking(False)
        self.slider_widget.setOrientation(QtCore.Qt.Horizontal)
        self.slider_widget.setMinimum(0)
        self.slider_widget.setMaximum(100)
        grid_layout.addWidget(slider_label, 1, 1)
        grid_layout.addWidget(self.slider_widget, 1, 2)
        
        spin_label = QtGui.QLabel("Rating:")
        self.spin_box = QtGui.QSpinBox()
        self.spin_box.setRange(-10,10)
        grid_layout.addWidget(spin_label, 2, 1)
        grid_layout.addWidget(self.spin_box, 2, 2)

        list_label = QtGui.QLabel("Fruit:")
        self.list_widget = QtGui.QListWidget()
        self.list_widget.setSelectionMode(
            QtGui.QAbstractItemView.SingleSelection)
        self.list_widget.addItem("Apple")
        self.list_widget.addItem("Banana")
        self.list_widget.addItem("Cherry")
        grid_layout.addWidget(list_label, 3, 1, 1, 2)
        grid_layout.addWidget(self.list_widget, 4, 1, 1, 2)

        button_layout = self.create_button_layout()
        grid_layout.addLayout(button_layout, 6, 1, 1, 2)

        self.setLayout(grid_layout)

        self.connect(self.slider_widget, QtCore.SIGNAL("valueChanged(int)"),
                     self.state_was_changed)
        self.connect(self.spin_box, QtCore.SIGNAL("valueChanged(int)"),
                     self.state_was_changed)
        self.connect(self.list_widget, QtCore.SIGNAL("itemSelectionChanged()"),
                     self.state_was_changed)

        # setting port visibility
        self.checkbox = QtGui.QCheckBox("Show Fruit Port")
        grid_layout.addWidget(self.checkbox, 5, 1, 1, 2)
        if "fruit" in self.module.visible_input_ports:
            self.checkbox.setChecked(True)
        self.connect(self.checkbox, QtCore.SIGNAL("toggled(bool)"),
                     self.toggle_fruit_visibility)

    # setting port visibility
    def toggle_fruit_visibility(self, *args, **kwargs):
        if self.checkbox.isChecked():
            self.module.visible_input_ports.add("fruit")
        else:
            self.module.visible_input_ports.discard("fruit")
        self.refresh_module()

    def set_gui_values(self):
        # each value will either be None or a list (usually with only
        # a single value)
        order = self.get_function_values("order")
        rating = self.get_function_values("rating")
        fruit = self.get_function_values("fruit")

        if order is not None:
            self.slider_widget.setValue(order[0])
        else:
            self.slider_widget.setValue(0)
        if rating is not None:
            self.spin_box.setValue(rating[0])
        else:
            self.spin_box.setValue(0)
        if fruit:
            items = self.list_widget.findItems(fruit[0], QtCore.Qt.MatchExactly)
            if len(items) > 0:
                self.list_widget.setCurrentItem(items[0])
            else:
                self.list_widget.setCurrentRow(-1)            
        else:
            self.list_widget.setCurrentRow(-1)

        # call reset since the setting above likely triggered
        # state_was_changed
        self.state_was_reset()    

    def set_vistrail_values(self):
        order = self.slider_widget.value()
        rating = self.spin_box.value()
        fruit = ''
        selected_items = self.list_widget.selectedItems()
        if len(selected_items) > 0:
            fruit = selected_items[0].text()
        
        # update_functions takes a list of tuples; each tupe is of the
        # form (function name, <list of param str values>)
        functions = [(f_name,self. get_function_str_values(f_name, f_val))
                     for (f_name, f_val) in [("order", [order]),
                                             ("rating", [rating]),
                                             ("fruit", [fruit])]]
        self.controller.update_functions(self.module,
                                         functions)

        # need to reset state before refreshing the module
        self.state_was_reset()

        # refresh the module so any parameter changes are visible
        self.refresh_module()