More PySide2 Tools

Over the past while I've written a couple of Qt-related tools to streamline my UI development. No big write-up this time, I'll just drop the tools and move on.

from PySide2 import QtWidgets

def getUserFiles(description="Python File", ext=".py"):
    """Create a file dialog and return user selected files.
    Args are string description and allowed file types."""
    fileWin = QtWidgets.QFileDialog(
        getMayaMainWindow(), filter="{0} (*{1})".format(description, ext))
    
    fileWin.setFileMode(QtWidgets.QFileDialog.ExistingFiles)
    
    if fileWin.exec_():
        inFiles = fileWin.selectedFiles()
    else:
        inFiles = []

    fileWin.setParent(None)

    return inFiles

Just a convenience, really, since getting user files via GUI is such a common operation.

import sys
import os
import pyside2uic

def compileUI(inFiles=None):  
    """compile .ui to .py - made easy.
    pass file as argument, or else a file browser opens
    for you to select one. Assumes same name for compiled .py"""
    if not inFiles:
        # File browser if no file given
        inFiles = getUserFiles("Qt Designer Files", ".ui")

    for inFile in inFiles:
        outFile = inFile.replace(".ui", ".py")
        pyFile = open(outFile, "w")
        try:
            pyside2uic.compileUi(inFile, pyFile, False, 4, False)
        except:
            print("Failed. Invalid file name:\n{0}".format(inFile))
            raise
        else:
            print("Success! Result: {0}".format(outFile))
        finally:
            pyFile.close()

If you prefer to work with .py (or .pyc) over .ui files, you'll be compiling them a lot. May as well shorten it.

from PySide2 import QtWidgets

def buildClass(custom, base="QWidget"):
    """Factory Function to build a single class from the given combination.
    The second argument (base class) may be class or a string, to allow use 
    without importing QtWidgets from caller's frame.
    Used frequently with QtDesigner objects."""
    if isinstance(base, str):
        base = getattr(QtWidgets, base)
    class UiWidg(custom, base):
        """Qt object composed of compiled (form) class and base class"""
        def __init__(self, parent=None):
            super(UiWidg, self).__init__(parent)
            # the compiled .py file has the content,
            # this class provides the widget to fill
            self.setupUi(self)
    return UiWidg


def buildMayaDock(otherCls):
    """Get a Maya-dockable UI from the given qwidget subclass"""
    try:
        assert issubclass(otherCls, QtWidgets.QWidget)
    except (TypeError, AssertionError):
        print(
            "Base class {0} must inherit QWidget in order "
            "to be a Maya dock widget.".format(otherCls))
        return
    
    class DockUi(mDock, otherCls):
        """Some dock signals are replaced by method stubs:
        - dockCloseEventTriggered
        - floatingChanged
        ALSO, .show() method has args the FIRST TIME it is called, by default:
            dockable=False, floating=True, area="left", allowedArea="all", 
            width=None, height=None, x=None, y=None"""
        def __init__(self, parent=None):
            super(DockUi, self).__init__(parent)

    return DockUi

These two functions just help facilitate easy interfacing. My contribution to the simple buildClass function is allowing the base class to be passed as a string, which allows the calling frame to skimp on importing PySide2 itself. The Maya dock function takes a given QWidget subclass and returns a new type which plays nicely with Maya 2017's new docking system.

You can, of course, chain these together - and even combine with loadUiType if you want to start from a straight .ui file:

dockUi = buildMayaDock(buildClass(*loadUiType(uiFile)))
dockUi.show(dockable=True)

Finally I have something that fixes the fact that PySide2 slots don't properly raise exceptions to the main application. The console silence is bad enough, but on top of that, my the UI that triggered the offending slot always locks up until I explicitly .update() it.

import traceback

def SlotExceptionRaiser(origSlot):
    """A decorator function for a PySide2 slot which will
    raise any errors encountered in execution to the console"""
    @wraps(origSlot)
    def wrapper(*args, **kwargs):
        try:
            origSlot(*args, **kwargs)
        except:
            print("\nUncaught Exception in PySide Slot!\n")
            traceback.print_exc()
            #traceback.format_exc()
    return wrapper

Just decorate any slot function with this guy, and it'll let you know what error occurred and ensure your UI stays free.

EDIT 7/16: It appears that as of Maya 2017 Update 4, this decorator is no longer needed, as errors in slots are now raised properly. The fact that slot exceptions are printed as straight text (without the usual error font formatting) suggests to me that the official fix was more or less identical to the one I posted above. Hooray!

Most of this is basic stuff, but these kind of quality-of-life improvements add up. Til next time!