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