Skip to content

Commit

Permalink
Merge pull request #13 from BigRoy/enhancement/layer_editor_add_remov…
Browse files Browse the repository at this point in the history
…e_layers

Enhancement: Implement Layer Editor add remove layers
  • Loading branch information
BigRoy authored Nov 24, 2023
2 parents fc73db6 + 72164f9 commit 8f30e20
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 40 deletions.
177 changes: 140 additions & 37 deletions usd_qtpy/layer_editor.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@


import os
import contextlib
import os.path
import logging
from functools import partial

from qtpy import QtWidgets, QtCore, QtGui

Expand All @@ -15,17 +14,56 @@
log = logging.getLogger(__name__)


def remove_sublayer(
identifier, parent
):
"""Remove a matching identifier as sublayer from parent layer
The `identifier` may be the full path `layer.identifier` but can also
be the relative anchored sublayer path (the actual value in the usd file).
Hence, the sublayer paths *may* be relative paths even though a layer's
identifier passed in may be the full path.
Arguments:
identifier (str): The layer identifier to remove; this may be the
anchored relative identifier in
parent (Sdf.Layer): The parent Sdf.Layer or layer
identifier to remove the child identifier for.
Returns:
Optional[int]: Returns an integer for the removed sublayer index
if a removal occurred, otherwise returns None
"""
absolute_identifier = parent.ComputeAbsolutePath(identifier)
for i, path in enumerate(parent.subLayerPaths):
if (
path == identifier
# Allow anchored relative paths to match the full identifier
or parent.ComputeAbsolutePath(path) == absolute_identifier
):
del parent.subLayerPaths[i]
return i


def set_tips(widget, tip):
widget.setStatusTip(tip)
widget.setToolTip(tip)


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

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):
super(LayerItem, self).__init__(key=layer.identifier)
super(LayerItem, self).__init__(key=key)
self.layer = layer
self.parent_layer = parent_layer


class LayerStackModel(AbstractTreeModelMixin, QtCore.QAbstractItemModel):
Expand Down Expand Up @@ -87,7 +125,7 @@ def flags(self, index: QtCore.QModelIndex) -> QtCore.Qt.ItemFlags:
)

def supportedDropActions(self):
return QtCore.Qt.MoveAction
return QtCore.Qt.MoveAction | QtCore.Qt.CopyAction

def mimeData(self, indexes):
mimedata = QtCore.QMimeData()
Expand Down Expand Up @@ -116,15 +154,54 @@ def mimeData(self, indexes):
def dropMimeData(self, data, action, row, column, parent):
if action == QtCore.Qt.IgnoreAction:
return True
if not data.hasFormat("text/plain"):
return False
if column > 0:
return False

new_parent_layer = parent.data(self.LayerRole)
if not new_parent_layer:
raise RuntimeError(
"Can't drop on index that does not refer to a layer"
)

# If urls are in the data we consider only those. These are usually
# URLs from file drops from e.g. OS file explorer or alike.
if data.hasUrls():
for url in reversed(data.urls()):
path = url.toLocalFile()
if not path:
continue

if not os.path.isfile(path):
# Ignore dropped folders
continue

# We first try to find or open the layer so see if it's a valid
# file format that way
try:
Sdf.Layer.FindOrOpen(path)
except Tf.ErrorException as exc:
log.error("Unable to drop unsupported file: %s",
path,
exc_info=exc)
continue

if row == -1:
# Dropped on parent
new_parent_layer.subLayerPaths.append(path)
else:
# Dropped in-between other layers
new_parent_layer.subLayerPaths.insert(row, path)
return True

if not data.hasFormat("text/plain"):
return False

# Consider plain text data second
# TODO: This is likely better represented as a custom byte stream
# and as internal mimetype data to the model
value = data.text()
# Parse the text data
separator = "<----"

sources = []
for line in value.split("\n"):
if separator not in line:
Expand All @@ -142,25 +219,15 @@ def dropMimeData(self, data, action, row, column, parent):
with Sdf.ChangeBlock():
for source_identifier, source_parent_identifier in sources:

removed_index = -1
removed_index = None
source_parent_layer = None
if source_parent_identifier:
self.log.debug("Removing old: %s -> %s",
source_parent_identifier, source_identifier)
source_parent_layer = Sdf.Find(source_parent_identifier)
removed_index = remove_sublayer(
source_identifier,
parent=source_parent_layer
)

# The sublayer paths *may* be relative paths even though a
# layer's identifier may be the full path. As such, we just
# compare whether all resolved layers are actually the
# same layer identifier or not
for i, path in enumerate(source_parent_layer.subLayerPaths):
path = source_parent_layer.ComputeAbsolutePath(path)
if path == source_identifier:
removed_index = i
del source_parent_layer.subLayerPaths[i]
break

new_parent_layer = parent.data(self.LayerRole)
if row < 0 and column < 0:
# Dropped on parent, add dropped layer as child
new_parent_layer.subLayerPaths.append(source_identifier)
Expand All @@ -173,7 +240,7 @@ def dropMimeData(self, data, action, row, column, parent):
if (
source_parent_layer
and source_parent_layer.identifier == new_parent_layer.identifier # noqa
and removed_index != -1 and row >= removed_index
and removed_index is not None and row >= removed_index
):
row -= 1

Expand All @@ -182,7 +249,7 @@ def dropMimeData(self, data, action, row, column, parent):
return True

def mimeTypes(self):
return ["text/plain"]
return ["text/plain", "text/uri-list"]

def canDropMimeData(self, data, action, row, column, parent) -> bool:

Expand All @@ -197,7 +264,7 @@ def canDropMimeData(self, data, action, row, column, parent) -> bool:
column,
parent)

# endregion
# endregion

# region Custom methods
def layer_count(self):
Expand Down Expand Up @@ -283,7 +350,8 @@ def refresh(self):
return

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

for sublayer_path in layer.subLayerPaths:
Expand Down Expand Up @@ -311,6 +379,7 @@ def on_layers_changed(self, notice, sender):
# rebuilding the layer list then at all actually. But we need the
# signal otherwise we can't detect layers added/removed to begin with.
schedule(self.refresh, 50, channel="layerschanged")
# endregion


class LayerWidget(QtWidgets.QWidget):
Expand Down Expand Up @@ -444,8 +513,8 @@ def on_save_layer(self):
# TODO: Perform an actual save
# TODO: Prompt for filepath if layer is anonymous?
# TODO: Allow making filepath relative to parent layer?
print(f"Saving: {layer}")
print(layer.ExportToString())
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 All @@ -461,7 +530,7 @@ def __init__(self, stage, include_session_layer=False, parent=None):
view = QtWidgets.QTreeView()
view.setModel(model)

view.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove)
view.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop)
view.setDragDropOverwriteMode(False)
view.setColumnHidden(1, True)
view.setHeaderHidden(True)
Expand All @@ -486,9 +555,9 @@ def __init__(self, stage, include_session_layer=False, parent=None):
def on_view_context_menu(self, point):
"""Generate a right mouse click context menu for the layer view"""

point_index = self.view.indexAt(point)
index = self.view.indexAt(point)
stage = self.model._stage
layer = point_index.data(self.model.LayerRole)
layer = index.data(self.model.LayerRole)
if not layer:
layer = stage.GetRootLayer()

Expand All @@ -498,6 +567,7 @@ def on_view_context_menu(self, point):
action.setToolTip(
"Add a new sublayer under the selected parent layer."
)
action.triggered.connect(partial(self.on_add_layer, index))

if layer:
action = menu.addAction("Reload")
Expand All @@ -519,21 +589,22 @@ 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 = menu.addAction("Show as text")
action.setToolTip(
"Shows the layer as USD ASCII"
)

def show_layer():
def show_layer_as_text():
text_edit = QtWidgets.QTextEdit(parent=self)
text_edit.setPlainText(layer.ExportToString())
text_edit.setWindowTitle(layer.identifier)
text_edit.setWindowFlags(QtCore.Qt.Dialog)
text_edit.resize(700, 500)
text_edit.show()

action.triggered.connect(show_layer)
action.triggered.connect(show_layer_as_text)

menu.exec_(self.view.mapToGlobal(point))

Expand All @@ -546,7 +617,7 @@ def refresh_widgets(self):
for row in iter_model_rows(self.model, column=0):
layer = row.data(self.model.LayerRole)
if layer is None:
print(f"Layer is None for {row}")
log.warning(f"Layer is None for %s", row)
continue
widget = LayerWidget(layer=layer,
stage=self.model._stage,
Expand All @@ -570,6 +641,38 @@ def on_set_edit_target(self, layer):
widget.edit_target.blockSignals(True)
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

removed_index = remove_sublayer(layer.identifier, parent=parent_layer)
if removed_index is not None:
log.debug(f"Removed layer: {layer.identifier}")

def on_add_layer(self, index):
layer = index.data(LayerStackModel.LayerRole)
if not layer:
return

filenames, _selected_filter = QtWidgets.QFileDialog.getOpenFileNames(
parent=self,
caption="Sublayer USD file",
filter="USD (*.usd *.usda *.usdc);"
)
if not filenames:
return

# TODO: Anchor path relative to the layer?
# TODO: Should we first confirm none of the layers is already a child
# or just let it error once it hits one matching path?
for filename in filenames:
log.debug("Adding sublayer: %s", filename)
layer.subLayerPaths.append(filename)

def hideEvent(self, event: QtGui.QCloseEvent) -> None:
# TODO: This should be on a better event when we know the window
Expand Down
2 changes: 1 addition & 1 deletion usd_qtpy/prim_spec_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ def on_delete(self):

with Sdf.ChangeBlock():
for spec in specs:
print(f"Removing spec: {spec.path}")
log.debug(f"Removing spec: %s", spec.path)
remove_spec(spec)

if not self._listeners:
Expand Down
2 changes: 0 additions & 2 deletions usd_qtpy/viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -472,8 +472,6 @@ def keyPressEvent(self, event):
# Implement some shortcuts for the widget
# todo: move this code

print(event)

key = event.key()
# TODO: Add CTRL + R for "quick render or playblast"
if key == QtCore.Qt.Key_Space:
Expand Down

0 comments on commit 8f30e20

Please sign in to comment.