Difference between revisions of "Module Configuration Example"
| Line 157: | Line 157: | ||
def refresh_module(self): | def refresh_module(self): | ||
self.controller.flush_delayed_actions() | self.controller.flush_delayed_actions() | ||
self.controller.current_pipeline_view.recreate_module( | |||
# 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): | def saveTriggered(self): | ||
Latest revision as of 18:44, 7 October 2013
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.
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()
