In the middle of developing the next optimised release of NNSuperResolution and also our upcoming Nuke plugin in the NN family (be sure to check back pretty soon for the release!), I did a little detour to try and fix one of Nuke’s annoying and long standing “features” – the dreaded “middle click to zoom out in the DAG”. I haven’t met a single compositor that likes or uses this feature, rather the opposite – people seem to more or less hate it.

The problem here is that Nuke doesn’t natively support turning off this behaviour. There are no preferences, and there are no python API calls either. To be able to tweak this behaviour you rather have to hack into the Qt widget stack that is making up the GUI in Nuke. You got to find the correct widget, in this case the DAG, and install an event filter on it which makes it possible to get a callback to your own function whenever something you are listening in on is happening. In this case the events we are listening for are middle clicks. What we then do is catching that event, and doing some hacks to get the behaviour we are after. There are some filtering for only triggering our override when you middle click (actually middle press and then middle release close to the same coordinates), and not when you are using the middle mouse (read: Wacom pen) button to pan around. We also create and delete a Dot node, and send a left mouse button click instead of the middle click to the DAG to make it work seamlessly in the background.

It wasn’t super straight forward to code, and needed quite a lot of trial and error, but after a few hours I managed to produce something that works well (for me at least). Here is the code for you to use if you would like:

from PySide2 import QtWidgets, QtGui, QtCore, QtOpenGL

class MouseEventGrabber(QtCore.QObject):
    def __init__(self):
        super(MouseEventGrabber, self).__init__()
        self.middleclicked = False
        self.clickpos = None
        self.app = QtWidgets.QApplication.instance()
        dags = [widget for widget in self.app.allWidgets() if widget.windowTitle() == "Node Graph"]
        if dags:
            self.dag = None
            if dags[0].size().height() > dags[1].size().height():
                self.dag = dags[1].findChild(QtOpenGL.QGLWidget)
            else:
                self.dag = dags[0].findChild(QtOpenGL.QGLWidget)
            if self.dag:
                print "Installing DAG event filter"
                self.dag.installEventFilter(self)
        if not dags or not self.dag:
            print "Couldn't install event filter, DAG not found"
    
    def eventFilter(self, widget, event):
        '''Grab mouse and key events.'''
        if event.type() == QtCore.QEvent.MouseButtonPress and event.button() == QtCore.Qt.MouseButton.MiddleButton:
            self.middleclicked = True
            self.clickpos = QtGui.QCursor.pos()
            #print "Set middle clicked: True (position: %d, %d)" % (self.clickpos.x(), self.clickpos.y())
        if event.type() == QtCore.QEvent.MouseButtonRelease and event.button() == QtCore.Qt.MouseButton.MiddleButton and self.middleclicked:
            newpos = QtGui.QCursor.pos()
            #print "Set middle clicked: False (position: %d, %d)" % (newpos.x(), newpos.y())
            self.middleclicked = False
            if newpos.x() > self.clickpos.x() - 5 and newpos.x() < self.clickpos.x() + 5 and newpos.y() > self.clickpos.y() - 5 and newpos.y() < self.clickpos.y() + 5:
                print "Blocked zoom out from middleclick"
                import nukescripts
                nukescripts.clear_selection_recursive()
                dot = nuke.createNode("Dot", inpanel=False)
                self.app = QtWidgets.QApplication.instance()
                dags = [widget for widget in self.app.allWidgets() if widget.windowTitle() == "Node Graph"]
                if dags:
                    self.dag = None
                    if dags[0].size().height() > dags[1].size().height():
                        self.dag = dags[1].findChild(QtOpenGL.QGLWidget)
                    else:
                        self.dag = dags[0].findChild(QtOpenGL.QGLWidget)
                QtWidgets.QApplication.sendEvent(self.dag, QtGui.QMouseEvent(QtCore.QEvent.MouseButtonPress, self.dag.mapFromGlobal(newpos), QtCore.Qt.LeftButton, QtCore.Qt.LeftButton, QtCore.Qt.NoModifier))
                nuke.delete(dot)
                return True
        return False


def SetupEventfilter():
    global mouseEventFilter
    if not "mouseEventFilter" in globals():
        mouseEventFilter = MouseEventGrabber()

Place the code above in for example your “menu.py” python file (in your “.nuke” folder). You then need to register it to get called whenever you open up a new script. This is because I haven’t found a way to make the code above work automatically from the startup of Nuke. The DAG widgets aren’t fully created when the loading code gets run, hence the install of the event filter doesn’t work. So instead we register it to when scripts are opened using the OnScriptLoad callback as such:

nuke.addOnScriptLoad(SetupEventFilter)

That’s it, I hope you like it!
Cheers, David

Fixing an annoying Nuke “feature”