workspaceControl, migrating dockControl so we can dock into the ChannelBox tabs

Hi all,

I’m trying to switch some of our base toolset so that the main UI (cmds built) now docks correctly in the same tab as the channelBox rather than docking to the right side of the main Maya UI. The ChannelBox is now a workspaceControl so I havce to call the UI through that as below, it’s a bit of a pain to switch the ui over but I hate the fact that the right dockControl space is now the entire length of the Maya UI, and outside of everything else.

So I did the following to make the UI tab into the workspace of the channelBox:

element=mel.eval('getUIComponentDockControl("Channel Box / Layer Editor", false);')  
windowcall='import Red9.core.Red9_AnimationUtils as r9Anim;animUI=r9Anim.AnimationUI();animUI._showUI()'
cmds.workspaceControl(animUI.workspaceCnt, label="Red9_Animation", 
                                      uiScript=windowcall, 
                                      tabToControl=(element, -1), 
                                      initialWidth=355, 
                                      initialHeight=720,
                                      retain=False,
                                      loadImmediately=False)  

cmds.workspaceControl(animUI.workspaceCnt, e=True,vis=True)  # ensure we set visible
cmds.workspaceControl(animUI.workspaceCnt, e=True, mw=355)  # set minimumWidth
cmds.workspaceControl(animUI.workspaceCnt, e=True,  r=True)  # raise

Now that all works but I’ll be damned if I can find a way to control and lock the width of this UI. If I go to the modelling tab and then my tab the ui is the correct width, presumably because I set the minimumWidth flag.

However, if I go to the attributeEditor tab, then mine the ui remains the width of the attributeEditor. There’s width query flags in the workspaceControl, but no edit.

So the question is, how the hell do you set a base width that is respected for these?

cheers

Mark

have you tried the “MayaQWidgetDockableMixin”?
you can hook into the visible state of your tab and change the width when it becomes visible.

Using some of Maya’s python examples and a gistthat liorbenhorin was kind enough to share, I’ve made this little example that you can copy and paste into Maya. Hopefully it can shed some light on how to do this properly.

The docking behavior is down below in the run2017 method.


# Copyright 2015 Autodesk, Inc. All rights reserved.
# 
# Use of this software is subject to the terms of the Autodesk
# license agreement provided at the time of installation or download,
# or which otherwise accompanies this software in either electronic
# or hard copy form.

"""
Attribute Editor style widget

Notes:
  * Uses mayaMixin to handle details of using PySide in Maya

Limitations:
  * Assumption that the node name does not change.  For handling node names, look into using MObjectHandle
  * File->New and File->Load not handled
  * Deleting node not handled
"""

from maya import cmds
from maya import mel
from maya import OpenMaya as om
from maya import OpenMayaUI as omui 

try:
  from PySide2.QtCore import * 
  from PySide2.QtGui import * 
  from PySide2.QtWidgets import *
  from PySide2.QtUiTools import *
  from shiboken2 import wrapInstance 
except ImportError:
  from PySide.QtCore import * 
  from PySide.QtGui import * 
  from PySide.QtUiTools import *
  from shiboken import wrapInstance 

from maya.app.general.mayaMixin import MayaQWidgetBaseMixin, MayaQWidgetDockableMixin
import functools


class MCallbackIdWrapper(object):
    '''Wrapper class to handle cleaning up of MCallbackIds from registered MMessage
    '''
    def __init__(self, callbackId):
        super(MCallbackIdWrapper, self).__init__()
        self.callbackId = callbackId

    def __del__(self):
        om.MMessage.removeCallback(self.callbackId)

    def __repr__(self):
        return 'MCallbackIdWrapper(%r)'%self.callbackId


def getDependNode(nodeName):
    """Get an MObject (depend node) for the associated node name

    :Parameters:
        nodeName
            String representing the node
    
    :Return: depend node (MObject)

    """
    dependNode = om.MObject()
    selList = om.MSelectionList()
    selList.add(nodeName)
    if selList.length() > 0: 
        selList.getDependNode(0, dependNode)
    return dependNode


class Example_connectAttr(MayaQWidgetDockableMixin, QScrollArea):
    def __init__(self, node=None, *args, **kwargs):
        super(Example_connectAttr, self).__init__(*args, **kwargs)

        # Member Variables
        self.nodeName = node               # Node name for the UI
        self.attrUI = None                 # Container widget for the attr UI widgets
        self.attrWidgets = {}              # Dict key=attrName, value=widget
        self.nodeCallbacks = []            # Node callbacks for handling attr value changes
        self._deferredUpdateRequest = {}   # Dict of widgets to update

        # Connect UI to the specified node
        self.attachToNode(node)


    def attachToNode(self, nodeName):
        '''Connect UI to the specified node
        '''
        self.nodeName = nodeName
        self.attrs = None
        self.nodeCallbacks = []
        self._deferredUpdateRequest.clear()
        self.attrWidgets.clear()

        # Get a sorted list of the attrs
        attrs = cmds.listAttr(self.nodeName)
        attrs.sort() # in-place sort the attrs

        # Create container for attr widgets
        self.setWindowTitle('ConnectAttrs: %s'%self.nodeName)
        self.attrUI = QWidget(parent=self)
        layout = QFormLayout()

        # Loop over the attrs and construct widgets
        acceptedAttrTypes = set(['doubleLinear', 'string', 'double', 'float', 'long', 'short', 'bool', 'time', 'doubleAngle', 'byte', 'enum'])
        for attr in attrs:
            # Get the attr value (and skip if invalid)
            try:
                attrType = cmds.getAttr('%s.%s'%(self.nodeName, attr), type=True)
                if attrType not in acceptedAttrTypes:
                    continue # skip attr
                v = cmds.getAttr('%s.%s'%(self.nodeName, attr))
            except Exception, e:
                continue  # skip attr

            # Create the widget and bind the function
            attrValueWidget = QLineEdit(parent=self.attrUI)
            attrValueWidget.setText(str(v))

            # Use functools.partial() to dynamically constructing a function
            # with additional parameters.  Perfect for menus too.
            onSetAttrFunc =  functools.partial(self.onSetAttr, widget=attrValueWidget, attrName=attr)
            attrValueWidget.editingFinished.connect( onSetAttrFunc )

            # Add to layout
            layout.addRow(attr, attrValueWidget)

            # Track the widget associated with a particular attr
            self.attrWidgets[attr] = attrValueWidget

        # Attach the QFormLayout to the root attrUI widget
        self.attrUI.setLayout(layout)

        # Assign the widget to this QScrollArea
        self.setWidget(self.attrUI)

        # Do a 'connectControl' style operation with MMessage callbacks
        if len(self.attrWidgets) > 0:
            # Note: addNodeDirtyPlugCallback better than addAttributeChangedCallback
            # for UI since the 'dirty' check will always refresh the value of the attr
            nodeObj = getDependNode(nodeName)
            cb = om.MNodeMessage.addNodeDirtyPlugCallback(nodeObj, self.onDirtyPlug, None)
            self.nodeCallbacks.append( MCallbackIdWrapper(cb) )


    def onSetAttr(self, widget, attrName, *args, **kwargs):
        '''Handle setting the attribute when the UI widget edits the value for it.
        If it fails to set the value, then restore the original value to the UI widget
        '''
        print "onSetAttr", attrName, widget, args, kwargs
        try:
            attrType = cmds.getAttr('%s.%s'%(self.nodeName, attrName), type=True)
            if attrType == 'string':
                cmds.setAttr('%s.%s'%(self.nodeName, attrName), widget.text(), type=attrType)
            else:
                cmds.setAttr('%s.%s'%(self.nodeName, attrName), eval(widget.text()))
        except Exception, e:
            print e
            curVal = cmds.getAttr('%s.%s'%(self.nodeName, attrName))
            widget.setText( str(curVal) )


    def onDirtyPlug(self, node, plug, *args, **kwargs):
        '''Add to the self._deferredUpdateRequest member variable that is then 
        deferred processed by self._processDeferredUpdateRequest(). 
        '''
        # get long name of the attr, to use as the dict key
        attrName = plug.partialName(False, False, False, False, False, True)

        # get widget associated with the attr
        widget = self.attrWidgets.get(attrName, None)
        if widget != None:
            # get node.attr string
            nodeAttrName = plug.partialName(True, False, False, False, False, True) 

            # Add to the dict of widgets to defer update
            self._deferredUpdateRequest[widget] = nodeAttrName

            # Trigger an evalDeferred action if not already done
            if len(self._deferredUpdateRequest) == 1:
                cmds.evalDeferred(self._processDeferredUpdateRequest, low=True)


    def _processDeferredUpdateRequest(self):
        '''Retrieve the attr value and set the widget value
        '''
        for widget,nodeAttrName in self._deferredUpdateRequest.items():
            v = cmds.getAttr(nodeAttrName)
            widget.setText(str(v))
            print "_processDeferredUpdateRequest ", widget, nodeAttrName, v
        self._deferredUpdateRequest.clear()
    
    
    def deleteControl(self, control):

        if cmds.workspaceControl(control, q=True, exists=True):
            cmds.workspaceControl(control, e=True, close=True)
            cmds.deleteUI(control, control=True)
    
    def run2017(self):
        # liorbenhorin's snippet

        self.setObjectName("ConnectAttribute")

        # 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 main():
    obj = cmds.polyCube()[0]
    ui = Example_connectAttr(node=obj)
    ui.run2017()
    #ui.show(dockable=True, floating=True)
    return ui


if __name__ == '__main__':
    main()


I load my UI the same way but like in your example the resizing layout to the window is not working, I can not found a way to streach my ui to the window when I resize the window.

Using loadUI works fine but not with this method, I am missing something?

Do you know how we can do this?

Cheers,
David

Hi! Did anyone manage to add menuBar and statusBar to a dockable window in Maya 2017+?

So far the only idea I have is to manually add those as simple widgets, as opposed to using setMenuBar command.