Detecting a Nuke Panel closing

This is a simple, straight-forward style post. [EDIT: lol, I wish – click here to see what I ACTUALLY ended up doing]. I am working with updating my tks “Suite” of tools for Shotgun-Nuke integration, and I wanted to be sure I was doing everything as safely as possible. As such, I wanted to make sure that when a user “hides” the panel in Nuke, my panel cleans up after itself. (I noticed that, by default, when the panel is “closed” with the X box, the panel itself and the thread keep going and going and going in the background).

With some dir() inspection and some super() magic, I was able to determine the following functions to override if you want to add special nuke panel close and open logic:

class NotesPanel( QWidget ):
	def __init__( self , scrollable=True):
		QWidget.__init__(self)

	def hideEvent(self, *args, **kwargs):
		#this function fires when the user closes the panel
		super(NotesPanel, self).hideEvent(*args, **kwargs)

	def showEvent(self, *args, **kwargs):
		#this function fires when the user opens the panel
		super(NotesPanel, self).showEvent(*args, **kwargs)

Short and sweet! Hopefully this will help others who are looking for similar logic.

UPDATED: Well, nothing is ever that simple, is it?

Unfortunately, after implementing the above listeners, I noticed that hide and show fire in nuke a LOT. Specifically they fire when the user clicks another tab in the same pane. Makes sense; however, if we’re using this to cleanup our resources on close (I have some threads running I want to manage), you’ll be doing a lot of extra work all the time.

In addition, when wrapping a QWidget like this for nuke (using the built in nukescripts.registerWidgetAsPanel), there’s some internal QDialog/QWidget wrapping going on that makes finding the true pane a real pain! *Ugh. Add to this the fact that the user can drag around the tab to other panes, all the while firing hide events.

Ultimately, I created a separate WrappedPanel class, extending QWidget, to handle all of my panels. This class keeps track of its parentage (using a really wonky top-down approach that could probably be improved now I know more) and handles events for both the pane itself (hide, show) as well as the tab (listen for the tab being closed and destroy). I simply need to define a function on my actual panel, destroyPanel(self), that will handle any cleanup, and this class will call it when appropriate. There’s a lot here, including some timers (to deal with the aforementioned ability to drag the pane around), but it seems to get the job done so far.

#store active panels by class name (string) : obj
activePanels={}

def appendToActivePanels(panel):
	global activePanels
	
	activePanels[panel.id]=(panel.name, panel)
	
def removeFromActivePanels(panel):
	global activePanels
	
	del activePanels[panel.id]
	
def getPanelByName(panelName):
	global activePanels
	
	for panelID in activePanels:
		if activePanels[panelID][0]==panelName:
			return activePanels[panelID][1]
	
	
def getMainWindow():		
	for obj in QApplication.topLevelWidgets():
		if obj.inherits('QMainWindow') and obj.metaObject().className() == 'Foundry::UI::DockMainWindow':
			return obj
	else:
		raise RuntimeError('Could not find DockMainWindow instance')

#define a panel type for this app with anything necessary to show/close appropriately	
class WrappedPanel( QWidget ):
	def __init__(self, name, *args, **kwargs):
		
		QWidget.__init__(self)
		
		self.name=name
		#create a unique id for this panel
		self.id=uuid4().hex

		
		self.tabParent=None
		self.parentCount=0
		self.widgetNum=0
		self.container=None
		
		self.destoryTimer=None
		
		appendToActivePanels(self)
				
	#fired when mouse leaves the panel
	def leaveEvent(self, *args, **kwargs):
		#self.debug(3, 'leave eventing.')
		super(WrappedPanel, self).leaveEvent(*args, **kwargs)
	
	#none of these events fired in nuke in my testing
	"""
	def dropEvent(self, *args, **kwargs):
		self.debug(3, 'drop eventing.')
		super(WrappedPanel, self).dropEvent(*args, **kwargs)
		
	def closeEvent(self, *args, **kwargs):
		self.debug(3, 'close eventing.')
		super(WrappedPanel, self).closeEvent(*args, **kwargs)
		
	def dragMoveEvent(self, *args, **kwargs):
		self.debug(3, 'drag move eventing.')
		super(WrappedPanel, self).dragMoveEvent(*args, **kwargs)
		
	def destroyEvent (self, *args, **kwargs):
		self.debug(3, 'destroy eventing.')
		super(WrappedPanel, self).destroy(*args, **kwargs)
	"""
		
	def showEvent(self, event):
		#self.debug(3, 'show eventing.')
		#self.debug(3, 'event: '+str(event))
		#self.debug(3, 'spontaneous: '+str(event.spontaneous()))
			
		super(WrappedPanel, self).showEvent(event)
		self.setup()
		self.show()
		
	def setup(self, force=False):
		#get the parent tab for nuke
		if not self.tabParent or force:
			self.tabParent=self.locateParent()
			if self.tabParent:
				self.parentCount=self.tabParent.count()
				self.widgetNum=self.tabParent.currentIndex()
				self.container=self.tabParent.currentWidget()
			
				#connect the destroy event to our widget
				self.tabParent.widgetRemoved.connect(self.widgetRemoved)
				
			else:
				self.destroyPanel()
				removeFromActivePanels(self)
		
	def hideEvent(self, event):
		super(WrappedPanel, self).hideEvent(event)
		self.hide()
		
		#nothing we check at this point will be accurate, since there's a pause
		#so we'll use a timer
		if not self.destoryTimer:
			self.destroyTimer=QtCore.QTimer.singleShot(5000, self.destroyCheck)
		
	def widgetRemoved(self):
		#this gets fired when the window is moved around in the same tab, to another tab, and when closed
		#we'll need to check if our parent is the same
		
		#nothing we check at this point will be accurate, since there's a pause
		#so we'll use a timer
		if not self.destoryTimer:
			self.destroyTimer=QtCore.QTimer.singleShot(5000, self.destroyCheck)
			
	def destroyCheck(self):
		#check if we're still a member of the tab
		self.widgetNum=self.tabParent.indexOf(self.container)
		if self.widgetNum==-1:
			#'no longer a child of '+str(self.container))
			try:
				self.tabParent.widgetRemoved.disconnect(self.destroy)
				
				#wait for the user to possibly assign to another pane
				QtCore.QTimer.singleShot(5000, lambda: self.setup(force=True))
			except RuntimeError:
				#this is bad practice, but since hide and widget removed can fire at the same time, this function can run twice
				#we'll check to see if we can't remove the signal, and assume that's because we're already doing it
				pass
			
		self.destroyTimer=None
		
	def locateParent(self):
		#unfortunately parents aren't reliable at this stage, so we'll need to walk down from the main app to find the tab widget
		window=getMainWindow()
		
		#this is a list because of the way recursive functions and global variables interact in python 2
		#the only element we are interested in is the first in the list
		qStackedWidget=[None]
		
		def getChildren(obj):
			#i'm sure there's a better way to do this walk, but i can't think of it right now
			if obj.children():
				lister={objector.metaObject().className():getattr(objector, 'id', None) for objector in obj.children()}
				if self.name in lister.keys():
					#since we can have two instances of the same pane, we need to ensure its this one by checking for our id attribute
					if self.id==lister[self.name]:
						
						#walk up and find the qstackedwidget parent
						parent=obj.parent()
						
						while parent:
							meta=parent.metaObject().className()
							# 'found widget: '+meta)
							if meta=="QStackedWidget":
								qStackedWidget[0]=parent
								break
							parent=parent.parent()
				else:	
					lister=[getChildren(objector) for objector in obj.children()]
		
		for obj in window.children():
			if not qStackedWidget[0]:
				getChildren(obj)
				
		qStackedWidget=qStackedWidget[0]
		return qStackedWidget

Leave a Reply

Post Navigation