Skip to content

Commit

Permalink
Merge pull request #21 from BigRoy/enhancement/prim_hierarchy_delegate
Browse files Browse the repository at this point in the history
Enhancement: Prim Hierarchy Delegate and default prim / variant set features
  • Loading branch information
BigRoy committed Nov 29, 2023
2 parents 96d16b7 + fb2708e commit 3e27bd0
Show file tree
Hide file tree
Showing 108 changed files with 8,016 additions and 489 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ The Qt components can be embedded in your own Qt interfaces and usually have
a `stage` entrypoint that you should pass a `pxr.Usd.Stage` instance.

However, a simple example Editor UI is also available to run standalone.

![USD Editor](/assets/images/editor_screenshot.png "USD Editor")

If you have the `usd_qtpy` package you can for example run it like:

```
Expand Down
Binary file added assets/images/editor_screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion usd_qtpy/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@ def main():
from pxr import Usd # noqa
from qtpy import QtWidgets # noqa
from usd_qtpy.editor import EditorWindow # noqa
from usd_qtpy.style import load_stylesheet

stage = Usd.Stage.Open(filepath)
app = QtWidgets.QApplication()
dialog = EditorWindow(stage=stage)
dialog.resize(600, 600)
dialog.resize(1200, 600)
dialog.setStyleSheet(load_stylesheet())
dialog.show()
app.exec_()

Expand Down
15 changes: 12 additions & 3 deletions usd_qtpy/editor.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import logging

from qtpy import QtWidgets
from qtpy import QtWidgets, QtCore

from . import (
prim_hierarchy,
Expand All @@ -17,13 +17,22 @@
HAS_VIEWER = False


class EditorWindow(QtWidgets.QDialog):
class EditorWindow(QtWidgets.QWidget):
"""Example editor window containing the available components."""

def __init__(self, stage, parent=None):
super(EditorWindow, self).__init__(parent=parent)

self.setWindowTitle("USD Editor")
title = "USD Editor"
if stage:
name = stage.GetRootLayer().GetDisplayName()
title = f"{title}: {name}"
self.setWindowTitle(title)

self.setWindowFlags(
self.windowFlags() |
QtCore.Qt.Dialog
)

layout = QtWidgets.QVBoxLayout(self)
splitter = QtWidgets.QSplitter(self)
Expand Down
49 changes: 43 additions & 6 deletions usd_qtpy/layer_diff.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import contextlib
import difflib

from qtpy import QtWidgets, QtCore
Expand All @@ -6,6 +7,39 @@
from .lib.qt import DifflibSyntaxHighlighter


@contextlib.contextmanager
def preserve_scroll(scroll_area):
"""Preserve scrollbar positions by percentage after context."""
def get_percent(scrollbar):
value = scrollbar.value()
minimum = scrollbar.minimum()
maximum = scrollbar.maximum()
if value <= minimum:
return 0
if value >= maximum:
return 1
if minimum == maximum:
return 0
return (value - minimum) / (maximum - minimum)

def set_percent(scrollbar, percent):
minimum = scrollbar.minimum()
maximum = scrollbar.maximum()
value = minimum + ((maximum - minimum) * percent)
scrollbar.setValue(value)

horizontal = scroll_area.horizontalScrollBar()
h_percent = get_percent(horizontal)
vertical = scroll_area.verticalScrollBar()
v_percent = get_percent(vertical)
try:
yield
finally:
print(h_percent, v_percent)
set_percent(horizontal, h_percent)
set_percent(vertical, v_percent)


class LayerDiffWidget(QtWidgets.QDialog):
"""Simple layer ASCII diff text view"""
# TODO: Add a dedicated 'toggle listen' and 'refresh' button to the widget
Expand All @@ -26,7 +60,8 @@ def __init__(self,
text_edit = QtWidgets.QTextEdit()

# Force monospace font for readability
text_edit.setStyleSheet('* { font-family: "Courier"; }')
text_edit.setProperty("font-style", "monospace")
text_edit.setLineWrapMode(QtWidgets.QTextEdit.NoWrap)
highlighter = DifflibSyntaxHighlighter(text_edit)
text_edit.setPlaceholderText("Layers match - no difference detected.")

Expand Down Expand Up @@ -59,16 +94,18 @@ def refresh(self):
a_ascii = layer_a.ExportToString()
b_ascii = layer_b.ExportToString()

# Print diff
self._text_edit.clear()
for line in difflib.unified_diff(
generator = difflib.unified_diff(
a_ascii.splitlines(),
b_ascii.splitlines(),
fromfile=self._layer_a_label or f"{layer_a.identifier} (A)",
tofile=self._layer_b_label or f"{layer_b.identifier} (B)",
lineterm=""
):
self._text_edit.insertPlainText(f"{line}\n")
)

with preserve_scroll(self._text_edit):
self._text_edit.clear()
for line in generator:
self._text_edit.insertPlainText(f"{line}\n")

def on_layers_changed(self, notice, sender):
# TODO: We could also cache the ASCII of the USD files so that on
Expand Down
69 changes: 38 additions & 31 deletions usd_qtpy/layer_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import contextlib
import logging
from functools import partial
from typing import List

from qtpy import QtWidgets, QtCore, QtGui

Expand All @@ -11,6 +12,7 @@
from .tree.base import AbstractTreeModelMixin
from .lib.qt import schedule, iter_model_rows
from .layer_diff import LayerDiffWidget
from .resources import get_icon

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -53,18 +55,22 @@ def set_tips(widget, tip):


class LayerItem(TreeItem):
__slots__ = ('layer', 'parent_layer')
__slots__ = ('layer', 'stack')

def __init__(self, layer: Sdf.Layer, parent_layer: Sdf.Layer = None):
if parent_layer:
separator = "<--sublayer-->"
key = separator.join([parent_layer.identifier, layer.identifier])
else:
key = layer.identifier
def __init__(self, layer: Sdf.Layer, parents: List[Sdf.Layer] = None):

# The key is the full layer stack (all parents) joined together
# by a unique separator so the layer identifier can uniquely appear
# anywhere on the layer stack
parents = parents or []
stack = list(parents)
stack.append(layer)
separator = "<--sublayer-->"
key = separator.join(stack_layer.identifier for stack_layer in stack)

super(LayerItem, self).__init__(key=key)
self.layer = layer
self.parent_layer = parent_layer
self.stack = stack


class LayerStackModel(AbstractTreeModelMixin, QtCore.QAbstractItemModel):
Expand Down Expand Up @@ -284,15 +290,15 @@ def set_stage(self, stage):
return

self._stage = stage
self.refresh()

if self._listeners:
self.log.debug("Revoking Tf.Notice listeners: %s", self._listeners)
# Tf.Notice.Revoke(self._listeners)
for listener in self._listeners:
listener.Revoke()

self._listeners.clear()
def register_listeners(self):
stage = self._stage
if stage and stage.GetPseudoRoot():
if self._listeners:
# Remove any existing listeners
self.revoke_listeners()

self.log.debug("Adding Tf.Notice Listeners..")
# Listen to changes
self._listeners.append(Tf.Notice.Register(
Expand Down Expand Up @@ -327,7 +333,14 @@ def set_stage(self, stage):
# self.on_layers_changed,
# ))

self.refresh()
def revoke_listeners(self):
if self._listeners:
self.log.debug("Revoking Tf.Notice listeners: %s", self._listeners)
# Tf.Notice.Revoke(self._listeners)
for listener in self._listeners:
listener.Revoke()

self._listeners.clear()

@contextlib.contextmanager
def reset_context(self):
Expand All @@ -351,8 +364,8 @@ def refresh(self):
return

def add_layer(layer: Sdf.Layer, parent=None):
parent_layer = parent.layer if parent else None
layer_item = LayerItem(layer, parent_layer=parent_layer)
parent_layers = parent.stack if parent else None
layer_item = LayerItem(layer, parents=parent_layers)
item_tree.add_items(layer_item, parent=parent)

for sublayer_path in layer.subLayerPaths:
Expand Down Expand Up @@ -406,24 +419,16 @@ def __init__(self, layer, stage, parent=None):
# Identifier label as display name
label = QtWidgets.QLabel("", parent=self)

resources = os.path.join(os.path.dirname(__file__),
"resources",
"feathericons")

# Save changes button
save_icon = QtGui.QIcon(os.path.join(resources, "save.svg"))
save = QtWidgets.QPushButton(self)
save = QtWidgets.QPushButton(get_icon("save"), "", self)
set_tips(
save, "Save layer to disk"
)
save.setIcon(save_icon)
save.setFixedWidth(25)
save.setFixedHeight(25)

# Set edit target (active or not button)
edit_icon = QtGui.QIcon(os.path.join(resources, "edit-2.svg"))
edit_target_btn = QtWidgets.QPushButton(self)
edit_target_btn.setIcon(edit_icon)
edit_target_btn = QtWidgets.QPushButton(get_icon("edit-2"), "", self)
edit_target_btn.setCheckable(True)
set_tips(
edit_target_btn,
Expand Down Expand Up @@ -515,7 +520,6 @@ def on_save_layer(self):
# TODO: Prompt for filepath if layer is anonymous?
# TODO: Allow making filepath relative to parent layer?
log.debug(f"Saving: {layer}")
log.debug(layer.ExportToString())
layer.Save()
# TODO: Do not update using this but base it off of signals from
# Sdf.Notice.LayerDidSaveLayerToFile
Expand Down Expand Up @@ -599,6 +603,7 @@ def on_view_context_menu(self, point):

def show_layer_as_text():
text_edit = QtWidgets.QTextEdit(parent=self)
text_edit.setProperty("font-style", "monospace")
text_edit.setPlainText(layer.ExportToString())
text_edit.setWindowTitle(layer.identifier)
text_edit.setWindowFlags(QtCore.Qt.Dialog)
Expand Down Expand Up @@ -692,9 +697,11 @@ def on_add_layer(self, index):
log.debug("Adding sublayer: %s", filename)
layer.subLayerPaths.append(filename)

def showEvent(self, event):
self.model.register_listeners()

def hideEvent(self, event: QtGui.QCloseEvent) -> None:
# TODO: This should be on a better event when we know the window
# will be gone and unused after. The `closeEvent` doesn't seem
# to trigger by default on closing a parent dialog?
log.debug("Clearing stage connection..")
self.model.set_stage(None) # clear listeners on close
self.model.revoke_listeners()
25 changes: 24 additions & 1 deletion usd_qtpy/lib/qt.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import re
import sys
import logging
from qtpy import QtCore, QtGui
from qtpy import QtCore, QtGui, QtWidgets


class SharedObjects:
Expand Down Expand Up @@ -93,3 +93,26 @@ def highlightBlock(self, text):
# Format the full block
self.setFormat(0, len(text), char_format)
return


class DropFilesPushButton(QtWidgets.QPushButton):
"""QPushButton that emits files_dropped signal when dropping files on it"""

files_dropped = QtCore.Signal(list)

def __init__(self, *args, **kwargs):
super(DropFilesPushButton, self).__init__(*args, **kwargs)
self.setAcceptDrops(True)

def dragEnterEvent(self, event):
if event.mimeData().hasUrls():
event.acceptProposedAction()
else:
super(DropFilesPushButton, self).dragEnterEvent(event)

def dropEvent(self, event):
if event.mimeData().hasUrls():
self.files_dropped.emit(event.mimeData().urls())
event.acceptProposedAction()
else:
super(DropFilesPushButton, self).dropEvent(event)
Loading

0 comments on commit 3e27bd0

Please sign in to comment.