Houdini 20.0 Examples Python panel examples

Viewport color editor

A PySide interface for editing viewport colors.

Example
<?xml version="1.0" encoding="UTF-8"?>
<pythonPanelDocument>
  <!--
    This file contains the definition for the viewport color editor panel
  -->
  <interface name="viewport_color_editor" label="Viewport Color Editor" icon="MISC_python">
    <script><![CDATA[########################################################################
# Replace the sample code below with your own to create a
# PyQt or PySide interface.  Your code must define a 'onCreateInterface()'
# function that returns the root widget of your interface.
########################################################################

import os
from hutil.Qt import QtCore, QtGui, QtWidgets

class ColorInfo:
    theDialog = None

    def __init__(self, name, values, comment):
        self.myName = name
        self.myValues = values
        self.myOrigValues = values
        self.myComment = comment

    def __str__(self):
        if isinstance(self.myValues, str):
            return self.myName + ":\t@" + \
                   self.myValues + "\t# " + \
                   self.myComment
        elif self.isAlpha():
            return self.myName + ":\tALPHA " + \
                   str(self.myValues[0]) + "\t# " + \
                   self.myComment
        else:
            return self.myName + ":\t" + \
                   str(self.myValues[0]) + " " + \
                   str(self.myValues[1]) + " " + \
                   str(self.myValues[2]) + "\t# " + \
                   self.myComment

    def getValueStr(self):
        if isinstance(self.myValues, str):
            return "@" + self.myValues
        elif self.isAlpha():
            valuestr = str(self.myValues[0])
        else:
            valuestr = str(self.myValues)
        return valuestr

    def isAlpha(self):
        if isinstance(self.myValues, str):
            return self.findReference(self.myValues).isAlpha()
        else:
            return len(self.myValues) == 1

    def getAlpha(self):
        if isinstance(self.myValues, str):
            return self.findReference(self.myValues).getAlpha()
        else:
            return self.myValues[0]

    def getColor(self):
        if isinstance(self.myValues, str):
            return self.findReference(self.myValues).getColor()
        else:
            return QtGui.QColor.fromRgbF(self.myValues[0], \
                                         self.myValues[1], \
                                         self.myValues[2])

    def getColorStyleStr(self):
        if isinstance(self.myValues, str):
            return self.findReference(self.myValues).getColorStyleStr()
        elif not self.isAlpha() and \
            (self.myValues[0] + self.myValues[1] + self.myValues[2]) < 0.75:
            return "background-color: rgb(" + \
                   str(int(self.myValues[0] * 255.0)) + ", " + \
                   str(int(self.myValues[1] * 255.0)) + ", " + \
                   str(int(self.myValues[2] * 255.0)) + "); " + \
                   "color: white;"
        else:
            return "background-color: rgb(" + \
                   str(int(self.myValues[0] * 255.0)) + ", " + \
                   str(int(self.myValues[1] * 255.0)) + ", " + \
                   str(int(self.myValues[2] * 255.0)) + ");"

    def twoDigits(self, val):
        if isinstance(val, float):
            return float(int(val * 100.0)) / 100.0
        else:
            l = []
            for v in val:
                l.append(float(int(v * 100.0)) / 100.0)
            return l

    def setAlpha(self, value):
        try:
            self.myValues[0] = float(value)
            for info in ColorInfo.theDialog.myInfos:
                if (info == self or info.mySelected.isChecked()) and \
                    info.myShown and info.isAlpha():
                    if info != self:
                        info.myValues = self.myName
                    ColorInfo.theDialog.myChanged = True
        except:
            pass

    def doneAlpha(self):
        for info in ColorInfo.theDialog.myInfos:
            if (info == self or info.mySelected.isChecked()) and \
                info.myShown and info.isAlpha():
                info.myAlphaValue.setText(info.getValueStr())

    def setColor(self):
        color = QtWidgets.QColorDialog.getColor(self.getColor())
        if color.isValid():
            self.myValues = list(self.twoDigits(color.getRgbF()))
            for info in ColorInfo.theDialog.myInfos:
                if (info == self or info.mySelected.isChecked()) and \
                    info.myShown and not info.isAlpha():
                    if info != self:
                        info.myValues = self.myName
                    info.myColorValue.setText(info.getValueStr())
                    info.myColorValue.setStyleSheet(info.getColorStyleStr())
                    ColorInfo.theDialog.myChanged = True

    def setComment(self, comment):
        self.myComment = comment

    def findReference(self, reference):
        return ColorInfo.theDialog.findColor(reference)

class Dialog(QtWidgets.QFrame):
    def __init__(self, parent=None):
        super(Dialog, self).__init__(parent)

        ColorInfo.theDialog = self

        # Create the filter input field and add the widgets to a scrolling
        # list in the dialog.
        self.myFilterRow = QtWidgets.QHBoxLayout()
        self.myFilterLabel = QtWidgets.QLabel("Filter:")
        self.myFilter = QtWidgets.QLineEdit()
        self.myFilter.textChanged.connect(self.doFilterUpdated)
        self.myFilterChanged = QtWidgets.QCheckBox("Changed Values")
        self.myFilterChanged.stateChanged.connect(self.doFilterChangedUpdated)
        self.myFilterRow.addWidget(self.myFilterLabel)
        self.myFilterRow.addWidget(self.myFilter)
        self.myFilterRow.addWidget(self.myFilterChanged)

        self.myScroller = QtWidgets.QScrollArea()
        self.myScroller.setWidgetResizable(True)

        self.myActionRow = QtWidgets.QHBoxLayout()
        self.myClearSelectionButton = QtWidgets.QPushButton("Clear Selection")
        self.myClearSelectionButton.clicked.connect(self.doClearSelection)
        self.mySchemeMenu = QtWidgets.QComboBox()
        self.mySchemeMenu.setEditable(False)
        self.mySchemeMenu.addItem("Light", "config/3DSceneColors.light")
        self.mySchemeMenu.addItem("Dark", "config/3DSceneColors.dark")
        self.mySchemeMenu.addItem("Grey", "config/3DSceneColors.bw")
        self.mySchemeMenu.currentIndexChanged.connect(self.doSchemeChanged)
        self.mySaveButton = QtWidgets.QPushButton("Save")
        self.mySaveButton.clicked.connect(self.doSave)
        self.myQuitButton = QtWidgets.QPushButton("Revert")
        self.myQuitButton.clicked.connect(self.doLoad)
        self.myActionRow.addWidget(self.myClearSelectionButton)
        self.myActionRow.addStretch(1)
        self.myActionRow.addWidget(QtWidgets.QLabel("Color Scheme:"))
        self.myActionRow.addWidget(self.mySchemeMenu)
        self.myActionRow.addStretch(1)
        self.myActionRow.addWidget(self.mySaveButton)
        self.myActionRow.addWidget(self.myQuitButton)

        # Create the initial dialog layout.
        mainLayout = QtWidgets.QBoxLayout(QtWidgets.QBoxLayout.Direction.TopToBottom)
        mainLayout.addLayout(self.myFilterRow)
        mainLayout.addWidget(self.myScroller)
        mainLayout.addLayout(self.myActionRow)
        self.setLayout(mainLayout)

        self.doLoad()

    def findColor(self, name):
        for info in self.myInfos:
            if info.myName == name:
                return info
        return ColorInfo(name, [0.0, 0.0, 0.0], "")

    def doFilterUpdated(self, state):
        self.fillColorList(self.myFilter.text(), \
                           self.myFilterChanged.isChecked())

    def doFilterChangedUpdated(self, state):
        self.fillColorList(self.myFilter.text(), \
                           self.myFilterChanged.isChecked())

    def fillColorList(self, filterStr = None, changedOnly = False):
        # Sort widgets into shown or hidden containers. The hidden container
        # exists to make sure the underlying QT widgets don't get deleted.
        hiddenWidget = QtWidgets.QWidget()
        hidden = QtWidgets.QGridLayout()
        shownWidget = QtWidgets.QWidget()
        shown = QtWidgets.QGridLayout()
        shown.setColumnStretch(4, 1)
        shown.setColumnMinimumWidth(4, 250)
        i = 0
        for info in self.myInfos:
            info.myShown = self.matchFilter(filterStr, changedOnly, info)
            if not info.myShown:
                info.mySelected.setChecked(False)
            layout = shown if info.myShown else hidden
            layout.addWidget(info.mySelected, i, 0)
            layout.addWidget(info.myNameLabel, i, 1)
            if info.isAlpha():
                layout.addWidget(info.myAlphaValue, i, 2)
                layout.addWidget(info.myOrigAlphaValue, i, 3)
            else:
                layout.addWidget(info.myColorValue, i, 2)
                layout.addWidget(info.myOrigColorValue, i, 3)
            layout.addWidget(info.myCommentText, i, 4)
            i = i + 1
        shownWidget.setLayout(shown)
        hiddenWidget.setLayout(hidden)
        self.myScroller.setWidget(shownWidget)
        self.myHidden = hiddenWidget
        self.myContainer = shownWidget;

    def matchFilter(self, filterStr, changedOnly, info):
        # First thing to chec is if he value is changed
        if changedOnly and info.myValues == info.myOrigValues:
            return False

        # Empty string matches anything
        if filterStr is None or filterStr == "":
            return True

        # With a string, look for each word individually.
        words = filterStr.split()
        for word in words:
            if info.myName.lower().find(word.lower()) != -1 or \
               info.myComment.lower().find(word.lower()) != -1:
                return True

        return False

    def doSchemeChanged(self, menu_index):
        if self.myChanged:
            if hou.displayMessage("You have unsaved changes.",
                                  button = ("Save", "Discard")) == 0:
                self.doSave()
        self.doLoad()

    def doClearSelection(self):
        for info in self.myInfos:
            info.mySelected.setChecked(False)

    def doSave(self):
        colorFilePath = hou.findFile(self.mySchemeMenu.itemData(self.mySchemeMenu.currentIndex()))
        try:
            colorFile = open(colorFilePath, "w")
        except IOError:
            colorFilePath = hou.homeHoudiniDirectory() + "/" + \
                            self.mySchemeMenu.itemData(self.mySchemeMenu.currentIndex())
            try:
                os.mkdir(colorFilePath[0:colorFilePath.rfind("/")])
            except OSError:
                pass
            colorFile = open(colorFilePath, "w")

        for info in self.myInfos:
            colorFile.write(str(info))
            colorFile.write("\n")
        colorFile.close()
        hou.ui.reloadViewportColorSchemes()
        self.myChanged = False

    def doLoad(self):
        # Read the scene colors file.
        colorFilePath = hou.findFile(self.mySchemeMenu.itemData(self.mySchemeMenu.currentIndex()))
        colorFile = open(colorFilePath)
        colorLines = colorFile.readlines()
        # Strip out empty lines and leading and trailing spaces.
        for i in reversed(range(0, len(colorLines))):
            colorLines[i] = colorLines[i].strip()
            if len(colorLines[i]) == 0:
                colorLines.pop(i)
        # Put the lines in alphabetical order by color name.
        colorLines.sort()
        # Convert the lines into a list of ColorInfo structures.
        self.myInfos = []
        for i in range(0, len(colorLines)):
            (rest, comment) = colorLines[i].split("#", 1)
            if len(rest) > 0:
                comment = comment.strip()
                (name, value) = rest.split(":", 1)
                name = name.strip()
                value = value.strip()
                if value.startswith("ALPHA"):
                    values = [float(value.split()[1])]
                elif value.startswith("@"):
                    values = value[1:len(value)]
                else:
                    values = value.split()
                    values = [float(values[0]), float(values[1]), float(values[2])]
                self.myInfos.append(ColorInfo(name, values, comment))

        # Build Qt widgets for displaying and editing the color information.
        frameStyle = QtWidgets.QFrame.Sunken | QtWidgets.QFrame.Panel
        for info in self.myInfos:
            info.mySelected = QtWidgets.QCheckBox()
            info.myNameLabel = QtWidgets.QLabel()
            info.myNameLabel.setText(info.myName)
            info.myNameLabel.setFrameStyle(frameStyle)
            if info.isAlpha():
                info.myAlphaValue = QtWidgets.QLineEdit()
                info.myAlphaValue.setText(info.getValueStr())
                info.myAlphaValue.textEdited.connect(info.setAlpha)
                info.myAlphaValue.editingFinished.connect(info.doneAlpha)
                info.myOrigAlphaValue = QtWidgets.QLabel()
                info.myOrigAlphaValue.setText(info.getValueStr())
            else:
                info.myColorValue = QtWidgets.QPushButton(info.getValueStr())
                info.myColorValue.setStyleSheet(info.getColorStyleStr())
                info.myColorValue.clicked.connect(info.setColor)
                info.myOrigColorValue = QtWidgets.QLabel()
                info.myOrigColorValue.setStyleSheet(info.getColorStyleStr())
                info.myOrigColorValue.setAlignment(QtCore.Qt.AlignCenter)
                info.myOrigColorValue.setText(info.getValueStr())
            info.myCommentText = QtWidgets.QLineEdit()
            info.myCommentText.setText(info.myComment)
            info.myCommentText.textChanged.connect(info.setComment)
        self.fillColorList()
        self.myChanged = False

def onCreateInterface():
    # Create the dialog to display and edit the color information.
    root = Dialog()
    return root
]]></script>
  </interface>
</pythonPanelDocument>

Python panel examples

  • Custom Graphics

    Custom OpenGL drawing in a Python Panel.

  • Drag and Drop

    How to implement drag and drop functionality in a Python Panel.

  • Linked parameters

    How to link PySide parameter widgets (i.e. text fields and sliders) to Houdini node parameters and vice versa.

  • Node path

    How to listen for changes to the current node path.

  • Qt designer

    How to load user interface layout from a Qt Designer file.

  • Qt events

    How Python Panels can listen to Qt events.

  • Viewport color editor

    A PySide interface for editing viewport colors.