Skip to content

Instantly share code, notes, and snippets.

@liorbenhorin
Last active October 3, 2023 13:01
Show Gist options
  • Save liorbenhorin/217bfb7e54c6f75b9b1b2b3d73a1a43a to your computer and use it in GitHub Desktop.
Save liorbenhorin/217bfb7e54c6f75b9b1b2b3d73a1a43a to your computer and use it in GitHub Desktop.
Maya 2017 PySide2 Docking Qt QMainWindow
"""
This is what you need to do in order to get a qt window to dock next to maya channel box,
In all maya versions, including 2017 with PySide2
"""
__author__ = "liorbenhorin@gmail.com"
import sys
import os
import logging
import xml.etree.ElementTree as xml
from cStringIO import StringIO
# Qt is a project by Marcus Ottosson ---> https://github.com/mottosso/Qt.py
from Qt import QtGui, QtWidgets, QtCore, QtCompat
try:
import pysideuic
from shiboken import wrapInstance
logging.Logger.manager.loggerDict["pysideuic.uiparser"].setLevel(logging.CRITICAL)
logging.Logger.manager.loggerDict["pysideuic.properties"].setLevel(logging.CRITICAL)
except ImportError:
import pyside2uic as pysideuic
from shiboken2 import wrapInstance
logging.Logger.manager.loggerDict["pyside2uic.uiparser"].setLevel(logging.CRITICAL)
logging.Logger.manager.loggerDict["pyside2uic.properties"].setLevel(logging.CRITICAL)
import maya.OpenMayaUI as omui
from maya.app.general.mayaMixin import MayaQWidgetDockableMixin
import maya.cmds as cmds
def loadUiType(uiFile):
"""
:author: Jason Parks
Pyside lacks the "loadUiType" command, so we have to convert the ui file to py code in-memory first
and then execute it in a special frame to retrieve the form_class.
"""
parsed = xml.parse(uiFile)
widget_class = parsed.find('widget').get('class')
form_class = parsed.find('class').text
with open(uiFile, 'r') as f:
o = StringIO()
frame = {}
pysideuic.compileUi(f, o, indent=0)
pyc = compile(o.getvalue(), '<string>', 'exec')
exec pyc in frame
# Fetch the base_class and form class based on their type in the xml from designer
form_class = frame['Ui_%s' % form_class]
base_class = getattr(QtWidgets, widget_class)
return form_class, base_class
def maya_main_window():
main_window_ptr = omui.MQtUtil.mainWindow()
return wrapInstance(long(main_window_ptr), QtWidgets.QWidget)
def maya_api_version():
return int(cmds.about(api=True))
class MyDockingWindow(MayaQWidgetDockableMixin, QtWidgets.QMainWindow):
MAYA2014 = 201400
MAYA2015 = 201500
MAYA2016 = 201600
MAYA2016_5 = 201650
MAYA2017 = 201700
def __init__(self, parent=None):
self.deleteInstances() # remove any instance of this window before starting
super(MyDockingWindow, self).__init__(parent)
self.setWindowFlags(QtCore.Qt.Tool)
"""
compile the .ui file on loadUiType(), a function that uses pysideuic / pyside2uic to compile .ui files
"""
uiFile = os.path.join(os.path.dirname(__file__), 'MyDockingWindow_ui.ui')
form_class, base_class = loadUiType(uiFile)
self.ui = form_class()
self.ui.setupUi(self)
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
def dockCloseEventTriggered(self):
self.deleteInstances()
# Delete any instances of this class
def deleteInstances(self):
def delete2016():
# Go through main window's children to find any previous instances
for obj in maya_main_window().children():
if str(type(
obj)) == "<class 'maya.app.general.mayaMixin.MayaQDockWidget'>": # ""<class 'maya.app.general.mayaMixin.MayaQDockWidget'>":
if obj.widget().__class__.__name__ == "MyDockingWindow": # Compare object names
obj.setParent(None)
obj.deleteLater()
def delete2017():
'''
Look like on 2017 this needs to be a little diffrents, like in this function,
However, i might be missing something since ive done this very late at night :)
'''
for obj in maya_main_window().children():
if str(type(obj)) == "<class '{}.MyDockingWindow'>".format(os.path.splitext(
os.path.basename(__file__)[0])): # ""<class 'moduleName.mayaMixin.MyDockingWindow'>":
if obj.__class__.__name__ == "MyDockingWindow": # Compare object names
obj.setParent(None)
obj.deleteLater()
if maya_api_version() < MyDockingWindow.MAYA2017:
delete2016()
else:
delete2017()
def deleteControl(self, control):
if cmds.workspaceControl(control, q=True, exists=True):
cmds.workspaceControl(control, e=True, close=True)
cmds.deleteUI(control, control=True)
# Show window with docking ability
def run(self):
'''
2017 docking is a little different...
'''
def run2017():
self.setObjectName("MyMainDockingWindow")
# The deleteInstances() dose not remove the workspace control, and we need to remove it manually
workspaceControlName = self.objectName() + 'WorkspaceControl'
self.deleteControl(workspaceControlName)
# this class is inheriting MayaQWidgetDockableMixin.show(), which will eventually call maya.cmds.workspaceControl.
# I'm calling it again, since the MayaQWidgetDockableMixin dose not have the option to use the "tabToControl" flag,
# which was the only way i found i can dock my window next to the channel controls, attributes editor and modelling toolkit.
self.show(dockable=True, area='right', floating=False)
cmds.workspaceControl(workspaceControlName, e=True, ttc=["AttributeEditor", -1], wp="preferred", mw=420)
self.raise_()
# size can be adjusted, of course
self.setDockableParameters(width=420)
def run2016():
self.setObjectName("MyMainDockingWindow")
# on maya < 2017, the MayaQWidgetDockableMixin.show() magiclly docks the window next
# to the channel controls, attributes editor and modelling toolkit.
self.show(dockable=True, area='right', floating=False)
self.raise_()
# size can be adjusted, of course
self.setDockableParameters(width=420)
self.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Preferred)
self.setMinimumWidth(420)
self.setMaximumWidth(600)
if maya_api_version() < MyDockingWindow.MAYA2017:
run2016()
else:
run2017()
def show():
'''
this is the funciton that start things up
'''
global MyDockingWindow
MyDockingWindow = MyDockingWindow(parent=maya_main_window())
MyDockingWindow.run()
return MyDockingWindow
@chisn8tech
Copy link

Have you found a simple way to make the docked tab remain in focus when tabbed with the Maya attribute editor etc? By default when you drag it to become a tab, it goes below the other tabs and must be selected to rise to the top again.

( "pm.workspaceControl(workspaceCtrlName, e=1, visibleChangeCommand=self.makeItRise)" initially looked hopeful, but it appears that it is called just BEFORE the dock is actually docked, so calling self.raise_() etc in self.makeItRise did nothing. )

I ended up having to 'post' a custom event to the QApplication with a lower than 'low' priority,
(or use pm.mel.evalDeferred('workspaceControl -r name'), but that was ugly,)
and catch it with an eventFilter,
in order to execute the 'raise' tab part after it had actually been tabbed :(

(I also had to use an eventFilter (on self.parent() ) to catch the close event (event.Close) when the tab/floating dock was closed, as I wanted to save the layout on closing...)

@marvolo3d
Copy link

Seems to work well other than 2014 which doesn't have mayaMixin

@ryanitto
Copy link

ryanitto commented Mar 10, 2017

This was quite helpful to create a dockable window in Maya 2017+. I might make a suggestion for line 116-122:

        if str(type(obj)) == "<class '{}.{}'>".format(
            os.path.splitext(os.path.basename(__file__)[0]),
            self.__class__.__name__
        ):

            # Compare object names
            if obj.__class__.__name__ == self.__class__.__name__:
                obj.setParent(None)
                obj.deleteLater()

This is a bit easier to just pull your class name from the __name__ type.

@Klaudikus
Copy link

Klaudikus commented Apr 18, 2017

I just tested a few things in Maya 2017, and in my tool that inherits from MayaQWidgetDockableMixin, the dockCloseEventTriggered method never gets called. The two only methods that get called when opening or closing the window are showEvent, and hideEvent, respectively. If you look at the source, there are Signals not connected to anything, moreover, dockCliseEventTriggered doesn't seem to have a super or Widget method override. Maybe I'm missing something, or my setup is incorrect, but this Mixin class is handy, but incomplete.

@griffinanimator
Copy link

This works great for me. The issue I currently have is that the statusBar only sends messages to the main maya window not the generated dockable window. In 2015 the status bar behaves as expected. Could this be something with the QT module? Anyone else experiencing this?

@liorbenhorin
Copy link
Author

Hey All, sorry for not replying, must have missed the party...
Anyways, here is a new, simpler and more stable way to achieve this:

https://gist.github.com/liorbenhorin/69da10ec6f22c6d7b92deefdb4a4f475

Enjoy!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment