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):