Live Reloading Qt Stylesheets

posted in: Uncategorized | 0

The more time I spend in PySide, the more I demand of it. Lately, I’ve put a lot of emphasis on the look and feel of my applications and have ventured deep into custom widget styles. Stylesheets do an awesome job at separating concerns of the functionality from the visuals. To date, they are my favorite system to change the appearance of QWidgets.

During a design phase, I found myself iterating rapidly on stylesheets to tweak the pixels between margins or dial in colors, all while having to relaunch my Python process between changes. This feedback loop is not immediate enough and gets tiresome pretty quickly. Today I’m sharing my recipe for live reloading stylesheets in the PySide framework! (This code should be easily reusable across PyQt as well.) The core is quite simple. Hold onto references of stylesheets when they’re applied to widgets and add QFileSystemWatchers to watch when they change. On modification, reload the stylesheet from disk and reapply the sheet to the widgets.

Here’s a quck demo. On the right side, I’m modifying the .css stylesheet. On the left side, the app window updates every time I save the file.

The StylesheetWatcher Class

from PySide import QtCore
import collections

class StylesheetWatcher(object):
    """
    A utility class for live-reloading Qt stylesheets.

    When watched, all changes made to a .css file automatically
    propagate to any running apps with live stylesheets.
    """

    def __init__(self):
        self._widget_sheet_map = collections.defaultdict(list)  # One-to-many map of stylesheets to their widgets
        self._watcher = None

    def watch(self, widget, stylesheet_path):
        """
        Establishes a live link between a widget and a stylesheet.
        When modified, the stylesheet will be reapplied to the widget.

        Args:
            widget: (QtGui.QWidget) A widget to apply style to.
            stylesheet_path: (str) Absolute file path on disk to a .css stylesheet.

        Returns: (None)
        """
        self._widget_sheet_map[stylesheet_path].append(widget)

        self._watcher = QtCore.QFileSystemWatcher()
        self._watcher.addPath(stylesheet_path)

        self._watcher.fileChanged.connect(self.update)

        self.update()

    def update(self):
        """
        Callback to reload and reapply a stylesheet when changed by the file system watcher.

        Returns: (None)
        """
        for stylesheet_path, widgets in self._widget_sheet_map.iteritems():
            with open(stylesheet_path, "r") as fid:
                raw_stylesheet = fid.read()

            for widget in widgets:
                widget.setStyleSheet(raw_stylesheet)

I’ll upload a complete demo app in the coming days with my “Steam” clone stylesheet. Happy designing!

PySide Decorators for Unity-like Context Menus

posted in: Uncategorized | 0

Today I wanted to share a recipe I’ve cooked up for creating menus in PySide. Having worked within the QWidgets framework of Qt for a while, I’ve always found the process of creating context menus and menu bars needlessly complex and error-prone. During my struggles trying to get hotkeys to work correctly and nested menus to build in the right order, I found myself reminiscing about Unity — in particular, its many intelligent API design choices made to aid with editor extensions. Fed up with the time I was spending on each new tool to get menu systems working correctly, I decided to develop a reusable library that would make me feel right at home.

The core problems I struggle with in PySide’s menu system are:

  • Creating hotkeys that actually fire QActions regardless of the state of the QMenu or QMenuItem
  • Building and managing nested QMenus
  • Flexibly specifying QMenuItem order in a QMenu
  • Creating callbacks for each QAction’s triggered signal

In PySide, I wanted to be able to do something like this Unity C# snippet:


// Add a new menu item with hotkey CTRL-A
[MenuItem("Tools/Create Actor %a")]
private static void CreateActor()
{
    // Do stuff...
}

And I ended up with this in:


# Adds a new menu item with hotkey CTRL-A
@AppContextMenu.register_action("Tools/Create Actor", shortcut="Ctrl+A")
def create_actor():
    # Do Stuff...
    pass

# App-specific code
class DemoMenuApp(QtGui.QMainWindow):
    def __init__(self, parent=None):
        super(DemoMenuApp, self).__init__(parent=parent)
        # Connect the context menu to tree_view_widget's right click
        AppContextMenu.add_widget(self.tree_view_widget)

Not bad, eh? For reference, in PySide the context menu appears like:

context_menus_01

A more complex example of a context menu created with this system.

What It Does

The register_action decorator handles building all QMenus and sub menus by parsing out /’s in your action name. It correctly hooks up QActions to fire when keyboard shortcuts are fired. It allows you to specify manual order with automatically placed separators by specifying a priority value. And lastly, it allows certain items to be enabled or disabled based by providing a validator callable.

The full source with the implementation of the BaseContextMenuTree class can be found on my GitHub. I’ve also included a mini PySide app to show different uses and demo its full feature set.
https://github.com/Mouthlessbobcat/Python-Projects/blob/master/milk/lib/qt/contextmenu.py

Afterthoughts

Implementing Unity’s API for creating menus was a neat exploration. In the process, I’ve come to terms with some of its design limitations. In both Unity’s and my implementation, it is not possible to pass arbitrary arguments into the decorated functions. Unity provides the user with some flexibility to unbox a generic “context object” into a specific type based on the context of the menu item. My system could be extended to do similar, but this relies on specific conventions and predetermined contexts. The system works best when functions are defined to operate on “static” objects and methods or where all the arguments required in the menu item’s action can be acquired from external systems.

Because Python evaluates its decorators as soon as the module is imported, all instances of my BaseContextMenuTree class must be declared in code as well. It is not possible to create new menu trees during runtime. However, it is possible to disable and enable certain menu items based on the “validator” parameter in the decorator. The validator is any callable that returns a boolean value. The callable is called each time the context menu is about to present itself. This allows for dynamic control of individual menu items. Validators offer more than enough freedom, so the limitation of not being able to create new menu trees becomes much easier to cope with.

For further reading, Unity’s documentation on its Menu Items system can be found here:

https://unity3d.com/learn/tutorials/modules/intermediate/editor/menu-items

Unity Editor Script – Group Selected

posted in: Uncategorized | 0

This post offers an editor convenience class for Maya-like grouping of objects in the Unity scene hierarchy. It configures this script to run with the hotkey (Ctrl+G). Drop this into the editor. Your artists will love it. It currently does not take into account any parent groups that might be above your selected transforms. It will always try to group the selected to the an empty node at the world root.

 

using UnityEngine;
using UnityEditor;
using System.Collections;

public class GroupSelected : EditorWindow
{
    /// <summary>
    ///  Creates an empty node at the center of all selected nodes and parents all selected underneath it. 
    ///  Basically a nice re-creation of Maya grouping!
    /// </summary>

    [MenuItem("GameObject/Group Selected %g", priority = 80)]
    static void Init()
    {
        Transform[] selected = Selection.GetTransforms(SelectionMode.ExcludePrefab | SelectionMode.TopLevel);

        GameObject emptyNode = new GameObject();
        Vector3 averagePosition = Vector3.zero;
        foreach(Transform node in selected)
        {
            averagePosition += node.position;
        }
        if (selected.Length > 0)
        {
            averagePosition /= selected.Length;
        }
        emptyNode.transform.position = averagePosition;
        emptyNode.name = "group";
        foreach (Transform node in selected)
        {
            node.parent = emptyNode.transform;
        }
   }
}

 

 

Select By Material – Python Snippet

posted in: Uncategorized | 0

Here’s another one-line snippet that selects all faces and objects in a scene based on shading group set. Material assignment in Maya is driven by “sets”. According to the Maya documentation, “a set is a logical grouping of an arbitrary collection of objects, attributes, or components of object.”  We’ll be querying a set to find everything assigned by a specific material and then selecting it.

cmds.select(cmds.sets("initialShadingGroup", q=True))

This line selects faces and objects assigned with lambert1, a helpful check I perform in scenes before exporting. (We never want artists to export default Maya materials.) It shows the artist where in the scene the lambert1 lives by highlighting it in the selection.

If you don’t know the name of the set associated with the material off hand (you probably won’t), you can easily translate from material name to set:

materialName = "lambert1"
shadingGroup = cmds.listConnections(materialName, type="shadingEngine")
componentsWithMaterial = cmds.sets(shadingGroup, q=True)
cmds.select(componentsWithMaterial)

 

List All Texture Files In Scene – Python Method

posted in: Uncategorized | 0

Here’s another Python snippet you can use in Maya to, in one line, gather a list of all the files referenced in your scene. This is of great use whenever you need to copy, export, or do other actions on textures associated with a particular Maya scene.

allFiles = [cmds.getAttr("%s.fileTextureName"%file) for file in cmds.ls(type="file")]

This Python list comprehension iterates over every file found using the ls command and then gets the attribute that points to the file node’s texture path. Easy!

 

As an alternative, if you’re concerned about only transferring specific file types, or handling certain file types differently, you can use the following extended version of the above line.

fileFilter = ".jpg"
typedFiles = [cmds.getAttr("%s.fileTextureName"%file) for file in cmds.ls(type="file") if fileFilter in cmds.getAttr("%s.fileTextureName"%file)]

Save To Clipboard Python Method

posted in: Uncategorized | 0

Here’s a super useful snippet you can use to copy any string to the user’s OS clipboard. We use this for any manual data-entry into databases or spreadsheets.

def addToClipboard(text):
    command = 'echo ' + text.strip() + '| clip'
    mel.eval("system("%s") "%command)

You’ll notice I wrap the system call with MEL. This is just a personal touch and definitely not needed. I’ve noticed the usual python call to os.system displays an empty command line window for a split second while executing the command. This is a bit distracting to me.. and  the MEL version seems to avoid that. :]