@@ -90,7 +90,7 @@ class JobMonitorTree(cuegui.AbstractTreeWidget.AbstractTreeWidget):
9090 """Tree widget to display a list of monitored jobs."""
9191
9292 __loadMine = True
93- __groupDependent = True
93+ __groupByMode = "Clear" # Options: "Clear", "Dependent", "Show-Shot", "Show-Shot-Username"
9494 view_object = QtCore .Signal (object )
9595
9696 def __init__ (self , parent ):
@@ -182,6 +182,8 @@ def __init__(self, parent):
182182 self .__dependentJobs = {}
183183 self ._dependent_items = {}
184184 self .__reverseDependents = {}
185+ self .__groupItems = {} # For Show-Shot and Show-Shot-Username grouping
186+ self .__groupExpansionState = {} # Track expansion state of group items
185187 self .local_plugin_saved_values = {}
186188 # Used to build right click context menus
187189 self .__menuActions = cuegui .MenuActions .MenuActions (
@@ -274,12 +276,25 @@ def setLoadMine(self, value):
274276 @type value: boolean or QtCore.Qt.Checked or QtCore.Qt.Unchecked"""
275277 self .__loadMine = (value is True or value == QtCore .Qt .Checked )
276278
277- def setGroupDependent (self , value ):
278- """Enables or disables the auto grouping of the dependent jobs
279- @param value: New groupDependent state
280- @type value: boolean or QtCore.Qt.Checked or QtCore.Qt.Unchecked"""
281- self .__groupDependent = (value is True or value == QtCore .Qt .Checked )
282- self .updateRequest ()
279+ def setGroupBy (self , mode ):
280+ """Sets the grouping mode for jobs
281+ @param mode: Grouping mode ("Clear", "Dependent", "Show-Shot", "Show-Shot-Username")
282+ @type mode: str"""
283+ if mode in ["Clear" , "Dependent" , "Show-Shot" , "Show-Shot-Username" ]:
284+ old_mode = self .__groupByMode
285+ self .__groupByMode = mode
286+
287+ # If we have existing jobs, regroup them
288+ if self ._items and old_mode != mode :
289+ current_jobs = {}
290+ for proxy , item in list (self ._items .items ()):
291+ current_jobs [proxy ] = item .rpcObject
292+
293+ # Process update with new grouping
294+ if current_jobs :
295+ self ._processUpdate (None , current_jobs )
296+
297+ self .updateRequest ()
283298
284299 def addJob (self , job , timestamp = None , loading_from_config = False ):
285300 """Adds a job to the list. With locking"
@@ -294,7 +309,7 @@ def addJob(self, job, timestamp=None, loading_from_config=False):
294309 try :
295310 if newJobObj :
296311 jobKey = cuegui .Utils .getObjectKey (newJobObj )
297- if not self .__groupDependent :
312+ if self .__groupByMode == "Clear" :
298313 self .__load [jobKey ] = newJobObj
299314 self .__jobTimeLoaded [jobKey ] = timestamp if timestamp else time .time ()
300315 else :
@@ -397,6 +412,7 @@ def removeAllItems(self):
397412 del self .__jobTimeLoaded [proxy ]
398413 self .__dependentJobs .clear ()
399414 self .__reverseDependents .clear ()
415+ self .__groupItems .clear ()
400416 cuegui .AbstractTreeWidget .AbstractTreeWidget .removeAllItems (self )
401417
402418 def removeFinishedItems (self ):
@@ -497,6 +513,20 @@ def contextMenuEvent(self, e):
497513 self .__menuActions .jobs ().addAction (color_menu , "setUserColor2" )
498514 self .__menuActions .jobs ().addAction (color_menu , "setUserColor3" )
499515 self .__menuActions .jobs ().addAction (color_menu , "setUserColor4" )
516+ self .__menuActions .jobs ().addAction (color_menu , "setUserColor5" )
517+ self .__menuActions .jobs ().addAction (color_menu , "setUserColor6" )
518+ self .__menuActions .jobs ().addAction (color_menu , "setUserColor7" )
519+ self .__menuActions .jobs ().addAction (color_menu , "setUserColor8" )
520+ self .__menuActions .jobs ().addAction (color_menu , "setUserColor9" )
521+ self .__menuActions .jobs ().addAction (color_menu , "setUserColor10" )
522+ self .__menuActions .jobs ().addAction (color_menu , "setUserColor11" )
523+ self .__menuActions .jobs ().addAction (color_menu , "setUserColor12" )
524+ self .__menuActions .jobs ().addAction (color_menu , "setUserColor13" )
525+ self .__menuActions .jobs ().addAction (color_menu , "setUserColor14" )
526+ self .__menuActions .jobs ().addAction (color_menu , "setUserColor15" )
527+ color_menu .addSeparator ()
528+ self .__menuActions .jobs ().addAction (color_menu , "setUserCustomColor" )
529+ color_menu .addSeparator ()
500530 self .__menuActions .jobs ().addAction (color_menu , "clearUserColor" )
501531 menu .addMenu (color_menu )
502532
@@ -570,6 +600,62 @@ def actionSetUserColor(self, color):
570600 self .__userColors [objectKey ] = color
571601 item .setUserColor (color )
572602
603+ def actionSetUserCustomColor (self ):
604+ """Opens a dialog to set a custom RGB color for selected items"""
605+ dialog = QtWidgets .QDialog (self )
606+ dialog .setWindowTitle ("Set Custom Color (RGB)" )
607+ dialog .setModal (True )
608+
609+ layout = QtWidgets .QFormLayout ()
610+
611+ # Create spinboxes for RGB values
612+ r_spinbox = QtWidgets .QSpinBox ()
613+ r_spinbox .setRange (0 , 255 )
614+ r_spinbox .setValue (100 )
615+
616+ g_spinbox = QtWidgets .QSpinBox ()
617+ g_spinbox .setRange (0 , 255 )
618+ g_spinbox .setValue (100 )
619+
620+ b_spinbox = QtWidgets .QSpinBox ()
621+ b_spinbox .setRange (0 , 255 )
622+ b_spinbox .setValue (100 )
623+
624+ # Color preview label
625+ preview_label = QtWidgets .QLabel ()
626+ preview_label .setMinimumSize (200 , 50 )
627+ preview_label .setFrameStyle (QtWidgets .QFrame .Box )
628+ preview_label .setStyleSheet ("background-color: rgb(100, 100, 100);" )
629+
630+ def update_preview ():
631+ r = r_spinbox .value ()
632+ g = g_spinbox .value ()
633+ b = b_spinbox .value ()
634+ preview_label .setStyleSheet (f"background-color: rgb({ r } , { g } , { b } );" )
635+
636+ r_spinbox .valueChanged .connect (update_preview )
637+ g_spinbox .valueChanged .connect (update_preview )
638+ b_spinbox .valueChanged .connect (update_preview )
639+
640+ layout .addRow ("Red (0-255):" , r_spinbox )
641+ layout .addRow ("Green (0-255):" , g_spinbox )
642+ layout .addRow ("Blue (0-255):" , b_spinbox )
643+ layout .addRow ("Preview:" , preview_label )
644+
645+ # Buttons
646+ button_box = QtWidgets .QDialogButtonBox (
647+ QtWidgets .QDialogButtonBox .Ok | QtWidgets .QDialogButtonBox .Cancel )
648+ button_box .accepted .connect (dialog .accept )
649+ button_box .rejected .connect (dialog .reject )
650+ layout .addRow (button_box )
651+
652+ dialog .setLayout (layout )
653+
654+ if dialog .exec_ () == QtWidgets .QDialog .Accepted :
655+ # Create custom color from RGB values
656+ custom_color = QtGui .QColor (r_spinbox .value (), g_spinbox .value (), b_spinbox .value ())
657+ self .actionSetUserColor (custom_color )
658+
573659 def actionEatSelectedItems (self ):
574660 """Eats all dead frames for selected jobs"""
575661 self .__menuActions .jobs ().eatDead ()
@@ -635,11 +721,15 @@ def _getUpdate(self):
635721 monitored_proxies .remove (proxy )
636722
637723 if monitored_proxies :
638- for job in opencue .api .getJobs (
639- id = [proxyId .split ('.' )[- 1 ] for proxyId in monitored_proxies ],
640- include_finished = True ):
641- objectKey = cuegui .Utils .getObjectKey (job )
642- jobs [objectKey ] = job
724+ # Batch fetch jobs to improve performance
725+ batch_size = 50 # Fetch in smaller batches to avoid timeouts
726+ for i in range (0 , len (monitored_proxies ), batch_size ):
727+ batch = monitored_proxies [i :i + batch_size ]
728+ for job in opencue .api .getJobs (
729+ id = [proxyId .split ('.' )[- 1 ] for proxyId in batch ],
730+ include_finished = True ):
731+ objectKey = cuegui .Utils .getObjectKey (job )
732+ jobs [objectKey ] = job
643733
644734 except opencue .exception .CueException as e :
645735 list (map (logger .warning , cuegui .Utils .exceptionOutput (e )))
@@ -672,16 +762,92 @@ def _processUpdate(self, work, rpcObjects):
672762 for item in self ._dependent_items .values ():
673763 self .__jobTimeLoaded [cuegui .Utils .getObjectKey (item .rpcObject )] = item .created
674764
765+ # Save expansion state of current group items
766+ for group_key , group_item in self .__groupItems .items ():
767+ self .__groupExpansionState [group_key ] = group_item .isExpanded ()
768+
675769 self ._items = {}
770+ self .__groupItems = {}
676771 self .clear ()
677772
678773 for proxy , job in iteritems (rpcObjects ):
679- self ._items [proxy ] = JobWidgetItem (job ,
680- self .invisibleRootItem (),
681- self .__jobTimeLoaded .get (proxy , None ))
682- if proxy in self .__userColors :
683- self ._items [proxy ].setUserColor (self .__userColors [proxy ])
684- if self .__groupDependent :
774+ # Handle different grouping modes
775+ if self .__groupByMode == "Clear" :
776+ # No grouping - flat list
777+ self ._items [proxy ] = JobWidgetItem (job ,
778+ self .invisibleRootItem (),
779+ self .__jobTimeLoaded .get (proxy , None ))
780+
781+ elif self .__groupByMode == "Show-Shot" :
782+ # Group by show-shot
783+ job_name = job .data .name
784+ parts = job_name .split ("-" )
785+ if len (parts ) >= 2 :
786+ show = parts [0 ]
787+ shot = parts [1 ]
788+ group_key = f"{ show } -{ shot } "
789+
790+ # Create or get group parent item
791+ if group_key not in self .__groupItems :
792+ self .__groupItems [group_key ] = GroupWidgetItem (
793+ group_key , self .invisibleRootItem (), "show-shot" )
794+ # Restore expansion state or default to expanded
795+ is_expanded = self .__groupExpansionState .get (group_key , True )
796+ self .__groupItems [group_key ].setExpanded (is_expanded )
797+
798+ # Add job as child of group
799+ self ._items [proxy ] = JobWidgetItem (job ,
800+ self .__groupItems [group_key ],
801+ self .__jobTimeLoaded .get (proxy , None ))
802+ else :
803+ # Can't parse show-shot, add to root
804+ self ._items [proxy ] = JobWidgetItem (job ,
805+ self .invisibleRootItem (),
806+ self .__jobTimeLoaded .get (proxy , None ))
807+
808+ elif self .__groupByMode == "Show-Shot-Username" :
809+ # Group by show-shot-username
810+ job_name = job .data .name
811+ parts = job_name .split ("-" )
812+ if len (parts ) >= 2 :
813+ show = parts [0 ]
814+ shot = parts [1 ]
815+ # Extract username from the rest
816+ if len (parts ) >= 3 :
817+ rest = "-" .join (parts [2 :])
818+ username_parts = rest .split ("_" )
819+ if username_parts :
820+ username = username_parts [0 ]
821+ else :
822+ username = "unknown"
823+ else :
824+ username = "unknown"
825+
826+ group_key = f"{ show } -{ shot } -{ username } "
827+
828+ # Create or get group parent item
829+ if group_key not in self .__groupItems :
830+ self .__groupItems [group_key ] = GroupWidgetItem (
831+ group_key , self .invisibleRootItem (), "show-shot-username" )
832+ # Restore expansion state or default to expanded
833+ is_expanded = self .__groupExpansionState .get (group_key , True )
834+ self .__groupItems [group_key ].setExpanded (is_expanded )
835+
836+ # Add job as child of group
837+ self ._items [proxy ] = JobWidgetItem (job ,
838+ self .__groupItems [group_key ],
839+ self .__jobTimeLoaded .get (proxy , None ))
840+ else :
841+ # Can't parse show-shot-username, add to root
842+ self ._items [proxy ] = JobWidgetItem (job ,
843+ self .invisibleRootItem (),
844+ self .__jobTimeLoaded .get (proxy , None ))
845+
846+ elif self .__groupByMode == "Dependent" :
847+ # Dependent mode - group by job dependencies
848+ self ._items [proxy ] = JobWidgetItem (job ,
849+ self .invisibleRootItem (),
850+ self .__jobTimeLoaded .get (proxy , None ))
685851 dependent_jobs = self .__dependentJobs .get (proxy , [])
686852 for djob in dependent_jobs :
687853 item = JobWidgetItem (djob ,
@@ -693,6 +859,9 @@ def _processUpdate(self, work, rpcObjects):
693859 self ._dependent_items [dkey ].setUserColor (
694860 self .__userColors [dkey ])
695861
862+ if proxy in self .__userColors :
863+ self ._items [proxy ].setUserColor (self .__userColors [proxy ])
864+
696865 self .verticalScrollBar ().setRange (scrolled , len (rpcObjects .keys ()) - scrolled )
697866 list (map (lambda key : self ._items [key ].setSelected (True ),
698867 [key for key in selectedKeys if key in self ._items ]))
@@ -797,3 +966,41 @@ def data(self, col, role):
797966 return self .rpcObject .isPaused ()
798967
799968 return cuegui .Constants .QVARIANT_NULL
969+
970+
971+ class GroupWidgetItem (QtWidgets .QTreeWidgetItem ):
972+ """Represents a group parent item in the JobMonitorTree."""
973+
974+ def __init__ (self , group_name , parent , group_type ):
975+ """Initialize a group widget item.
976+ @param group_name: The name of the group (e.g., "show-shot" or "show-shot-username")
977+ @param parent: The parent item
978+ @param group_type: Type of grouping ("show-shot" or "show-shot-username")
979+ """
980+ QtWidgets .QTreeWidgetItem .__init__ (self , parent )
981+ self .group_name = group_name
982+ self .group_type = group_type
983+ self .setText (0 , group_name )
984+
985+ # Set bold font for group headers
986+ font = QtGui .QFont ()
987+ font .setBold (True )
988+ self .setFont (0 , font )
989+
990+ # Make group headers non-selectable
991+ self .setFlags (self .flags () & ~ QtCore .Qt .ItemIsSelectable )
992+
993+ def data (self , col , role ):
994+ """Return data for the given column and role."""
995+ if role == QtCore .Qt .DisplayRole :
996+ if col == 0 :
997+ return self .group_name
998+ return ""
999+
1000+ if role == QtCore .Qt .FontRole and col == 0 :
1001+ font = QtGui .QFont ()
1002+ font .setBold (True )
1003+ return font
1004+
1005+ # Let the parent handle all other roles (including selection colors)
1006+ return QtWidgets .QTreeWidgetItem .data (self , col , role )
0 commit comments