{"id":350,"date":"2020-09-14T12:36:34","date_gmt":"2020-09-14T16:36:34","guid":{"rendered":"http:\/\/www.tks-designs.com\/blog\/?p=350"},"modified":"2021-01-23T14:26:34","modified_gmt":"2021-01-23T19:26:34","slug":"detecting-a-nuke-panel-closing","status":"publish","type":"post","link":"https:\/\/www.tks-designs.com\/blog\/?p=350","title":{"rendered":"Detecting a Nuke Panel closing"},"content":{"rendered":"\n<p>This is a simple, straight-forward style post. <strong>[EDIT: lol, I wish &#8211; <a href=\"#update\" data-type=\"internal\" data-id=\"#update\">click here <\/a>to see what I ACTUALLY ended up doing]<\/strong>. I am working with updating my tks &#8220;Suite&#8221; 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 &#8220;hides&#8221; the panel in Nuke, my panel cleans up after itself. (I noticed that, by default, when the panel is &#8220;closed&#8221; with the X box, the panel itself and the thread keep going and going and going in the background).<\/p>\n\n\n\n<p>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:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>class NotesPanel( QWidget ):\n\tdef __init__( self , scrollable=True):\n\t\tQWidget.__init__(self)\n\n\tdef hideEvent(self, *args, **kwargs):\n\t\t#this function fires when the user closes the panel\n\t\tsuper(NotesPanel, self).hideEvent(*args, **kwargs)\n\n\tdef showEvent(self, *args, **kwargs):\n\t\t#this function fires when the user opens the panel\n\t\tsuper(NotesPanel, self).showEvent(*args, **kwargs)<\/code><\/pre>\n\n\n\n<p>Short and sweet! Hopefully this will help others who are looking for similar logic.<\/p>\n\n\n\n<p id=\"update\"><strong>UPDATED:<\/strong> Well, nothing is ever that simple, is it?<\/p>\n\n\n\n<!--more-->\n\n\n\n<p>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&#8217;re using this to cleanup our resources on close (I have some threads running I want to manage), you&#8217;ll be doing a lot of extra work all the time.<\/p>\n\n\n\n<p>In addition, when wrapping a QWidget like this for nuke (using the built in nukescripts.registerWidgetAsPanel), there&#8217;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.<\/p>\n\n\n\n<p>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&#8217;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.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>#store active panels by class name (string) : obj\nactivePanels={}\n\ndef appendToActivePanels(panel):\n\tglobal activePanels\n\t\n\tactivePanels&#91;panel.id]=(panel.name, panel)\n\t\ndef removeFromActivePanels(panel):\n\tglobal activePanels\n\t\n\tdel activePanels&#91;panel.id]\n\t\ndef getPanelByName(panelName):\n\tglobal activePanels\n\t\n\tfor panelID in activePanels:\n\t\tif activePanels&#91;panelID]&#91;0]==panelName:\n\t\t\treturn activePanels&#91;panelID]&#91;1]\n\t\n\t\ndef getMainWindow():\t\t\n\tfor obj in QApplication.topLevelWidgets():\n\t\tif obj.inherits('QMainWindow') and obj.metaObject().className() == 'Foundry::UI::DockMainWindow':\n\t\t\treturn obj\n\telse:\n\t\traise RuntimeError('Could not find DockMainWindow instance')\n\n#define a panel type for this app with anything necessary to show\/close appropriately\t\nclass WrappedPanel( QWidget ):\n\tdef __init__(self, name, *args, **kwargs):\n\t\t\n\t\tQWidget.__init__(self)\n\t\t\n\t\tself.name=name\n\t\t#create a unique id for this panel\n\t\tself.id=uuid4().hex\n\n\t\t\n\t\tself.tabParent=None\n\t\tself.parentCount=0\n\t\tself.widgetNum=0\n\t\tself.container=None\n\t\t\n\t\tself.destoryTimer=None\n\t\t\n\t\tappendToActivePanels(self)\n\t\t\t\t\n\t#fired when mouse leaves the panel\n\tdef leaveEvent(self, *args, **kwargs):\n\t\t#self.debug(3, 'leave eventing.')\n\t\tsuper(WrappedPanel, self).leaveEvent(*args, **kwargs)\n\t\n\t#none of these events fired in nuke in my testing\n\t\"\"\"\n\tdef dropEvent(self, *args, **kwargs):\n\t\tself.debug(3, 'drop eventing.')\n\t\tsuper(WrappedPanel, self).dropEvent(*args, **kwargs)\n\t\t\n\tdef closeEvent(self, *args, **kwargs):\n\t\tself.debug(3, 'close eventing.')\n\t\tsuper(WrappedPanel, self).closeEvent(*args, **kwargs)\n\t\t\n\tdef dragMoveEvent(self, *args, **kwargs):\n\t\tself.debug(3, 'drag move eventing.')\n\t\tsuper(WrappedPanel, self).dragMoveEvent(*args, **kwargs)\n\t\t\n\tdef destroyEvent (self, *args, **kwargs):\n\t\tself.debug(3, 'destroy eventing.')\n\t\tsuper(WrappedPanel, self).destroy(*args, **kwargs)\n\t\"\"\"\n\t\t\n\tdef showEvent(self, event):\n\t\t#self.debug(3, 'show eventing.')\n\t\t#self.debug(3, 'event: '+str(event))\n\t\t#self.debug(3, 'spontaneous: '+str(event.spontaneous()))\n\t\t\t\n\t\tsuper(WrappedPanel, self).showEvent(event)\n\t\tself.setup()\n\t\tself.show()\n\t\t\n\tdef setup(self, force=False):\n\t\t#get the parent tab for nuke\n\t\tif not self.tabParent or force:\n\t\t\tself.tabParent=self.locateParent()\n\t\t\tif self.tabParent:\n\t\t\t\tself.parentCount=self.tabParent.count()\n\t\t\t\tself.widgetNum=self.tabParent.currentIndex()\n\t\t\t\tself.container=self.tabParent.currentWidget()\n\t\t\t\n\t\t\t\t#connect the destroy event to our widget\n\t\t\t\tself.tabParent.widgetRemoved.connect(self.widgetRemoved)\n\t\t\t\t\n\t\t\telse:\n\t\t\t\tself.destroyPanel()\n\t\t\t\tremoveFromActivePanels(self)\n\t\t\n\tdef hideEvent(self, event):\n\t\tsuper(WrappedPanel, self).hideEvent(event)\n\t\tself.hide()\n\t\t\n\t\t#nothing we check at this point will be accurate, since there's a pause\n\t\t#so we'll use a timer\n\t\tif not self.destoryTimer:\n\t\t\tself.destroyTimer=QtCore.QTimer.singleShot(5000, self.destroyCheck)\n\t\t\n\tdef widgetRemoved(self):\n\t\t#this gets fired when the window is moved around in the same tab, to another tab, and when closed\n\t\t#we'll need to check if our parent is the same\n\t\t\n\t\t#nothing we check at this point will be accurate, since there's a pause\n\t\t#so we'll use a timer\n\t\tif not self.destoryTimer:\n\t\t\tself.destroyTimer=QtCore.QTimer.singleShot(5000, self.destroyCheck)\n\t\t\t\n\tdef destroyCheck(self):\n\t\t#check if we're still a member of the tab\n\t\tself.widgetNum=self.tabParent.indexOf(self.container)\n\t\tif self.widgetNum==-1:\n\t\t\t#'no longer a child of '+str(self.container))\n\t\t\ttry:\n\t\t\t\tself.tabParent.widgetRemoved.disconnect(self.destroy)\n\t\t\t\t\n\t\t\t\t#wait for the user to possibly assign to another pane\n\t\t\t\tQtCore.QTimer.singleShot(5000, lambda: self.setup(force=True))\n\t\t\texcept RuntimeError:\n\t\t\t\t#this is bad practice, but since hide and widget removed can fire at the same time, this function can run twice\n\t\t\t\t#we'll check to see if we can't remove the signal, and assume that's because we're already doing it\n\t\t\t\tpass\n\t\t\t\n\t\tself.destroyTimer=None\n\t\t\n\tdef locateParent(self):\n\t\t#unfortunately parents aren't reliable at this stage, so we'll need to walk down from the main app to find the tab widget\n\t\twindow=getMainWindow()\n\t\t\n\t\t#this is a list because of the way recursive functions and global variables interact in python 2\n\t\t#the only element we are interested in is the first in the list\n\t\tqStackedWidget=&#91;None]\n\t\t\n\t\tdef getChildren(obj):\n\t\t\t#i'm sure there's a better way to do this walk, but i can't think of it right now\n\t\t\tif obj.children():\n\t\t\t\tlister={objector.metaObject().className():getattr(objector, 'id', None) for objector in obj.children()}\n\t\t\t\tif self.name in lister.keys():\n\t\t\t\t\t#since we can have two instances of the same pane, we need to ensure its this one by checking for our id attribute\n\t\t\t\t\tif self.id==lister&#91;self.name]:\n\t\t\t\t\t\t\n\t\t\t\t\t\t#walk up and find the qstackedwidget parent\n\t\t\t\t\t\tparent=obj.parent()\n\t\t\t\t\t\t\n\t\t\t\t\t\twhile parent:\n\t\t\t\t\t\t\tmeta=parent.metaObject().className()\n\t\t\t\t\t\t\t# 'found widget: '+meta)\n\t\t\t\t\t\t\tif meta==\"QStackedWidget\":\n\t\t\t\t\t\t\t\tqStackedWidget&#91;0]=parent\n\t\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t\tparent=parent.parent()\n\t\t\t\telse:\t\n\t\t\t\t\tlister=&#91;getChildren(objector) for objector in obj.children()]\n\t\t\n\t\tfor obj in window.children():\n\t\t\tif not qStackedWidget&#91;0]:\n\t\t\t\tgetChildren(obj)\n\t\t\t\t\n\t\tqStackedWidget=qStackedWidget&#91;0]\n\t\treturn qStackedWidget<\/code><\/pre>\n","protected":false},"excerpt":{"rendered":"<p>This is a simple, straight-forward style post. [EDIT: lol, I wish &#8211; click here to see what I ACTUALLY ended up doing]. I am working with updating my tks &#8220;Suite&#8221; 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 <span class=\"ellipsis\">&hellip;<\/span> <span class=\"more-link-wrap\"><a href=\"https:\/\/www.tks-designs.com\/blog\/?p=350\" class=\"more-link\"><span>Continue Reading &rarr;<\/span><\/a><\/span><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"kia_subtitle":"hopefully a new google result","footnotes":""},"categories":[3,4,19],"tags":[],"class_list":["post-350","post","type-post","status-publish","format-standard","hentry","category-nuke","category-python","category-qt"],"_links":{"self":[{"href":"https:\/\/www.tks-designs.com\/blog\/index.php?rest_route=\/wp\/v2\/posts\/350","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.tks-designs.com\/blog\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.tks-designs.com\/blog\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.tks-designs.com\/blog\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.tks-designs.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=350"}],"version-history":[{"count":6,"href":"https:\/\/www.tks-designs.com\/blog\/index.php?rest_route=\/wp\/v2\/posts\/350\/revisions"}],"predecessor-version":[{"id":385,"href":"https:\/\/www.tks-designs.com\/blog\/index.php?rest_route=\/wp\/v2\/posts\/350\/revisions\/385"}],"wp:attachment":[{"href":"https:\/\/www.tks-designs.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=350"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.tks-designs.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=350"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.tks-designs.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=350"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}