Package mrv :: Package maya :: Package ui :: Module qa
[hide private]
[frames] | no frames]

Source Code for Module mrv.maya.ui.qa

  1  # -*- coding: utf-8 -*- 
  2  """ 
  3  Contains a modular UI able to display quality assurance checks, run them and 
  4  present their results. It should be easy to override and adjust it to suit additional needs 
  5  """ 
  6  __docformat__ = "restructuredtext" 
  7  import control 
  8  import util as uiutil 
  9  import layout 
 10  from mrv.automation.qa import QAWorkflow 
 11  import maya.OpenMaya as api 
 12  from itertools import chain 
 13  import re 
 14  from mrv.util import capitalize 
 15   
 16  import logging 
 17  log = logging.getLogger("mrv.maya.ui.qa") 
18 19 -class QACheckLayout( layout.RowLayout ):
20 """Row Layout able to display a qa check and related information 21 22 :note: currently we make assumptions about the positions of the children in the 23 RowLayout, thus you may only append new ones""" 24 reNiceNamePattern = re.compile( "[A-Z][a-z]" ) 25 26 #{ Configuration 27 # paths to icons to display 28 # [0] = check not run 29 # [1] = check success 30 # [2] = check failed 31 # [3] = check threw exception 32 icons = [ "offRadioBtnIcon.xpm", "onRadioBtnIcon.xpm", "fstop.xpm", "fstop.xpm" ] # explicitly a list to allow assignments 33 34 # height of the UI control 35 height = 25 36 37 # number of columns to use - assure to fill the respective slots 38 numcols = 3 39 #} END configuration 40
41 - def __new__( cls, *args, **kwargs ):
42 """Initialize this RowColumnLayout instance with a check instance 43 44 :param kwargs: 45 * check: 46 the check this instance should attach itself to - it needs to be set 47 or the instance creation will fail""" 48 check = kwargs.pop( "check" ) 49 50 numcols = cls.numcols # without fix 51 if check.plug.implements_fix: 52 numcols += 1 53 54 assert numcols < 7 # more than 6 not supported by underlying layout 55 56 kwargs[ 'numberOfColumns' ] = numcols 57 kwargs[ 'adj' ] = 1 58 kwargs[ 'h' ] = cls.height 59 kwargs[ 'cw%i' % numcols ] = ( cls.height + 2, ) * numcols 60 self = super( QACheckLayout, cls ).__new__( cls, *args, **kwargs ) 61 62 # create instance variables 63 self._check = check 64 return self
65
66 - def __init__( self, *args, **kwargs ):
67 """Initialize our instance with members""" 68 super( QACheckLayout, self ).__init__( *args, **kwargs ) 69 70 # populate 71 self._create( )
72 73 @staticmethod
74 - def _replInsSpace( match ):
75 """Generate a replace string from the match in the match object 76 77 :note: match should contain only a range of two chars""" 78 assert match.end() - match.start() == 2 79 if match.start() == 0: # in the beginning , replace by itself 80 return match.string[ match.start() : match.end() ] 81 82 # otherwise insert a space between items 83 return " " + match.string[ match.start() : match.end() ]
84 85
86 - def _toNiceName( self, name ):
87 """ 88 :return: nice name version of name, replacing underscores by spaces, and 89 separating camel cases, as well as chaning to the capitalizaion of word""" 90 name_tokens = name.split( "_" ) 91 92 # parse camel case 93 for i, token in enumerate( name_tokens ): 94 repl_token = self.reNiceNamePattern.sub( self._replInsSpace, token ) 95 name_tokens[ i ] = repl_token 96 # END for each token camel case parse 97 98 final_tokens = list() 99 100 # split once more on inserted spaces, capitalize 101 for token in name_tokens: 102 final_tokens.extend( capitalize( t ) for t in token.split( " " ) ) 103 104 return " ".join( final_tokens )
105 106
107 - def _create( self ):
108 """Create our layout elements according to the details given in check""" 109 # assume we are active 110 checkplug = self.check().plug 111 nice_name = self._toNiceName( checkplug.name() ) 112 self.add( control.Text( label = nice_name, ann = checkplug.annotation ) ) 113 114 ibutton = self.add( control.IconTextButton( style="iconOnly", 115 h = self.height, w = self.height ) ) 116 sbutton = self.add( control.Button( label = "S", w = self.height, 117 ann = "Select faild or fixed items" ) ) 118 119 # if we can actually fix the item, we add an additional button 120 if checkplug.implements_fix: 121 fbutton = self.add( control.Button( label = "Fix", ann = "Attempt to fix failed items" ) ) 122 fbutton.e_released = self._runCheck 123 # END fix button setup 124 125 # attach callbacks 126 ibutton.e_command = self._runCheck 127 sbutton.e_released = self.selectPressed
128
129 - def update( self ):
130 """Update ourselves to match information in our stored check""" 131 # check the cache for a result - if so, ask it for its state 132 # otherwise we are not run and indicate that 133 bicon = self.listChildren()[1] 134 bicon.p_image = self.icons[0] 135 136 check = self.check() 137 if check.hasCache(): 138 result = check.cache() 139 self.setResult( result )
140 # END if previous result exists 141 142
143 - def check( self ):
144 """:return: check we are operating upon""" 145 return self._check
146 147 #{ Check Callbacks 148
149 - def _runCheck( self, *args, **kwargs ):
150 """Run our check 151 152 :note: we may also be used as a ui callback and figure out ourselves 153 whether we have been pressed by the fix button or by the run button 154 :param kwargs: will be passed to the workflow's runChecks method. The following 155 additional kwargs may be specified: 156 157 * force_check: 158 if True, default True, a computation will be forced, 159 otherwise a cached result may be used 160 161 :return: result of our check""" 162 check = self.check() 163 wfl = check.node.workflow() 164 force_check = kwargs.pop( "force_check", True ) 165 166 mode = check.node.eMode.query 167 if args and isinstance( args[0], control.Button ): 168 mode = check.node.eMode.fix 169 # END fix button handling 170 171 return wfl.runChecks( [ check ], mode = mode, clear_result = force_check, **kwargs )[0][1]
172
173 - def selectPressed( self, *args ):
174 """Called if the selected button has been pressed 175 Triggers a workflow run if not yet done""" 176 # use the cache directly to prevent the whole runprocess to be kicked on 177 # although the result is already known 178 check = self.check() 179 result = None 180 if check.hasCache(): 181 result = check.cache() 182 else: 183 result = self._runCheck( force_check = False ) 184 185 # select items , ignore erorrs if it is not selectable 186 sellist = api.MSelectionList() 187 for item in chain( result.fixedItems(), result.failedItems() ): 188 try: 189 sellist.add( str( item ) ) 190 except RuntimeError: 191 pass 192 # END for each item to select 193 194 api.MGlobal.setActiveSelectionList( sellist )
195
196 - def preCheck( self ):
197 """Runs before the check starts""" 198 text = self.listChildren()[0] 199 text.p_label = "Running ..."
200
201 - def postCheck( self, result ):
202 """Runs after the check has finished including the given result""" 203 text = self.listChildren()[0] 204 text.p_label = str( self._toNiceName( self.check().plug.name() ) ) 205 206 self.setResult( result )
207
208 - def checkError( self, exception, workflow ):
209 """Called if the checks fails with an error 210 211 :param exception: exception object that was thrown by our check 212 :param workflow: workflow that ran the check""" 213 text = self.listChildren()[0] 214 text.p_label = str( self._toNiceName( self.check().plug.name() ) + " ( ERROR )" ) 215 log.error(str( exception ))
216
217 - def setResult( self, result ):
218 """Setup ourselves to indicate the given check result 219 220 :return: our adjusted iconTextButton Member""" 221 target_icon = self.icons[2] # failed by default 222 223 if result.isSuccessful(): 224 target_icon = self.icons[1] 225 elif result.isNull(): # indicates failure, something bad happened 226 target_icon = self.icons[3] 227 228 # annotate the text with the result 229 children = self.listChildren() 230 text = children[0] 231 232 bicon = children[1] 233 bicon.p_image = target_icon 234 235 return bicon
236 #} END interface
237 238 -class QALayout( layout.FormLayout, uiutil.iItemSet ):
239 """Layout able to dynamically display QAChecks, run them and display their result""" 240 241 #{ Configuration 242 # class used to create a layout displaying details about the check 243 # it must be compatible to QACheckLayout as a certain API is expected 244 checkuicls = QACheckLayout 245 246 # if True, a button to run all checks at once will be appended 247 # Can be passed in as per-instance value during creation 248 run_all_button = True 249 250 # class used to access default workflow events 251 qaworkflowcls = QAWorkflow 252 253 # if True, there will be an informational text if no checks have been found 254 # otherwiise the layout will simply be empty 255 show_text_if_empty = True 256 257 258 # if True, a scroll layout will be created around the layout containing a 259 # possibly long list of checks. Set False if you would like to handle the 260 # scrolling with an own interface 261 # Can be passed in as per-instance value during creation 262 scrollable = True 263 #} END configuration 264
265 - def __new__( cls, *args, **kwargs ):
266 """Set some default arguments""" 267 scrollable = kwargs.pop( "scrollable", cls.scrollable ) 268 run_all_button = kwargs.pop( "run_all_button", cls.run_all_button ) 269 self = super( QALayout, cls ).__new__( cls, *args, **kwargs ) 270 self.scrollable = scrollable 271 self.run_all_button = run_all_button 272 273 return self
274
275 - def __init__( self, *args, **kwargs ):
276 """Initialize our basic interface involving a column layout to store the 277 actual check widgets""" 278 super( QALayout, self ).__init__( *args, **kwargs ) 279 scroll_layout = None 280 281 if self.scrollable: 282 scroll_layout = self.add( layout.ScrollLayout( cr=1 ) ) 283 284 # will contain the checks 285 self.col_layout = layout.ColumnLayout( adj = 1 ) 286 if scroll_layout: 287 scroll_layout.add( self.col_layout ) 288 else: 289 self.add( self.col_layout ) 290 291 # END scroll_layout 292 self.setActive() 293 294 # name of text indicating there are no checks set 295 self.no_checks_text = None
296 #{ Interface 297
298 - def setChecks( self, checks ):
299 """Set the checks this layout should display 300 301 :param checks: iterable of qa checks as retrieved by `checks` 302 :raise ValueErorr: if one check is from a different workflow and there is a run_all button""" 303 # we might change the layout, so be active 304 # IMPORTANT: if this is not the case, we might easily confuse layouts ... 305 # figure out why exactly that happens 306 curparent = self.parent() 307 self.setActive() 308 309 # map check names to actual checks 310 name_to_check_map = dict( ( ( str( c ), c ) for c in checks ) ) 311 name_to_child_map = dict() 312 313 self.setItems( name_to_check_map.keys(), name_to_check_map = name_to_check_map, 314 name_to_child_map = name_to_child_map ) 315 316 # HANDLE NO CHECKS 317 #################### 318 if checks and self.no_checks_text: 319 self.no_checks_text.delete() 320 self.no_checks_text = None 321 # END checks text existed 322 323 if not checks and self.no_checks_text is None and self.show_text_if_empty: 324 prevparent = self.parent() 325 self.col_layout.setActive() 326 self.no_checks_text = control.Text( label = "No checks available" ) 327 prevparent.setActive() 328 # END no checks existed handling 329 330 331 # SET EVENTS 332 ############# 333 # NOTE: currently we only register ourselves for callbacks, and deregeister 334 # automatically through the weak reference system 335 wfls_done = list() 336 for check in checks: 337 cwfl = check.node.workflow() 338 if cwfl in wfls_done: 339 continue 340 wfls_done.append( cwfl ) 341 342 if self.run_all_button and len( wfls_done ) > 1: 343 raise ValueError( "This UI can currently only handle checks from only one workflow at a time if run_all_button is set" ) 344 345 cwfl.e_preCheck = self.checkHandler 346 cwfl.e_postCheck = self.checkHandler 347 cwfl.e_checkError = self.checkHandler 348 # END for each check 349 350 # POSSIBLY ADD BUTTON TO THE END 351 ################################# 352 # remove possibly existing button ( ignore the flag, its about the button here ) 353 # its stored in a column layout 354 button_layout_name = "additionals_column_layout" 355 layout_child = self.listChildren( predicate = lambda c: c.basename() == button_layout_name ) 356 if layout_child: 357 assert len( layout_child ) == 1 358 self.deleteChild( layout_child[0] ) 359 360 # create child layout ? 361 if self.run_all_button: 362 self.setActive() 363 layout_child = self.add( layout.ColumnLayout( adj = 1, name = button_layout_name ) ) 364 if layout_child: 365 control.Separator( style = "single", h = 10 ) 366 run_button = control.Button( label = "Run All", ann = "Run all checks in one go", 367 enable = len( checks ) > 0 ) 368 run_button.e_pressed = self.runAllPressed 369 # END button layout setup 370 self.setActive() 371 # END if run all button is requested 372 373 # setup form layout - depending on the amount of items - we have 1 or two 374 # children, never more 375 children = self.listChildren() 376 assert len( children ) < 3 377 o = 2 # offset 378 t,b,l,r = self.kSides 379 if len( children ) == 1: 380 c = children[0] 381 self.setup( af = ( ( c, b, o ), ( c, t, o ), ( c, l, o ), ( c, r, o ) ) ) 382 # END case one child 383 else: 384 c1 = children[0] 385 c2 = children[1] 386 self.setup( af = ( ( c1, l, o ), ( c1, r, o ), ( c1, t, o ), 387 ( c2, l, o ), ( c2, r, o ), ( c2, b, o ) ), 388 ac = ( ( c1, b, o, c2 ) ), 389 an = ( ( c2, t ) ) ) 390 # END case two children 391 392 # reset to the previous parent 393 curparent.setActive()
394 395
396 - def checkLayouts( self ):
397 """:return: list of checkLayouts representing our checks""" 398 ntcm = dict() 399 self.currentItemIds( name_to_child_map = ntcm ) 400 return ntcm.values()
401
402 - def checks( self ):
403 """:return: list of checks we are currently holding in our layout""" 404 return [ l.check() for l in self.checkLayouts() ]
405 406 #} END interface 407
408 - def currentItemIds( self, name_to_child_map = None, **kwargs ):
409 """:return: current check ids as defined by exsiting children. 410 :note: additionally fills in the name_to_child_map""" 411 outids = list() 412 for child in self.col_layout.listChildren( predicate = lambda c: isinstance( c, QACheckLayout ) ): 413 check = child.check() 414 cid = str( check ) 415 outids.append( cid ) 416 417 name_to_child_map[ cid ] = child 418 # END for each of our children 419 return outids
420
421 - def handleEvent( self, eventid, **kwargs ):
422 """Assure we have the proper layouts active""" 423 if eventid == self.eSetItemCBID.preCreate: 424 self.col_layout.setActive() 425 if eventid == self.eSetItemCBID.postCreate: 426 self.setActive()
427
428 - def createItem( self, checkid, name_to_child_map = None, name_to_check_map = None, **kwargs ):
429 """Create and return a layout displaying the given check instance 430 431 :param kwargs: will be passed to checkui class's initializer, allowing subclasses to easily 432 adjust the paramter list 433 :note: its using self.checkuicls to create the instance""" 434 self.col_layout.setActive() 435 check_child = self.checkuicls( check = name_to_check_map[ checkid ], **kwargs ) 436 name_to_child_map[ checkid ] = check_child 437 newItem = self.col_layout.add( check_child ) 438 439 return newItem
440
441 - def updateItem( self, checkid, name_to_child_map = None, **kwargs ):
442 """Update the item identified by the given checkid so that it represents the 443 current state of the application""" 444 name_to_child_map[ checkid ].update( )
445
446 - def removeItem( self, checkid, name_to_child_map = None, **kwargs ):
447 """Delete the user interface portion representing the checkid""" 448 self.col_layout.deleteChild( name_to_child_map[ checkid ] )
449 450 #{ Eventhandlers 451
452 - def _checkLayoutHasCheck( self, checkLayout, check ):
453 """:return: True if the given `QACheckLayout` manages the given check""" 454 return checkLayout.check() == check
455
456 - def checkHandler( self, event, check, *args ):
457 """Called for the given event - it will find the UI element handling the 458 call respective function on the UI instance 459 460 :note: find the check using predefined names as they server as unique-enough keys. 461 This would possibly be faster, but might not make a difference after all""" 462 463 # as we do not track the deletion of the window, our class might actually 464 # persist even though the window is long gone - throw if we are not existing 465 # to get auto-removed from the handler 466 assert self.exists() 467 468 # find a child handling the given check 469 # skip ones we do not find 470 checkchild = None 471 for child in self.checkLayouts(): 472 if self._checkLayoutHasCheck( child, check ): 473 checkchild = child 474 break 475 # END if check matches 476 # END for each child in children 477 478 # this could actually happen as we get calls for all checks, not only 479 # for the ones we actually have 480 if checkchild is None: 481 return 482 483 if event == self.qaworkflowcls.e_preCheck: 484 checkchild.preCheck( ) 485 elif event == self.qaworkflowcls.e_postCheck: 486 result = args[0] 487 checkchild.postCheck( result ) 488 elif event == self.qaworkflowcls.e_checkError: 489 exc = args[0] 490 wfl = args[1] 491 checkchild.checkError( exc, wfl )
492
493 - def runAllPressed( self, *args, **kwargs ):
494 """Called once the Run-All button is pressed 495 496 :param kwargs: will be passed to runChecks method of workflow 497 :note: we assume all checks are from one workflow only as we 498 do not sort them by workflow 499 :note: currently we only run in query mode as sort of safety measure - check and fix 500 on all might be too much and lead to unexpected results""" 501 checks = self.checks() 502 if not checks: 503 log.error("No checks found to run") 504 return 505 # END check assertion 506 507 wfl = checks[0].node.workflow() 508 wfl.runChecks( checks, clear_result = 1, **kwargs )
509 510 #} END Eventhandlers 511