Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhancement: Skip pseudoroot prim in Prim Hierarchy + fix Prim rename in variant edit content + some optimizations #40

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
2 changes: 2 additions & 0 deletions usd_qtpy/layer_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -790,9 +790,11 @@ def on_add_anonymous_layer(self, index):

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

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?
self.model.revoke_listeners()
super(LayerTreeWidget, self).hideEvent(event)
41 changes: 25 additions & 16 deletions usd_qtpy/lib/usd.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,22 +195,20 @@ def rename_prim(prim: Usd.Prim, new_name: str) -> bool:
new_prim_path = prim_path.ReplaceName(new_name)

# We want to map the path to the current edit target of its stage so that
# if the user is renaming a prim in an edit target currently editing
# within a variant set, that we rename that particular opinion. However,
# we only do that if the source prim path existed in the edit target
# otherwise we will edit it on the layer regularly
# WARNING This will crash on calls to `prim.GetPrimStack()` afterwards
# See: https://forum.aousd.org/t/perform-namespace-edit-inside-a-variant-set-edit-target/1006
# stage = prim.GetStage()
# edit_target = stage.GetEditTarget()
# remapped_prim_path = edit_target.MapToSpecPath(prim_path)
# if (
# prim_path != remapped_prim_path
# ):
# logging.debug("Remapping prim path to within edit target: %s",
# remapped_prim_path)
# prim_path = remapped_prim_path
# new_prim_path = edit_target.MapToSpecPath(new_prim_path)
# if the user is renaming a prim in an edit target within a variant set,
# that we rename that particular opinion. However, we only do that if the
# source prim path existed in the edit target otherwise we will edit it
# on the layer regularly
stage = prim.GetStage()
edit_target = stage.GetEditTarget()
remapped_prim_path = edit_target.MapToSpecPath(prim_path)
if (
prim_path != remapped_prim_path
):
logging.debug("Remapping prim path to within edit target: %s",
remapped_prim_path)
prim_path = remapped_prim_path
new_prim_path = edit_target.MapToSpecPath(new_prim_path)

stage = prim.GetStage()
with Sdf.ChangeBlock():
Expand All @@ -220,6 +218,17 @@ def rename_prim(prim: Usd.Prim, new_name: str) -> bool:
move_prim_spec(layer,
src_prim_path=prim_path,
dest_prim_path=new_prim_path)

# We deactivate the parent if renaming within a variant set edit target
# because of known crash/bug:
# https://github.com/PixarAnimationStudios/OpenUSD/issues/2844
if new_prim_path.ContainsPrimVariantSelection():
prim = stage.GetPrimAtPath(new_prim_path.StripAllVariantSelections())
if prim and prim.IsValid() and prim.GetPrimIndex() is None:
parent = prim.GetParent()
parent.SetActive(False)
parent.SetActive(True)

return True


Expand Down
4 changes: 0 additions & 4 deletions usd_qtpy/prim_hierarchy.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,10 +359,6 @@ def _delete_selected(self):
if spec.expired:
continue

# Warning: This would also remove it from layers from
# references/payloads!
# TODO: Filter specs for which their `.getLayer()` is a layer
# from the Stage's layer stack?
remove_spec(spec)

def _duplicate_selected(self):
Expand Down
12 changes: 5 additions & 7 deletions usd_qtpy/prim_hierarchy_cache.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import logging
from typing import List, Dict
from typing import List, Dict, Optional

from pxr import Usd, Sdf

Expand All @@ -8,6 +8,7 @@


class Proxy:
"""Item proxy for the HierarchyCache"""
def __init__(self, prim: Usd.Prim):
self._prim: Usd.Prim = prim
self._children: List[Sdf.Path] = []
Expand Down Expand Up @@ -54,9 +55,6 @@ def predicate(self):
def __contains__(self, item) -> bool:
return item in self._path_to_proxy

def __getitem__(self, item):
return self.get_proxy(item)

def get_proxy(self, path: Sdf.Path) -> Proxy:
return self._path_to_proxy[path]

Expand All @@ -74,11 +72,11 @@ def get_child(self, proxy: Proxy, index: int) -> Proxy:

return self._path_to_proxy[child_path]

def get_parent(self, proxy: Proxy):
def get_parent(self, proxy: Proxy) -> Optional[Proxy]:
prim = proxy.get_prim()
path = prim.GetPath()
parent_path = path.GetParentPath()
return self._path_to_proxy[parent_path]
return self._path_to_proxy.get(parent_path, None)

def get_child_count(self, proxy: Proxy) -> int:
if not proxy or not proxy.get_prim():
Expand Down Expand Up @@ -127,7 +125,7 @@ def resync_subtrees(self, paths: set[Sdf.Path]):
for child_path in original_children.union(new_children):
self._invalidate_subtree(child_path)

def is_root(self, proxy):
def is_root(self, proxy: Proxy) -> bool:
return self._root.get_prim() == proxy.get_prim()

def get_row(self, proxy: Proxy) -> int:
Expand Down
108 changes: 50 additions & 58 deletions usd_qtpy/prim_hierarchy_model.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import logging
import contextlib
from typing import Union, Optional
from typing import Optional

from qtpy import QtCore
from pxr import Usd, Sdf, Tf
from pxr import Usd, Tf

from .lib.qt import report_error
from .lib.usd import rename_prim
Expand Down Expand Up @@ -37,6 +37,23 @@ class HierarchyModel(QtCore.QAbstractItemModel):
"""
PrimRole = QtCore.Qt.UserRole + 1

# Constants for RectDataRole
IsDefaultPrimRect = {
"text": "DFT",
"tooltip": "This prim is the default prim on the stage's root layer.",
"background-color": "#553333"
}
HasReferencesRect = {
"text": "REF",
"tooltip": "This prim has one or more references and/or payloads.",
"background-color": "#333355"
}
HasVariantSetsRect = {
"text": "VAR",
"tooltip": "One or more variant sets exist on this prim.",
"background-color": "#335533"
}

def __init__(
self,
stage: Usd.Stage=None,
Expand All @@ -59,7 +76,7 @@ def __init__(

self._predicate = predicate
self._stage = None
self._index: Union[None, HierarchyCache] = None
self._cache: Optional[HierarchyCache] = None
self._listeners = []
self._icon_provider = PrimTypeIconProvider()
self.log = logging.getLogger("HierarchyModel")
Expand Down Expand Up @@ -88,13 +105,13 @@ def set_stage(self, stage: Usd.Stage):
self._stage = stage
with self.reset_model():
if self._is_stage_valid():
self._index = HierarchyCache(
self._cache = HierarchyCache(
root=stage.GetPrimAtPath("/"),
predicate=self._predicate
)
self.register_listeners()
else:
self._index = None
self._cache = None

def _is_stage_valid(self):
return self._stage and self._stage.GetPseudoRoot()
Expand Down Expand Up @@ -161,16 +178,16 @@ def on_objects_changed(self, notice, sender):
):
index_to_path[index] = index_path

self._index.resync_subtrees(resynced_paths)
self._cache.resync_subtrees(resynced_paths)

from_indices = []
to_indices = []
for index in index_to_path:
path = index_to_path[index]

if path in self._index:
new_proxy = self._index.get_proxy(path)
new_row = self._index.get_row(new_proxy)
if path in self._cache:
new_proxy = self._cache.get_proxy(path)
new_row = self._cache.get_row(new_proxy)

if index.row() != new_row:
for _i in range(
Expand All @@ -185,35 +202,26 @@ def on_objects_changed(self, notice, sender):
to_indices.append(QtCore.QModelIndex())
self.changePersistentIndexList(from_indices, to_indices)

def _prim_to_row_index(self,
path: Sdf.Path) -> Optional[QtCore.QModelIndex]:
"""Given a path, retrieve the appropriate model index."""
if path in self._index:
proxy = self._index[path]
row = self._index.get_row(proxy)
return self.createIndex(row, 0, proxy)

def _index_to_prim(self,
model_index: QtCore.QModelIndex) -> Optional[Usd.Prim]:
model_index: QtCore.QModelIndex) -> Usd.Prim:
"""Retrieve the prim for the input model index

External clients should use `UsdQt.roles.HierarchyPrimRole` to access
the prim for an index.
"""
if model_index.isValid():
proxy = model_index.internalPointer() # -> Proxy
if type(proxy) is Proxy:
return proxy.get_prim()
return self.get_item_proxy(model_index).get_prim()

# region Qt methods
def flags(self, index):
# Make name editable
if index.column() == 0:
return (
QtCore.Qt.ItemIsEnabled
| QtCore.Qt.ItemIsSelectable
| QtCore.Qt.ItemIsEditable
)
prim: Usd.Prim = index.data(self.PrimRole)
if prim and not prim.IsPseudoRoot():
return (
QtCore.Qt.ItemIsEnabled
| QtCore.Qt.ItemIsSelectable
| QtCore.Qt.ItemIsEditable
)
return super(HierarchyModel, self).flags(index)

def setData(self, index, value, role):
Expand All @@ -240,11 +248,13 @@ def rowCount(self, parent):
if parent.column() > 0:
return 0

if not parent.isValid():
return 1
proxy = self.get_item_proxy(parent)
return self._cache.get_child_count(proxy)

parent_proxy = parent.internalPointer()
return self._index.get_child_count(parent_proxy)
def get_item_proxy(self, index: QtCore.QModelIndex) -> Proxy:
if not index.isValid():
return self._cache.root
return index.internalPointer() # noqa

def index(self, row, column, parent):
if not self._is_stage_valid():
Expand All @@ -254,13 +264,8 @@ def index(self, row, column, parent):
self.log.debug("Index does not exist: %s %s %s", row, column, parent)
return QtCore.QModelIndex()

if not parent.isValid():
# We assume the root has already been registered.
root = self._index.root
return self.createIndex(row, column, root)

parent_proxy = parent.internalPointer()
child = self._index.get_child(parent_proxy, row)
parent_proxy = self.get_item_proxy(parent)
child = self._cache.get_child(parent_proxy, row)
return self.createIndex(row, column, child)

def parent(self, index):
Expand All @@ -274,11 +279,12 @@ def parent(self, index):
if proxy is None:
return QtCore.QModelIndex()

if self._index.is_root(proxy):
parent_proxy = self._cache.get_parent(proxy)
if not parent_proxy or self._cache.is_root(parent_proxy):
# We do not display the root
return QtCore.QModelIndex()

parent_proxy = self._index.get_parent(proxy)
parent_row = self._index.get_row(parent_proxy)
parent_row = self._cache.get_row(parent_proxy)
return self.createIndex(parent_row, index.column(), parent_proxy)

def data(self, index, role):
Expand Down Expand Up @@ -308,25 +314,11 @@ def data(self, index, role):
prim = index.internalPointer().get_prim()
rects = []
if prim == self.stage.GetDefaultPrim():
rects.append(
{"text": "DFT",
"tooltip": "This prim is the default prim on "
"the stage's root layer.",
"background-color": "#553333"}
)
rects.append(self.IsDefaultPrimRect)
if prim.HasAuthoredPayloads() or prim.HasAuthoredReferences():
rects.append(
{"text": "REF",
"tooltip": "This prim has one or more references "
"and/or payloads.",
"background-color": "#333355"},
)
rects.append(self.HasReferencesRect)
if prim.HasVariantSets():
rects.append(
{"text": "VAR",
"tooltip": "One or more variant sets exist on this prim.",
"background-color": "#335533"},
)
rects.append(self.HasVariantSetsRect)

return rects
# endregion
Loading