updated asset creator to be able create and load in both silo and non silo projects

This commit is contained in:
iLLiCiTiT 2019-09-20 19:04:45 +02:00
parent 73de6b85d9
commit ce1d2e1e6e
3 changed files with 238 additions and 168 deletions

View file

@ -50,13 +50,13 @@ class Window(QtWidgets.QDialog):
input_outlink.setStyleSheet("background-color: #333333;")
checkbox_outlink = QtWidgets.QCheckBox("Use outlink")
# Parent
label_parent = QtWidgets.QLabel("Parent:")
label_parent = QtWidgets.QLabel("*Parent:")
input_parent = QtWidgets.QLineEdit()
input_parent.setReadOnly(True)
input_parent.setStyleSheet("background-color: #333333;")
# Name
label_name = QtWidgets.QLabel("Name:")
label_name = QtWidgets.QLabel("*Name:")
input_name = QtWidgets.QLineEdit()
input_name.setPlaceholderText("<asset name>")
@ -103,7 +103,7 @@ class Window(QtWidgets.QDialog):
task_view = QtWidgets.QTreeView()
task_view.setIndentation(0)
task_model = model.TasksTemplateModel()
task_model = model.TasksModel()
task_view.setModel(task_model)
info_layout.addWidget(inputs_widget)
@ -162,6 +162,7 @@ class Window(QtWidgets.QDialog):
# signals
btn_create_asset.clicked.connect(self.create_asset)
assets.selection_changed.connect(self.on_asset_changed)
input_name.textChanged.connect(self.on_asset_name_change)
checkbox_outlink.toggled.connect(self.on_outlink_checkbox_change)
combo_task_template.currentTextChanged.connect(
self.on_task_template_changed
@ -198,6 +199,8 @@ class Window(QtWidgets.QDialog):
schemas_items = config.get_presets().get('ftrack', {}).get(
'project_schemas', {}
)
# Get info if it is silo project
self.silos = io.distinct("silo")
key = "default"
if schema_name in schemas_items:
@ -374,9 +377,6 @@ class Window(QtWidgets.QDialog):
session.create('Task', task_data)
av_project = io.find_one({'type': 'project'})
silo = parent['silo']
if silo is None:
silo = parent['name']
hiearchy_items = []
hiearchy_items.extend(self.get_avalon_parent(parent))
@ -395,10 +395,14 @@ class Window(QtWidgets.QDialog):
'parent': av_project['_id'],
'name': name,
'schema': "avalon-core:asset-3.0",
'silo': silo,
'type': 'asset',
'data': new_asset_data
}
# Backwards compatibility (add silo from parent if is silo project)
if self.silos:
new_asset_info["silo"] = parent["silo"]
try:
schema.validate(new_asset_info)
except Exception:
@ -576,17 +580,35 @@ class Window(QtWidgets.QDialog):
assets_model = self.data["model"]["assets"]
parent_input = self.data['inputs']['parent']
selected = assets_model.get_selected_assets()
self.valid_parent = False
if len(selected) > 1:
self.valid_parent = False
parent_input.setText('< Please select only one asset! >')
elif len(selected) == 1:
self.valid_parent = True
asset = io.find_one({"_id": selected[0], "type": "asset"})
parent_input.setText(asset['name'])
if isinstance(selected[0], io.ObjectId):
self.valid_parent = True
asset = io.find_one({"_id": selected[0], "type": "asset"})
parent_input.setText(asset['name'])
else:
parent_input.setText('< Selected invalid parent(silo) >')
else:
self.valid_parent = False
parent_input.setText('< Nothing is selected >')
self.creatability_check()
def on_asset_name_change(self):
self.creatability_check()
def creatability_check(self):
name_input = self.data['inputs']['name']
name = str(name_input.text()).strip()
creatable = False
if name and self.valid_parent:
creatable = True
self.data["buttons"]["create_asset"].setEnabled(creatable)
def show(parent=None, debug=False, context=None):
"""Display Loader GUI

View file

@ -3,26 +3,26 @@ import logging
import collections
from avalon.vendor.Qt import QtCore, QtWidgets
from avalon.vendor import qtawesome as awesome
from avalon.vendor import qtawesome
from avalon import io
from avalon import style
log = logging.getLogger(__name__)
class Node(dict):
"""A node that can be represented in a tree view.
class Item(dict):
"""An item that can be represented in a tree view using `TreeModel`.
The node can store data just like a dictionary.
The item can store data just like a regular dictionary.
>>> data = {"name": "John", "score": 10}
>>> node = Node(data)
>>> assert node["name"] == "John"
>>> item = Item(data)
>>> assert item["name"] == "John"
"""
def __init__(self, data=None):
super(Node, self).__init__()
super(Item, self).__init__()
self._children = list()
self._parent = None
@ -51,36 +51,36 @@ class Node(dict):
def row(self):
"""
Returns:
int: Index of this node under parent"""
int: Index of this item under parent"""
if self._parent is not None:
siblings = self.parent().children()
return siblings.index(self)
def add_child(self, child):
"""Add a child to this node"""
"""Add a child to this item"""
child._parent = self
self._children.append(child)
class TreeModel(QtCore.QAbstractItemModel):
COLUMNS = list()
NodeRole = QtCore.Qt.UserRole + 1
Columns = list()
ItemRole = QtCore.Qt.UserRole + 1
def __init__(self, parent=None):
super(TreeModel, self).__init__(parent)
self._root_node = Node()
self._root_item = Item()
def rowCount(self, parent):
if parent.isValid():
node = parent.internalPointer()
item = parent.internalPointer()
else:
node = self._root_node
item = self._root_item
return node.childCount()
return item.childCount()
def columnCount(self, parent):
return len(self.COLUMNS)
return len(self.Columns)
def data(self, index, role):
@ -89,17 +89,17 @@ class TreeModel(QtCore.QAbstractItemModel):
if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
node = index.internalPointer()
item = index.internalPointer()
column = index.column()
key = self.COLUMNS[column]
return node.get(key, None)
key = self.Columns[column]
return item.get(key, None)
if role == self.NodeRole:
if role == self.ItemRole:
return index.internalPointer()
def setData(self, index, value, role=QtCore.Qt.EditRole):
"""Change the data on the nodes.
"""Change the data on the items.
Returns:
bool: Whether the edit was successful
@ -108,10 +108,10 @@ class TreeModel(QtCore.QAbstractItemModel):
if index.isValid():
if role == QtCore.Qt.EditRole:
node = index.internalPointer()
item = index.internalPointer()
column = index.column()
key = self.COLUMNS[column]
node[key] = value
key = self.Columns[column]
item[key] = value
# passing `list()` for PyQt5 (see PYSIDE-462)
self.dataChanged.emit(index, index, list())
@ -123,78 +123,96 @@ class TreeModel(QtCore.QAbstractItemModel):
def setColumns(self, keys):
assert isinstance(keys, (list, tuple))
self.COLUMNS = keys
self.Columns = keys
def headerData(self, section, orientation, role):
if role == QtCore.Qt.DisplayRole:
if section < len(self.COLUMNS):
return self.COLUMNS[section]
if section < len(self.Columns):
return self.Columns[section]
super(TreeModel, self).headerData(section, orientation, role)
def flags(self, index):
return (
QtCore.Qt.ItemIsEnabled |
QtCore.Qt.ItemIsSelectable
)
flags = QtCore.Qt.ItemIsEnabled
item = index.internalPointer()
if item.get("enabled", True):
flags |= QtCore.Qt.ItemIsSelectable
return flags
def parent(self, index):
node = index.internalPointer()
parent_node = node.parent()
item = index.internalPointer()
parent_item = item.parent()
# If it has no parents we return invalid
if parent_node == self._root_node or not parent_node:
if parent_item == self._root_item or not parent_item:
return QtCore.QModelIndex()
return self.createIndex(parent_node.row(), 0, parent_node)
return self.createIndex(parent_item.row(), 0, parent_item)
def index(self, row, column, parent):
"""Return index for row/column under parent"""
if not parent.isValid():
parentNode = self._root_node
parent_item = self._root_item
else:
parentNode = parent.internalPointer()
parent_item = parent.internalPointer()
childItem = parentNode.child(row)
if childItem:
return self.createIndex(row, column, childItem)
child_item = parent_item.child(row)
if child_item:
return self.createIndex(row, column, child_item)
else:
return QtCore.QModelIndex()
def add_child(self, node, parent=None):
def add_child(self, item, parent=None):
if parent is None:
parent = self._root_node
parent = self._root_item
parent.add_child(node)
parent.add_child(item)
def column_name(self, column):
"""Return column key by index"""
if column < len(self.COLUMNS):
return self.COLUMNS[column]
if column < len(self.Columns):
return self.Columns[column]
def clear(self):
self.beginResetModel()
self._root_node = Node()
self._root_item = Item()
self.endResetModel()
class TasksTemplateModel(TreeModel):
class TasksModel(TreeModel):
"""A model listing the tasks combined for a list of assets"""
COLUMNS = ["Tasks"]
Columns = ["Tasks"]
def __init__(self):
super(TasksTemplateModel, self).__init__()
self.selectable = False
super(TasksModel, self).__init__()
self._num_assets = 0
self._icons = {
"__default__": awesome.icon("fa.folder-o",
color=style.colors.default)
"__default__": qtawesome.icon("fa.male",
color=style.colors.default),
"__no_task__": qtawesome.icon("fa.exclamation-circle",
color=style.colors.mid)
}
self._get_task_icons()
def _get_task_icons(self):
# Get the project configured icons from database
project = io.find_one({"type": "project"})
tasks = project["config"].get("tasks", [])
for task in tasks:
icon_name = task.get("icon", None)
if icon_name:
icon = qtawesome.icon("fa.{}".format(icon_name),
color=style.colors.default)
self._icons[task["name"]] = icon
def set_tasks(self, tasks):
"""Set assets to track by their database id
@ -213,23 +231,28 @@ class TasksTemplateModel(TreeModel):
icon = self._icons["__default__"]
for task in tasks:
node = Node({
item = Item({
"Tasks": task,
"icon": icon
})
self.add_child(node)
self.add_child(item)
self.endResetModel()
def flags(self, index):
if self.selectable is False:
return QtCore.Qt.ItemIsEnabled
else:
return (
QtCore.Qt.ItemIsEnabled |
QtCore.Qt.ItemIsSelectable
)
return QtCore.Qt.ItemIsEnabled
def headerData(self, section, orientation, role):
# Override header for count column to show amount of assets
# it is listing the tasks for
if role == QtCore.Qt.DisplayRole:
if orientation == QtCore.Qt.Horizontal:
if section == 1: # count column
return "count ({0})".format(self._num_assets)
return super(TasksModel, self).headerData(section, orientation, role)
def data(self, index, role):
@ -239,9 +262,9 @@ class TasksTemplateModel(TreeModel):
# Add icon to the first column
if role == QtCore.Qt.DecorationRole:
if index.column() == 0:
return index.internalPointer()['icon']
return index.internalPointer()["icon"]
return super(TasksTemplateModel, self).data(index, role)
return super(TasksModel, self).data(index, role)
class DeselectableTreeView(QtWidgets.QTreeView):
@ -259,33 +282,6 @@ class DeselectableTreeView(QtWidgets.QTreeView):
QtWidgets.QTreeView.mousePressEvent(self, event)
class ExactMatchesFilterProxyModel(QtCore.QSortFilterProxyModel):
"""Filter model to where key column's value is in the filtered tags"""
def __init__(self, *args, **kwargs):
super(ExactMatchesFilterProxyModel, self).__init__(*args, **kwargs)
self._filters = set()
def setFilters(self, filters):
self._filters = set(filters)
def filterAcceptsRow(self, source_row, source_parent):
# No filter
if not self._filters:
return True
else:
model = self.sourceModel()
column = self.filterKeyColumn()
idx = model.index(source_row, column, source_parent)
data = model.data(idx, self.filterRole())
if data in self._filters:
return True
else:
return False
class RecursiveSortFilterProxyModel(QtCore.QSortFilterProxyModel):
"""Filters to the regex if any of the children matches allow parent"""
def filterAcceptsRow(self, row, parent):

View file

@ -1,14 +1,15 @@
import logging
import contextlib
import collections
from avalon.vendor import qtawesome as awesome
from avalon.vendor import qtawesome
from avalon.vendor.Qt import QtWidgets, QtCore, QtGui
from avalon import io
from avalon import style
from .model import (
TreeModel,
Node,
Item,
RecursiveSortFilterProxyModel,
DeselectableTreeView
)
@ -150,7 +151,7 @@ class AssetModel(TreeModel):
"""
COLUMNS = ["label"]
Columns = ["label"]
Name = 0
Deprecated = 2
ObjectId = 3
@ -162,50 +163,88 @@ class AssetModel(TreeModel):
super(AssetModel, self).__init__(parent=parent)
self.refresh()
def _add_hierarchy(self, parent=None):
def _add_hierarchy(self, assets, parent=None, silos=None):
"""Add the assets that are related to the parent as children items.
# Find the assets under the parent
find_data = {
"type": "asset"
}
if parent is None:
find_data['$or'] = [
{'data.visualParent': {'$exists': False}},
{'data.visualParent': None}
]
else:
find_data["data.visualParent"] = parent['_id']
This method does *not* query the database. These instead are queried
in a single batch upfront as an optimization to reduce database
queries. Resulting in up to 10x speed increase.
assets = io.find(find_data).sort('name', 1)
for asset in assets:
Args:
assets (dict): All assets in the currently active silo stored
by key/value
Returns:
None
"""
if silos:
# WARNING: Silo item "_id" is set to silo value
# mainly because GUI issue with perserve selection and expanded row
# and because of easier hierarchy parenting (in "assets")
for silo in silos:
item = Item({
"_id": silo,
"name": silo,
"label": silo,
"type": "silo"
})
self.add_child(item, parent=parent)
self._add_hierarchy(assets, parent=item)
parent_id = parent["_id"] if parent else None
current_assets = assets.get(parent_id, list())
for asset in current_assets:
# get label from data, otherwise use name
data = asset.get("data", {})
label = data.get("label", asset['name'])
label = data.get("label", asset["name"])
tags = data.get("tags", [])
# store for the asset for optimization
deprecated = "deprecated" in tags
node = Node({
"_id": asset['_id'],
item = Item({
"_id": asset["_id"],
"name": asset["name"],
"label": label,
"type": asset['type'],
"type": asset["type"],
"tags": ", ".join(tags),
"deprecated": deprecated,
"_document": asset
})
self.add_child(node, parent=parent)
self.add_child(item, parent=parent)
# Add asset's children recursively
self._add_hierarchy(node)
# Add asset's children recursively if it has children
if asset["_id"] in assets:
self._add_hierarchy(assets, parent=item)
def refresh(self):
"""Refresh the data for the model."""
self.clear()
self.beginResetModel()
self._add_hierarchy(parent=None)
# Get all assets in current silo sorted by name
db_assets = io.find({"type": "asset"}).sort("name", 1)
silos = db_assets.distinct("silo") or None
# Group the assets by their visual parent's id
assets_by_parent = collections.defaultdict(list)
for asset in db_assets:
parent_id = (
asset.get("data", {}).get("visualParent") or
asset.get("silo")
)
assets_by_parent[parent_id].append(asset)
# Build the hierarchical tree items recursively
self._add_hierarchy(
assets_by_parent,
parent=None,
silos=silos
)
self.endResetModel()
def flags(self, index):
@ -216,15 +255,17 @@ class AssetModel(TreeModel):
if not index.isValid():
return
node = index.internalPointer()
item = index.internalPointer()
if role == QtCore.Qt.DecorationRole: # icon
column = index.column()
if column == self.Name:
# Allow a custom icon and custom icon color to be defined
data = node["_document"]["data"]
data = item.get("_document", {}).get("data", {})
icon = data.get("icon", None)
if icon is None and item.get("type") == "silo":
icon = "database"
color = data.get("color", style.colors.default)
if icon is None:
@ -235,12 +276,12 @@ class AssetModel(TreeModel):
icon = "folder" if has_children else "folder-o"
# Make the color darker when the asset is deprecated
if node.get("deprecated", False):
if item.get("deprecated", False):
color = QtGui.QColor(color).darker(250)
try:
key = "fa.{0}".format(icon) # font-awesome key
icon = awesome.icon(key, color=color)
icon = qtawesome.icon(key, color=color)
return icon
except Exception as exception:
# Log an error message instead of erroring out completely
@ -250,32 +291,18 @@ class AssetModel(TreeModel):
return
if role == QtCore.Qt.ForegroundRole: # font color
if "deprecated" in node.get("tags", []):
if "deprecated" in item.get("tags", []):
return QtGui.QColor(style.colors.light).darker(250)
if role == self.ObjectIdRole:
return node.get("_id", None)
return item.get("_id", None)
if role == self.DocumentRole:
return node.get("_document", None)
return item.get("_document", None)
return super(AssetModel, self).data(index, role)
class AssetView(DeselectableTreeView):
"""Item view.
This implements a context menu.
"""
def __init__(self):
super(AssetView, self).__init__()
self.setIndentation(15)
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.setHeaderHidden(True)
class AssetWidget(QtWidgets.QWidget):
"""A Widget to display a tree of assets with filter
@ -286,7 +313,6 @@ class AssetWidget(QtWidgets.QWidget):
"""
silo_changed = QtCore.Signal(str) # on silo combobox change
assets_refreshed = QtCore.Signal() # on model refresh
selection_changed = QtCore.Signal() # on view selection change
current_changed = QtCore.Signal() # on view current index change
@ -300,17 +326,21 @@ class AssetWidget(QtWidgets.QWidget):
layout.setSpacing(4)
# Tree View
model = AssetModel()
model = AssetModel(self)
proxy = RecursiveSortFilterProxyModel()
proxy.setSourceModel(model)
proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
view = AssetView()
view = DeselectableTreeView()
view.setIndentation(15)
view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
view.setHeaderHidden(True)
view.setModel(proxy)
# Header
header = QtWidgets.QHBoxLayout()
icon = awesome.icon("fa.refresh", color=style.colors.light)
icon = qtawesome.icon("fa.refresh", color=style.colors.light)
refresh = QtWidgets.QPushButton(icon, "")
refresh.setToolTip("Refresh items")
@ -337,7 +367,14 @@ class AssetWidget(QtWidgets.QWidget):
self.view = view
def _refresh_model(self):
self.model.refresh()
with preserve_expanded_rows(
self.view, column=0, role=self.model.ObjectIdRole
):
with preserve_selection(
self.view, column=0, role=self.model.ObjectIdRole
):
self.model.refresh()
self.assets_refreshed.emit()
def refresh(self):
@ -346,7 +383,7 @@ class AssetWidget(QtWidgets.QWidget):
def get_active_asset(self):
"""Return the asset id the current asset."""
current = self.view.currentIndex()
return current.data(self.model.ObjectIdRole)
return current.data(self.model.ItemRole)
def get_active_index(self):
return self.view.currentIndex()
@ -357,7 +394,7 @@ class AssetWidget(QtWidgets.QWidget):
rows = selection.selectedRows()
return [row.data(self.model.ObjectIdRole) for row in rows]
def select_assets(self, assets, expand=True):
def select_assets(self, assets, expand=True, key="name"):
"""Select assets by name.
Args:
@ -370,8 +407,14 @@ class AssetWidget(QtWidgets.QWidget):
"""
# TODO: Instead of individual selection optimize for many assets
assert isinstance(assets,
(tuple, list)), "Assets must be list or tuple"
if not isinstance(assets, (tuple, list)):
assets = [assets]
assert isinstance(
assets, (tuple, list)
), "Assets must be list or tuple"
# convert to list - tuple cant be modified
assets = list(assets)
# Clear selection
selection_model = self.view.selectionModel()
@ -379,16 +422,25 @@ class AssetWidget(QtWidgets.QWidget):
# Select
mode = selection_model.Select | selection_model.Rows
for index in _iter_model_rows(self.proxy,
column=0,
include_root=False):
data = index.data(self.model.NodeRole)
name = data['name']
if name in assets:
selection_model.select(index, mode)
for index in iter_model_rows(
self.proxy, column=0, include_root=False
):
# stop iteration if there are no assets to process
if not assets:
break
if expand:
self.view.expand(index)
value = index.data(self.model.ItemRole).get(key)
if value not in assets:
continue
# Set the currently active index
self.view.setCurrentIndex(index)
# Remove processed asset
assets.pop(assets.index(value))
selection_model.select(index, mode)
if expand:
# Expand parent index
self.view.expand(self.proxy.parent(index))
# Set the currently active index
self.view.setCurrentIndex(index)