[Maya][UI] Handling communication between rows of dynamically created UI elements

Say I have a UI that is dynamically generating rows of controls that affect elements in a scene. Some of the controls influence each other (like a button that toggles a slider between min/max), and all of the controls affect elements in the scene (blendshape weights, creating/deleting targets, etc).

What is your preferred method(s) for managing these UI groupings? Do you create a dictionary of the elements of each row and add that to another dictionary that maps it to a name/label, and then pass that name/label to all callbacks? Do you just pass that row dictionary to each command? Do you try to pass all necessary data in the callback command for each control? Do you have a class that wraps all the data that each row needs and have the callbacks just calling instance methods?

Up to this point I’ve generally mapped a dictionary of row elements to a name/label, and passed that name/label to the callback. However if I can avoid passing strings around that are used to reference data stored elsewhere, I would prefer to. I have used classes to store all the data before, and while I like the interface that method provides it becomes difficult to manage when the calls you’re making also need to update/change UI elements and the instance methods are separated from the UI class.

Just looking for some ideas/implementations that I may not have considered.

i would build my own class around elements that effect each other, like say you got a button that effects a slider, build a class around those, and only expose the data, and methods that are needed. Than create some callback methods for things like onChange that make sure they are synced up.

Also if every row has the same UI elements, just different data, i find this approach works good, since you can have all the creation code for the elements in the init() method.

Than when instantiating the objects, you can just do that into a list or dict to make it easy to find and iterate though them later.

I take that approach in a UI for a tool i made for managing and applying materials.
https://github.com/cmcpasserby/pbdDo

Take a look at my matUI class, and how it’s objects are created in the tabUI class.

each matUI object, contains 4 UI buttons in a rowLayout, with some elements dynamically set, from data in the object based in the init methods, like the name and colour.

from a coding standpoint you want to maintain localized scope as much as possible. I’d do it by making a class that generates an entire row of controls for a given object, and write that class as if it were completely self contained - all the functions and local variables are part of that class. The outer UI should just host a list of those class instances, adding or removing as necessary but otherwise trusting the row class to do the work. This will make it much easier to manage - and, if the need arises to mix and match contextually sensitive row types (eg, different row controls for Nurbs and polys) you’re already prepared to add new row classes without changing the outer GUI.

I would not bother managing any global state at all unless you needed some functionality that only worked on the global state (ie, if one of the things to tool could do was to boolean two meshes together you’d need to be able to select both and have the outer tool know what was selected)

For a simple example of this pattern in action : Maya module manager. The ModuleManager class finds any .mod files on the current MAYA_MODULE_PATH and can toggle them 'on' and 'off' by changing the + character in the module lines to a - or vice-versa. The ModuleManagerDialog class is a Maya GUI for the same.This has only been tested on Maya 2012. · GitHub
The ModuleWidget class represents the row, and ModuleManagerDialog is the outer dialog, ie, a list of ModuleWidgets

PS you’ll note that in that example the ‘real’ functionality is in the ModuleManager class, and ModuleWidget wraps that - as always, keeping the functional code decoupled from the gui :slight_smile:

Thanks for the replies guys (a month ago, I know, but this is on topic).

How do you handle communication between widgets and their parents? After watching Alex Forsythe’s video from a while back I tried using a simple callback notifier, which worked well for the single case that I tried it with. Are callbacks the best method for that type of communication or have you guys had good luck with other implementations?

And on that note, should callbacks pass much data with them, or should it be up to the registered functions to store any widget-specific information when creating the callback function. By that I mean should a widget ever pass itself, or it’s name, through a callback on, say, a selection event, or should the registered callback function have the widget/name wrapped with it on creation?

I feel like I need a good UI design patterns book because I’m sure all of these issues have been solved/discussed before.

My most common case is just to treat the widgets as disposable – if the parent container’s conditions change, I just rebuild the whole widget list, so the widgets don’t ‘get messages’ from the parent, they just created or destroyed as needed. Generally the widgets don’t send messages either; I try to hook it up so the way underlying data is a the authoritative data store. If the widget changes the underlying data it is already synced up; if the underlying data changes I usually just rebuild the widget list from scratch (in very data intensive situations it may be smarter to selectively update the list, but I haven’t run into a case where I felt I needed it.)

This all amounts to a lightweight implementation of the Model-View-Controller pattern, with the underlying collection as the ‘model’, the widgets as the ‘view’ and the enclosing UI and functions as ‘controller’.

Here’s the code I derive from in these cases. Items in the model forward any of their own update events to the container, which in turn forwards them to the UI.


import maya.cmds as cmds
from events import Event
from maya.utils import executeInMainThreadWithResult
import time


	
class ModelContainer(object):
	'''
	more or less an implementation of ObservableCollection.  The Updated and
	ItemUpdated events should be used by a GUI element to trigger redraws or
	changes. This class and it's contents are only responsible for saying "i've
	changed" , they should not know or care how the changes are reflected
	'''
	
	def __init__(self, *models):
		self.Updated = Event()
		self.ItemUpdated = Event()
		self.Data = list( models)
		self.Filter = self._null_filter
		
	def set_filter(self, new_filter):
		'''
		Set the filter expression to the supplied callable filter (typically a lambda
		that returns true or false).  If <new_filter> is none, the filter is set to
		_null_filter.  Refresh is called when the filter changes.
		'''
		
		self.Filter = new_filter or self._null_filter
		self.refresh()
		
		
	def add(self, *models):
		'''
		add <models> to the collection, and fires an update event when complete
		'''
		for m  in models:
			self.Data.append(m)
			if hasattr(m, "Updated") and isinstance (m.Updated, Event):
				m.Updated += self.ItemUpdated			
		self.Updated(size = len(self.Data))
		
	def remove(self, *models):
		'''
		remove every item in <models> from the collection, fire an update event when complete
		
		'''
		if models:
			delenda = [d for d in models if d in self.Data]
			
			for deletable in delenda:
				if hasattr(deletable, "Updated") and isinstance (deletable.Updated, Event):
					try:
						deletable.Updated -= self.Updated
					except RuntimeError:
						pass
				while deletable in self.Data:
					self.Data.remove(deletable)
			self.Updated(size = len(self.Data))

		
	def insert(self, model, index):
		'''
		Add item <model> at index
		'''
		self.Data.insert(index, model)
		model.Updated += self.ItemUpdated
		self.Updated(size = len(self.Data), position = index)

	def sort(self, *args, **kwargs):
		'''
		sorts the underlying collection
		
		@note this is not a true MVC view sort - the base collection is reordered
		'''
		self.Data.sort(*args, **kwargs)
		self.Updated(size = len(self.Data))
		
	def refresh(self):
		'''
		re-apply the current filter and fire the updated event. Returns the filtered collection as a tuple
		'''
		view = [i for i in self.Data if self.Filter(i)]
		self.Updated(size = len(view), refresh = True)
		return tuple(view)

	def clear(self):
		'''
		clear this container
		'''
		for item in self.Data:
			item.Updated -= self.ItemUpdated
		self.Data = []
		self.Updated(size = 0)

		
	def items(self):
		'''
		yields all of the elements in this collection that pass the current filter
		'''
		view = [i for i in self.Data if self.Filter(i)]
		for item in view:
			yield item
			
	@property
	def Count(self):
		'''
		total count of items in this container
		'''
		return len (self.Data)
	
	@property
	def Active(self):
		'''
		count of items that pass the current filter
		'''
		return len ([i for i in self.items()])
	

	
	def _null_filter (self, p):
		'''
		placeholder nullop filter
		'''
		return p
	
	

class ItemTemplate(object):
	'''
	Correspondes to an itemTemplate in WPF: it's a class that dictates how to draw the GUI item for a given ModelContainer's contents
	'''
	
	def __init__(self, container, parent):
		self.Container = container
		self.Container.Updated += self.redraw
		self.Container.ItemUpdated += self.redraw
		self.Parent = parent
		self.Enabled = True
		self._lock = False
		
	def controls(self):
		'''
		yields all of the controls generated from the current container.
		'''
		for item in self.Container.items():
			yield self.widget(item, parent = self.Parent)
			
			
	
	def redraw(self, *args, **kwargs):
		'''
		rebuild the collection of widgets for the associated collection if
		self.Enabled is true.  If self.Parent is set to a layout, delete the layout's
		children first.
		'''
		if not self._lock and self.Enabled:
			self._lock = True  #pseudo locking in case of multiple events contending for the right to redraw
			if self.Parent:
				try:
					for item in cmds.layout(self.Parent, q=True, ca=True) or []:
						# this is needed because it's possible to click fast enough that the same
						# item wants to be deleted twice, and deleting it again crashes Maya
						# gee, thanks Maya!
						if cmds.control(item, q=True, exists=True):
							cmds.deleteUI(item)						
					for each_widget in self.controls():
						pass  # enumerating controls creates the control
				except RuntimeError:
					pass				
			self._lock = False
			
	def widget(self, datum, parent=None):
		'''
		for a given datum, return a GUI element 
		
		In this base class, defaults to a maya text label. Override in derived
		classes.  Use the parent flag to force widgets to be parented to a particular
		container
		'''
		p = parent or self.Parent
		if parent:
			cmds.setParent(parent)
		result = cmds.text(label = str(datum))
		return result
	
class Binding(object):
	'''
	Provides a simple way for GUI contols to update the underlying model objects when their values change.
	
	Sample usage:
	
	    start_field = cmds.intField(v=datum.Start, cc = mvc.Binding(datum, "Start"))

	as long as datum is a reference type with a field named "Start", the binding will update the datum with the new values
	'''
	def __init__(self, datum, fieldName):
		
		self.Datum = datum
		self.FieldName = fieldName
		
	def __call__(self,*args, **kwargs):
		if isinstance(self.Datum, dict):
			self.Datum[self.FieldName] = args[0]
		else:
			v = getattr(self.Datum, self.FieldName)
			if callable(v):
				v(args[0])
			else:
				setattr(self.Datum, self.FieldName, args[0])