diff --git a/usd_qtpy/layer_editor.py b/usd_qtpy/layer_editor.py
index 0e6e15a..dd5b53c 100644
--- a/usd_qtpy/layer_editor.py
+++ b/usd_qtpy/layer_editor.py
@@ -1,6 +1,7 @@
import os
import contextlib
import logging
+import sys
from functools import partial
from typing import List
@@ -17,6 +18,15 @@
log = logging.getLogger(__name__)
+def get_tag_from_layer_identifier(identifier: str) -> str:
+ """Return the 'tag' from the anonymous layer identifier"""
+ if Sdf.Layer.IsAnonymousLayerIdentifier(identifier):
+ name, kwargs = Sdf.Layer.SplitIdentifier(identifier)
+ if name.count(":") > 1:
+ return name.rsplit(":", 1)[-1]
+ return ""
+
+
def remove_sublayer(
identifier, parent
):
@@ -226,6 +236,10 @@ def dropMimeData(self, data, action, row, column, parent):
with Sdf.ChangeBlock():
for source_identifier, source_parent_identifier in sources:
+ if source_identifier == new_parent_layer.identifier:
+ # Do nothing when trying to parent to itself
+ continue
+
removed_index = None
source_parent_layer = None
if source_parent_identifier:
@@ -369,9 +383,30 @@ def add_layer(layer: Sdf.Layer, parent=None):
item_tree.add_items(layer_item, parent=parent)
for sublayer_path in layer.subLayerPaths:
- sublayer = Sdf.Layer.FindOrOpenRelativeToLayer(
- layer, sublayer_path
- )
+ try:
+ sublayer = Sdf.Layer.FindOrOpenRelativeToLayer(
+ layer, sublayer_path
+ )
+ except Tf.ErrorException:
+ # Unable to find or open the layer path
+ log.warning(f"Unable to find or open layer: %s",
+ sublayer_path, exc_info=sys.exc_info())
+ # Warning: This does not show as "dirty" even though
+ # the file does not exist on disk.
+ sublayer = Sdf.Layer.CreateNew(
+ sublayer_path
+ )
+
+ if sublayer is None:
+ log.error(
+ "Failed to create a layer for sublayer path: %s",
+ sublayer_path
+ )
+ tag = get_tag_from_layer_identifier(sublayer_path)
+ sublayer = Sdf.Layer.CreateAnonymous(tag)
+ layer.UpdateCompositionAssetDependency(
+ sublayer_path, sublayer.identifier
+ )
add_layer(sublayer, parent=layer_item)
return layer_item
@@ -418,6 +453,9 @@ def __init__(self, layer, stage, parent=None):
# Identifier label as display name
label = QtWidgets.QLabel("", parent=self)
+ # No text interaction fixes drag behavior for labels with html italics
+ # formatting, e.g. `text`
+ label.setTextInteractionFlags(QtCore.Qt.NoTextInteraction)
# Save changes button
save = QtWidgets.QPushButton(get_icon("save"), "", self)
@@ -473,6 +511,10 @@ def update(self):
is_session_layer = stage.GetSessionLayer() == layer
label_str = layer.GetDisplayName()
+ if not label_str:
+ # No display name is usually an anonymous layer without tag
+ label_str = "anonymousLayer" if layer.anonymous else "unknownLayer"
+
if layer.anonymous:
label_str = f"{label_str}" # make anonymous layers italic
if layer.dirty:
@@ -484,7 +526,7 @@ def update(self):
enabled.setChecked(not is_layer_muted)
if is_root_layer or is_session_layer:
enabled.setEnabled(False)
- save.setHidden(not layer.dirty)
+ save.setVisible(layer.dirty or layer.anonymous)
edit_target_btn.setEnabled(not is_layer_muted)
edit_target_btn.setChecked(stage.GetEditTarget() == layer)
@@ -515,9 +557,28 @@ def on_mute_layer(self, enabled):
self.stage.MuteLayer(self.layer.identifier)
def on_save_layer(self):
- layer = self.layer
- # TODO: Perform an actual save
- # TODO: Prompt for filepath if layer is anonymous?
+ layer: Sdf.Layer = self.layer
+
+ if layer.anonymous:
+ # We must choose where to save the layer
+ filename, _selected_filter = QtWidgets.QFileDialog.getSaveFileName(
+ parent=self,
+ caption="Save anonymous USD file",
+ filter="USD (*.usd *.usda *.usdc);"
+ )
+ if not filename:
+ return
+ anonymous_identifier = layer.identifier
+ layer.identifier = filename
+ layer.Save()
+
+ for layer in self.stage.GetLayerStack():
+ layer.UpdateCompositionAssetDependency(anonymous_identifier,
+ filename)
+ layer.UpdateAssetInfo() # re-resolve the layer
+ self.update()
+ return
+
# TODO: Allow making filepath relative to parent layer?
log.debug(f"Saving: {layer}")
layer.Save()
@@ -535,6 +596,7 @@ def __init__(self, stage, include_session_layer=False, parent=None):
view = QtWidgets.QTreeView()
view.setModel(model)
+ view.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
view.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop)
view.setDragDropOverwriteMode(False)
view.setColumnHidden(1, True)
@@ -574,15 +636,22 @@ def on_view_context_menu(self, point):
)
action.triggered.connect(partial(self.on_add_layer, index))
+ action = menu.addAction("Add anonymous layer")
+ action.setToolTip(
+ "Add a new anonymous sublayer under the selected parent layer."
+ )
+ action.triggered.connect(partial(self.on_add_anonymous_layer, index))
+
if layer:
action = menu.addAction("Reload")
- action.setToolTip(
- "Reloads the layer. This discards any unsaved local changes."
+ set_tips(
+ action,
+ "Reloads the layer.
"
+ "This discards any unsaved local changes.
"
+ "Reverts the layer to the file on disk (or empty if anonymous "
+ "layer.)"
)
- action.setStatusTip(
- "Reloads the layer. This discards any unsaved local changes."
- )
- action.triggered.connect(lambda: layer.Reload())
+ action.triggered.connect(self.on_reload_layers)
is_root_layer = layer == stage.GetRootLayer()
is_session_layer = layer == stage.GetSessionLayer()
@@ -594,7 +663,7 @@ def on_view_context_menu(self, point):
"Removes the layer from the layer stack. "
"Does not remove files from disk"
)
- action.triggered.connect(partial(self.on_remove_layer, index))
+ action.triggered.connect(self.on_remove_layers)
action = menu.addAction("Show as text")
action.setToolTip(
@@ -665,17 +734,31 @@ def on_set_edit_target(self, layer):
widget.edit_target.setChecked(layer == widget.layer)
widget.edit_target.blockSignals(False)
- def on_remove_layer(self, index):
- parent_index = self.model.parent(index)
-
- layer = index.data(LayerStackModel.LayerRole)
- parent_layer = parent_index.data(LayerStackModel.LayerRole)
- if not layer or not parent_layer:
- return
+ def on_remove_layers(self):
+
+ indexes = self.view.selectionModel().selectedIndexes()
+ for index in indexes:
+ parent_index = self.model.parent(index)
+ layer = index.data(LayerStackModel.LayerRole)
+ parent_layer = parent_index.data(LayerStackModel.LayerRole)
+ if not layer or not parent_layer:
+ return
+
+ removed_index = remove_sublayer(layer.identifier,
+ parent=parent_layer)
+ if removed_index is not None:
+ log.debug(f"Removed layer: {layer.identifier}")
+
+ def on_reload_layers(self):
- removed_index = remove_sublayer(layer.identifier, parent=parent_layer)
- if removed_index is not None:
- log.debug(f"Removed layer: {layer.identifier}")
+ indexes = self.view.selectionModel().selectedIndexes()
+ for index in indexes:
+ parent_index = self.model.parent(index)
+ layer = index.data(LayerStackModel.LayerRole)
+ if not layer:
+ continue
+
+ layer.Reload()
def on_add_layer(self, index):
layer = index.data(LayerStackModel.LayerRole)
@@ -697,6 +780,14 @@ def on_add_layer(self, index):
log.debug("Adding sublayer: %s", filename)
layer.subLayerPaths.append(filename)
+ def on_add_anonymous_layer(self, index):
+ layer = index.data(LayerStackModel.LayerRole)
+ if not layer:
+ return
+
+ anonymous_layer = Sdf.Layer.CreateAnonymous()
+ layer.subLayerPaths.append(anonymous_layer.identifier)
+
def showEvent(self, event):
self.model.register_listeners()
diff --git a/usd_qtpy/prim_spec_editor.py b/usd_qtpy/prim_spec_editor.py
index 856ac97..dae90cd 100644
--- a/usd_qtpy/prim_spec_editor.py
+++ b/usd_qtpy/prim_spec_editor.py
@@ -117,7 +117,7 @@ def refresh(self):
for layer in stage.GetLayerStack():
layer_item = Item({
- "name": layer.GetDisplayName(),
+ "name": layer.GetDisplayName() or layer.identifier,
"identifier": layer.identifier,
"specifier": None,
"type": layer.__class__.__name__
@@ -484,7 +484,8 @@ def on_context_menu(self, point):
stage = self.model._stage
for layer in stage.GetLayerStack():
- action = move_menu.addAction(layer.GetDisplayName())
+ label = layer.GetDisplayName() or layer.identifier
+ action = move_menu.addAction(label)
action.setData(layer)
def move_to(action):