From 3db98b88d67b115fdb678fc6706f6adc464303ab Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Sep 2019 10:50:42 +0200 Subject: [PATCH] updated standalone publisher with changes in avalon-core --- pype/standalonepublish/app.py | 13 ++- pype/standalonepublish/widgets/__init__.py | 5 +- pype/standalonepublish/widgets/model_asset.py | 101 ++++++++++++------ .../widgets/model_tasks_template.py | 4 +- .../standalonepublish/widgets/widget_asset.py | 96 ++++++++++------- .../widgets/widget_asset_view.py | 16 --- .../widgets/widget_family.py | 23 ++-- .../widgets/widget_family_desc.py | 4 +- 8 files changed, 155 insertions(+), 107 deletions(-) delete mode 100644 pype/standalonepublish/widgets/widget_asset_view.py diff --git a/pype/standalonepublish/app.py b/pype/standalonepublish/app.py index da5fbbba10..37d4b1cd27 100644 --- a/pype/standalonepublish/app.py +++ b/pype/standalonepublish/app.py @@ -2,6 +2,7 @@ import os import sys import json from subprocess import Popen +from bson.objectid import ObjectId from pype import lib as pypelib from avalon.vendor.Qt import QtWidgets, QtCore from avalon import api, style, schema @@ -40,13 +41,13 @@ class Window(QtWidgets.QDialog): self.valid_parent = False # assets widget - widget_assets = AssetWidget(self) + widget_assets = AssetWidget(dbcon=self._db, parent=self) # family widget - widget_family = FamilyWidget(self) + widget_family = FamilyWidget(dbcon=self._db, parent=self) # components widget - widget_components = ComponentsWidget(self) + widget_components = ComponentsWidget(parent=self) # Body body = QtWidgets.QSplitter() @@ -70,6 +71,7 @@ class Window(QtWidgets.QDialog): # signals widget_assets.selection_changed.connect(self.on_asset_changed) + widget_family.stateChanged.connect(self.set_valid_family) self.widget_assets = widget_assets self.widget_family = widget_family @@ -123,7 +125,10 @@ class Window(QtWidgets.QDialog): Updates the task view. ''' - selected = self.widget_assets.get_selected_assets() + selected = [ + asset_id for asset_id in self.widget_assets.get_selected_assets() + if isinstance(asset_id, ObjectId) + ] if len(selected) == 1: self.valid_parent = True asset = self.db.find_one({"_id": selected[0], "type": "asset"}) diff --git a/pype/standalonepublish/widgets/__init__.py b/pype/standalonepublish/widgets/__init__.py index 4c6a0e85a5..ae74be029b 100644 --- a/pype/standalonepublish/widgets/__init__.py +++ b/pype/standalonepublish/widgets/__init__.py @@ -1,5 +1,5 @@ from avalon.vendor.Qt import * -from avalon.vendor import qtawesome as awesome +from avalon.vendor import qtawesome from avalon import style HelpRole = QtCore.Qt.UserRole + 2 @@ -12,13 +12,12 @@ from .button_from_svgs import SvgResizable, SvgButton from .model_node import Node from .model_tree import TreeModel -from .model_asset import AssetModel +from .model_asset import AssetModel, _iter_model_rows from .model_filter_proxy_exact_match import ExactMatchesFilterProxyModel from .model_filter_proxy_recursive_sort import RecursiveSortFilterProxyModel from .model_tasks_template import TasksTemplateModel from .model_tree_view_deselectable import DeselectableTreeView -from .widget_asset_view import AssetView from .widget_asset import AssetWidget from .widget_family_desc import FamilyDescriptionWidget diff --git a/pype/standalonepublish/widgets/model_asset.py b/pype/standalonepublish/widgets/model_asset.py index fdf844342e..6bea35ebd7 100644 --- a/pype/standalonepublish/widgets/model_asset.py +++ b/pype/standalonepublish/widgets/model_asset.py @@ -1,7 +1,8 @@ import logging +import collections from . import QtCore, QtGui from . import TreeModel, Node -from . import style, awesome +from . import style, qtawesome log = logging.getLogger(__name__) @@ -44,64 +45,102 @@ class AssetModel(TreeModel): DocumentRole = QtCore.Qt.UserRole + 2 ObjectIdRole = QtCore.Qt.UserRole + 3 - def __init__(self, parent): + def __init__(self, dbcon, parent=None): super(AssetModel, self).__init__(parent=parent) - self.parent_widget = parent + self.dbcon = dbcon self.refresh() - @property - def db(self): - return self.parent_widget.db + def _add_hierarchy(self, assets, parent=None, silos=None): + """Add the assets that are related to the parent as children items. - def _add_hierarchy(self, parent=None): + 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. - # 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'] + Args: + assets (dict): All assets in the currently active silo stored + by key/value - assets = self.db.find(find_data).sort('name', 1) - for asset in assets: + 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: + node = Node({ + "_id": silo, + "name": silo, + "label": silo, + "type": "silo" + }) + self.add_child(node, parent=parent) + self._add_hierarchy(assets, parent=node) + + 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'], + "_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) - # 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=node) def refresh(self): """Refresh the data for the model.""" self.clear() if ( - self.db.active_project() is None or - self.db.active_project() == '' + self.dbcon.active_project() is None or + self.dbcon.active_project() == '' ): return + self.beginResetModel() - self._add_hierarchy(parent=None) + + # Get all assets in current silo sorted by name + db_assets = self.dbcon.find({"type": "asset"}).sort("name", 1) + silos = db_assets.distinct("silo") or None + # if any silo is set to None then it's expected it should not be used + if silos and None in silos: + silos = 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): @@ -119,8 +158,10 @@ class AssetModel(TreeModel): if column == self.Name: # Allow a custom icon and custom icon color to be defined - data = node["_document"]["data"] + data = node.get("_document", {}).get("data", {}) icon = data.get("icon", None) + if icon is None and node.get("type") == "silo": + icon = "database" color = data.get("color", style.colors.default) if icon is None: @@ -136,7 +177,7 @@ class AssetModel(TreeModel): 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 diff --git a/pype/standalonepublish/widgets/model_tasks_template.py b/pype/standalonepublish/widgets/model_tasks_template.py index bd1984029c..336921b37a 100644 --- a/pype/standalonepublish/widgets/model_tasks_template.py +++ b/pype/standalonepublish/widgets/model_tasks_template.py @@ -1,6 +1,6 @@ from . import QtCore, TreeModel from . import Node -from . import awesome, style +from . import qtawesome, style class TasksTemplateModel(TreeModel): @@ -11,7 +11,7 @@ class TasksTemplateModel(TreeModel): def __init__(self, selectable=True): super(TasksTemplateModel, self).__init__() self.selectable = selectable - self.icon = awesome.icon( + self.icon = qtawesome.icon( 'fa.calendar-check-o', color=style.colors.default ) diff --git a/pype/standalonepublish/widgets/widget_asset.py b/pype/standalonepublish/widgets/widget_asset.py index 54b7f7db44..d129ff0fdc 100644 --- a/pype/standalonepublish/widgets/widget_asset.py +++ b/pype/standalonepublish/widgets/widget_asset.py @@ -1,9 +1,9 @@ import contextlib from . import QtWidgets, QtCore -from . import RecursiveSortFilterProxyModel, AssetModel, AssetView -from . import awesome, style +from . import RecursiveSortFilterProxyModel, AssetModel +from . import qtawesome, style from . import TasksTemplateModel, DeselectableTreeView - +from . import _iter_model_rows @contextlib.contextmanager def preserve_expanded_rows(tree_view, @@ -124,11 +124,11 @@ class AssetWidget(QtWidgets.QWidget): selection_changed = QtCore.Signal() # on view selection change current_changed = QtCore.Signal() # on view current index change - def __init__(self, parent): + def __init__(self, dbcon, parent=None): super(AssetWidget, self).__init__(parent=parent) self.setContentsMargins(0, 0, 0, 0) - self.parent_widget = parent + self.dbcon = dbcon layout = QtWidgets.QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) @@ -139,17 +139,21 @@ class AssetWidget(QtWidgets.QWidget): self._set_projects() self.combo_projects.currentTextChanged.connect(self.on_project_change) # Tree View - model = AssetModel(self) + model = AssetModel(dbcon=self.dbcon, parent=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") @@ -195,13 +199,9 @@ class AssetWidget(QtWidgets.QWidget): self.proxy = proxy self.view = view - @property - def db(self): - return self.parent_widget.db - def collect_data(self): - project = self.db.find_one({'type': 'project'}) - asset = self.db.find_one({'_id': self.get_active_asset()}) + project = self.dbcon.find_one({'type': 'project'}) + asset = self.dbcon.find_one({'_id': self.get_active_asset()}) try: index = self.task_view.selectedIndexes()[0] @@ -211,41 +211,50 @@ class AssetWidget(QtWidgets.QWidget): data = { 'project': project['name'], 'asset': asset['name'], + 'silo': asset.get("silo") 'parents': self.get_parents(asset), 'task': task } + return data def get_parents(self, entity): output = [] if entity.get('data', {}).get('visualParent', None) is None: return output - parent = self.db.find_one({'_id': entity['data']['visualParent']}) + parent = self.dbcon.find_one({'_id': entity['data']['visualParent']}) output.append(parent['name']) output.extend(self.get_parents(parent)) return output def _set_projects(self): projects = list() - for project in self.db.projects(): + for project in self.dbcon.projects(): projects.append(project['name']) self.combo_projects.clear() if len(projects) > 0: self.combo_projects.addItems(projects) - self.db.activate_project(projects[0]) + self.dbcon.activate_project(projects[0]) def on_project_change(self): projects = list() - for project in self.db.projects(): + for project in self.dbcon.projects(): projects.append(project['name']) project_name = self.combo_projects.currentText() if project_name in projects: - self.db.activate_project(project_name) + self.dbcon.activate_project(project_name) self.refresh() 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): @@ -255,7 +264,7 @@ class AssetWidget(QtWidgets.QWidget): tasks = [] selected = self.get_selected_assets() if len(selected) == 1: - asset = self.db.find_one({ + asset = self.dbcon.find_one({ "_id": selected[0], "type": "asset" }) if asset: @@ -266,7 +275,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() @@ -277,7 +286,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: @@ -290,8 +299,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() @@ -299,16 +314,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 lib.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) diff --git a/pype/standalonepublish/widgets/widget_asset_view.py b/pype/standalonepublish/widgets/widget_asset_view.py deleted file mode 100644 index 27bf374599..0000000000 --- a/pype/standalonepublish/widgets/widget_asset_view.py +++ /dev/null @@ -1,16 +0,0 @@ -from . import QtCore -from . import DeselectableTreeView - - -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) diff --git a/pype/standalonepublish/widgets/widget_family.py b/pype/standalonepublish/widgets/widget_family.py index 63776b1df3..9d26f876e3 100644 --- a/pype/standalonepublish/widgets/widget_family.py +++ b/pype/standalonepublish/widgets/widget_family.py @@ -19,11 +19,11 @@ class FamilyWidget(QtWidgets.QWidget): Separator = "---separator---" NOT_SELECTED = '< Nothing is selected >' - def __init__(self, parent): - super().__init__(parent) + def __init__(self, dbcon, parent=None): + super(FamilyWidget, self).__init__(parent=parent) # Store internal states in here self.state = {"valid": False} - self.parent_widget = parent + self.dbcon = dbcon self.asset_name = self.NOT_SELECTED body = QtWidgets.QWidget() @@ -67,7 +67,7 @@ class FamilyWidget(QtWidgets.QWidget): layout = QtWidgets.QVBoxLayout(container) - header = FamilyDescriptionWidget(self) + header = FamilyDescriptionWidget(parent=self) layout.addWidget(header) layout.addWidget(QtWidgets.QLabel("Family")) @@ -124,10 +124,6 @@ class FamilyWidget(QtWidgets.QWidget): } return data - @property - def db(self): - return self.parent_widget.db - def change_asset(self, name): if name is None: name = self.NOT_SELECTED @@ -136,7 +132,6 @@ class FamilyWidget(QtWidgets.QWidget): def _on_state_changed(self, state): self.state['valid'] = state - self.parent_widget.set_valid_family(state) def _build_menu(self, default_names): """Create optional predefined subset names @@ -183,7 +178,7 @@ class FamilyWidget(QtWidgets.QWidget): assets = None if asset_name != self.NOT_SELECTED: # Get the assets from the database which match with the name - assets_db = self.db.find( + assets_db = self.dbcon.find( filter={"type": "asset"}, projection={"name": 1} ) @@ -206,7 +201,7 @@ class FamilyWidget(QtWidgets.QWidget): if assets: # Get all subsets of the current asset asset_ids = [asset["_id"] for asset in assets] - subsets = self.db.find(filter={"type": "subset", + subsets = self.dbcon.find(filter={"type": "subset", "name": {"$regex": "{}*".format(family), "$options": "i"}, "parent": {"$in": asset_ids}}) or [] @@ -259,17 +254,17 @@ class FamilyWidget(QtWidgets.QWidget): asset_name != self.NOT_SELECTED and subset_name.strip() != '' ): - asset = self.db.find_one({ + asset = self.dbcon.find_one({ 'type': 'asset', 'name': asset_name }) - subset = self.db.find_one({ + subset = self.dbcon.find_one({ 'type': 'subset', 'parent': asset['_id'], 'name': subset_name }) if subset: - versions = self.db.find({ + versions = self.dbcon.find({ 'type': 'version', 'parent': subset['_id'] }) diff --git a/pype/standalonepublish/widgets/widget_family_desc.py b/pype/standalonepublish/widgets/widget_family_desc.py index e329f28ba6..9b9dad7b46 100644 --- a/pype/standalonepublish/widgets/widget_family_desc.py +++ b/pype/standalonepublish/widgets/widget_family_desc.py @@ -5,7 +5,7 @@ import json from . import QtWidgets, QtCore, QtGui from . import HelpRole, FamilyRole, ExistsRole, PluginRole -from . import awesome +from . import qtawesome from pype.vendor import six from pype import lib as pypelib @@ -87,7 +87,7 @@ class FamilyDescriptionWidget(QtWidgets.QWidget): plugin = item.data(PluginRole) icon = getattr(plugin, "icon", "info-circle") assert isinstance(icon, six.string_types) - icon = awesome.icon("fa.{}".format(icon), color="white") + icon = qtawesome.icon("fa.{}".format(icon), color="white") pixmap = icon.pixmap(self.SIZE, self.SIZE) pixmap = pixmap.scaled(self.SIZE, self.SIZE)