Skip to content

Instantly share code, notes, and snippets.

@BigRoy
Last active February 27, 2024 23:16
Show Gist options
  • Save BigRoy/4d2bf2eef6c6a83f4fda3c58db1489a5 to your computer and use it in GitHub Desktop.
Save BigRoy/4d2bf2eef6c6a83f4fda3c58db1489a5 to your computer and use it in GitHub Desktop.
Quick and dirty "List USD Layer Edits" to allow removal of Sdf.PrimSpec, Sdf.PropertySpec, Sdf.AttributeSpec, Sdf.RelationshipSpec through a Python Qt interface
from PySide2 import QtCore, QtWidgets, QtGui
from pxr import Usd, Tf, Sdf
# See: https://github.com/PixarAnimationStudios/OpenUSD/blob/release/pxr/usd/sdf/fileIO_Common.cpp#L879-L892
SPECIFIER_LABEL = {
Sdf.SpecifierDef: "def",
Sdf.SpecifierOver: "over",
Sdf.SpecifierClass: "abstract"
}
def shorten(s, width, placeholder="..."):
"""Shorten string to `width`"""
if len(s) <= width:
return s
return "{}{}".format(s[:width], placeholder)
def remove_spec(spec):
"""Remove Sdf.Spec authored opinion."""
if spec.expired:
return
if isinstance(spec, Sdf.PrimSpec):
# PrimSpec
parent = spec.nameParent
if parent:
view = parent.nameChildren
else:
# Assume PrimSpec is root prim
view = spec.layer.rootPrims
del view[spec.name]
elif isinstance(spec, Sdf.PropertySpec):
# Relationship and Attribute specs
del spec.owner.properties[spec.name]
else:
raise TypeError(f"Unsupported spec type: {spec}")
class TreeModel(QtCore.QAbstractItemModel):
Columns = list()
ItemRole = QtCore.Qt.UserRole + 1
def __init__(self, parent=None):
super(TreeModel, self).__init__(parent)
self._root_item = Item()
def rowCount(self, parent=None):
if parent is None or not parent.isValid():
parent_item = self._root_item
else:
parent_item = parent.internalPointer()
return parent_item.childCount()
def columnCount(self, parent):
return len(self.Columns)
def data(self, index, role):
if not index.isValid():
return None
if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
item = index.internalPointer()
column = index.column()
key = self.Columns[column]
return item.get(key, None)
if role == self.ItemRole:
return index.internalPointer()
def setData(self, index, value, role=QtCore.Qt.EditRole):
"""Change the data on the items.
Returns:
bool: Whether the edit was successful
"""
if index.isValid():
if role == QtCore.Qt.EditRole:
item = index.internalPointer()
column = index.column()
key = self.Columns[column]
item[key] = value
self.dataChanged.emit(index, index)
return True
return False
def setColumns(self, keys):
assert isinstance(keys, (list, tuple))
self.Columns = keys
def headerData(self, section, orientation, role):
if role == QtCore.Qt.DisplayRole:
if section < len(self.Columns):
return self.Columns[section]
super(TreeModel, self).headerData(section, orientation, role)
def flags(self, index):
flags = QtCore.Qt.ItemIsEnabled
item = index.internalPointer()
if item.get("enabled", True):
flags |= QtCore.Qt.ItemIsSelectable
return flags
def parent(self, index):
item = index.internalPointer()
parent_item = item.parent()
# If it has no parents we return invalid
if parent_item == self._root_item or not parent_item:
return QtCore.QModelIndex()
return self.createIndex(parent_item.row(), 0, parent_item)
def index(self, row, column, parent=None):
"""Return index for row/column under parent"""
if parent is None or not parent.isValid():
parent_item = self._root_item
else:
parent_item = parent.internalPointer()
child_item = parent_item.child(row)
if child_item:
return self.createIndex(row, column, child_item)
else:
return QtCore.QModelIndex()
def add_child(self, item, parent=None):
if parent is None:
parent = self._root_item
parent.add_child(item)
def column_name(self, column):
"""Return column key by index"""
if column < len(self.Columns):
return self.Columns[column]
def clear(self):
self.beginResetModel()
self._root_item = Item()
self.endResetModel()
class Item(dict):
"""An item that can be represented in a tree view using `TreeModel`.
The item can store data just like a regular dictionary.
>>> data = {"name": "John", "score": 10}
>>> item = Item(data)
>>> assert item["name"] == "John"
"""
def __init__(self, data=None):
super(Item, self).__init__()
self._children = list()
self._parent = None
if data is not None:
assert isinstance(data, dict)
self.update(data)
def childCount(self):
return len(self._children)
def child(self, row):
if row >= len(self._children):
log.warning("Invalid row as child: {0}".format(row))
return
return self._children[row]
def children(self):
return self._children
def parent(self):
return self._parent
def row(self):
"""
Returns:
int: Index of this item under parent"""
if self._parent is not None:
siblings = self.parent().children()
return siblings.index(self)
return -1
def add_child(self, child):
"""Add a child to this item"""
child._parent = self
self._children.append(child)
class StageSdfModel(TreeModel):
Columns = ["name", "specifier", "typeName", "default", "type"]
Colors = {
"Layer": QtGui.QColor("#008EC5"),
"PseudoRootSpec": QtGui.QColor("#A2D2EF"),
"PrimSpec": QtGui.QColor("#A2D2EF"),
"RelationshipSpec": QtGui.QColor("#FCD057"),
"AttributeSpec": QtGui.QColor("#FFC8DD"),
}
def __init__(self, stage, parent=None):
super(StageSdfModel, self).__init__(parent)
self._stage = stage
def refresh(self):
self.clear()
for layer in stage.GetLayerStack():
layer_item = Item({
"name": layer.identifier,
"identifier": layer.identifier,
"specifier": None,
"color": "red",
"type": layer.__class__.__name__
})
self.add_child(layer_item)
items_by_path = {}
def _traverse(path):
spec = layer.GetObjectAtPath(path)
if not spec:
# ignore target list binding entries
items_by_path[path] = Item({
"name": path.elementString,
"path": path,
"type": path.__class__.__name__
})
return
spec_item = Item({
"name": spec.name,
"spec": spec,
"path": path,
"type": spec.__class__.__name__
})
if isinstance(spec, Sdf.PrimSpec):
spec_item["specifier"] = SPECIFIER_LABEL.get(spec.specifier)
type_name = spec.typeName
spec_item["typeName"] = type_name
elif isinstance(spec, Sdf.AttributeSpec):
spec_item["default"] = shorten(str(spec.default), 60)
items_by_path[path] = spec_item
layer.Traverse("/", _traverse)
# Build hierarchy of item of specs
for path, item in sorted(items_by_path.items()):
parent = path.GetParentPath()
parent_item = items_by_path.get(parent, layer_item)
parent_item.add_child(item)
def data(self, index, role):
if role == QtCore.Qt.ForegroundRole:
item = index.data(TreeModel.ItemRole)
class_type_name = item.get("type")
color = self.Colors.get(class_type_name)
if color:
return color
return super(StageSdfModel, self).data(index, role)
class SpecEditsWidget(QtWidgets.QDialog):
def __init__(self, stage, parent=None):
super(SpecEditsWidget, self).__init__(parent=parent)
layout = QtWidgets.QVBoxLayout(self)
self.setWindowTitle("USD Layer Spec Editor")
model = StageSdfModel(stage)
view = QtWidgets.QTreeView()
view.setModel(model)
view.setIndentation(10)
view.setStyleSheet("QTreeView::item { padding: 3px }")
view.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
auto_refresh = QtWidgets.QCheckBox("Auto Refresh on Stage Changes")
auto_refresh.setChecked(True)
refresh = QtWidgets.QPushButton("Refresh")
delete = QtWidgets.QPushButton("Delete")
layout.addWidget(view)
layout.addWidget(auto_refresh)
layout.addWidget(refresh)
layout.addWidget(delete)
self.auto_refresh = auto_refresh
self.model = model
self.view = view
auto_refresh.stateChanged.connect(self.set_refresh_on_changes)
refresh.clicked.connect(self.on_refresh)
delete.clicked.connect(self.on_delete)
self._listeners = []
self.set_refresh_on_changes(True)
self.on_refresh()
def set_refresh_on_changes(self, state):
if state:
if self._listeners:
return
print("Adding listener")
sender = self.model._stage
listener = Tf.Notice.Register(Usd.Notice.StageContentsChanged,
self.on_stage_changed_notice,
sender)
self._listeners.append(listener)
else:
if not self._listeners:
return
print("Removing listener")
for listener in self._listeners:
listener.Revoke()
self._listeners.clear()
def on_stage_changed_notice(self, notice, sender):
self.on_refresh()
def showEvent(self, event):
state = self.auto_refresh.checkState() == QtCore.Qt.Checked
self.set_refresh_on_changes(state)
def hideEvent(self, event):
# Remove any callbacks if they exist
self.set_refresh_on_changes(False)
def on_refresh(self):
self.model.refresh()
self.view.resizeColumnToContents(0)
self.view.expandAll()
self.view.resizeColumnToContents(1)
self.view.resizeColumnToContents(2)
self.view.resizeColumnToContents(3)
self.view.resizeColumnToContents(4)
def on_delete(self):
selection_model = self.view.selectionModel()
rows = selection_model.selectedRows()
specs = []
for row in rows:
item = row.data(TreeModel.ItemRole)
spec = item.get("spec")
if spec:
specs.append(spec)
if not specs:
return
with Sdf.ChangeBlock():
for spec in specs:
print(f"Removing spec: {spec.path}")
remove_spec(spec)
self.on_refresh()
@BigRoy
Copy link
Author

BigRoy commented Sep 21, 2023

Example code to run it in Maya 2024 with the USD plug-in:

from maya import cmds
import mayaUsd.ufe


def get_main_window():
    """Acquire Maya's main window"""
    import maya.OpenMayaUI as omui
    from shiboken2 import wrapInstance
    main_window_ptr = omui.MQtUtil.mainWindow()
    return wrapInstance(int(main_window_ptr), QtWidgets.QWidget)


# Example usage inside Maya (this just takes the first `mayaUsdProxyShape`
# in your scene to get the stage from and initialize the widget as window
proxy = cmds.ls(type="mayaUsdProxyShape", long=True)[0]
stage = mayaUsd.ufe.getStage(proxy)
widget = SpecEditsWidget(stage, parent=get_main_window())
widget.resize(900, 500)
widget.show()

image

@BigRoy
Copy link
Author

BigRoy commented Oct 11, 2023

This code snippet was originally written for Maya USD Issue: USD Layer Editor - List layer Edits

@BigRoy
Copy link
Author

BigRoy commented Oct 11, 2023

Slightly more elaborate example with:

  • Maya USD Type Icons
  • Filter text field (filters by name (left column))
  • Filter list
  • Show more data like references, payloads

image

Code

import os
import glob

from maya import cmds
import mayaUsd.ufe
from pxr import Usd, Tf, Sdf

from PySide2 import QtCore, QtWidgets, QtGui


class Scheduled:
    jobs = {}


def schedule(func, time, channel="default"):
    """Run `func` at a later `time` in a dedicated `channel`

    Given an arbitrary function, call this function after a given
    timeout. It will ensure that only one "job" is running within
    the given channel at any one time and cancel any currently
    running job if a new job is submitted before the timeout.

    """

    try:
        Scheduled.jobs[channel].stop()
    except (AttributeError, KeyError, RuntimeError):
        pass

    timer = QtCore.QTimer()
    timer.setSingleShot(True)
    timer.timeout.connect(func)
    timer.start(time)

    Scheduled.jobs[channel] = timer


def get_maya_usd_type_icon_paths():
    icon_by_typename = {}
    prefix = len("out_USD_")
    suffix = len(".png")
    for folder in os.getenv("XBMLANGPATH").split(os.pathsep):
        files = glob.glob(os.path.join(folder, "out_USD_*.png"))
        if not files:
            continue

        for filepath in files:
            basename = os.path.basename(filepath)
            typename = basename[prefix:-suffix]

            if typename.endswith("_200") or typename.endswith("_150"):
                # Ignore different sizes
                continue

            if typename not in icon_by_typename:
                icon_by_typename[typename] = filepath

    return icon_by_typename


class MayaUsdIconProvider(object):
    _instance = None

    def __init__(self):
        self._icon_paths = {}
        self._icon_qt_cache = {}

    def refresh(self):
        self._icon_paths = get_maya_usd_type_icon_paths()
        self._icon_qt_cache.clear()

    def get_qt_icon_from_type_name(self, nodetype):
        if nodetype in self._icon_qt_cache:
            return self._icon_qt_cache[nodetype]

        if nodetype in self._icon_paths:
            icon_path = self._icon_paths[nodetype]
            icon = QtGui.QIcon(icon_path)
            self._icon_qt_cache[nodetype] = icon
            return icon

    def get_qt_icon_from_prim(self, prim):
        for nodetype in self.iter_prim_type_names(prim):
            icon = self.get_qt_icon_from_type_name(nodetype)
            if icon:
                return icon

    # Using just the UsdPrim
    @staticmethod
    def iter_prim_type_names(prim):
        if not prim.IsValid():
            return

        if not prim.GetTypeName():
            # unknown type
            return

        type_info = prim.GetPrimTypeInfo()
        schema_type = type_info.GetSchemaType()
        for t in schema_type.GetAllAncestorTypes():
            yield Usd.SchemaRegistry.GetConcreteSchemaTypeName(t) or t.typeName

    @classmethod
    def instance(cls):
        if cls._instance is not None:
            return cls._instance

        provider = cls()
        provider.refresh()
        cls._instance = provider
        return provider


# See: https://github.com/PixarAnimationStudios/OpenUSD/blob/release/pxr/usd/sdf/fileIO_Common.cpp#L879-L892
SPECIFIER_LABEL = {
    Sdf.SpecifierDef: "def",
    Sdf.SpecifierOver: "over",
    Sdf.SpecifierClass: "abstract"
}


def shorten(s, width, placeholder="..."):
    """Shorten string to `width`"""
    if len(s) <= width:
        return s
    return "{}{}".format(s[:width], placeholder)


def remove_spec(spec):
    """Remove Sdf.Spec authored opinion."""
    if spec.expired:
        return

    if isinstance(spec, Sdf.PrimSpec):
        # PrimSpec
        parent = spec.nameParent
        if parent:
            view = parent.nameChildren
        else:
            # Assume PrimSpec is root prim
            view = spec.layer.rootPrims
        del view[spec.name]

    elif isinstance(spec, Sdf.PropertySpec):
        # Relationship and Attribute specs
        del spec.owner.properties[spec.name]
    else:
        raise TypeError(f"Unsupported spec type: {spec}")


class TreeModel(QtCore.QAbstractItemModel):
    Columns = list()
    ItemRole = QtCore.Qt.UserRole + 1

    def __init__(self, parent=None):
        super(TreeModel, self).__init__(parent)
        self._root_item = Item()

    def rowCount(self, parent=None):
        if parent is None or not parent.isValid():
            parent_item = self._root_item
        else:
            parent_item = parent.internalPointer()
        return parent_item.childCount()

    def columnCount(self, parent):
        return len(self.Columns)

    def data(self, index, role):
        if not index.isValid():
            return None

        if role == QtCore.Qt.DisplayRole:
            item = index.internalPointer()
            column = index.column()

            key = self.Columns[column]
            return item.get(key, None)

        if role == self.ItemRole:
            return index.internalPointer()

    def setColumns(self, keys):
        assert isinstance(keys, (list, tuple))
        self.Columns = keys

    def headerData(self, section, orientation, role):

        if role == QtCore.Qt.DisplayRole:
            if section < len(self.Columns):
                return self.Columns[section]
        return

        return super(TreeModel, self).headerData(section, orientation, role)

    def flags(self, index):
        flags = QtCore.Qt.ItemIsEnabled

        item = index.internalPointer()
        if item.get("enabled", True):
            flags |= QtCore.Qt.ItemIsSelectable

        return flags

    def parent(self, index):

        item = index.internalPointer()
        parent_item = item.parent()

        # If it has no parents we return invalid
        if parent_item == self._root_item or not parent_item:
            return QtCore.QModelIndex()

        return self.createIndex(parent_item.row(), 0, parent_item)

    def index(self, row, column, parent=None):
        """Return index for row/column under parent"""

        if parent is None or not parent.isValid():
            parent_item = self._root_item
        else:
            parent_item = parent.internalPointer()

        child_item = parent_item.child(row)
        if child_item:
            return self.createIndex(row, column, child_item)
        else:
            return QtCore.QModelIndex()

    def add_child(self, item, parent=None):
        if parent is None:
            parent = self._root_item

        parent.add_child(item)

    def column_name(self, column):
        """Return column key by index"""

        if column < len(self.Columns):
            return self.Columns[column]

    def clear(self):
        self.beginResetModel()
        self._root_item = Item()
        self.endResetModel()


class Item(dict):
    """An item that can be represented in a tree view using `TreeModel`.

    The item can store data just like a regular dictionary.

    >>> data = {"name": "John", "score": 10}
    >>> item = Item(data)
    >>> assert item["name"] == "John"

    """

    def __init__(self, data=None):
        super(Item, self).__init__()

        self._children = list()
        self._parent = None

        if data is not None:
            assert isinstance(data, dict)
            self.update(data)

    def childCount(self):
        return len(self._children)

    def child(self, row):

        if row >= len(self._children):
            log.warning("Invalid row as child: {0}".format(row))
            return

        return self._children[row]

    def children(self):
        return self._children

    def parent(self):
        return self._parent

    def row(self):
        """
        Returns:
             int: Index of this item under parent"""
        if self._parent is not None:
            siblings = self.parent().children()
            return siblings.index(self)
        return -1

    def add_child(self, child):
        """Add a child to this item"""
        child._parent = self
        self._children.append(child)


class StageSdfModel(TreeModel):
    Columns = [
        "name", "specifier", "typeName", "default", "type",
        # "variantSelections", "variantSetNameList", "variantSets",
        # "referenceList", "payloadList", "relocates"
    ]

    Colors = {
        "Layer": QtGui.QColor("#008EC5"),
        "PseudoRootSpec": QtGui.QColor("#A2D2EF"),
        "PrimSpec": QtGui.QColor("#A2D2EF"),
        "RelationshipSpec": QtGui.QColor("#FCD057"),
        "AttributeSpec": QtGui.QColor("#FFC8DD"),
    }

    def __init__(self, stage=None, parent=None):
        super(StageSdfModel, self).__init__(parent)
        self._stage = stage
        self._icon_provider = MayaUsdIconProvider.instance()
        
    def setStage(self, stage):
        self._stage = stage

    def refresh(self):
        self.clear()
        if not self._stage:
            return

        for layer in stage.GetLayerStack():

            layer_item = Item({
                "name": layer.identifier,
                "identifier": layer.identifier,
                "specifier": None,
                "color": "red",
                "type": layer.__class__.__name__
            })
            self.add_child(layer_item)

            items_by_path = {}

            def _traverse(path):
                spec = layer.GetObjectAtPath(path)
                if not spec:
                    # ignore target list binding entries
                    items_by_path[path] = Item({
                        "name": path.elementString,
                        "path": path,
                        "type": path.__class__.__name__
                    })
                    return

                icon = None
                spec_item = Item({
                    "name": spec.name,
                    "spec": spec,
                    "path": path,
                    "type": spec.__class__.__name__
                })

                if hasattr(spec, "GetTypeName"):
                    spec_type_name = spec.GetTypeName()
                    icon = self._icon_provider.get_qt_icon_from_type_name(
                        spec_type_name)
                    if icon:
                        spec_item["icon"] = icon

                if isinstance(spec, Sdf.PrimSpec):
                    if not icon:
                        prim = stage.GetPrimAtPath(path)
                        if prim:
                            icon = self._icon_provider.get_qt_icon_from_prim(
                                prim)
                            if icon:
                                spec_item["icon"] = icon

                    spec_item["specifier"] = SPECIFIER_LABEL.get(
                        spec.specifier)
                    type_name = spec.typeName
                    spec_item["typeName"] = type_name

                    # TODO: Implement some good UX for variants, references, payloads and relocates
                    # "variantSelections",
                    # "variantSets",
                    #for variant_selection in spec.variantSelections:
                    #    selection_item = Item({
                    #        "name": "TEST",
                    #        "type": "variantSelection"
                    #    })
                    #    spec_item.add_child(selection_item)
                    
                    for key in ["variantSetName",
                                "reference",
                                "payload"]:
                         list_changes = getattr(spec, key + "List")
                         for change_type in ['added', 'appended', 'deleted', 'explicit', 'ordered', 'prepended']:
                             changes_for_type = getattr(list_changes, change_type + "Items")
                             for change in changes_for_type:
                                 list_change_item = Item({
                                     "name": change.assetPath,
                                     "default": change_type,
                                     "type": key
                                 })
                                 spec_item.add_child(list_change_item)
                         if list_changes:
                            spec_item[key] = str(list_changes)
                    
                    print(spec.relocates)

                elif isinstance(spec, Sdf.AttributeSpec):
                    spec_item["default"] = shorten(str(spec.default), 60)

                items_by_path[path] = spec_item

            layer.Traverse("/", _traverse)

            # Build hierarchy of item of specs
            for path, item in sorted(items_by_path.items()):
                parent = path.GetParentPath()
                parent_item = items_by_path.get(parent, layer_item)
                parent_item.add_child(item)

    def data(self, index, role):

        if role == QtCore.Qt.ForegroundRole:
            item = index.data(TreeModel.ItemRole)
            class_type_name = item.get("type")
            color = self.Colors.get(class_type_name)
            return color

        if index.column() == 2 and role == QtCore.Qt.DecorationRole:
            item = index.data(TreeModel.ItemRole)
            return item.get("icon")

        return super(StageSdfModel, self).data(index, role)


class Proxy(QtCore.QSortFilterProxyModel):
    
    def __init__(self, *args, **kwargs):
        super(Proxy, self).__init__(*args, **kwargs)
        self._filter_types = set(["RelationshipSpec"])
    
    def set_types_filter(self, types):
        self._filter_types = set(types)
        self.invalidateFilter()
    
    def filterAcceptsRow(self, source_row, source_parent):
        model = self.sourceModel()
        index = model.index(source_row, 0, source_parent)
        if not index.isValid():
            return False
        
        item = index.data(TreeModel.ItemRole)
        item_type = item.get("type")
        if self._filter_types and item_type and item_type not in self._filter_types:
            return False
            
        return super(Proxy, self).filterAcceptsRow(source_row, source_parent)
        

class FilterListWidget(QtWidgets.QListWidget):
    def __init__(self):
        super(FilterListWidget, self).__init__()
        self.addItems([
            "Layer",
            "PseudoRootSpec",
            "PrimSpec",
            "AttributeSpec",
            "RelationshipSpec",
            
            # PrimSpec changes
            "variantSetName",
            "reference",
            "payload",
            
            "variantSelections",
            "variantSets",
            "relocates"
        ])
        self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)


class SpecEditorWindow(QtWidgets.QDialog):
    def __init__(self, stage, parent=None):
        super(SpecEditorWindow, self).__init__(parent=parent)

        self.setWindowTitle("USD Layer Spec Editor")

        layout = QtWidgets.QVBoxLayout(self)
        self.setContentsMargins(0, 0, 0, 0)
        splitter = QtWidgets.QSplitter()
        
        filter_list = FilterListWidget()
        filter_list.itemSelectionChanged.connect(
            self._on_filter_selection_changed
        )
        
        editor = SpecEditsWidget(stage)
        
        splitter.addWidget(filter_list)
        splitter.addWidget(editor)
        splitter.setSizes([100, 700])
        layout.addWidget(splitter)        
        
        self.editor = editor
        self.filter_list = filter_list
        
    def _on_filter_selection_changed(self):
        items = self.filter_list.selectedItems()
        types = {item.text() for item in items}
        self.editor.proxy.set_types_filter(types)
        self.editor.view.expandAll()
        


class SpecEditsWidget(QtWidgets.QWidget):
    def __init__(self, stage=None, parent=None):
        super(SpecEditsWidget, self).__init__(parent=parent)

        layout = QtWidgets.QVBoxLayout(self)
        layout.setContentsMargins(0, 0, 0, 0)

        filter_edit = QtWidgets.QLineEdit()
        filter_edit.setPlaceholderText("Filter")

        model = StageSdfModel(stage)
        proxy = Proxy()
        proxy.setRecursiveFilteringEnabled(True)
        proxy.setSourceModel(model)
        view = QtWidgets.QTreeView()
        view.setModel(proxy)
        view.setIndentation(10)
        view.setIconSize(QtCore.QSize(20, 20))
        view.setStyleSheet("QTreeView::item { height: 20px; padding: 0px; margin: 1px 5px 1px 5px; }")
        view.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
        view.setUniformRowHeights(True)

        auto_refresh = QtWidgets.QCheckBox("Auto Refresh on Stage Changes")
        auto_refresh.setChecked(True)
        refresh = QtWidgets.QPushButton("Refresh")
        delete = QtWidgets.QPushButton("Delete")

        layout.addWidget(filter_edit)
        layout.addWidget(view)
        layout.addWidget(auto_refresh)
        layout.addWidget(refresh)
        layout.addWidget(delete)

        self.filter_edit = QtWidgets
        self.auto_refresh = auto_refresh
        self.model = model
        self.proxy = proxy
        self.view = view

        auto_refresh.stateChanged.connect(self.set_refresh_on_changes)
        refresh.clicked.connect(self.on_refresh)
        delete.clicked.connect(self.on_delete)
        filter_edit.textChanged.connect(self.on_filter_changed)

        self._listeners = []

        self.set_refresh_on_changes(True)
        self.on_refresh()

    def set_refresh_on_changes(self, state):
        if state:
            if self._listeners:
                return
            print("Adding listener")
            sender = self.model._stage
            listener = Tf.Notice.Register(Usd.Notice.StageContentsChanged,
                                          self.on_stage_changed_notice,
                                          sender)
            self._listeners.append(listener)
        else:
            if not self._listeners:
                return
            print("Removing listener")
            for listener in self._listeners:
                listener.Revoke()
            self._listeners.clear()

    def on_stage_changed_notice(self, notice, sender):
        self.proxy.invalidate()
        schedule(self.on_refresh, 100, channel="changes")

    def on_filter_changed(self, text):
        self.proxy.setFilterRegularExpression(".*{}.*".format(text))
        self.proxy.invalidateFilter()
        self.expandAll()

    def showEvent(self, event):
        state = self.auto_refresh.checkState() == QtCore.Qt.Checked
        self.set_refresh_on_changes(state)

    def hideEvent(self, event):
        # Remove any callbacks if they exist
        self.set_refresh_on_changes(False)

    def on_refresh(self):
        self.model.refresh()
        self.proxy.invalidate()
        self.view.resizeColumnToContents(0)
        self.view.expandAll()
        self.view.resizeColumnToContents(1)
        self.view.resizeColumnToContents(2)
        self.view.resizeColumnToContents(3)
        self.view.resizeColumnToContents(4)

    def on_delete(self):
        selection_model = self.view.selectionModel()
        rows = selection_model.selectedRows()
        specs = []
        for row in rows:
            item = row.data(TreeModel.ItemRole)
            spec = item.get("spec")
            if item.get("type") == "PseudoRootSpec":
                continue

            if spec:
                specs.append(spec)

        if not specs:
            return

        with Sdf.ChangeBlock():
            for spec in specs:
                print(f"Removing spec: {spec.path}")
                remove_spec(spec)

        if not self._listeners:
            self.on_refresh()


def get_main_window():
    """Acquire Maya's main window"""
    import maya.OpenMayaUI as omui
    from shiboken2 import wrapInstance
    main_window_ptr = omui.MQtUtil.mainWindow()
    return wrapInstance(int(main_window_ptr), QtWidgets.QWidget)


# Example usage inside Maya (this just takes the first `mayaUsdProxyShape`
# in your scene to get the stage from and initialize the widget as window
proxy = cmds.ls(type="mayaUsdProxyShape", long=True)[0]
stage = mayaUsd.ufe.getStage(proxy)
widget = SpecEditorWindow(stage, parent=get_main_window())
widget.resize(1000, 500)
widget.show()

@BigRoy
Copy link
Author

BigRoy commented Nov 30, 2023

This tool is currently incorporated in usd_qtpy with more features and bugfixes.

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