mrv.maya.ui.util
Covered: 176 lines
Missed: 66 lines
Skipped 116 lines
Percent: 72 %
  2
"""
  3
Utilities and classes useful for user interfaces
  4
"""
  5
__docformat__ = "restructuredtext"
  7
from mrv.util import EventSender, Event, Call, WeakInstFunction
  8
import maya.cmds as cmds
  9
from mrv.enum import create as enum
 10
import weakref
 14
def makeEditOrQueryMethod( flag, isEdit=False, methodName=None ):
 15
	"""Create a function calling inFunc with an edit or query flag set.
 17
	:note: only works on mrv wrapped ui elements
 18
	:note: THIS IS MOSTLY A DUPLICATION OF PROVEN CODE FROM ``mrv.maya.util`` !
 19
	:param flag: name of the query or edit flag
 20
	:param isEdit: If not False, the method returned will be an edit function
 21
	:param methodName: the name of the method returned, defaults to inCmd name"""
 23
	func = None
 24
	if isEdit:
 25
		def editFunc(self, val, **kwargs):
 26
			kwargs[ 'edit' ] = True
 27
			kwargs[ flag ] = val
 28
			return self.__melcmd__( self, **kwargs )
 30
		func = editFunc
 32
	else:
 33
		def queryFunc(self, **kwargs):
 34
			kwargs[ 'query' ] = True
 35
			kwargs[ flag ] = True
 36
			return self.__melcmd__( self, **kwargs )
 38
		func = queryFunc
 41
	if not methodName:
 42
		methodName = flag
 43
	func.__name__ = methodName
 45
	return func
 48
def queryMethod( flag, methodName = None ):
 49
	""" Shorthand query version of makeEditOrQueryMethod """
 50
	return makeEditOrQueryMethod( flag, isEdit=False, methodName=methodName )
 52
def editMethod( flag, methodName = None ):
 53
	""" Shorthand edit version of makeEditOrQueryMethod """
 54
	return makeEditOrQueryMethod( flag, isEdit=True, methodName=methodName )
 56
def propertyQE( flag, methodName = None ):
 57
	""" Shorthand for simple query and edit properties """
 58
	editFunc = editMethod( flag, methodName = methodName )
 59
	queryFunc = queryMethod( flag, methodName = methodName )
 60
	return property( queryFunc, editFunc )
 65
class Signal( Event ):
 66
	"""User interface signal which keeps assigned functions as strong reference to
 67
	assure we can always call it. This implies that we may leave many dangling objects
 68
	unless we are being properly cleaned up on deletion.
 70
	Calls generated from this event will not put the sender as first argument, but 
 71
	you may retrieve it using ``self.sender()``."""
 73
	use_weakref = False
 74
	remove_on_error = False
 75
	sender_as_argument = False
 79
class EventSenderUI( EventSender ):
 80
	"""Allows registration of a typical UI callback
 81
	It basically streamlines the registration for a callback such that any
 82
	number of listeners can be called when an event occours - this works by
 83
	keeping an own list of callbacks registered for a specific event, and calling them
 84
	whenever the maya ui callback has been triggered
 86
	To use this class , see the documentation of ``EventSender``, but use the Event
 87
	instead.
 88
	If you want to add your own events, use your own ``Signal`` s.
 90
	The class does NOT use weakreferences for the main callbacks to make it easier to use.
 91
	Use the ``WeakFunction`` to properly and weakly bind an instance function
 93
	When registered for an event, the sender will be provided to each callback as first
 94
	argument.
 95
	"""
 99
	sender_as_argument = True
100
	reraise_on_error  =True
104
	class _UIEvent( Event ):
105
		"""Event suitable to deal with user interface callback"""
107
		use_weakref = False
108
		remove_on_error = True
111
		def __init__( self, eventname, **kwargs ):
112
			"""Allows to set additional arguments to be given when a callback
113
			is actually set"""
114
			super( EventSenderUI._UIEvent, self ).__init__( **kwargs )
115
			self._name =eventname
116
			self._kwargs = kwargs
118
		def send( self, inst, *args, **kwargs ):
119
			"""Sets our instance prior to calling the super class"""
120
			self._last_inst_ref = weakref.ref(inst)
121
			super(EventSenderUI._UIEvent, self).send(*args, **kwargs)
123
		def __set__(  self, inst, eventfunc ):
124
			"""Set the given event to be called when this event is being triggered"""
125
			eventset = self._getFunctionSet( inst )
129
			if not eventset:
130
				kwargs = dict()
132
				dyncall = lambda *args, **kwargs: self.send(inst, *args, **kwargs)
134
				kwargs[ 'e' ] = 1
135
				kwargs[ self._name ] = dyncall
136
				kwargs.update( self._kwargs )		# allow user kwargs
137
				inst.__melcmd__( str( inst ) , **kwargs )
140
			super( EventSenderUI._UIEvent, self ).__set__( inst, eventfunc )
144
class UIContainerBase( object ):
145
	"""A ui container is a base for all classes that can have child controls or
146
	other containers.
148
	This class is just supposed to keep references to it's children so that additional
149
	information stored in python will not get lost
151
	Child-Instances are always unique, thus adding them several times will not
152
	keep them several times , but just once"""
155
	def __init__( self, *args, **kwargs ):
156
		self._children = list()
157
		super( UIContainerBase, self ).__init__( *args, **kwargs )
159
	def __getitem__( self, key ):
160
		"""
161
		:return: the child with the given name, see `childByName`
162
		:param key: if integer, will return the given list index, if string, the child
163
			matching the id"""
164
		if isinstance( key, basestring ):
165
			return self.childByName( key )
166
		else:
167
			return self._children[ key ]
169
	def __enter__(self):
170
		return self
172
	def __exit__(self, type, value, traceback):
173
		if type is None and hasattr(self, 'setParentActive'):
174
			self.setParentActive()
177
	def add( self, child, set_self_active = False, revert_to_previous_parent = True ):
178
		"""Add the given child UI item to our list of children
180
		:param set_self_active: if True, we explicitly make ourselves the current parent
181
			for newly created UI elements
182
		:param revert_to_previous_parent: if True, the previous parent will be restored
183
			once we are done, if False we keep the parent - only effective if set_self_active is True
184
		:return: the newly added child, allowing contructs like
185
			button = layout.addChild( Button( ) )"""
186
		if child in self._children:
187
			return child
189
		prevparent = None
190
		if set_self_active:
191
			prevparent = self.activeParent()
192
			self.setActive( )
195
		self._children.append( child )
197
		if revert_to_previous_parent and prevparent:
198
			prevparent.setActive()
200
		return child
202
	def removeChild( self, child ):
203
		"""Remove the given child from our list
205
		:return: True if the child was found and has been removed, False otherwise"""
206
		try:
207
			self._children.remove( child )
208
			return True
209
		except ValueError:
210
			return False
212
	def deleteChild( self, child ):
213
		"""Delete the given child ui physically so it will not be shown anymore
214
		after removing it from our list of children"""
215
		if self.removeChild( child ):
216
			child.delete()
218
	def listChildren( self, predicate = lambda c: True ):
219
		"""
220
		:return: list with our child instances
221
		:param predicate: function returning True for each child to include in result,
222
			allows to easily filter children
223
		:note: it's a copy, so you can freely act on the list
224
		:note: children will be returned in the order in which they have been added"""
225
		return [ c for c in self._children if predicate( c ) ]
227
	def childByName( self, childname ):
228
		"""
229
		:return: stored child instance, specified either as short name ( without pipes )
230
			or fully qualified ( i.e. mychild or parent|subparent|mychild" )
231
		:raise KeyError: if a child with that name does not exist"""
232
		if "|" in childname:
233
			for child in self._children:
234
				if child == childname:
235
					return child
239
		childname = childname.split( '|' )[-1]		# |hello|world -> world
240
		for child in self._children:
241
			if child.basename() == childname:
242
				return child
245
		raise KeyError( "Child named %s could not be found below %s" % ( childname, self ) )
247
	def setActive( self ):
248
		"""Set this container active, such that newly created items will be children
249
		of this layout
251
		:return: self
252
		:note: always use the addChild function to add the children !"""
253
		cmds.setParent( self )
254
		return self
256
	def clearChildren( self ):
257
		"""Clear our child arrays to quickly forget about our children"""
258
		self._children = list()
261
class iItemSet( object ):
262
	"""Interface allowing to dynamically add, update and remove items to a layout
263
	to match a given input set of item ids.
264
	Its abstacted to be implemented by subclasses"""
267
	eSetItemCBID = enum( "preCreate", "preUpdate", "preRemove", "postCreate", "postUpdate", "postRemove" )
270
	def setItems( self, item_ids, **kwargs ):
271
		"""Set the UI to display items identified by the given item_ids
273
		:param item_ids: ids behaving appropriately if put into a set
274
		:param kwargs: passed on to the handler methods ( which are implemented by the subclass ).
275
			Use these to pass on additional data that you might want to use to keep additional information about
276
			your item ids
277
		:note: you are responsible for generating a list of item_ids and call this
278
			method to trigger the update
279
		:return: tuple( SetOfDeletedItemIds, SetOfCreatedItemIds ) """
280
		existing_items = set( self.currentItemIds( **kwargs ) )
281
		todo_items = set( item_ids )
283
		items_to_create = todo_items - existing_items
284
		items_to_remove = existing_items - todo_items
288
		if items_to_remove:
289
			self.handleEvent( self.eSetItemCBID.preRemove, **kwargs )
290
			for item in items_to_remove:
291
				self.removeItem( item, **kwargs )
292
			self.handleEvent( self.eSetItemCBID.postRemove, **kwargs )
297
		if items_to_create:
298
			self.handleEvent( self.eSetItemCBID.preCreate, **kwargs )
299
			for item in items_to_create:
300
				result = self.createItem( item, **kwargs )
302
				if result is None:
303
					todo_items.remove( item )
305
			self.handleEvent( self.eSetItemCBID.postCreate, **kwargs )
310
		if todo_items:
311
			self.handleEvent( self.eSetItemCBID.preUpdate, **kwargs )
312
			for item in todo_items:
313
				self.updateItem( item, **kwargs )
314
			self.handleEvent( self.eSetItemCBID.postUpdate, **kwargs )
317
		return ( items_to_remove, items_to_create )
323
	def currentItemIds( self, **kwargs ):
324
		"""
325
		:return: list of item ids that are currently available in your layout.
326
			They will be passed around to the `createItem`, `updateItem` and`removeItem`
327
			methods and is the foundation of the `setItems` method. Ids returned here
328
			must be compatible to the ids passed in to `setItems`"""
329
		raise NotImplementedError( "To be implemented by subClass" )
331
	def handleEvent( self, eventid, **kwargs ):
332
		"""Called whenever a block of items is being handled for an operation identified
333
		by eventid, allowing you to prepare for such a block or finish it
335
		:param eventid: eSetItemCBID identifying the event to handle"""
336
		pass
338
	def createItem( self, itemid, **kwargs ):
339
		"""Create an item identified by the given itemid and add it to your layout
341
		:return: created item or None to indicate error. On error, the item will not
342
			be updated anymore"""
343
		raise NotImplementedError( "To be implemented by subClass" )
345
	def updateItem( self, itemid, **kwargs ):
346
		"""Update the item identified by the given itemid so that it represents the
347
		current state of the application"""
348
		raise NotImplementedError( "To be implemented by subClass" )
350
	def removeItem( self, itemid, **kwargs ):
351
		"""Remove the given item identified by itemid so it does not show up in this
352
		layout anymore
354
		:note: its up to you how you remove the item, as long as it is not visible anymore"""
355
		raise NotImplementedError( "To be implemented by subClass" )