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])