import os
import sys
import datetime
import pprint
import traceback
import collections
from qtpy import QtWidgets, QtCore, QtGui
from openpype.client import (
get_subset_families,
get_subset_by_id,
get_subsets,
get_version_by_id,
get_versions,
get_representations,
get_thumbnail_id_from_source,
get_thumbnail,
)
from openpype.client.operations import OperationsSession, REMOVED_VALUE
from openpype.pipeline import HeroVersionType, Anatomy
from openpype.pipeline.thumbnail import get_thumbnail_binary
from openpype.pipeline.load import (
discover_loader_plugins,
SubsetLoaderPlugin,
loaders_from_repre_context,
get_repres_contexts,
get_subset_contexts,
load_with_repre_context,
load_with_subset_context,
load_with_subset_contexts,
LoadError,
IncompatibleLoaderError,
)
from openpype.tools.utils import (
ErrorMessageBox,
lib as tools_lib
)
from openpype.tools.utils.lib import checkstate_int_to_enum
from openpype.tools.utils.delegates import (
VersionDelegate,
PrettyTimeDelegate
)
from openpype.tools.utils.widgets import (
OptionalMenu,
PlaceholderLineEdit
)
from openpype.tools.utils.views import (
TreeViewSpinner,
DeselectableTreeView
)
from openpype.tools.utils.constants import (
LOCAL_PROVIDER_ROLE,
REMOTE_PROVIDER_ROLE,
LOCAL_AVAILABILITY_ROLE,
REMOTE_AVAILABILITY_ROLE,
)
from openpype.tools.assetlinks.widgets import SimpleLinkView
from .model import (
SubsetsModel,
SubsetFilterProxyModel,
FamiliesFilterProxyModel,
RepresentationModel,
RepresentationSortProxyModel,
ITEM_ID_ROLE
)
from . import lib
from .delegates import LoadedInSceneDelegate
class OverlayFrame(QtWidgets.QFrame):
def __init__(self, label, parent):
super(OverlayFrame, self).__init__(parent)
label_widget = QtWidgets.QLabel(label, self)
label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
main_layout = QtWidgets.QVBoxLayout(self)
main_layout.addWidget(label_widget, 1, QtCore.Qt.AlignCenter)
self.label_widget = label_widget
self.setStyleSheet((
"background: rgba(0, 0, 0, 127);"
"font-size: 60pt;"
))
def set_label(self, label):
self.label_widget.setText(label)
class LoadErrorMessageBox(ErrorMessageBox):
def __init__(self, messages, parent=None):
self._messages = messages
super(LoadErrorMessageBox, self).__init__("Loading failed", parent)
def _create_top_widget(self, parent_widget):
label_widget = QtWidgets.QLabel(parent_widget)
label_widget.setText(
"Failed to load items"
)
return label_widget
def _get_report_data(self):
report_data = []
for exc_msg, tb_text, repre, subset, version in self._messages:
report_message = (
"During load error happened on Subset: \"{subset}\""
" Representation: \"{repre}\" Version: {version}"
"\n\nError message: {message}"
).format(
subset=subset,
repre=repre,
version=version,
message=exc_msg
)
if tb_text:
report_message += "\n\n{}".format(tb_text)
report_data.append(report_message)
return report_data
def _create_content(self, content_layout):
item_name_template = (
"Subset: {}
"
"Version: {}
"
"Representation: {}
"
)
exc_msg_template = "{}"
for exc_msg, tb_text, repre, subset, version in self._messages:
line = self._create_line()
content_layout.addWidget(line)
item_name = item_name_template.format(subset, version, repre)
item_name_widget = QtWidgets.QLabel(
item_name.replace("\n", "
"), self
)
item_name_widget.setWordWrap(True)
content_layout.addWidget(item_name_widget)
exc_msg = exc_msg_template.format(exc_msg.replace("\n", "
"))
message_label_widget = QtWidgets.QLabel(exc_msg, self)
message_label_widget.setWordWrap(True)
content_layout.addWidget(message_label_widget)
if tb_text:
line = self._create_line()
tb_widget = self._create_traceback_widget(tb_text, self)
content_layout.addWidget(line)
content_layout.addWidget(tb_widget)
class SubsetWidget(QtWidgets.QWidget):
"""A widget that lists the published subsets for an asset"""
active_changed = QtCore.Signal() # active index changed
version_changed = QtCore.Signal() # version state changed for a subset
load_started = QtCore.Signal()
load_ended = QtCore.Signal()
refreshed = QtCore.Signal(bool)
default_widths = (
("subset", 200),
("asset", 130),
("family", 90),
("version", 60),
("time", 125),
("author", 75),
("frames", 75),
("duration", 60),
("handles", 55),
("step", 10),
("loaded_in_scene", 25),
("repre_info", 65)
)
def __init__(
self,
dbcon,
groups_config,
family_config_cache,
enable_grouping=True,
tool_name=None,
parent=None
):
super(SubsetWidget, self).__init__(parent=parent)
self.dbcon = dbcon
self.tool_name = tool_name
model = SubsetsModel(
dbcon,
groups_config,
family_config_cache,
grouping=enable_grouping
)
proxy = SubsetFilterProxyModel()
proxy.setSourceModel(model)
proxy.setDynamicSortFilter(True)
proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
family_proxy = FamiliesFilterProxyModel()
family_proxy.setSourceModel(proxy)
subset_filter = PlaceholderLineEdit(self)
subset_filter.setPlaceholderText("Filter subsets..")
group_checkbox = QtWidgets.QCheckBox("Enable Grouping", self)
group_checkbox.setChecked(enable_grouping)
top_bar_layout = QtWidgets.QHBoxLayout()
top_bar_layout.addWidget(subset_filter)
top_bar_layout.addWidget(group_checkbox)
view = TreeViewSpinner(self)
view.setModel(family_proxy)
view.setObjectName("SubsetView")
view.setIndentation(20)
view.setAllColumnsShowFocus(True)
view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
view.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
view.setSortingEnabled(True)
view.sortByColumn(1, QtCore.Qt.AscendingOrder)
view.setAlternatingRowColors(True)
# Set view delegates
version_delegate = VersionDelegate(self.dbcon, view)
column = model.Columns.index("version")
view.setItemDelegateForColumn(column, version_delegate)
time_delegate = PrettyTimeDelegate(view)
column = model.Columns.index("time")
view.setItemDelegateForColumn(column, time_delegate)
avail_delegate = AvailabilityDelegate(self.dbcon, view)
column = model.Columns.index("repre_info")
view.setItemDelegateForColumn(column, avail_delegate)
loaded_in_scene_delegate = LoadedInSceneDelegate(view)
column = model.Columns.index("loaded_in_scene")
view.setItemDelegateForColumn(column, loaded_in_scene_delegate)
layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addLayout(top_bar_layout)
layout.addWidget(view)
# settings and connections
for column_name, width in self.default_widths:
idx = model.Columns.index(column_name)
view.setColumnWidth(idx, width)
self.model = model
self.view = view
self.on_project_change(dbcon.current_project())
view.customContextMenuRequested.connect(self.on_context_menu)
selection = view.selectionModel()
selection.selectionChanged.connect(self.active_changed)
version_delegate.version_changed.connect(self.version_changed)
group_checkbox.stateChanged.connect(self.set_grouping)
subset_filter.textChanged.connect(self._subset_changed)
model.refreshed.connect(self.refreshed)
self.proxy = proxy
self.family_proxy = family_proxy
self._subset_filter = subset_filter
self._group_checkbox = group_checkbox
self._version_delegate = version_delegate
self._time_delegate = time_delegate
self.model.refresh()
def get_subsets_families(self):
return self.model.get_subsets_families()
def set_family_filters(self, families):
self.family_proxy.setFamiliesFilter(families)
def is_groupable(self):
return self._group_checkbox.isChecked()
def set_grouping(self, state):
with tools_lib.preserve_selection(tree_view=self.view,
current_index=False):
self.model.set_grouping(state)
def _subset_changed(self, text):
if hasattr(self.proxy, "setFilterRegExp"):
self.proxy.setFilterRegExp(text)
else:
self.proxy.setFilterRegularExpression(text)
self.view.expandAll()
def set_loading_state(self, loading, empty):
view = self.view
if view.is_loading != loading:
if loading:
view.spinner.repaintNeeded.connect(view.viewport().update)
else:
view.spinner.repaintNeeded.disconnect()
view.is_loading = loading
view.is_empty = empty
def _repre_contexts_for_loaders_filter(self, items):
version_docs_by_id = {
item["version_document"]["_id"]: item["version_document"]
for item in items
}
version_docs_by_subset_id = collections.defaultdict(list)
for item in items:
subset_id = item["version_document"]["parent"]
version_docs_by_subset_id[subset_id].append(
item["version_document"]
)
project_name = self.dbcon.active_project()
subset_docs = list(get_subsets(
project_name,
subset_ids=version_docs_by_subset_id.keys(),
fields=["schema", "data.families"]
))
subset_docs_by_id = {
subset_doc["_id"]: subset_doc
for subset_doc in subset_docs
}
version_ids = list(version_docs_by_id.keys())
repre_docs = get_representations(
project_name,
version_ids=version_ids,
fields=["name", "parent", "data", "context"]
)
repre_docs_by_version_id = {
version_id: []
for version_id in version_ids
}
repre_context_by_id = {}
for repre_doc in repre_docs:
version_id = repre_doc["parent"]
repre_docs_by_version_id[version_id].append(repre_doc)
version_doc = version_docs_by_id[version_id]
repre_context_by_id[repre_doc["_id"]] = {
"representation": repre_doc,
"version": version_doc,
"subset": subset_docs_by_id[version_doc["parent"]]
}
return repre_context_by_id, repre_docs_by_version_id
def on_project_change(self, project_name):
"""
Called on each project change in parent widget.
Checks if Sync Server is enabled for a project, pushes changes to
model.
"""
enabled = False
if project_name:
self.model.reset_sync_server(project_name)
sync_server = self.model.sync_server
if sync_server:
enabled = sync_server.is_project_enabled(project_name,
single=True)
lib.change_visibility(self.model, self.view, "repre_info", enabled)
def get_selected_items(self):
selection_model = self.view.selectionModel()
indexes = selection_model.selectedIndexes()
item_ids = set()
for index in indexes:
item_id = index.data(ITEM_ID_ROLE)
if item_id is not None:
item_ids.add(item_id)
output = []
for item_id in item_ids:
item = self.model.get_item_by_id(item_id)
if item is not None:
output.append(item)
return output
def get_selected_merge_items(self):
output = []
items = collections.deque(self.get_selected_items())
item_ids = set()
while items:
item = items.popleft()
if item.get("isGroup"):
for child in item.children():
items.appendleft(child)
elif item.get("isMerged"):
item_id = item["id"]
if item_id not in item_ids:
item_ids.add(item_id)
output.append(item)
return output
def get_selected_subsets(self):
output = []
items = collections.deque(self.get_selected_items())
item_ids = set()
while items:
item = items.popleft()
if item.get("isGroup") or item.get("isMerged"):
for child in item.children():
items.appendleft(child)
else:
item_id = item["id"]
if item_id not in item_ids:
item_ids.add(item_id)
output.append(item)
return output
def on_context_menu(self, point):
"""Shows menu with loader actions on Right-click.
Registered actions are filtered by selection and help of
`loaders_from_representation` from avalon api. Intersection of actions
is shown when more subset is selected. When there are not available
actions for selected subsets then special action is shown (works as
info message to user): "*No compatible loaders for your selection"
"""
point_index = self.view.indexAt(point)
if not point_index.isValid():
return
# Get selected subsets without groups
items = self.get_selected_subsets()
# Get all representation->loader combinations available for the
# index under the cursor, so we can list the user the options.
project_name = self.dbcon.active_project()
available_loaders = discover_loader_plugins(project_name)
if self.tool_name:
available_loaders = lib.remove_tool_name_from_loaders(
available_loaders, self.tool_name
)
repre_loaders = []
subset_loaders = []
for loader in available_loaders:
if not loader.enabled:
continue
# Skip if its a SubsetLoader.
if issubclass(loader, SubsetLoaderPlugin):
subset_loaders.append(loader)
else:
repre_loaders.append(loader)
loaders = list()
# Bool if is selected only one subset
one_item_selected = (len(items) == 1)
# Prepare variables for multiple selected subsets
first_loaders = []
found_combinations = None
is_first = True
repre_context_by_id, repre_docs_by_version_id = (
self._repre_contexts_for_loaders_filter(items)
)
for item in items:
_found_combinations = []
version_id = item["version_document"]["_id"]
repre_docs = repre_docs_by_version_id[version_id]
for repre_doc in repre_docs:
repre_context = repre_context_by_id[repre_doc["_id"]]
for loader in loaders_from_repre_context(
repre_loaders,
repre_context
):
# do not allow download whole repre, select specific repre
if tools_lib.is_sync_loader(loader):
continue
# skip multiple select variant if one is selected
if one_item_selected:
loaders.append((repre_doc, loader))
continue
# store loaders of first subset
if is_first:
first_loaders.append((repre_doc, loader))
# store combinations to compare with other subsets
_found_combinations.append(
(repre_doc["name"].lower(), loader)
)
# skip multiple select variant if one is selected
if one_item_selected:
continue
is_first = False
# Store first combinations to compare
if found_combinations is None:
found_combinations = _found_combinations
# Intersect found combinations with all previous subsets
else:
found_combinations = list(
set(found_combinations) & set(_found_combinations)
)
if not one_item_selected:
# Filter loaders from first subset by intersected combinations
for repre, loader in first_loaders:
if (repre["name"].lower(), loader) not in found_combinations:
continue
loaders.append((repre, loader))
# Subset Loaders.
for loader in subset_loaders:
loaders.append((None, loader))
loaders = lib.sort_loaders(loaders)
# Prepare menu content based on selected items
menu = OptionalMenu(self)
if not loaders:
action = lib.get_no_loader_action(menu, one_item_selected)
menu.addAction(action)
else:
repre_contexts = get_repres_contexts(
repre_context_by_id.keys(), self.dbcon)
menu = lib.add_representation_loaders_to_menu(
loaders, menu, repre_contexts)
# Show the context action menu
global_point = self.view.mapToGlobal(point)
action = menu.exec_(global_point)
if not action or not action.data():
return
# Find the representation name and loader to trigger
action_representation, loader = action.data()
self.load_started.emit()
if issubclass(loader, SubsetLoaderPlugin):
subset_ids = []
subset_version_docs = {}
for item in items:
subset_id = item["version_document"]["parent"]
subset_ids.append(subset_id)
subset_version_docs[subset_id] = item["version_document"]
# get contexts only for selected menu option
subset_contexts_by_id = get_subset_contexts(subset_ids, self.dbcon)
subset_contexts = list(subset_contexts_by_id.values())
options = lib.get_options(action, loader, self, subset_contexts)
error_info = _load_subsets_by_loader(
loader, subset_contexts, options, subset_version_docs
)
else:
representation_name = action_representation["name"]
# Run the loader for all selected indices, for those that have the
# same representation available
# Trigger
project_name = self.dbcon.active_project()
subset_name_by_version_id = dict()
for item in items:
version_id = item["version_document"]["_id"]
subset_name_by_version_id[version_id] = item["subset"]
version_ids = set(subset_name_by_version_id.keys())
repre_docs = get_representations(
project_name,
representation_names=[representation_name],
version_ids=version_ids,
fields=["_id", "parent"]
)
repre_ids = []
for repre_doc in repre_docs:
repre_ids.append(repre_doc["_id"])
# keep only version ids without representation with that name
version_id = repre_doc["parent"]
version_ids.discard(version_id)
if version_ids:
# report versions that didn't have valid representation
joined_subset_names = ", ".join([
'"{}"'.format(subset_name_by_version_id[version_id])
for version_id in version_ids
])
self.echo("Subsets {} don't have representation '{}'".format(
joined_subset_names, representation_name
))
# get contexts only for selected menu option
repre_contexts = get_repres_contexts(repre_ids, self.dbcon)
options = lib.get_options(
action, loader, self, list(repre_contexts.values())
)
error_info = _load_representations_by_loader(
loader, repre_contexts, options=options
)
self.load_ended.emit()
if error_info:
box = LoadErrorMessageBox(error_info, self)
box.show()
def group_subsets(self, name, asset_ids, items):
subset_ids = {
item["_id"]
for item in items
if item.get("_id")
}
if not subset_ids:
return
if name:
self.echo("Group subsets to '%s'.." % name)
else:
self.echo("Ungroup subsets..")
project_name = self.dbcon.active_project()
op_session = OperationsSession()
for subset_id in subset_ids:
op_session.update_entity(
project_name,
"subset",
subset_id,
{"data.subsetGroup": name or REMOVED_VALUE}
)
op_session.commit()
def echo(self, message):
print(message)
class VersionTextEdit(QtWidgets.QTextEdit):
"""QTextEdit that displays version specific information.
This also overrides the context menu to add actions like copying
source path to clipboard or copying the raw data of the version
to clipboard.
"""
def __init__(self, dbcon, parent=None):
super(VersionTextEdit, self).__init__(parent=parent)
self.dbcon = dbcon
self.data = {
"source": None,
"raw": None
}
self._anatomy = None
# Reset
self.set_version(None)
def set_version(self, version_doc=None, version_id=None):
# TODO expect only filling data (do not query them here!)
if not version_doc and not version_id:
# Reset state to empty
self.data = {
"source": None,
"raw": None,
}
self.setText("")
self.setEnabled(True)
return
self.setEnabled(True)
print("Querying..")
project_name = self.dbcon.active_project()
if not version_doc:
version_doc = get_version_by_id(project_name, version_id)
assert version_doc, "Not a valid version id"
if version_doc["type"] == "hero_version":
_version_doc = get_version_by_id(
project_name, version_doc["version_id"]
)
version_doc["data"] = _version_doc["data"]
version_doc["name"] = HeroVersionType(
_version_doc["name"]
)
subset = get_subset_by_id(project_name, version_doc["parent"])
assert subset, "No valid subset parent for version"
# Define readable creation timestamp
created = version_doc["data"]["time"]
created = datetime.datetime.strptime(created, "%Y%m%dT%H%M%SZ")
created = datetime.datetime.strftime(created, "%b %d %Y %H:%M")
comment = version_doc["data"].get("comment", None) or "No comment"
source = version_doc["data"].get("source", None)
source_label = source if source else "No source"
# Store source and raw data
self.data["source"] = source
self.data["raw"] = version_doc
if version_doc["type"] == "hero_version":
version_name = "hero"
else:
version_name = tools_lib.format_version(version_doc["name"])
data = {
"subset": subset["name"],
"version": version_name,
"comment": comment,
"created": created,
"source": source_label
}
self.setHtml((
"