Module Configuration Example
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 fruit
port'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.
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() self.controller.current_pipeline_view.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()