UI state management in scripted modules

I need design guidance for choosing where to keep canonical UI state in a scripted Slicer module. The module in question is PickAndPaint. The complicated design of the data structures in this module seems to be the root cause of many bugs.

Like other modules, PickAndPaint registers callbacks with various UI widgets in order to be notified of changes. I think that ideally these callbacks would update a vtkMRMLNode representing (fairly literally) the UI state, and then launch any required computation. This would allow UI state to be reinitialized from a scene file. For simplicity and correctness, code would prefer to rederive values from this representation of the UI state, instead of keeping additional state in the logic module, with exceptions only where absolutely necessary for performance reasons.

This solution would also allow QT functions that query the UI state to be completely avoided. Is avoiding these desirable? Or can I safely use the QT widget itself in place of a vtkMRMLNode in what I described above? If it is safe, couldn’t UI persistence (such as model selection persistence) be implemented in Slicer for all scripted modules by serializing any UI state referenced in the .ui file into XML and stuffing it into a Node? I can explain why I think querying state with QT might not be safe if anyone wants to know, but I’m really ignorant of QT.

It seems that a vtkMRMLNode like what I describe is recommended for modules written in C++. See also this cryptic code comment which seems to recommend something similar for scripted modules.

There’s room for developers to have some flexibility here, and different tasks may require more or less complexity to address performance issues, but at least for some cases I have had good luck with just saving a json representation of the state as an attribute of a scripted module node. This will automatically be saved/restored with the scene, and it’s pretty low overhead to parse to populate the GUI and to serialize when the user interacts with the GUI.

An example is implemented in the SlicerAnimator project which has a reasonably sophisticated GUI.

We discourage people from using the GUI to store the state, since we want it to be possible for more than one user interface element to display and edit the same state (e.g. a markup that is active in 2D, 3D, and GUI displays that all hot update).

When PickAndPaint module was developed, Python scripted module development was still quite immature. Nowadays I would recommend to create the module using Qt Designer and use updateParameterNodeFromGUI/updateGUIFromParameterNode methods to synchronize parameter node content with GUI. See ScriptedDesigner module template as an example and this discussion for some more details.

Also note that the new markups widgets offer possibilities that were not feasible before. For example, @Sunderlandkyl just implemented drawing of open and closed curves on surfaces (will be announced tomorrow) and you can use these surfaces for defining surface regions. It would be also feasible now to implement constraining of markup points to a selected surface in Slicer core (and in 3D views, markups snap to visible surfaces already).

There are a few other small issues, such as:

  • Node ID must never be stored in a node attribute, because node IDs can be changed when a scene is loaded and the existing scene contains nodes by the same IDs. Always use node references instead.
  • Logic should never use and GUI functions (currently the logic may display popup windows)
  • Logging macros should be used instead of print