From 7cdae95c73412d343bc1bd50d454b8eaea10dfb9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 15 Nov 2021 18:52:19 +0100 Subject: [PATCH 01/16] copied sceneinventory to openpype --- openpype/tools/sceneinventory/__init__.py | 7 + openpype/tools/sceneinventory/app.py | 1953 +++++++++++++++++++++ openpype/tools/sceneinventory/lib.py | 8 + openpype/tools/sceneinventory/model.py | 424 +++++ openpype/tools/sceneinventory/proxy.py | 148 ++ 5 files changed, 2540 insertions(+) create mode 100644 openpype/tools/sceneinventory/__init__.py create mode 100644 openpype/tools/sceneinventory/app.py create mode 100644 openpype/tools/sceneinventory/lib.py create mode 100644 openpype/tools/sceneinventory/model.py create mode 100644 openpype/tools/sceneinventory/proxy.py diff --git a/openpype/tools/sceneinventory/__init__.py b/openpype/tools/sceneinventory/__init__.py new file mode 100644 index 0000000000..694caf15fe --- /dev/null +++ b/openpype/tools/sceneinventory/__init__.py @@ -0,0 +1,7 @@ +from .app import ( + show, +) + +__all__ = [ + "show", +] diff --git a/openpype/tools/sceneinventory/app.py b/openpype/tools/sceneinventory/app.py new file mode 100644 index 0000000000..5304b7ac12 --- /dev/null +++ b/openpype/tools/sceneinventory/app.py @@ -0,0 +1,1953 @@ +import os +import sys +import logging +import collections +from functools import partial + +from ...vendor.Qt import QtWidgets, QtCore +from ...vendor import qtawesome +from ... import io, api, style +from ...lib import HeroVersionType + +from .. import lib as tools_lib +from ..delegates import VersionDelegate + +from .proxy import FilterProxyModel +from .model import InventoryModel + +from openpype.modules import ModulesManager + +DEFAULT_COLOR = "#fb9c15" + +module = sys.modules[__name__] +module.window = None + +log = logging.getLogger("SceneInventory") + + +class View(QtWidgets.QTreeView): + data_changed = QtCore.Signal() + hierarchy_view = QtCore.Signal(bool) + + def __init__(self, parent=None): + super(View, self).__init__(parent=parent) + + if not parent: + self.setWindowFlags( + self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint + ) + # view settings + self.setIndentation(12) + self.setAlternatingRowColors(True) + self.setSortingEnabled(True) + self.setSelectionMode(self.ExtendedSelection) + self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.customContextMenuRequested.connect(self.show_right_mouse_menu) + self._hierarchy_view = False + self._selected = None + + manager = ModulesManager() + self.sync_server = manager.modules_by_name["sync_server"] + self.sync_enabled = self.sync_server.enabled + + def enter_hierarchy(self, items): + self._selected = set(i["objectName"] for i in items) + self._hierarchy_view = True + self.hierarchy_view.emit(True) + self.data_changed.emit() + self.expandToDepth(1) + self.setStyleSheet(""" + QTreeView { + border-color: #fb9c15; + } + """) + + def leave_hierarchy(self): + self._hierarchy_view = False + self.hierarchy_view.emit(False) + self.data_changed.emit() + self.setStyleSheet("QTreeView {}") + + def build_item_menu_for_selection(self, items, menu): + if not items: + return + + repre_ids = [] + for item in items: + item_id = io.ObjectId(item["representation"]) + if item_id not in repre_ids: + repre_ids.append(item_id) + + repre_docs = io.find( + { + "type": "representation", + "_id": {"$in": repre_ids} + }, + {"parent": 1} + ) + + version_ids = [] + for repre_doc in repre_docs: + version_id = repre_doc["parent"] + if version_id not in version_ids: + version_ids.append(version_id) + + loaded_versions = io.find({ + "_id": {"$in": version_ids}, + "type": {"$in": ["version", "hero_version"]} + }) + + loaded_hero_versions = [] + versions_by_parent_id = collections.defaultdict(list) + version_parents = [] + for version in loaded_versions: + if version["type"] == "hero_version": + loaded_hero_versions.append(version) + else: + parent_id = version["parent"] + versions_by_parent_id[parent_id].append(version) + if parent_id not in version_parents: + version_parents.append(parent_id) + + all_versions = io.find({ + "type": {"$in": ["hero_version", "version"]}, + "parent": {"$in": version_parents} + }) + hero_versions = [] + versions = [] + for version in all_versions: + if version["type"] == "hero_version": + hero_versions.append(version) + else: + versions.append(version) + + has_loaded_hero_versions = len(loaded_hero_versions) > 0 + has_available_hero_version = len(hero_versions) > 0 + has_outdated = False + + for version in versions: + parent_id = version["parent"] + current_versions = versions_by_parent_id[parent_id] + for current_version in current_versions: + if current_version["name"] < version["name"]: + has_outdated = True + break + + if has_outdated: + break + + switch_to_versioned = None + if has_loaded_hero_versions: + def _on_switch_to_versioned(items): + repre_ids = [] + for item in items: + item_id = io.ObjectId(item["representation"]) + if item_id not in repre_ids: + repre_ids.append(item_id) + + repre_docs = io.find( + { + "type": "representation", + "_id": {"$in": repre_ids} + }, + {"parent": 1} + ) + + version_ids = [] + version_id_by_repre_id = {} + for repre_doc in repre_docs: + version_id = repre_doc["parent"] + version_id_by_repre_id[repre_doc["_id"]] = version_id + if version_id not in version_ids: + version_ids.append(version_id) + hero_versions = io.find( + { + "_id": {"$in": version_ids}, + "type": "hero_version" + }, + {"version_id": 1} + ) + version_ids = set() + for hero_version in hero_versions: + version_id = hero_version["version_id"] + version_ids.add(version_id) + hero_version_id = hero_version["_id"] + for _repre_id, current_version_id in ( + version_id_by_repre_id.items() + ): + if current_version_id == hero_version_id: + version_id_by_repre_id[_repre_id] = version_id + + version_docs = io.find( + { + "_id": {"$in": list(version_ids)}, + "type": "version" + }, + {"name": 1} + ) + version_name_by_id = {} + for version_doc in version_docs: + version_name_by_id[version_doc["_id"]] = \ + version_doc["name"] + + for item in items: + repre_id = io.ObjectId(item["representation"]) + version_id = version_id_by_repre_id.get(repre_id) + version_name = version_name_by_id.get(version_id) + if version_name is not None: + try: + api.update(item, version_name) + except AssertionError: + self._show_version_error_dialog(version_name, + [item]) + log.warning("Update failed", exc_info=True) + + self.data_changed.emit() + + update_icon = qtawesome.icon( + "fa.asterisk", + color=DEFAULT_COLOR + ) + switch_to_versioned = QtWidgets.QAction( + update_icon, + "Switch to versioned", + menu + ) + switch_to_versioned.triggered.connect( + lambda: _on_switch_to_versioned(items) + ) + + update_to_latest_action = None + if has_outdated or has_loaded_hero_versions: + # update to latest version + def _on_update_to_latest(items): + for item in items: + try: + api.update(item, -1) + except AssertionError: + self._show_version_error_dialog(None, [item]) + log.warning("Update failed", exc_info=True) + self.data_changed.emit() + + update_icon = qtawesome.icon( + "fa.angle-double-up", + color=DEFAULT_COLOR + ) + update_to_latest_action = QtWidgets.QAction( + update_icon, + "Update to latest", + menu + ) + update_to_latest_action.triggered.connect( + lambda: _on_update_to_latest(items) + ) + + change_to_hero = None + if has_available_hero_version: + # change to hero version + def _on_update_to_hero(items): + for item in items: + try: + api.update(item, HeroVersionType(-1)) + except AssertionError: + self._show_version_error_dialog('hero', [item]) + log.warning("Update failed", exc_info=True) + self.data_changed.emit() + + # TODO change icon + change_icon = qtawesome.icon( + "fa.asterisk", + color="#00b359" + ) + change_to_hero = QtWidgets.QAction( + change_icon, + "Change to hero", + menu + ) + change_to_hero.triggered.connect( + lambda: _on_update_to_hero(items) + ) + + # set version + set_version_icon = qtawesome.icon("fa.hashtag", color=DEFAULT_COLOR) + set_version_action = QtWidgets.QAction( + set_version_icon, + "Set version", + menu + ) + set_version_action.triggered.connect( + lambda: self.show_version_dialog(items)) + + # switch asset + switch_asset_icon = qtawesome.icon("fa.sitemap", color=DEFAULT_COLOR) + switch_asset_action = QtWidgets.QAction( + switch_asset_icon, + "Switch Asset", + menu + ) + switch_asset_action.triggered.connect( + lambda: self.show_switch_dialog(items)) + + # remove + remove_icon = qtawesome.icon("fa.remove", color=DEFAULT_COLOR) + remove_action = QtWidgets.QAction(remove_icon, "Remove items", menu) + remove_action.triggered.connect( + lambda: self.show_remove_warning_dialog(items)) + + # add the actions + if switch_to_versioned: + menu.addAction(switch_to_versioned) + + if update_to_latest_action: + menu.addAction(update_to_latest_action) + + if change_to_hero: + menu.addAction(change_to_hero) + + menu.addAction(set_version_action) + menu.addAction(switch_asset_action) + + menu.addSeparator() + + menu.addAction(remove_action) + + menu.addSeparator() + + if self.sync_enabled: + menu = self.handle_sync_server(menu, repre_ids) + + def handle_sync_server(self, menu, repre_ids): + """ + Adds actions for download/upload when SyncServer is enabled + + Args: + menu (OptionMenu) + repre_ids (list) of object_ids + Returns: + (OptionMenu) + """ + download_icon = qtawesome.icon("fa.download", color=DEFAULT_COLOR) + download_active_action = QtWidgets.QAction( + download_icon, + "Download", + menu + ) + download_active_action.triggered.connect( + lambda: self._add_sites(repre_ids, 'active_site')) + + upload_icon = qtawesome.icon("fa.upload", color=DEFAULT_COLOR) + upload_remote_action = QtWidgets.QAction( + upload_icon, + "Upload", + menu + ) + upload_remote_action.triggered.connect( + lambda: self._add_sites(repre_ids, 'remote_site')) + + menu.addAction(download_active_action) + menu.addAction(upload_remote_action) + + return menu + + def _add_sites(self, repre_ids, side): + """ + (Re)sync all 'repre_ids' to specific site. + + It checks if opposite site has fully available content to limit + accidents. (ReSync active when no remote >> losing active content) + + Args: + repre_ids (list) + side (str): 'active_site'|'remote_site' + """ + project = io.Session["AVALON_PROJECT"] + active_site = self.sync_server.get_active_site(project) + remote_site = self.sync_server.get_remote_site(project) + + for repre_id in repre_ids: + representation = io.find_one({"type": "representation", + "_id": repre_id}) + if not representation: + continue + + progress = tools_lib.get_progress_for_repre(representation, + active_site, + remote_site) + if side == 'active_site': + # check opposite from added site, must be 1 or unable to sync + check_progress = progress[remote_site] + site = active_site + else: + check_progress = progress[active_site] + site = remote_site + + if check_progress == 1: + self.sync_server.add_site(project, repre_id, site, force=True) + + self.data_changed.emit() + + def build_item_menu(self, items): + """Create menu for the selected items""" + + menu = QtWidgets.QMenu(self) + + # add the actions + self.build_item_menu_for_selection(items, menu) + + # These two actions should be able to work without selection + # expand all items + expandall_action = QtWidgets.QAction(menu, text="Expand all items") + expandall_action.triggered.connect(self.expandAll) + + # collapse all items + collapse_action = QtWidgets.QAction(menu, text="Collapse all items") + collapse_action.triggered.connect(self.collapseAll) + + menu.addAction(expandall_action) + menu.addAction(collapse_action) + + custom_actions = self.get_custom_actions(containers=items) + if custom_actions: + submenu = QtWidgets.QMenu("Actions", self) + for action in custom_actions: + + color = action.color or DEFAULT_COLOR + icon = qtawesome.icon("fa.%s" % action.icon, color=color) + action_item = QtWidgets.QAction(icon, action.label, submenu) + action_item.triggered.connect( + partial(self.process_custom_action, action, items)) + + submenu.addAction(action_item) + + menu.addMenu(submenu) + + # go back to flat view + if self._hierarchy_view: + back_to_flat_icon = qtawesome.icon("fa.list", color=DEFAULT_COLOR) + back_to_flat_action = QtWidgets.QAction( + back_to_flat_icon, + "Back to Full-View", + menu + ) + back_to_flat_action.triggered.connect(self.leave_hierarchy) + + # send items to hierarchy view + enter_hierarchy_icon = qtawesome.icon("fa.indent", color="#d8d8d8") + enter_hierarchy_action = QtWidgets.QAction( + enter_hierarchy_icon, + "Cherry-Pick (Hierarchy)", + menu + ) + enter_hierarchy_action.triggered.connect( + lambda: self.enter_hierarchy(items)) + + if items: + menu.addAction(enter_hierarchy_action) + + if self._hierarchy_view: + menu.addAction(back_to_flat_action) + + return menu + + def get_custom_actions(self, containers): + """Get the registered Inventory Actions + + Args: + containers(list): collection of containers + + Returns: + list: collection of filter and initialized actions + """ + + def sorter(Plugin): + """Sort based on order attribute of the plugin""" + return Plugin.order + + # Fedd an empty dict if no selection, this will ensure the compat + # lookup always work, so plugin can interact with Scene Inventory + # reversely. + containers = containers or [dict()] + + # Check which action will be available in the menu + Plugins = api.discover(api.InventoryAction) + compatible = [p() for p in Plugins if + any(p.is_compatible(c) for c in containers)] + + return sorted(compatible, key=sorter) + + def process_custom_action(self, action, containers): + """Run action and if results are returned positive update the view + + If the result is list or dict, will select view items by the result. + + Args: + action (InventoryAction): Inventory Action instance + containers (list): Data of currently selected items + + Returns: + None + """ + + result = action.process(containers) + if result: + self.data_changed.emit() + + if isinstance(result, (list, set)): + self.select_items_by_action(result) + + if isinstance(result, dict): + self.select_items_by_action(result["objectNames"], + result["options"]) + + def select_items_by_action(self, object_names, options=None): + """Select view items by the result of action + + Args: + object_names (list or set): A list/set of container object name + options (dict): GUI operation options. + + Returns: + None + + """ + options = options or dict() + + if options.get("clear", True): + self.clearSelection() + + object_names = set(object_names) + if (self._hierarchy_view and + not self._selected.issuperset(object_names)): + # If any container not in current cherry-picked view, update + # view before selecting them. + self._selected.update(object_names) + self.data_changed.emit() + + model = self.model() + selection_model = self.selectionModel() + + select_mode = { + "select": selection_model.Select, + "deselect": selection_model.Deselect, + "toggle": selection_model.Toggle, + }[options.get("mode", "select")] + + for item in tools_lib.iter_model_rows(model, 0): + item = item.data(InventoryModel.ItemRole) + if item.get("isGroupNode"): + continue + + name = item.get("objectName") + if name in object_names: + self.scrollTo(item) # Ensure item is visible + flags = select_mode | selection_model.Rows + selection_model.select(item, flags) + + object_names.remove(name) + + if len(object_names) == 0: + break + + def show_right_mouse_menu(self, pos): + """Display the menu when at the position of the item clicked""" + + globalpos = self.viewport().mapToGlobal(pos) + + if not self.selectionModel().hasSelection(): + print("No selection") + # Build menu without selection, feed an empty list + menu = self.build_item_menu([]) + menu.exec_(globalpos) + return + + active = self.currentIndex() # index under mouse + active = active.sibling(active.row(), 0) # get first column + + # move index under mouse + indices = self.get_indices() + if active in indices: + indices.remove(active) + + indices.append(active) + + # Extend to the sub-items + all_indices = self.extend_to_children(indices) + items = [dict(i.data(InventoryModel.ItemRole)) for i in all_indices + if i.parent().isValid()] + + if self._hierarchy_view: + # Ensure no group item + items = [n for n in items if not n.get("isGroupNode")] + + menu = self.build_item_menu(items) + menu.exec_(globalpos) + + def get_indices(self): + """Get the selected rows""" + selection_model = self.selectionModel() + return selection_model.selectedRows() + + def extend_to_children(self, indices): + """Extend the indices to the children indices. + + Top-level indices are extended to its children indices. Sub-items + are kept as is. + + Args: + indices (list): The indices to extend. + + Returns: + list: The children indices + + """ + def get_children(i): + model = i.model() + rows = model.rowCount(parent=i) + for row in range(rows): + child = model.index(row, 0, parent=i) + yield child + + subitems = set() + for i in indices: + valid_parent = i.parent().isValid() + if valid_parent and i not in subitems: + subitems.add(i) + + if self._hierarchy_view: + # Assume this is a group item + for child in get_children(i): + subitems.add(child) + else: + # is top level item + for child in get_children(i): + subitems.add(child) + + return list(subitems) + + def show_version_dialog(self, items): + """Create a dialog with the available versions for the selected file + + Args: + items (list): list of items to run the "set_version" for + + Returns: + None + """ + + active = items[-1] + + # Get available versions for active representation + representation_id = io.ObjectId(active["representation"]) + representation = io.find_one({"_id": representation_id}) + version = io.find_one({ + "_id": representation["parent"] + }) + + versions = list(io.find( + { + "parent": version["parent"], + "type": "version" + }, + sort=[("name", 1)] + )) + + hero_version = io.find_one({ + "parent": version["parent"], + "type": "hero_version" + }) + if hero_version: + _version_id = hero_version["version_id"] + for _version in versions: + if _version["_id"] != _version_id: + continue + + hero_version["name"] = HeroVersionType( + _version["name"] + ) + hero_version["data"] = _version["data"] + break + + # Get index among the listed versions + current_item = None + current_version = active["version"] + if isinstance(current_version, HeroVersionType): + current_item = hero_version + else: + for version in versions: + if version["name"] == current_version: + current_item = version + break + + all_versions = [] + if hero_version: + all_versions.append(hero_version) + all_versions.extend(reversed(versions)) + + if current_item: + index = all_versions.index(current_item) + else: + index = 0 + + versions_by_label = dict() + labels = [] + for version in all_versions: + is_hero = version["type"] == "hero_version" + label = tools_lib.format_version(version["name"], is_hero) + labels.append(label) + versions_by_label[label] = version["name"] + + label, state = QtWidgets.QInputDialog.getItem( + self, + "Set version..", + "Set version number to", + labels, + current=index, + editable=False + ) + if not state: + return + + if label: + version = versions_by_label[label] + for item in items: + try: + api.update(item, version) + except AssertionError: + self._show_version_error_dialog(version, [item]) + log.warning("Update failed", exc_info=True) + # refresh model when done + self.data_changed.emit() + + def show_switch_dialog(self, items): + """Display Switch dialog""" + dialog = SwitchAssetDialog(self, items) + dialog.switched.connect(self.data_changed.emit) + dialog.show() + + def show_remove_warning_dialog(self, items): + """Prompt a dialog to inform the user the action will remove items""" + + accept = QtWidgets.QMessageBox.Ok + buttons = accept | QtWidgets.QMessageBox.Cancel + + message = ("Are you sure you want to remove " + "{} item(s)".format(len(items))) + state = QtWidgets.QMessageBox.question(self, "Are you sure?", + message, + buttons=buttons, + defaultButton=accept) + + if state != accept: + return + + for item in items: + api.remove(item) + self.data_changed.emit() + + def _show_version_error_dialog(self, version, items): + """Shows QMessageBox when version switch doesn't work + + Args: + version: str or int or None + """ + if not version: + version_str = "latest" + elif version == "hero": + version_str = "hero" + elif isinstance(version, int): + version_str = "v{:03d}".format(version) + else: + version_str = version + + dialog = QtWidgets.QMessageBox() + dialog.setIcon(QtWidgets.QMessageBox.Warning) + dialog.setStyleSheet(style.load_stylesheet()) + dialog.setWindowTitle("Update failed") + + switch_btn = dialog.addButton("Switch Asset", + QtWidgets.QMessageBox.ActionRole) + switch_btn.clicked.connect(lambda: self.show_switch_dialog(items)) + + dialog.addButton(QtWidgets.QMessageBox.Cancel) + + msg = "Version update to '{}' ".format(version_str) + \ + "failed as representation doesn't exist.\n\n" \ + "Please update to version with a valid " \ + "representation OR \n use 'Switch Asset' button " \ + "to change asset." + dialog.setText(msg) + dialog.exec_() + + +class SearchComboBox(QtWidgets.QComboBox): + """Searchable ComboBox with empty placeholder value as first value""" + + def __init__(self, parent=None, placeholder=""): + super(SearchComboBox, self).__init__(parent) + + self.setEditable(True) + self.setInsertPolicy(self.NoInsert) + self.lineEdit().setPlaceholderText(placeholder) + + # Apply completer settings + completer = self.completer() + completer.setCompletionMode(completer.PopupCompletion) + completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive) + + # Force style sheet on popup menu + # It won't take the parent stylesheet for some reason + # todo: better fix for completer popup stylesheet + if module.window: + popup = completer.popup() + popup.setStyleSheet(module.window.styleSheet()) + + def populate(self, items): + self.clear() + self.addItems([""]) # ensure first item is placeholder + self.addItems(items) + + def get_valid_value(self): + """Return the current text if it's a valid value else None + + Note: The empty placeholder value is valid and returns as "" + + """ + + text = self.currentText() + lookup = set(self.itemText(i) for i in range(self.count())) + if text not in lookup: + return None + + return text or None + + def set_valid_value(self, value): + """Try to locate 'value' and pre-select it in dropdown.""" + index = self.findText(value) + if index > -1: + self.setCurrentIndex(index) + + +class ValidationState: + def __init__(self): + self.asset_ok = True + self.subset_ok = True + self.repre_ok = True + + @property + def all_ok(self): + return ( + self.asset_ok + and self.subset_ok + and self.repre_ok + ) + + +class SwitchAssetDialog(QtWidgets.QDialog): + """Widget to support asset switching""" + + MIN_WIDTH = 550 + + fill_check = False + switched = QtCore.Signal() + + def __init__(self, parent=None, items=None): + QtWidgets.QDialog.__init__(self, parent) + + self.setWindowTitle("Switch selected items ...") + + # Force and keep focus dialog + self.setModal(True) + + self._assets_box = SearchComboBox(placeholder="") + self._subsets_box = SearchComboBox(placeholder="") + self._representations_box = SearchComboBox( + placeholder="" + ) + + self._asset_label = QtWidgets.QLabel("") + self._subset_label = QtWidgets.QLabel("") + self._repre_label = QtWidgets.QLabel("") + + self.current_asset_btn = QtWidgets.QPushButton("Use current asset") + + main_layout = QtWidgets.QGridLayout(self) + + accept_icon = qtawesome.icon("fa.check", color="white") + accept_btn = QtWidgets.QPushButton() + accept_btn.setIcon(accept_icon) + accept_btn.setFixedWidth(24) + accept_btn.setFixedHeight(24) + + # Asset column + main_layout.addWidget(self.current_asset_btn, 0, 0) + main_layout.addWidget(self._assets_box, 1, 0) + main_layout.addWidget(self._asset_label, 2, 0) + # Subset column + main_layout.addWidget(self._subsets_box, 1, 1) + main_layout.addWidget(self._subset_label, 2, 1) + # Representation column + main_layout.addWidget(self._representations_box, 1, 2) + main_layout.addWidget(self._repre_label, 2, 2) + # Btn column + main_layout.addWidget(accept_btn, 1, 3) + + self._accept_btn = accept_btn + + self._assets_box.currentIndexChanged.connect( + self._combobox_value_changed + ) + self._subsets_box.currentIndexChanged.connect( + self._combobox_value_changed + ) + self._representations_box.currentIndexChanged.connect( + self._combobox_value_changed + ) + self._accept_btn.clicked.connect(self._on_accept) + self.current_asset_btn.clicked.connect(self._on_current_asset) + + self._init_asset_name = None + self._init_subset_name = None + self._init_repre_name = None + + self._items = items + self._prepare_content_data() + self.refresh(True) + + self.setMinimumWidth(self.MIN_WIDTH) + + # Set default focus to accept button so you don't directly type in + # first asset field, this also allows to see the placeholder value. + accept_btn.setFocus() + + def _prepare_content_data(self): + repre_ids = [ + io.ObjectId(item["representation"]) + for item in self._items + ] + repres = list(io.find({ + "type": {"$in": ["representation", "archived_representation"]}, + "_id": {"$in": repre_ids} + })) + repres_by_id = {repre["_id"]: repre for repre in repres} + + # stash context values, works only for single representation + if len(repres) == 1: + self._init_asset_name = repres[0]["context"]["asset"] + self._init_subset_name = repres[0]["context"]["subset"] + self._init_repre_name = repres[0]["context"]["representation"] + + content_repres = {} + archived_repres = [] + missing_repres = [] + version_ids = [] + for repre_id in repre_ids: + if repre_id not in repres_by_id: + missing_repres.append(repre_id) + elif repres_by_id[repre_id]["type"] == "archived_representation": + repre = repres_by_id[repre_id] + archived_repres.append(repre) + version_ids.append(repre["parent"]) + else: + repre = repres_by_id[repre_id] + content_repres[repre_id] = repres_by_id[repre_id] + version_ids.append(repre["parent"]) + + versions = io.find({ + "type": {"$in": ["version", "hero_version"]}, + "_id": {"$in": list(set(version_ids))} + }) + content_versions = {} + hero_version_ids = set() + for version in versions: + content_versions[version["_id"]] = version + if version["type"] == "hero_version": + hero_version_ids.add(version["_id"]) + + missing_versions = [] + subset_ids = [] + for version_id in version_ids: + if version_id not in content_versions: + missing_versions.append(version_id) + else: + subset_ids.append(content_versions[version_id]["parent"]) + + subsets = io.find({ + "type": {"$in": ["subset", "archived_subset"]}, + "_id": {"$in": subset_ids} + }) + subsets_by_id = {sub["_id"]: sub for sub in subsets} + + asset_ids = [] + archived_subsets = [] + missing_subsets = [] + content_subsets = {} + for subset_id in subset_ids: + if subset_id not in subsets_by_id: + missing_subsets.append(subset_id) + elif subsets_by_id[subset_id]["type"] == "archived_subset": + subset = subsets_by_id[subset_id] + asset_ids.append(subset["parent"]) + archived_subsets.append(subset) + else: + subset = subsets_by_id[subset_id] + asset_ids.append(subset["parent"]) + content_subsets[subset_id] = subset + + assets = io.find({ + "type": {"$in": ["asset", "archived_asset"]}, + "_id": {"$in": list(asset_ids)} + }) + assets_by_id = {asset["_id"]: asset for asset in assets} + + missing_assets = [] + archived_assets = [] + content_assets = {} + for asset_id in asset_ids: + if asset_id not in assets_by_id: + missing_assets.append(asset_id) + elif assets_by_id[asset_id]["type"] == "archived_asset": + archived_assets.append(assets_by_id[asset_id]) + else: + content_assets[asset_id] = assets_by_id[asset_id] + + self.content_assets = content_assets + self.content_subsets = content_subsets + self.content_versions = content_versions + self.content_repres = content_repres + + self.hero_version_ids = hero_version_ids + + self.missing_assets = missing_assets + self.missing_versions = missing_versions + self.missing_subsets = missing_subsets + self.missing_repres = missing_repres + self.missing_docs = ( + bool(missing_assets) + or bool(missing_versions) + or bool(missing_subsets) + or bool(missing_repres) + ) + + self.archived_assets = archived_assets + self.archived_subsets = archived_subsets + self.archived_repres = archived_repres + + def _combobox_value_changed(self, *args, **kwargs): + self.refresh() + + def refresh(self, init_refresh=False): + """Build the need comboboxes with content""" + if not self.fill_check and not init_refresh: + return + + self.fill_check = False + + if init_refresh: + asset_values = self._get_asset_box_values() + self._fill_combobox(asset_values, "asset") + + validation_state = ValidationState() + + # Set other comboboxes to empty if any document is missing or any asset + # of loaded representations is archived. + self._is_asset_ok(validation_state) + if validation_state.asset_ok: + subset_values = self._get_subset_box_values() + self._fill_combobox(subset_values, "subset") + self._is_subset_ok(validation_state) + + if validation_state.asset_ok and validation_state.subset_ok: + repre_values = sorted(self._representations_box_values()) + self._fill_combobox(repre_values, "repre") + self._is_repre_ok(validation_state) + + # Fill comboboxes with values + self.set_labels() + self.apply_validations(validation_state) + + if init_refresh: # pre select context if possible + self._assets_box.set_valid_value(self._init_asset_name) + self._subsets_box.set_valid_value(self._init_subset_name) + self._representations_box.set_valid_value(self._init_repre_name) + + self.fill_check = True + + def _get_loaders(self, representations): + if not representations: + return list() + + available_loaders = filter( + lambda l: not (hasattr(l, "is_utility") and l.is_utility), + api.discover(api.Loader) + ) + + loaders = set() + + for representation in representations: + for loader in api.loaders_from_representation( + available_loaders, + representation + ): + loaders.add(loader) + + return loaders + + def _fill_combobox(self, values, combobox_type): + if combobox_type == "asset": + combobox_widget = self._assets_box + elif combobox_type == "subset": + combobox_widget = self._subsets_box + elif combobox_type == "repre": + combobox_widget = self._representations_box + else: + return + selected_value = combobox_widget.get_valid_value() + + # Fill combobox + if values is not None: + combobox_widget.populate(list(sorted(values))) + if selected_value and selected_value in values: + index = None + for idx in range(combobox_widget.count()): + if selected_value == str(combobox_widget.itemText(idx)): + index = idx + break + if index is not None: + combobox_widget.setCurrentIndex(index) + + def set_labels(self): + asset_label = self._assets_box.get_valid_value() + subset_label = self._subsets_box.get_valid_value() + repre_label = self._representations_box.get_valid_value() + + default = "*No changes" + self._asset_label.setText(asset_label or default) + self._subset_label.setText(subset_label or default) + self._repre_label.setText(repre_label or default) + + def apply_validations(self, validation_state): + error_msg = "*Please select" + error_sheet = "border: 1px solid red;" + success_sheet = "border: 1px solid green;" + + asset_sheet = None + subset_sheet = None + repre_sheet = None + accept_sheet = None + if validation_state.asset_ok is False: + asset_sheet = error_sheet + self._asset_label.setText(error_msg) + elif validation_state.subset_ok is False: + subset_sheet = error_sheet + self._subset_label.setText(error_msg) + elif validation_state.repre_ok is False: + repre_sheet = error_sheet + self._repre_label.setText(error_msg) + + if validation_state.all_ok: + accept_sheet = success_sheet + + self._assets_box.setStyleSheet(asset_sheet or "") + self._subsets_box.setStyleSheet(subset_sheet or "") + self._representations_box.setStyleSheet(repre_sheet or "") + + self._accept_btn.setEnabled(validation_state.all_ok) + self._accept_btn.setStyleSheet(accept_sheet or "") + + def _get_asset_box_values(self): + asset_docs = io.find( + {"type": "asset"}, + {"_id": 1, "name": 1} + ) + asset_names_by_id = { + asset_doc["_id"]: asset_doc["name"] + for asset_doc in asset_docs + } + subsets = io.find( + { + "type": "subset", + "parent": {"$in": list(asset_names_by_id.keys())} + }, + { + "parent": 1 + } + ) + + filtered_assets = [] + for subset in subsets: + asset_name = asset_names_by_id[subset["parent"]] + if asset_name not in filtered_assets: + filtered_assets.append(asset_name) + return sorted(filtered_assets) + + def _get_subset_box_values(self): + selected_asset = self._assets_box.get_valid_value() + if selected_asset: + asset_doc = io.find_one({"type": "asset", "name": selected_asset}) + asset_ids = [asset_doc["_id"]] + else: + asset_ids = list(self.content_assets.keys()) + + subsets = io.find( + { + "type": "subset", + "parent": {"$in": asset_ids} + }, + { + "parent": 1, + "name": 1 + } + ) + + subset_names_by_parent_id = collections.defaultdict(set) + for subset in subsets: + subset_names_by_parent_id[subset["parent"]].add(subset["name"]) + + possible_subsets = None + for subset_names in subset_names_by_parent_id.values(): + if possible_subsets is None: + possible_subsets = subset_names + else: + possible_subsets = (possible_subsets & subset_names) + + if not possible_subsets: + break + + return list(possible_subsets or list()) + + def _representations_box_values(self): + # NOTE hero versions are not used because it is expected that + # hero version has same representations as latests + selected_asset = self._assets_box.currentText() + selected_subset = self._subsets_box.currentText() + + # If nothing is selected + # [ ] [ ] [?] + if not selected_asset and not selected_subset: + # Find all representations of selection's subsets + possible_repres = list(io.find( + { + "type": "representation", + "parent": {"$in": list(self.content_versions.keys())} + }, + { + "parent": 1, + "name": 1 + } + )) + + possible_repres_by_parent = collections.defaultdict(set) + for repre in possible_repres: + possible_repres_by_parent[repre["parent"]].add(repre["name"]) + + output_repres = None + for repre_names in possible_repres_by_parent.values(): + if output_repres is None: + output_repres = repre_names + else: + output_repres = (output_repres & repre_names) + + if not output_repres: + break + + return list(output_repres or list()) + + # [x] [x] [?] + if selected_asset and selected_subset: + asset_doc = io.find_one( + {"type": "asset", "name": selected_asset}, + {"_id": 1} + ) + subset_doc = io.find_one( + { + "type": "subset", + "name": selected_subset, + "parent": asset_doc["_id"] + }, + {"_id": 1} + ) + subset_id = subset_doc["_id"] + last_versions_by_subset_id = self.find_last_versions([subset_id]) + version_doc = last_versions_by_subset_id.get(subset_id) + repre_docs = io.find( + { + "type": "representation", + "parent": version_doc["_id"] + }, + { + "name": 1 + } + ) + return [ + repre_doc["name"] + for repre_doc in repre_docs + ] + + # [x] [ ] [?] + # If asset only is selected + if selected_asset: + asset_doc = io.find_one( + {"type": "asset", "name": selected_asset}, + {"_id": 1} + ) + if not asset_doc: + return list() + + # Filter subsets by subset names from content + subset_names = set() + for subset_doc in self.content_subsets.values(): + subset_names.add(subset_doc["name"]) + subset_docs = io.find( + { + "type": "subset", + "parent": asset_doc["_id"], + "name": {"$in": list(subset_names)} + }, + {"_id": 1} + ) + subset_ids = [ + subset_doc["_id"] + for subset_doc in subset_docs + ] + if not subset_ids: + return list() + + last_versions_by_subset_id = self.find_last_versions(subset_ids) + subset_id_by_version_id = {} + for subset_id, last_version in last_versions_by_subset_id.items(): + version_id = last_version["_id"] + subset_id_by_version_id[version_id] = subset_id + + if not subset_id_by_version_id: + return list() + + repre_docs = list(io.find( + { + "type": "representation", + "parent": {"$in": list(subset_id_by_version_id.keys())} + }, + { + "name": 1, + "parent": 1 + } + )) + if not repre_docs: + return list() + + repre_names_by_parent = collections.defaultdict(set) + for repre_doc in repre_docs: + repre_names_by_parent[repre_doc["parent"]].add( + repre_doc["name"] + ) + + available_repres = None + for repre_names in repre_names_by_parent.values(): + if available_repres is None: + available_repres = repre_names + continue + + available_repres = available_repres.intersection(repre_names) + + return list(available_repres) + + # [ ] [x] [?] + subset_docs = list(io.find( + { + "type": "subset", + "parent": {"$in": list(self.content_assets.keys())}, + "name": selected_subset + }, + {"_id": 1, "parent": 1} + )) + if not subset_docs: + return list() + + subset_docs_by_id = { + subset_doc["_id"]: subset_doc + for subset_doc in subset_docs + } + last_versions_by_subset_id = self.find_last_versions( + subset_docs_by_id.keys() + ) + + subset_id_by_version_id = {} + for subset_id, last_version in last_versions_by_subset_id.items(): + version_id = last_version["_id"] + subset_id_by_version_id[version_id] = subset_id + + if not subset_id_by_version_id: + return list() + + repre_docs = list(io.find( + { + "type": "representation", + "parent": {"$in": list(subset_id_by_version_id.keys())} + }, + { + "name": 1, + "parent": 1 + } + )) + if not repre_docs: + return list() + + repre_names_by_asset_id = {} + for repre_doc in repre_docs: + subset_id = subset_id_by_version_id[repre_doc["parent"]] + asset_id = subset_docs_by_id[subset_id]["parent"] + if asset_id not in repre_names_by_asset_id: + repre_names_by_asset_id[asset_id] = set() + repre_names_by_asset_id[asset_id].add(repre_doc["name"]) + + available_repres = None + for repre_names in repre_names_by_asset_id.values(): + if available_repres is None: + available_repres = repre_names + continue + + available_repres = available_repres.intersection(repre_names) + + return list(available_repres) + + def _is_asset_ok(self, validation_state): + selected_asset = self._assets_box.get_valid_value() + if ( + selected_asset is None + and (self.missing_docs or self.archived_assets) + ): + validation_state.asset_ok = False + + def _is_subset_ok(self, validation_state): + selected_asset = self._assets_box.get_valid_value() + selected_subset = self._subsets_box.get_valid_value() + + # [?] [x] [?] + # If subset is selected then must be ok + if selected_subset is not None: + return + + # [ ] [ ] [?] + if selected_asset is None: + # If there were archived subsets and asset is not selected + if self.archived_subsets: + validation_state.subset_ok = False + return + + # [x] [ ] [?] + asset_doc = io.find_one( + {"type": "asset", "name": selected_asset}, + {"_id": 1} + ) + subset_docs = io.find( + {"type": "subset", "parent": asset_doc["_id"]}, + {"name": 1} + ) + subset_names = set( + subset_doc["name"] + for subset_doc in subset_docs + ) + + for subset_doc in self.content_subsets.values(): + if subset_doc["name"] not in subset_names: + validation_state.subset_ok = False + break + + def find_last_versions(self, subset_ids): + _pipeline = [ + # Find all versions of those subsets + {"$match": { + "type": "version", + "parent": {"$in": list(subset_ids)} + }}, + # Sorting versions all together + {"$sort": {"name": 1}}, + # Group them by "parent", but only take the last + {"$group": { + "_id": "$parent", + "_version_id": {"$last": "$_id"}, + "type": {"$last": "$type"} + }} + ] + last_versions_by_subset_id = dict() + for doc in io.aggregate(_pipeline): + doc["parent"] = doc["_id"] + doc["_id"] = doc.pop("_version_id") + last_versions_by_subset_id[doc["parent"]] = doc + return last_versions_by_subset_id + + def _is_repre_ok(self, validation_state): + selected_asset = self._assets_box.get_valid_value() + selected_subset = self._subsets_box.get_valid_value() + selected_repre = self._representations_box.get_valid_value() + + # [?] [?] [x] + # If subset is selected then must be ok + if selected_repre is not None: + return + + # [ ] [ ] [ ] + if selected_asset is None and selected_subset is None: + if ( + self.archived_repres + or self.missing_versions + or self.missing_repres + ): + validation_state.repre_ok = False + return + + # [x] [x] [ ] + if selected_asset is not None and selected_subset is not None: + asset_doc = io.find_one( + {"type": "asset", "name": selected_asset}, + {"_id": 1} + ) + subset_doc = io.find_one( + { + "type": "subset", + "parent": asset_doc["_id"], + "name": selected_subset + }, + {"_id": 1} + ) + last_versions_by_subset_id = self.find_last_versions( + [subset_doc["_id"]] + ) + last_version = last_versions_by_subset_id.get(subset_doc["_id"]) + if not last_version: + validation_state.repre_ok = False + return + + repre_docs = io.find( + { + "type": "representation", + "parent": last_version["_id"] + }, + {"name": 1} + ) + + repre_names = set( + repre_doc["name"] + for repre_doc in repre_docs + ) + for repre_doc in self.content_repres.values(): + if repre_doc["name"] not in repre_names: + validation_state.repre_ok = False + break + return + + # [x] [ ] [ ] + if selected_asset is not None: + asset_doc = io.find_one( + {"type": "asset", "name": selected_asset}, + {"_id": 1} + ) + subset_docs = list(io.find( + { + "type": "subset", + "parent": asset_doc["_id"] + }, + {"_id": 1, "name": 1} + )) + + subset_name_by_id = {} + subset_ids = set() + for subset_doc in subset_docs: + subset_id = subset_doc["_id"] + subset_ids.add(subset_id) + subset_name_by_id[subset_id] = subset_doc["name"] + + last_versions_by_subset_id = self.find_last_versions(subset_ids) + + subset_id_by_version_id = {} + for subset_id, last_version in last_versions_by_subset_id.items(): + version_id = last_version["_id"] + subset_id_by_version_id[version_id] = subset_id + + repre_docs = io.find( + { + "type": "representation", + "parent": {"$in": list(subset_id_by_version_id.keys())} + }, + { + "name": 1, + "parent": 1 + } + ) + repres_by_subset_name = {} + for repre_doc in repre_docs: + subset_id = subset_id_by_version_id[repre_doc["parent"]] + subset_name = subset_name_by_id[subset_id] + if subset_name not in repres_by_subset_name: + repres_by_subset_name[subset_name] = set() + repres_by_subset_name[subset_name].add(repre_doc["name"]) + + for repre_doc in self.content_repres.values(): + version_doc = self.content_versions[repre_doc["parent"]] + subset_doc = self.content_subsets[version_doc["parent"]] + repre_names = ( + repres_by_subset_name.get(subset_doc["name"]) or [] + ) + if repre_doc["name"] not in repre_names: + validation_state.repre_ok = False + break + return + + # [ ] [x] [ ] + # Subset documents + subset_docs = io.find( + { + "type": "subset", + "parent": {"$in": list(self.content_assets.keys())}, + "name": selected_subset + }, + {"_id": 1, "name": 1, "parent": 1} + ) + + subset_docs_by_id = {} + for subset_doc in subset_docs: + subset_docs_by_id[subset_doc["_id"]] = subset_doc + + last_versions_by_subset_id = self.find_last_versions( + subset_docs_by_id.keys() + ) + subset_id_by_version_id = {} + for subset_id, last_version in last_versions_by_subset_id.items(): + version_id = last_version["_id"] + subset_id_by_version_id[version_id] = subset_id + + repre_docs = io.find( + { + "type": "representation", + "parent": {"$in": list(subset_id_by_version_id.keys())} + }, + { + "name": 1, + "parent": 1 + } + ) + repres_by_asset_id = {} + for repre_doc in repre_docs: + subset_id = subset_id_by_version_id[repre_doc["parent"]] + asset_id = subset_docs_by_id[subset_id]["parent"] + if asset_id not in repres_by_asset_id: + repres_by_asset_id[asset_id] = set() + repres_by_asset_id[asset_id].add(repre_doc["name"]) + + for repre_doc in self.content_repres.values(): + version_doc = self.content_versions[repre_doc["parent"]] + subset_doc = self.content_subsets[version_doc["parent"]] + asset_id = subset_doc["parent"] + repre_names = ( + repres_by_asset_id.get(asset_id) or [] + ) + if repre_doc["name"] not in repre_names: + validation_state.repre_ok = False + break + + def _on_current_asset(self): + # Set initial asset as current. + asset_name = api.Session["AVALON_ASSET"] + index = self._assets_box.findText( + asset_name, QtCore.Qt.MatchFixedString + ) + if index >= 0: + print("Setting asset to {}".format(asset_name)) + self._assets_box.setCurrentIndex(index) + + def _on_accept(self): + # Use None when not a valid value or when placeholder value + selected_asset = self._assets_box.get_valid_value() + selected_subset = self._subsets_box.get_valid_value() + selected_representation = self._representations_box.get_valid_value() + + if selected_asset: + asset_doc = io.find_one({"type": "asset", "name": selected_asset}) + asset_docs_by_id = {asset_doc["_id"]: asset_doc} + else: + asset_docs_by_id = self.content_assets + + asset_docs_by_name = { + asset_doc["name"]: asset_doc + for asset_doc in asset_docs_by_id.values() + } + + asset_ids = list(asset_docs_by_id.keys()) + + subset_query = { + "type": "subset", + "parent": {"$in": asset_ids} + } + if selected_subset: + subset_query["name"] = selected_subset + + subset_docs = list(io.find(subset_query)) + subset_ids = [] + subset_docs_by_parent_and_name = collections.defaultdict(dict) + for subset in subset_docs: + subset_ids.append(subset["_id"]) + parent_id = subset["parent"] + name = subset["name"] + subset_docs_by_parent_and_name[parent_id][name] = subset + + # versions + version_docs = list(io.find({ + "type": "version", + "parent": {"$in": subset_ids} + }, sort=[("name", -1)])) + + hero_version_docs = list(io.find({ + "type": "hero_version", + "parent": {"$in": subset_ids} + })) + + version_ids = list() + + version_docs_by_parent_id = {} + for version_doc in version_docs: + parent_id = version_doc["parent"] + if parent_id not in version_docs_by_parent_id: + version_ids.append(version_doc["_id"]) + version_docs_by_parent_id[parent_id] = version_doc + + hero_version_docs_by_parent_id = {} + for hero_version_doc in hero_version_docs: + version_ids.append(hero_version_doc["_id"]) + parent_id = hero_version_doc["parent"] + hero_version_docs_by_parent_id[parent_id] = hero_version_doc + + repre_docs = io.find({ + "type": "representation", + "parent": {"$in": version_ids} + }) + repre_docs_by_parent_id_by_name = collections.defaultdict(dict) + for repre_doc in repre_docs: + parent_id = repre_doc["parent"] + name = repre_doc["name"] + repre_docs_by_parent_id_by_name[parent_id][name] = repre_doc + + for container in self._items: + container_repre_id = io.ObjectId(container["representation"]) + container_repre = self.content_repres[container_repre_id] + container_repre_name = container_repre["name"] + + container_version_id = container_repre["parent"] + container_version = self.content_versions[container_version_id] + + container_subset_id = container_version["parent"] + container_subset = self.content_subsets[container_subset_id] + container_subset_name = container_subset["name"] + + container_asset_id = container_subset["parent"] + container_asset = self.content_assets[container_asset_id] + container_asset_name = container_asset["name"] + + if selected_asset: + asset_doc = asset_docs_by_name[selected_asset] + else: + asset_doc = asset_docs_by_name[container_asset_name] + + subsets_by_name = subset_docs_by_parent_and_name[asset_doc["_id"]] + if selected_subset: + subset_doc = subsets_by_name[selected_subset] + else: + subset_doc = subsets_by_name[container_subset_name] + + repre_doc = None + subset_id = subset_doc["_id"] + if container_version["type"] == "hero_version": + hero_version = hero_version_docs_by_parent_id.get( + subset_id + ) + if hero_version: + _repres = repre_docs_by_parent_id_by_name.get( + hero_version["_id"] + ) + if selected_representation: + repre_doc = _repres.get(selected_representation) + else: + repre_doc = _repres.get(container_repre_name) + + if not repre_doc: + version_doc = version_docs_by_parent_id[subset_id] + version_id = version_doc["_id"] + repres_by_name = repre_docs_by_parent_id_by_name[version_id] + if selected_representation: + repre_doc = repres_by_name[selected_representation] + else: + repre_doc = repres_by_name[container_repre_name] + + try: + api.switch(container, repre_doc) + except Exception: + log.warning( + ( + "Couldn't switch asset." + "See traceback for more information." + ), + exc_info=True + ) + dialog = QtWidgets.QMessageBox() + dialog.setStyleSheet(style.load_stylesheet()) + dialog.setWindowTitle("Switch asset failed") + msg = "Switch asset failed. "\ + "Search console log for more details" + dialog.setText(msg) + dialog.exec_() + + self.switched.emit() + + self.close() + + +class Window(QtWidgets.QDialog): + """Scene Inventory window""" + + def __init__(self, parent=None): + QtWidgets.QDialog.__init__(self, parent) + + self.resize(1100, 480) + self.setWindowTitle( + "Scene Inventory 1.0 - {}".format( + os.getenv("AVALON_PROJECT") or "" + ) + ) + self.setObjectName("SceneInventory") + self.setProperty("saveWindowPref", True) # Maya only property! + + layout = QtWidgets.QVBoxLayout(self) + + # region control + control_layout = QtWidgets.QHBoxLayout() + filter_label = QtWidgets.QLabel("Search") + text_filter = QtWidgets.QLineEdit() + + outdated_only = QtWidgets.QCheckBox("Filter to outdated") + outdated_only.setToolTip("Show outdated files only") + outdated_only.setChecked(False) + + icon = qtawesome.icon("fa.refresh", color="white") + refresh_button = QtWidgets.QPushButton() + refresh_button.setIcon(icon) + + control_layout.addWidget(filter_label) + control_layout.addWidget(text_filter) + control_layout.addWidget(outdated_only) + control_layout.addWidget(refresh_button) + + # endregion control + self.family_config_cache = tools_lib.global_family_cache() + + model = InventoryModel(self.family_config_cache) + proxy = FilterProxyModel() + view = View() + view.setModel(proxy) + + # apply delegates + version_delegate = VersionDelegate(io, self) + column = model.Columns.index("version") + view.setItemDelegateForColumn(column, version_delegate) + + layout.addLayout(control_layout) + layout.addWidget(view) + + self.filter = text_filter + self.outdated_only = outdated_only + self.view = view + self.refresh_button = refresh_button + self.model = model + self.proxy = proxy + + # signals + text_filter.textChanged.connect(self.proxy.setFilterRegExp) + outdated_only.stateChanged.connect(self.proxy.set_filter_outdated) + refresh_button.clicked.connect(self.refresh) + view.data_changed.connect(self.refresh) + view.hierarchy_view.connect(self.model.set_hierarchy_view) + view.hierarchy_view.connect(self.proxy.set_hierarchy_view) + + # proxy settings + proxy.setSourceModel(self.model) + proxy.setDynamicSortFilter(True) + proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) + + self.data = { + "delegates": { + "version": version_delegate + } + } + + # set some nice default widths for the view + self.view.setColumnWidth(0, 250) # name + self.view.setColumnWidth(1, 55) # version + self.view.setColumnWidth(2, 55) # count + self.view.setColumnWidth(3, 150) # family + self.view.setColumnWidth(4, 100) # namespace + + self.family_config_cache.refresh() + + def keyPressEvent(self, event): + """Custom keyPressEvent. + + Override keyPressEvent to do nothing so that Maya's panels won't + take focus when pressing "SHIFT" whilst mouse is over viewport or + outliner. This way users don't accidently perform Maya commands + whilst trying to name an instance. + + """ + + def refresh(self, items=None): + with tools_lib.preserve_expanded_rows(tree_view=self.view, + role=self.model.UniqueRole): + with tools_lib.preserve_selection(tree_view=self.view, + role=self.model.UniqueRole, + current_index=False): + if self.view._hierarchy_view: + self.model.refresh(selected=self.view._selected, + items=items) + else: + self.model.refresh(items=items) + + +def show(root=None, debug=False, parent=None, items=None): + """Display Scene Inventory GUI + + Arguments: + debug (bool, optional): Run in debug-mode, + defaults to False + parent (QtCore.QObject, optional): When provided parent the interface + to this QObject. + items (list) of dictionaries - for injection of items for standalone + testing + + """ + + try: + module.window.close() + del module.window + except (RuntimeError, AttributeError): + pass + + if debug is True: + io.install() + + if not os.environ.get("AVALON_PROJECT"): + any_project = next( + project for project in io.projects() + if project.get("active", True) is not False + ) + + api.Session["AVALON_PROJECT"] = any_project["name"] + else: + api.Session["AVALON_PROJECT"] = os.environ.get("AVALON_PROJECT") + + with tools_lib.application(): + window = Window(parent) + window.setStyleSheet(style.load_stylesheet()) + window.show() + window.refresh(items=items) + + module.window = window + + # Pull window to the front. + module.window.raise_() + module.window.activateWindow() diff --git a/openpype/tools/sceneinventory/lib.py b/openpype/tools/sceneinventory/lib.py new file mode 100644 index 0000000000..0ac7622d65 --- /dev/null +++ b/openpype/tools/sceneinventory/lib.py @@ -0,0 +1,8 @@ +def walk_hierarchy(node): + """Recursively yield group node.""" + for child in node.children(): + if child.get("isGroupNode"): + yield child + + for _child in walk_hierarchy(child): + yield _child diff --git a/openpype/tools/sceneinventory/model.py b/openpype/tools/sceneinventory/model.py new file mode 100644 index 0000000000..7b4e051b36 --- /dev/null +++ b/openpype/tools/sceneinventory/model.py @@ -0,0 +1,424 @@ +import logging + +from collections import defaultdict + +from ... import api, io, style, schema +from ...vendor.Qt import QtCore, QtGui +from ...vendor import qtawesome + +from .. import lib as tools_lib +from ...lib import HeroVersionType +from ..models import TreeModel, Item + +from . import lib + +from openpype.modules import ModulesManager + + +class InventoryModel(TreeModel): + """The model for the inventory""" + + Columns = ["Name", "version", "count", "family", "loader", "objectName"] + + OUTDATED_COLOR = QtGui.QColor(235, 30, 30) + CHILD_OUTDATED_COLOR = QtGui.QColor(200, 160, 30) + GRAYOUT_COLOR = QtGui.QColor(160, 160, 160) + + UniqueRole = QtCore.Qt.UserRole + 2 # unique label role + + def __init__(self, family_config_cache, parent=None): + super(InventoryModel, self).__init__(parent) + self.log = logging.getLogger(self.__class__.__name__) + + self.family_config_cache = family_config_cache + + self._hierarchy_view = False + + manager = ModulesManager() + sync_server = manager.modules_by_name["sync_server"] + self.sync_enabled = sync_server.enabled + self._icons = {} + self.active_site = self.remote_site = None + self.active_provider = self.remote_provider = None + + if self.sync_enabled: + project = io.Session['AVALON_PROJECT'] + active_site = sync_server.get_active_site(project) + remote_site = sync_server.get_remote_site(project) + + # TODO refactor + active_provider = \ + sync_server.get_provider_for_site(project, + active_site) + if active_site == 'studio': + active_provider = 'studio' # sanitized for icon + + remote_provider = \ + sync_server.get_provider_for_site(project, + remote_site) + if remote_site == 'studio': + remote_provider = 'studio' + + # self.sync_server = sync_server + self.active_site = active_site + self.active_provider = active_provider + self.remote_site = remote_site + self.remote_provider = remote_provider + self._icons = tools_lib.get_repre_icons() + if 'active_site' not in self.Columns and \ + 'remote_site' not in self.Columns: + self.Columns.extend(['active_site', 'remote_site']) + + def outdated(self, item): + value = item.get("version") + if isinstance(value, HeroVersionType): + return False + + if item.get("version") == item.get("highest_version"): + return False + return True + + def data(self, index, role): + + if not index.isValid(): + return + + item = index.internalPointer() + + if role == QtCore.Qt.FontRole: + # Make top-level entries bold + if item.get("isGroupNode") or item.get("isNotSet"): # group-item + font = QtGui.QFont() + font.setBold(True) + return font + + if role == QtCore.Qt.ForegroundRole: + # Set the text color to the OUTDATED_COLOR when the + # collected version is not the same as the highest version + key = self.Columns[index.column()] + if key == "version": # version + if item.get("isGroupNode"): # group-item + if self.outdated(item): + return self.OUTDATED_COLOR + + if self._hierarchy_view: + # If current group is not outdated, check if any + # outdated children. + for _node in lib.walk_hierarchy(item): + if self.outdated(_node): + return self.CHILD_OUTDATED_COLOR + else: + + if self._hierarchy_view: + # Although this is not a group item, we still need + # to distinguish which one contain outdated child. + for _node in lib.walk_hierarchy(item): + if self.outdated(_node): + return self.CHILD_OUTDATED_COLOR.darker(150) + + return self.GRAYOUT_COLOR + + if key == "Name" and not item.get("isGroupNode"): + return self.GRAYOUT_COLOR + + # Add icons + if role == QtCore.Qt.DecorationRole: + if index.column() == 0: + # Override color + color = item.get("color", style.colors.default) + if item.get("isGroupNode"): # group-item + return qtawesome.icon("fa.folder", color=color) + elif item.get("isNotSet"): + return qtawesome.icon("fa.exclamation-circle", color=color) + else: + return qtawesome.icon("fa.file-o", color=color) + + if index.column() == 3: + # Family icon + return item.get("familyIcon", None) + + if item.get("isGroupNode"): + column_name = self.Columns[index.column()] + if column_name == 'active_site': + return self._icons.get(item.get('active_site_provider')) + if column_name == 'remote_site': + return self._icons.get(item.get('remote_site_provider')) + + if role == QtCore.Qt.DisplayRole and item.get("isGroupNode"): + column_name = self.Columns[index.column()] + progress = None + if column_name == 'active_site': + progress = item.get("active_site_progress", 0) + elif column_name == 'remote_site': + progress = item.get("remote_site_progress", 0) + if progress is not None: + return "{}%".format(max(progress, 0) * 100) + + if role == self.UniqueRole: + return item["representation"] + item.get("objectName", "") + + return super(InventoryModel, self).data(index, role) + + def set_hierarchy_view(self, state): + """Set whether to display subsets in hierarchy view.""" + state = bool(state) + + if state != self._hierarchy_view: + self._hierarchy_view = state + + def refresh(self, selected=None, items=None): + """Refresh the model""" + + host = api.registered_host() + if not items: # for debugging or testing, injecting items from outside + items = host.ls() + + self.clear() + + if self._hierarchy_view and selected: + + if not hasattr(host.pipeline, "update_hierarchy"): + # If host doesn't support hierarchical containers, then + # cherry-pick only. + self.add_items((item for item in items + if item["objectName"] in selected)) + + # Update hierarchy info for all containers + items_by_name = {item["objectName"]: item + for item in host.pipeline.update_hierarchy(items)} + + selected_items = set() + + def walk_children(names): + """Select containers and extend to chlid containers""" + for name in [n for n in names if n not in selected_items]: + selected_items.add(name) + item = items_by_name[name] + yield item + + for child in walk_children(item["children"]): + yield child + + items = list(walk_children(selected)) # Cherry-picked and extended + + # Cut unselected upstream containers + for item in items: + if not item.get("parent") in selected_items: + # Parent not in selection, this is root item. + item["parent"] = None + + parents = [self._root_item] + + # The length of `items` array is the maximum depth that a + # hierarchy could be. + # Take this as an easiest way to prevent looping forever. + maximum_loop = len(items) + count = 0 + while items: + if count > maximum_loop: + self.log.warning("Maximum loop count reached, possible " + "missing parent node.") + break + + _parents = list() + for parent in parents: + _unparented = list() + + def _children(): + """Child item provider""" + for item in items: + if item.get("parent") == parent.get("objectName"): + # (NOTE) + # Since `self._root_node` has no "objectName" + # entry, it will be paired with root item if + # the value of key "parent" is None, or not + # having the key. + yield item + else: + # Not current parent's child, try next + _unparented.append(item) + + self.add_items(_children(), parent) + + items[:] = _unparented + + # Parents of next level + for group_node in parent.children(): + _parents += group_node.children() + + parents[:] = _parents + count += 1 + + else: + self.add_items(items) + + def add_items(self, items, parent=None): + """Add the items to the model. + + The items should be formatted similar to `api.ls()` returns, an item + is then represented as: + {"filename_v001.ma": [full/filename/of/loaded/filename_v001.ma, + full/filename/of/loaded/filename_v001.ma], + "nodetype" : "reference", + "node": "referenceNode1"} + + Note: When performing an additional call to `add_items` it will *not* + group the new items with previously existing item groups of the + same type. + + Args: + items (generator): the items to be processed as returned by `ls()` + parent (Item, optional): Set this item as parent for the added + items when provided. Defaults to the root of the model. + + Returns: + node.Item: root node which has children added based on the data + """ + + self.beginResetModel() + + # Group by representation + grouped = defaultdict(lambda: {"items": list()}) + for item in items: + grouped[item["representation"]]["items"].append(item) + + # Add to model + not_found = defaultdict(list) + not_found_ids = [] + for repre_id, group_dict in sorted(grouped.items()): + group_items = group_dict["items"] + # Get parenthood per group + representation = io.find_one({"_id": io.ObjectId(repre_id)}) + if not representation: + not_found["representation"].append(group_items) + not_found_ids.append(repre_id) + continue + + version = io.find_one({"_id": representation["parent"]}) + if not version: + not_found["version"].append(group_items) + not_found_ids.append(repre_id) + continue + + elif version["type"] == "hero_version": + _version = io.find_one({ + "_id": version["version_id"] + }) + version["name"] = HeroVersionType(_version["name"]) + version["data"] = _version["data"] + + subset = io.find_one({"_id": version["parent"]}) + if not subset: + not_found["subset"].append(group_items) + not_found_ids.append(repre_id) + continue + + asset = io.find_one({"_id": subset["parent"]}) + if not asset: + not_found["asset"].append(group_items) + not_found_ids.append(repre_id) + continue + + grouped[repre_id].update({ + "representation": representation, + "version": version, + "subset": subset, + "asset": asset + }) + + for id in not_found_ids: + grouped.pop(id) + + for where, group_items in not_found.items(): + # create the group header + group_node = Item() + name = "< NOT FOUND - {} >".format(where) + group_node["Name"] = name + group_node["representation"] = name + group_node["count"] = len(group_items) + group_node["isGroupNode"] = False + group_node["isNotSet"] = True + + self.add_child(group_node, parent=parent) + + for items in group_items: + item_node = Item() + item_node["Name"] = ", ".join( + [item["objectName"] for item in items] + ) + self.add_child(item_node, parent=group_node) + + for repre_id, group_dict in sorted(grouped.items()): + group_items = group_dict["items"] + representation = grouped[repre_id]["representation"] + version = grouped[repre_id]["version"] + subset = grouped[repre_id]["subset"] + asset = grouped[repre_id]["asset"] + + # Get the primary family + no_family = "" + maj_version, _ = schema.get_schema_version(subset["schema"]) + if maj_version < 3: + prim_family = version["data"].get("family") + if not prim_family: + families = version["data"].get("families") + prim_family = families[0] if families else no_family + else: + families = subset["data"].get("families") or [] + prim_family = families[0] if families else no_family + + # Get the label and icon for the family if in configuration + family_config = self.family_config_cache.family_config(prim_family) + family = family_config.get("label", prim_family) + family_icon = family_config.get("icon", None) + + # Store the highest available version so the model can know + # whether current version is currently up-to-date. + highest_version = io.find_one({ + "type": "version", + "parent": version["parent"] + }, sort=[("name", -1)]) + + # create the group header + group_node = Item() + group_node["Name"] = "%s_%s: (%s)" % (asset["name"], + subset["name"], + representation["name"]) + group_node["representation"] = repre_id + group_node["version"] = version["name"] + group_node["highest_version"] = highest_version["name"] + group_node["family"] = family + group_node["familyIcon"] = family_icon + group_node["count"] = len(group_items) + group_node["isGroupNode"] = True + + if self.sync_enabled: + progress = tools_lib.get_progress_for_repre(representation, + self.active_site, + self.remote_site) + group_node["active_site"] = self.active_site + group_node["active_site_provider"] = self.active_provider + group_node["remote_site"] = self.remote_site + group_node["remote_site_provider"] = self.remote_provider + group_node["active_site_progress"] = progress[self.active_site] + group_node["remote_site_progress"] = progress[self.remote_site] + + self.add_child(group_node, parent=parent) + + for item in group_items: + item_node = Item() + item_node.update(item) + + # store the current version on the item + item_node["version"] = version["name"] + + # Remapping namespace to item name. + # Noted that the name key is capital "N", by doing this, we + # can view namespace in GUI without changing container data. + item_node["Name"] = item["namespace"] + + self.add_child(item_node, parent=group_node) + + self.endResetModel() + + return self._root_item diff --git a/openpype/tools/sceneinventory/proxy.py b/openpype/tools/sceneinventory/proxy.py new file mode 100644 index 0000000000..307e032eb6 --- /dev/null +++ b/openpype/tools/sceneinventory/proxy.py @@ -0,0 +1,148 @@ +import re + +from ...vendor.Qt import QtCore + +from . import lib + + +class FilterProxyModel(QtCore.QSortFilterProxyModel): + """Filter model to where key column's value is in the filtered tags""" + + def __init__(self, *args, **kwargs): + super(FilterProxyModel, self).__init__(*args, **kwargs) + self._filter_outdated = False + self._hierarchy_view = False + + def filterAcceptsRow(self, row, parent): + + model = self.sourceModel() + source_index = model.index(row, + self.filterKeyColumn(), + parent) + + # Always allow bottom entries (individual containers), since their + # parent group hidden if it wouldn't have been validated. + rows = model.rowCount(source_index) + if not rows: + return True + + # Filter by regex + if not self.filterRegExp().isEmpty(): + pattern = re.escape(self.filterRegExp().pattern()) + + if not self._matches(row, parent, pattern): + return False + + if self._filter_outdated: + # When filtering to outdated we filter the up to date entries + # thus we "allow" them when they are outdated + if not self._is_outdated(row, parent): + return False + + return True + + def set_filter_outdated(self, state): + """Set whether to show the outdated entries only.""" + state = bool(state) + + if state != self._filter_outdated: + self._filter_outdated = bool(state) + self.invalidateFilter() + + def set_hierarchy_view(self, state): + state = bool(state) + + if state != self._hierarchy_view: + self._hierarchy_view = state + + def _is_outdated(self, row, parent): + """Return whether row is outdated. + + A row is considered outdated if it has "version" and "highest_version" + data and in the internal data structure, and they are not of an + equal value. + + """ + def outdated(node): + version = node.get("version", None) + highest = node.get("highest_version", None) + + # Always allow indices that have no version data at all + if version is None and highest is None: + return True + + # If either a version or highest is present but not the other + # consider the item invalid. + if not self._hierarchy_view: + # Skip this check if in hierarchy view, or the child item + # node will be hidden even it's actually outdated. + if version is None or highest is None: + return False + return version != highest + + index = self.sourceModel().index(row, self.filterKeyColumn(), parent) + + # The scene contents are grouped by "representation", e.g. the same + # "representation" loaded twice is grouped under the same header. + # Since the version check filters these parent groups we skip that + # check for the individual children. + has_parent = index.parent().isValid() + if has_parent and not self._hierarchy_view: + return True + + # Filter to those that have the different version numbers + node = index.internalPointer() + is_outdated = outdated(node) + + if is_outdated: + return True + + elif self._hierarchy_view: + for _node in lib.walk_hierarchy(node): + if outdated(_node): + return True + return False + else: + return False + + def _matches(self, row, parent, pattern): + """Return whether row matches regex pattern. + + Args: + row (int): row number in model + parent (QtCore.QModelIndex): parent index + pattern (regex.pattern): pattern to check for in key + + Returns: + bool + + """ + model = self.sourceModel() + column = self.filterKeyColumn() + role = self.filterRole() + + def matches(row, parent, pattern): + index = model.index(row, column, parent) + key = model.data(index, role) + if re.search(pattern, key, re.IGNORECASE): + return True + + if not matches(row, parent, pattern): + # Also allow if any of the children matches + source_index = model.index(row, column, parent) + rows = model.rowCount(source_index) + + if not any(matches(i, source_index, pattern) + for i in range(rows)): + + if self._hierarchy_view: + for i in range(rows): + child_i = model.index(i, column, source_index) + child_rows = model.rowCount(child_i) + return any(self._matches(ch_i, child_i, pattern) + for ch_i in range(child_rows)) + + else: + return False + + return True From 48005747cdf956b1356919840ec2b4cb2c79a322 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 15 Nov 2021 18:53:45 +0100 Subject: [PATCH 02/16] renamed app.py to window.py and renamed Window to SceneInventoryWindow --- openpype/tools/sceneinventory/__init__.py | 8 +++++--- openpype/tools/sceneinventory/{app.py => window.py} | 6 +++--- 2 files changed, 8 insertions(+), 6 deletions(-) rename openpype/tools/sceneinventory/{app.py => window.py} (99%) diff --git a/openpype/tools/sceneinventory/__init__.py b/openpype/tools/sceneinventory/__init__.py index 694caf15fe..410b52e5fe 100644 --- a/openpype/tools/sceneinventory/__init__.py +++ b/openpype/tools/sceneinventory/__init__.py @@ -1,7 +1,9 @@ -from .app import ( +from .window import ( show, + SceneInventoryWindow ) -__all__ = [ +__all__ = ( "show", -] + "SceneInventoryWindow" +) diff --git a/openpype/tools/sceneinventory/app.py b/openpype/tools/sceneinventory/window.py similarity index 99% rename from openpype/tools/sceneinventory/app.py rename to openpype/tools/sceneinventory/window.py index 5304b7ac12..93c1debe3d 100644 --- a/openpype/tools/sceneinventory/app.py +++ b/openpype/tools/sceneinventory/window.py @@ -1799,11 +1799,11 @@ class SwitchAssetDialog(QtWidgets.QDialog): self.close() -class Window(QtWidgets.QDialog): +class SceneInventoryWindow(QtWidgets.QDialog): """Scene Inventory window""" def __init__(self, parent=None): - QtWidgets.QDialog.__init__(self, parent) + super(SceneInventoryWindow, self).__init__(parent) self.resize(1100, 480) self.setWindowTitle( @@ -1941,7 +1941,7 @@ def show(root=None, debug=False, parent=None, items=None): api.Session["AVALON_PROJECT"] = os.environ.get("AVALON_PROJECT") with tools_lib.application(): - window = Window(parent) + window = SceneInventoryWindow(parent) window.setStyleSheet(style.load_stylesheet()) window.show() window.refresh(items=items) From 0c8a517d075fddd3eff73e68a124a167231977ee Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 15 Nov 2021 19:06:16 +0100 Subject: [PATCH 03/16] separated switch dialog from window file --- .../tools/sceneinventory/switch_dialog.py | 989 ++++++++++++++++ openpype/tools/sceneinventory/widgets.py | 51 + openpype/tools/sceneinventory/window.py | 1021 +---------------- 3 files changed, 1041 insertions(+), 1020 deletions(-) create mode 100644 openpype/tools/sceneinventory/switch_dialog.py create mode 100644 openpype/tools/sceneinventory/widgets.py diff --git a/openpype/tools/sceneinventory/switch_dialog.py b/openpype/tools/sceneinventory/switch_dialog.py new file mode 100644 index 0000000000..37659b2370 --- /dev/null +++ b/openpype/tools/sceneinventory/switch_dialog.py @@ -0,0 +1,989 @@ +import collections +from Qt import QtWidgets, QtCore + +from avalon import io, api, style +from avalon.vendor import qtawesome + +from .widgets import SearchComboBox + + +class ValidationState: + def __init__(self): + self.asset_ok = True + self.subset_ok = True + self.repre_ok = True + + @property + def all_ok(self): + return ( + self.asset_ok + and self.subset_ok + and self.repre_ok + ) + + +class SwitchAssetDialog(QtWidgets.QDialog): + """Widget to support asset switching""" + + MIN_WIDTH = 550 + + switched = QtCore.Signal() + + def __init__(self, parent=None, items=None): + super(SwitchAssetDialog, self).__init__(parent) + + self.setWindowTitle("Switch selected items ...") + + # Force and keep focus dialog + self.setModal(True) + + assets_combox = SearchComboBox(self) + subsets_combox = SearchComboBox(self) + repres_combobox = SearchComboBox(self) + + assets_combox.set_placeholder("") + subsets_combox.set_placeholder("") + repres_combobox.set_placeholder("") + + asset_label = QtWidgets.QLabel(self) + subset_label = QtWidgets.QLabel(self) + repre_label = QtWidgets.QLabel(self) + + current_asset_btn = QtWidgets.QPushButton("Use current asset") + + accept_icon = qtawesome.icon("fa.check", color="white") + accept_btn = QtWidgets.QPushButton(self) + accept_btn.setIcon(accept_icon) + + main_layout = QtWidgets.QGridLayout(self) + # Asset column + main_layout.addWidget(current_asset_btn, 0, 0) + main_layout.addWidget(assets_combox, 1, 0) + main_layout.addWidget(asset_label, 2, 0) + # Subset column + main_layout.addWidget(subsets_combox, 1, 1) + main_layout.addWidget(subset_label, 2, 1) + # Representation column + main_layout.addWidget(repres_combobox, 1, 2) + main_layout.addWidget(repre_label, 2, 2) + # Btn column + main_layout.addWidget(accept_btn, 1, 3) + + assets_combox.currentIndexChanged.connect( + self._combobox_value_changed + ) + subsets_combox.currentIndexChanged.connect( + self._combobox_value_changed + ) + repres_combobox.currentIndexChanged.connect( + self._combobox_value_changed + ) + accept_btn.clicked.connect(self._on_accept) + current_asset_btn.clicked.connect(self._on_current_asset) + + self._current_asset_btn = current_asset_btn + + self._assets_box = assets_combox + self._subsets_box = subsets_combox + self._representations_box = repres_combobox + + self._asset_label = asset_label + self._subset_label = subset_label + self._repre_label = repre_label + + self._accept_btn = accept_btn + + self._init_asset_name = None + self._init_subset_name = None + self._init_repre_name = None + + self._fill_check = False + + self._items = items + self._prepare_content_data() + self.refresh(True) + + self.setMinimumWidth(self.MIN_WIDTH) + + # Set default focus to accept button so you don't directly type in + # first asset field, this also allows to see the placeholder value. + accept_btn.setFocus() + + def _prepare_content_data(self): + repre_ids = [ + io.ObjectId(item["representation"]) + for item in self._items + ] + repres = list(io.find({ + "type": {"$in": ["representation", "archived_representation"]}, + "_id": {"$in": repre_ids} + })) + repres_by_id = {repre["_id"]: repre for repre in repres} + + # stash context values, works only for single representation + if len(repres) == 1: + self._init_asset_name = repres[0]["context"]["asset"] + self._init_subset_name = repres[0]["context"]["subset"] + self._init_repre_name = repres[0]["context"]["representation"] + + content_repres = {} + archived_repres = [] + missing_repres = [] + version_ids = [] + for repre_id in repre_ids: + if repre_id not in repres_by_id: + missing_repres.append(repre_id) + elif repres_by_id[repre_id]["type"] == "archived_representation": + repre = repres_by_id[repre_id] + archived_repres.append(repre) + version_ids.append(repre["parent"]) + else: + repre = repres_by_id[repre_id] + content_repres[repre_id] = repres_by_id[repre_id] + version_ids.append(repre["parent"]) + + versions = io.find({ + "type": {"$in": ["version", "hero_version"]}, + "_id": {"$in": list(set(version_ids))} + }) + content_versions = {} + hero_version_ids = set() + for version in versions: + content_versions[version["_id"]] = version + if version["type"] == "hero_version": + hero_version_ids.add(version["_id"]) + + missing_versions = [] + subset_ids = [] + for version_id in version_ids: + if version_id not in content_versions: + missing_versions.append(version_id) + else: + subset_ids.append(content_versions[version_id]["parent"]) + + subsets = io.find({ + "type": {"$in": ["subset", "archived_subset"]}, + "_id": {"$in": subset_ids} + }) + subsets_by_id = {sub["_id"]: sub for sub in subsets} + + asset_ids = [] + archived_subsets = [] + missing_subsets = [] + content_subsets = {} + for subset_id in subset_ids: + if subset_id not in subsets_by_id: + missing_subsets.append(subset_id) + elif subsets_by_id[subset_id]["type"] == "archived_subset": + subset = subsets_by_id[subset_id] + asset_ids.append(subset["parent"]) + archived_subsets.append(subset) + else: + subset = subsets_by_id[subset_id] + asset_ids.append(subset["parent"]) + content_subsets[subset_id] = subset + + assets = io.find({ + "type": {"$in": ["asset", "archived_asset"]}, + "_id": {"$in": list(asset_ids)} + }) + assets_by_id = {asset["_id"]: asset for asset in assets} + + missing_assets = [] + archived_assets = [] + content_assets = {} + for asset_id in asset_ids: + if asset_id not in assets_by_id: + missing_assets.append(asset_id) + elif assets_by_id[asset_id]["type"] == "archived_asset": + archived_assets.append(assets_by_id[asset_id]) + else: + content_assets[asset_id] = assets_by_id[asset_id] + + self.content_assets = content_assets + self.content_subsets = content_subsets + self.content_versions = content_versions + self.content_repres = content_repres + + self.hero_version_ids = hero_version_ids + + self.missing_assets = missing_assets + self.missing_versions = missing_versions + self.missing_subsets = missing_subsets + self.missing_repres = missing_repres + self.missing_docs = ( + bool(missing_assets) + or bool(missing_versions) + or bool(missing_subsets) + or bool(missing_repres) + ) + + self.archived_assets = archived_assets + self.archived_subsets = archived_subsets + self.archived_repres = archived_repres + + def _combobox_value_changed(self, *args, **kwargs): + self.refresh() + + def refresh(self, init_refresh=False): + """Build the need comboboxes with content""" + if not self._fill_check and not init_refresh: + return + + self._fill_check = False + + if init_refresh: + asset_values = self._get_asset_box_values() + self._fill_combobox(asset_values, "asset") + + validation_state = ValidationState() + + # Set other comboboxes to empty if any document is missing or any asset + # of loaded representations is archived. + self._is_asset_ok(validation_state) + if validation_state.asset_ok: + subset_values = self._get_subset_box_values() + self._fill_combobox(subset_values, "subset") + self._is_subset_ok(validation_state) + + if validation_state.asset_ok and validation_state.subset_ok: + repre_values = sorted(self._representations_box_values()) + self._fill_combobox(repre_values, "repre") + self._is_repre_ok(validation_state) + + # Fill comboboxes with values + self.set_labels() + self.apply_validations(validation_state) + + if init_refresh: # pre select context if possible + self._assets_box.set_valid_value(self._init_asset_name) + self._subsets_box.set_valid_value(self._init_subset_name) + self._representations_box.set_valid_value(self._init_repre_name) + + self._fill_check = True + + def _get_loaders(self, representations): + if not representations: + return list() + + available_loaders = filter( + lambda l: not (hasattr(l, "is_utility") and l.is_utility), + api.discover(api.Loader) + ) + + loaders = set() + + for representation in representations: + for loader in api.loaders_from_representation( + available_loaders, + representation + ): + loaders.add(loader) + + return loaders + + def _fill_combobox(self, values, combobox_type): + if combobox_type == "asset": + combobox_widget = self._assets_box + elif combobox_type == "subset": + combobox_widget = self._subsets_box + elif combobox_type == "repre": + combobox_widget = self._representations_box + else: + return + selected_value = combobox_widget.get_valid_value() + + # Fill combobox + if values is not None: + combobox_widget.populate(list(sorted(values))) + if selected_value and selected_value in values: + index = None + for idx in range(combobox_widget.count()): + if selected_value == str(combobox_widget.itemText(idx)): + index = idx + break + if index is not None: + combobox_widget.setCurrentIndex(index) + + def set_labels(self): + asset_label = self._assets_box.get_valid_value() + subset_label = self._subsets_box.get_valid_value() + repre_label = self._representations_box.get_valid_value() + + default = "*No changes" + self._asset_label.setText(asset_label or default) + self._subset_label.setText(subset_label or default) + self._repre_label.setText(repre_label or default) + + def apply_validations(self, validation_state): + error_msg = "*Please select" + error_sheet = "border: 1px solid red;" + success_sheet = "border: 1px solid green;" + + asset_sheet = None + subset_sheet = None + repre_sheet = None + accept_sheet = None + if validation_state.asset_ok is False: + asset_sheet = error_sheet + self._asset_label.setText(error_msg) + elif validation_state.subset_ok is False: + subset_sheet = error_sheet + self._subset_label.setText(error_msg) + elif validation_state.repre_ok is False: + repre_sheet = error_sheet + self._repre_label.setText(error_msg) + + if validation_state.all_ok: + accept_sheet = success_sheet + + self._assets_box.setStyleSheet(asset_sheet or "") + self._subsets_box.setStyleSheet(subset_sheet or "") + self._representations_box.setStyleSheet(repre_sheet or "") + + self._accept_btn.setEnabled(validation_state.all_ok) + self._accept_btn.setStyleSheet(accept_sheet or "") + + def _get_asset_box_values(self): + asset_docs = io.find( + {"type": "asset"}, + {"_id": 1, "name": 1} + ) + asset_names_by_id = { + asset_doc["_id"]: asset_doc["name"] + for asset_doc in asset_docs + } + subsets = io.find( + { + "type": "subset", + "parent": {"$in": list(asset_names_by_id.keys())} + }, + { + "parent": 1 + } + ) + + filtered_assets = [] + for subset in subsets: + asset_name = asset_names_by_id[subset["parent"]] + if asset_name not in filtered_assets: + filtered_assets.append(asset_name) + return sorted(filtered_assets) + + def _get_subset_box_values(self): + selected_asset = self._assets_box.get_valid_value() + if selected_asset: + asset_doc = io.find_one({"type": "asset", "name": selected_asset}) + asset_ids = [asset_doc["_id"]] + else: + asset_ids = list(self.content_assets.keys()) + + subsets = io.find( + { + "type": "subset", + "parent": {"$in": asset_ids} + }, + { + "parent": 1, + "name": 1 + } + ) + + subset_names_by_parent_id = collections.defaultdict(set) + for subset in subsets: + subset_names_by_parent_id[subset["parent"]].add(subset["name"]) + + possible_subsets = None + for subset_names in subset_names_by_parent_id.values(): + if possible_subsets is None: + possible_subsets = subset_names + else: + possible_subsets = (possible_subsets & subset_names) + + if not possible_subsets: + break + + return list(possible_subsets or list()) + + def _representations_box_values(self): + # NOTE hero versions are not used because it is expected that + # hero version has same representations as latests + selected_asset = self._assets_box.currentText() + selected_subset = self._subsets_box.currentText() + + # If nothing is selected + # [ ] [ ] [?] + if not selected_asset and not selected_subset: + # Find all representations of selection's subsets + possible_repres = list(io.find( + { + "type": "representation", + "parent": {"$in": list(self.content_versions.keys())} + }, + { + "parent": 1, + "name": 1 + } + )) + + possible_repres_by_parent = collections.defaultdict(set) + for repre in possible_repres: + possible_repres_by_parent[repre["parent"]].add(repre["name"]) + + output_repres = None + for repre_names in possible_repres_by_parent.values(): + if output_repres is None: + output_repres = repre_names + else: + output_repres = (output_repres & repre_names) + + if not output_repres: + break + + return list(output_repres or list()) + + # [x] [x] [?] + if selected_asset and selected_subset: + asset_doc = io.find_one( + {"type": "asset", "name": selected_asset}, + {"_id": 1} + ) + subset_doc = io.find_one( + { + "type": "subset", + "name": selected_subset, + "parent": asset_doc["_id"] + }, + {"_id": 1} + ) + subset_id = subset_doc["_id"] + last_versions_by_subset_id = self.find_last_versions([subset_id]) + version_doc = last_versions_by_subset_id.get(subset_id) + repre_docs = io.find( + { + "type": "representation", + "parent": version_doc["_id"] + }, + { + "name": 1 + } + ) + return [ + repre_doc["name"] + for repre_doc in repre_docs + ] + + # [x] [ ] [?] + # If asset only is selected + if selected_asset: + asset_doc = io.find_one( + {"type": "asset", "name": selected_asset}, + {"_id": 1} + ) + if not asset_doc: + return list() + + # Filter subsets by subset names from content + subset_names = set() + for subset_doc in self.content_subsets.values(): + subset_names.add(subset_doc["name"]) + subset_docs = io.find( + { + "type": "subset", + "parent": asset_doc["_id"], + "name": {"$in": list(subset_names)} + }, + {"_id": 1} + ) + subset_ids = [ + subset_doc["_id"] + for subset_doc in subset_docs + ] + if not subset_ids: + return list() + + last_versions_by_subset_id = self.find_last_versions(subset_ids) + subset_id_by_version_id = {} + for subset_id, last_version in last_versions_by_subset_id.items(): + version_id = last_version["_id"] + subset_id_by_version_id[version_id] = subset_id + + if not subset_id_by_version_id: + return list() + + repre_docs = list(io.find( + { + "type": "representation", + "parent": {"$in": list(subset_id_by_version_id.keys())} + }, + { + "name": 1, + "parent": 1 + } + )) + if not repre_docs: + return list() + + repre_names_by_parent = collections.defaultdict(set) + for repre_doc in repre_docs: + repre_names_by_parent[repre_doc["parent"]].add( + repre_doc["name"] + ) + + available_repres = None + for repre_names in repre_names_by_parent.values(): + if available_repres is None: + available_repres = repre_names + continue + + available_repres = available_repres.intersection(repre_names) + + return list(available_repres) + + # [ ] [x] [?] + subset_docs = list(io.find( + { + "type": "subset", + "parent": {"$in": list(self.content_assets.keys())}, + "name": selected_subset + }, + {"_id": 1, "parent": 1} + )) + if not subset_docs: + return list() + + subset_docs_by_id = { + subset_doc["_id"]: subset_doc + for subset_doc in subset_docs + } + last_versions_by_subset_id = self.find_last_versions( + subset_docs_by_id.keys() + ) + + subset_id_by_version_id = {} + for subset_id, last_version in last_versions_by_subset_id.items(): + version_id = last_version["_id"] + subset_id_by_version_id[version_id] = subset_id + + if not subset_id_by_version_id: + return list() + + repre_docs = list(io.find( + { + "type": "representation", + "parent": {"$in": list(subset_id_by_version_id.keys())} + }, + { + "name": 1, + "parent": 1 + } + )) + if not repre_docs: + return list() + + repre_names_by_asset_id = {} + for repre_doc in repre_docs: + subset_id = subset_id_by_version_id[repre_doc["parent"]] + asset_id = subset_docs_by_id[subset_id]["parent"] + if asset_id not in repre_names_by_asset_id: + repre_names_by_asset_id[asset_id] = set() + repre_names_by_asset_id[asset_id].add(repre_doc["name"]) + + available_repres = None + for repre_names in repre_names_by_asset_id.values(): + if available_repres is None: + available_repres = repre_names + continue + + available_repres = available_repres.intersection(repre_names) + + return list(available_repres) + + def _is_asset_ok(self, validation_state): + selected_asset = self._assets_box.get_valid_value() + if ( + selected_asset is None + and (self.missing_docs or self.archived_assets) + ): + validation_state.asset_ok = False + + def _is_subset_ok(self, validation_state): + selected_asset = self._assets_box.get_valid_value() + selected_subset = self._subsets_box.get_valid_value() + + # [?] [x] [?] + # If subset is selected then must be ok + if selected_subset is not None: + return + + # [ ] [ ] [?] + if selected_asset is None: + # If there were archived subsets and asset is not selected + if self.archived_subsets: + validation_state.subset_ok = False + return + + # [x] [ ] [?] + asset_doc = io.find_one( + {"type": "asset", "name": selected_asset}, + {"_id": 1} + ) + subset_docs = io.find( + {"type": "subset", "parent": asset_doc["_id"]}, + {"name": 1} + ) + subset_names = set( + subset_doc["name"] + for subset_doc in subset_docs + ) + + for subset_doc in self.content_subsets.values(): + if subset_doc["name"] not in subset_names: + validation_state.subset_ok = False + break + + def find_last_versions(self, subset_ids): + _pipeline = [ + # Find all versions of those subsets + {"$match": { + "type": "version", + "parent": {"$in": list(subset_ids)} + }}, + # Sorting versions all together + {"$sort": {"name": 1}}, + # Group them by "parent", but only take the last + {"$group": { + "_id": "$parent", + "_version_id": {"$last": "$_id"}, + "type": {"$last": "$type"} + }} + ] + last_versions_by_subset_id = dict() + for doc in io.aggregate(_pipeline): + doc["parent"] = doc["_id"] + doc["_id"] = doc.pop("_version_id") + last_versions_by_subset_id[doc["parent"]] = doc + return last_versions_by_subset_id + + def _is_repre_ok(self, validation_state): + selected_asset = self._assets_box.get_valid_value() + selected_subset = self._subsets_box.get_valid_value() + selected_repre = self._representations_box.get_valid_value() + + # [?] [?] [x] + # If subset is selected then must be ok + if selected_repre is not None: + return + + # [ ] [ ] [ ] + if selected_asset is None and selected_subset is None: + if ( + self.archived_repres + or self.missing_versions + or self.missing_repres + ): + validation_state.repre_ok = False + return + + # [x] [x] [ ] + if selected_asset is not None and selected_subset is not None: + asset_doc = io.find_one( + {"type": "asset", "name": selected_asset}, + {"_id": 1} + ) + subset_doc = io.find_one( + { + "type": "subset", + "parent": asset_doc["_id"], + "name": selected_subset + }, + {"_id": 1} + ) + last_versions_by_subset_id = self.find_last_versions( + [subset_doc["_id"]] + ) + last_version = last_versions_by_subset_id.get(subset_doc["_id"]) + if not last_version: + validation_state.repre_ok = False + return + + repre_docs = io.find( + { + "type": "representation", + "parent": last_version["_id"] + }, + {"name": 1} + ) + + repre_names = set( + repre_doc["name"] + for repre_doc in repre_docs + ) + for repre_doc in self.content_repres.values(): + if repre_doc["name"] not in repre_names: + validation_state.repre_ok = False + break + return + + # [x] [ ] [ ] + if selected_asset is not None: + asset_doc = io.find_one( + {"type": "asset", "name": selected_asset}, + {"_id": 1} + ) + subset_docs = list(io.find( + { + "type": "subset", + "parent": asset_doc["_id"] + }, + {"_id": 1, "name": 1} + )) + + subset_name_by_id = {} + subset_ids = set() + for subset_doc in subset_docs: + subset_id = subset_doc["_id"] + subset_ids.add(subset_id) + subset_name_by_id[subset_id] = subset_doc["name"] + + last_versions_by_subset_id = self.find_last_versions(subset_ids) + + subset_id_by_version_id = {} + for subset_id, last_version in last_versions_by_subset_id.items(): + version_id = last_version["_id"] + subset_id_by_version_id[version_id] = subset_id + + repre_docs = io.find( + { + "type": "representation", + "parent": {"$in": list(subset_id_by_version_id.keys())} + }, + { + "name": 1, + "parent": 1 + } + ) + repres_by_subset_name = {} + for repre_doc in repre_docs: + subset_id = subset_id_by_version_id[repre_doc["parent"]] + subset_name = subset_name_by_id[subset_id] + if subset_name not in repres_by_subset_name: + repres_by_subset_name[subset_name] = set() + repres_by_subset_name[subset_name].add(repre_doc["name"]) + + for repre_doc in self.content_repres.values(): + version_doc = self.content_versions[repre_doc["parent"]] + subset_doc = self.content_subsets[version_doc["parent"]] + repre_names = ( + repres_by_subset_name.get(subset_doc["name"]) or [] + ) + if repre_doc["name"] not in repre_names: + validation_state.repre_ok = False + break + return + + # [ ] [x] [ ] + # Subset documents + subset_docs = io.find( + { + "type": "subset", + "parent": {"$in": list(self.content_assets.keys())}, + "name": selected_subset + }, + {"_id": 1, "name": 1, "parent": 1} + ) + + subset_docs_by_id = {} + for subset_doc in subset_docs: + subset_docs_by_id[subset_doc["_id"]] = subset_doc + + last_versions_by_subset_id = self.find_last_versions( + subset_docs_by_id.keys() + ) + subset_id_by_version_id = {} + for subset_id, last_version in last_versions_by_subset_id.items(): + version_id = last_version["_id"] + subset_id_by_version_id[version_id] = subset_id + + repre_docs = io.find( + { + "type": "representation", + "parent": {"$in": list(subset_id_by_version_id.keys())} + }, + { + "name": 1, + "parent": 1 + } + ) + repres_by_asset_id = {} + for repre_doc in repre_docs: + subset_id = subset_id_by_version_id[repre_doc["parent"]] + asset_id = subset_docs_by_id[subset_id]["parent"] + if asset_id not in repres_by_asset_id: + repres_by_asset_id[asset_id] = set() + repres_by_asset_id[asset_id].add(repre_doc["name"]) + + for repre_doc in self.content_repres.values(): + version_doc = self.content_versions[repre_doc["parent"]] + subset_doc = self.content_subsets[version_doc["parent"]] + asset_id = subset_doc["parent"] + repre_names = ( + repres_by_asset_id.get(asset_id) or [] + ) + if repre_doc["name"] not in repre_names: + validation_state.repre_ok = False + break + + def _on_current_asset(self): + # Set initial asset as current. + asset_name = io.Session["AVALON_ASSET"] + index = self._assets_box.findText( + asset_name, QtCore.Qt.MatchFixedString + ) + if index >= 0: + print("Setting asset to {}".format(asset_name)) + self._assets_box.setCurrentIndex(index) + + def _on_accept(self): + # Use None when not a valid value or when placeholder value + selected_asset = self._assets_box.get_valid_value() + selected_subset = self._subsets_box.get_valid_value() + selected_representation = self._representations_box.get_valid_value() + + if selected_asset: + asset_doc = io.find_one({"type": "asset", "name": selected_asset}) + asset_docs_by_id = {asset_doc["_id"]: asset_doc} + else: + asset_docs_by_id = self.content_assets + + asset_docs_by_name = { + asset_doc["name"]: asset_doc + for asset_doc in asset_docs_by_id.values() + } + + asset_ids = list(asset_docs_by_id.keys()) + + subset_query = { + "type": "subset", + "parent": {"$in": asset_ids} + } + if selected_subset: + subset_query["name"] = selected_subset + + subset_docs = list(io.find(subset_query)) + subset_ids = [] + subset_docs_by_parent_and_name = collections.defaultdict(dict) + for subset in subset_docs: + subset_ids.append(subset["_id"]) + parent_id = subset["parent"] + name = subset["name"] + subset_docs_by_parent_and_name[parent_id][name] = subset + + # versions + version_docs = list(io.find({ + "type": "version", + "parent": {"$in": subset_ids} + }, sort=[("name", -1)])) + + hero_version_docs = list(io.find({ + "type": "hero_version", + "parent": {"$in": subset_ids} + })) + + version_ids = list() + + version_docs_by_parent_id = {} + for version_doc in version_docs: + parent_id = version_doc["parent"] + if parent_id not in version_docs_by_parent_id: + version_ids.append(version_doc["_id"]) + version_docs_by_parent_id[parent_id] = version_doc + + hero_version_docs_by_parent_id = {} + for hero_version_doc in hero_version_docs: + version_ids.append(hero_version_doc["_id"]) + parent_id = hero_version_doc["parent"] + hero_version_docs_by_parent_id[parent_id] = hero_version_doc + + repre_docs = io.find({ + "type": "representation", + "parent": {"$in": version_ids} + }) + repre_docs_by_parent_id_by_name = collections.defaultdict(dict) + for repre_doc in repre_docs: + parent_id = repre_doc["parent"] + name = repre_doc["name"] + repre_docs_by_parent_id_by_name[parent_id][name] = repre_doc + + for container in self._items: + container_repre_id = io.ObjectId(container["representation"]) + container_repre = self.content_repres[container_repre_id] + container_repre_name = container_repre["name"] + + container_version_id = container_repre["parent"] + container_version = self.content_versions[container_version_id] + + container_subset_id = container_version["parent"] + container_subset = self.content_subsets[container_subset_id] + container_subset_name = container_subset["name"] + + container_asset_id = container_subset["parent"] + container_asset = self.content_assets[container_asset_id] + container_asset_name = container_asset["name"] + + if selected_asset: + asset_doc = asset_docs_by_name[selected_asset] + else: + asset_doc = asset_docs_by_name[container_asset_name] + + subsets_by_name = subset_docs_by_parent_and_name[asset_doc["_id"]] + if selected_subset: + subset_doc = subsets_by_name[selected_subset] + else: + subset_doc = subsets_by_name[container_subset_name] + + repre_doc = None + subset_id = subset_doc["_id"] + if container_version["type"] == "hero_version": + hero_version = hero_version_docs_by_parent_id.get( + subset_id + ) + if hero_version: + _repres = repre_docs_by_parent_id_by_name.get( + hero_version["_id"] + ) + if selected_representation: + repre_doc = _repres.get(selected_representation) + else: + repre_doc = _repres.get(container_repre_name) + + if not repre_doc: + version_doc = version_docs_by_parent_id[subset_id] + version_id = version_doc["_id"] + repres_by_name = repre_docs_by_parent_id_by_name[version_id] + if selected_representation: + repre_doc = repres_by_name[selected_representation] + else: + repre_doc = repres_by_name[container_repre_name] + + try: + api.switch(container, repre_doc) + except Exception: + log.warning( + ( + "Couldn't switch asset." + "See traceback for more information." + ), + exc_info=True + ) + dialog = QtWidgets.QMessageBox() + dialog.setStyleSheet(style.load_stylesheet()) + dialog.setWindowTitle("Switch asset failed") + msg = "Switch asset failed. "\ + "Search console log for more details" + dialog.setText(msg) + dialog.exec_() + + self.switched.emit() + + self.close() diff --git a/openpype/tools/sceneinventory/widgets.py b/openpype/tools/sceneinventory/widgets.py new file mode 100644 index 0000000000..6bb74d2d1b --- /dev/null +++ b/openpype/tools/sceneinventory/widgets.py @@ -0,0 +1,51 @@ +from Qt import QtWidgets, QtCore + + +class SearchComboBox(QtWidgets.QComboBox): + """Searchable ComboBox with empty placeholder value as first value""" + + def __init__(self, parent=None): + super(SearchComboBox, self).__init__(parent) + + self.setEditable(True) + self.setInsertPolicy(self.NoInsert) + + # Apply completer settings + completer = self.completer() + completer.setCompletionMode(completer.PopupCompletion) + completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive) + + # Force style sheet on popup menu + # It won't take the parent stylesheet for some reason + # todo: better fix for completer popup stylesheet + # if module.window: + # popup = completer.popup() + # popup.setStyleSheet(module.window.styleSheet()) + + def set_placeholder(self, placeholder): + self.lineEdit().setPlaceholderText(placeholder) + + def populate(self, items): + self.clear() + self.addItems([""]) # ensure first item is placeholder + self.addItems(items) + + def get_valid_value(self): + """Return the current text if it's a valid value else None + + Note: The empty placeholder value is valid and returns as "" + + """ + + text = self.currentText() + lookup = set(self.itemText(i) for i in range(self.count())) + if text not in lookup: + return None + + return text or None + + def set_valid_value(self, value): + """Try to locate 'value' and pre-select it in dropdown.""" + index = self.findText(value) + if index > -1: + self.setCurrentIndex(index) diff --git a/openpype/tools/sceneinventory/window.py b/openpype/tools/sceneinventory/window.py index 93c1debe3d..1bd96ef85e 100644 --- a/openpype/tools/sceneinventory/window.py +++ b/openpype/tools/sceneinventory/window.py @@ -14,6 +14,7 @@ from ..delegates import VersionDelegate from .proxy import FilterProxyModel from .model import InventoryModel +from .switch_dialog import SwitchAssetDialog from openpype.modules import ModulesManager @@ -779,1026 +780,6 @@ class View(QtWidgets.QTreeView): dialog.exec_() -class SearchComboBox(QtWidgets.QComboBox): - """Searchable ComboBox with empty placeholder value as first value""" - - def __init__(self, parent=None, placeholder=""): - super(SearchComboBox, self).__init__(parent) - - self.setEditable(True) - self.setInsertPolicy(self.NoInsert) - self.lineEdit().setPlaceholderText(placeholder) - - # Apply completer settings - completer = self.completer() - completer.setCompletionMode(completer.PopupCompletion) - completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive) - - # Force style sheet on popup menu - # It won't take the parent stylesheet for some reason - # todo: better fix for completer popup stylesheet - if module.window: - popup = completer.popup() - popup.setStyleSheet(module.window.styleSheet()) - - def populate(self, items): - self.clear() - self.addItems([""]) # ensure first item is placeholder - self.addItems(items) - - def get_valid_value(self): - """Return the current text if it's a valid value else None - - Note: The empty placeholder value is valid and returns as "" - - """ - - text = self.currentText() - lookup = set(self.itemText(i) for i in range(self.count())) - if text not in lookup: - return None - - return text or None - - def set_valid_value(self, value): - """Try to locate 'value' and pre-select it in dropdown.""" - index = self.findText(value) - if index > -1: - self.setCurrentIndex(index) - - -class ValidationState: - def __init__(self): - self.asset_ok = True - self.subset_ok = True - self.repre_ok = True - - @property - def all_ok(self): - return ( - self.asset_ok - and self.subset_ok - and self.repre_ok - ) - - -class SwitchAssetDialog(QtWidgets.QDialog): - """Widget to support asset switching""" - - MIN_WIDTH = 550 - - fill_check = False - switched = QtCore.Signal() - - def __init__(self, parent=None, items=None): - QtWidgets.QDialog.__init__(self, parent) - - self.setWindowTitle("Switch selected items ...") - - # Force and keep focus dialog - self.setModal(True) - - self._assets_box = SearchComboBox(placeholder="") - self._subsets_box = SearchComboBox(placeholder="") - self._representations_box = SearchComboBox( - placeholder="" - ) - - self._asset_label = QtWidgets.QLabel("") - self._subset_label = QtWidgets.QLabel("") - self._repre_label = QtWidgets.QLabel("") - - self.current_asset_btn = QtWidgets.QPushButton("Use current asset") - - main_layout = QtWidgets.QGridLayout(self) - - accept_icon = qtawesome.icon("fa.check", color="white") - accept_btn = QtWidgets.QPushButton() - accept_btn.setIcon(accept_icon) - accept_btn.setFixedWidth(24) - accept_btn.setFixedHeight(24) - - # Asset column - main_layout.addWidget(self.current_asset_btn, 0, 0) - main_layout.addWidget(self._assets_box, 1, 0) - main_layout.addWidget(self._asset_label, 2, 0) - # Subset column - main_layout.addWidget(self._subsets_box, 1, 1) - main_layout.addWidget(self._subset_label, 2, 1) - # Representation column - main_layout.addWidget(self._representations_box, 1, 2) - main_layout.addWidget(self._repre_label, 2, 2) - # Btn column - main_layout.addWidget(accept_btn, 1, 3) - - self._accept_btn = accept_btn - - self._assets_box.currentIndexChanged.connect( - self._combobox_value_changed - ) - self._subsets_box.currentIndexChanged.connect( - self._combobox_value_changed - ) - self._representations_box.currentIndexChanged.connect( - self._combobox_value_changed - ) - self._accept_btn.clicked.connect(self._on_accept) - self.current_asset_btn.clicked.connect(self._on_current_asset) - - self._init_asset_name = None - self._init_subset_name = None - self._init_repre_name = None - - self._items = items - self._prepare_content_data() - self.refresh(True) - - self.setMinimumWidth(self.MIN_WIDTH) - - # Set default focus to accept button so you don't directly type in - # first asset field, this also allows to see the placeholder value. - accept_btn.setFocus() - - def _prepare_content_data(self): - repre_ids = [ - io.ObjectId(item["representation"]) - for item in self._items - ] - repres = list(io.find({ - "type": {"$in": ["representation", "archived_representation"]}, - "_id": {"$in": repre_ids} - })) - repres_by_id = {repre["_id"]: repre for repre in repres} - - # stash context values, works only for single representation - if len(repres) == 1: - self._init_asset_name = repres[0]["context"]["asset"] - self._init_subset_name = repres[0]["context"]["subset"] - self._init_repre_name = repres[0]["context"]["representation"] - - content_repres = {} - archived_repres = [] - missing_repres = [] - version_ids = [] - for repre_id in repre_ids: - if repre_id not in repres_by_id: - missing_repres.append(repre_id) - elif repres_by_id[repre_id]["type"] == "archived_representation": - repre = repres_by_id[repre_id] - archived_repres.append(repre) - version_ids.append(repre["parent"]) - else: - repre = repres_by_id[repre_id] - content_repres[repre_id] = repres_by_id[repre_id] - version_ids.append(repre["parent"]) - - versions = io.find({ - "type": {"$in": ["version", "hero_version"]}, - "_id": {"$in": list(set(version_ids))} - }) - content_versions = {} - hero_version_ids = set() - for version in versions: - content_versions[version["_id"]] = version - if version["type"] == "hero_version": - hero_version_ids.add(version["_id"]) - - missing_versions = [] - subset_ids = [] - for version_id in version_ids: - if version_id not in content_versions: - missing_versions.append(version_id) - else: - subset_ids.append(content_versions[version_id]["parent"]) - - subsets = io.find({ - "type": {"$in": ["subset", "archived_subset"]}, - "_id": {"$in": subset_ids} - }) - subsets_by_id = {sub["_id"]: sub for sub in subsets} - - asset_ids = [] - archived_subsets = [] - missing_subsets = [] - content_subsets = {} - for subset_id in subset_ids: - if subset_id not in subsets_by_id: - missing_subsets.append(subset_id) - elif subsets_by_id[subset_id]["type"] == "archived_subset": - subset = subsets_by_id[subset_id] - asset_ids.append(subset["parent"]) - archived_subsets.append(subset) - else: - subset = subsets_by_id[subset_id] - asset_ids.append(subset["parent"]) - content_subsets[subset_id] = subset - - assets = io.find({ - "type": {"$in": ["asset", "archived_asset"]}, - "_id": {"$in": list(asset_ids)} - }) - assets_by_id = {asset["_id"]: asset for asset in assets} - - missing_assets = [] - archived_assets = [] - content_assets = {} - for asset_id in asset_ids: - if asset_id not in assets_by_id: - missing_assets.append(asset_id) - elif assets_by_id[asset_id]["type"] == "archived_asset": - archived_assets.append(assets_by_id[asset_id]) - else: - content_assets[asset_id] = assets_by_id[asset_id] - - self.content_assets = content_assets - self.content_subsets = content_subsets - self.content_versions = content_versions - self.content_repres = content_repres - - self.hero_version_ids = hero_version_ids - - self.missing_assets = missing_assets - self.missing_versions = missing_versions - self.missing_subsets = missing_subsets - self.missing_repres = missing_repres - self.missing_docs = ( - bool(missing_assets) - or bool(missing_versions) - or bool(missing_subsets) - or bool(missing_repres) - ) - - self.archived_assets = archived_assets - self.archived_subsets = archived_subsets - self.archived_repres = archived_repres - - def _combobox_value_changed(self, *args, **kwargs): - self.refresh() - - def refresh(self, init_refresh=False): - """Build the need comboboxes with content""" - if not self.fill_check and not init_refresh: - return - - self.fill_check = False - - if init_refresh: - asset_values = self._get_asset_box_values() - self._fill_combobox(asset_values, "asset") - - validation_state = ValidationState() - - # Set other comboboxes to empty if any document is missing or any asset - # of loaded representations is archived. - self._is_asset_ok(validation_state) - if validation_state.asset_ok: - subset_values = self._get_subset_box_values() - self._fill_combobox(subset_values, "subset") - self._is_subset_ok(validation_state) - - if validation_state.asset_ok and validation_state.subset_ok: - repre_values = sorted(self._representations_box_values()) - self._fill_combobox(repre_values, "repre") - self._is_repre_ok(validation_state) - - # Fill comboboxes with values - self.set_labels() - self.apply_validations(validation_state) - - if init_refresh: # pre select context if possible - self._assets_box.set_valid_value(self._init_asset_name) - self._subsets_box.set_valid_value(self._init_subset_name) - self._representations_box.set_valid_value(self._init_repre_name) - - self.fill_check = True - - def _get_loaders(self, representations): - if not representations: - return list() - - available_loaders = filter( - lambda l: not (hasattr(l, "is_utility") and l.is_utility), - api.discover(api.Loader) - ) - - loaders = set() - - for representation in representations: - for loader in api.loaders_from_representation( - available_loaders, - representation - ): - loaders.add(loader) - - return loaders - - def _fill_combobox(self, values, combobox_type): - if combobox_type == "asset": - combobox_widget = self._assets_box - elif combobox_type == "subset": - combobox_widget = self._subsets_box - elif combobox_type == "repre": - combobox_widget = self._representations_box - else: - return - selected_value = combobox_widget.get_valid_value() - - # Fill combobox - if values is not None: - combobox_widget.populate(list(sorted(values))) - if selected_value and selected_value in values: - index = None - for idx in range(combobox_widget.count()): - if selected_value == str(combobox_widget.itemText(idx)): - index = idx - break - if index is not None: - combobox_widget.setCurrentIndex(index) - - def set_labels(self): - asset_label = self._assets_box.get_valid_value() - subset_label = self._subsets_box.get_valid_value() - repre_label = self._representations_box.get_valid_value() - - default = "*No changes" - self._asset_label.setText(asset_label or default) - self._subset_label.setText(subset_label or default) - self._repre_label.setText(repre_label or default) - - def apply_validations(self, validation_state): - error_msg = "*Please select" - error_sheet = "border: 1px solid red;" - success_sheet = "border: 1px solid green;" - - asset_sheet = None - subset_sheet = None - repre_sheet = None - accept_sheet = None - if validation_state.asset_ok is False: - asset_sheet = error_sheet - self._asset_label.setText(error_msg) - elif validation_state.subset_ok is False: - subset_sheet = error_sheet - self._subset_label.setText(error_msg) - elif validation_state.repre_ok is False: - repre_sheet = error_sheet - self._repre_label.setText(error_msg) - - if validation_state.all_ok: - accept_sheet = success_sheet - - self._assets_box.setStyleSheet(asset_sheet or "") - self._subsets_box.setStyleSheet(subset_sheet or "") - self._representations_box.setStyleSheet(repre_sheet or "") - - self._accept_btn.setEnabled(validation_state.all_ok) - self._accept_btn.setStyleSheet(accept_sheet or "") - - def _get_asset_box_values(self): - asset_docs = io.find( - {"type": "asset"}, - {"_id": 1, "name": 1} - ) - asset_names_by_id = { - asset_doc["_id"]: asset_doc["name"] - for asset_doc in asset_docs - } - subsets = io.find( - { - "type": "subset", - "parent": {"$in": list(asset_names_by_id.keys())} - }, - { - "parent": 1 - } - ) - - filtered_assets = [] - for subset in subsets: - asset_name = asset_names_by_id[subset["parent"]] - if asset_name not in filtered_assets: - filtered_assets.append(asset_name) - return sorted(filtered_assets) - - def _get_subset_box_values(self): - selected_asset = self._assets_box.get_valid_value() - if selected_asset: - asset_doc = io.find_one({"type": "asset", "name": selected_asset}) - asset_ids = [asset_doc["_id"]] - else: - asset_ids = list(self.content_assets.keys()) - - subsets = io.find( - { - "type": "subset", - "parent": {"$in": asset_ids} - }, - { - "parent": 1, - "name": 1 - } - ) - - subset_names_by_parent_id = collections.defaultdict(set) - for subset in subsets: - subset_names_by_parent_id[subset["parent"]].add(subset["name"]) - - possible_subsets = None - for subset_names in subset_names_by_parent_id.values(): - if possible_subsets is None: - possible_subsets = subset_names - else: - possible_subsets = (possible_subsets & subset_names) - - if not possible_subsets: - break - - return list(possible_subsets or list()) - - def _representations_box_values(self): - # NOTE hero versions are not used because it is expected that - # hero version has same representations as latests - selected_asset = self._assets_box.currentText() - selected_subset = self._subsets_box.currentText() - - # If nothing is selected - # [ ] [ ] [?] - if not selected_asset and not selected_subset: - # Find all representations of selection's subsets - possible_repres = list(io.find( - { - "type": "representation", - "parent": {"$in": list(self.content_versions.keys())} - }, - { - "parent": 1, - "name": 1 - } - )) - - possible_repres_by_parent = collections.defaultdict(set) - for repre in possible_repres: - possible_repres_by_parent[repre["parent"]].add(repre["name"]) - - output_repres = None - for repre_names in possible_repres_by_parent.values(): - if output_repres is None: - output_repres = repre_names - else: - output_repres = (output_repres & repre_names) - - if not output_repres: - break - - return list(output_repres or list()) - - # [x] [x] [?] - if selected_asset and selected_subset: - asset_doc = io.find_one( - {"type": "asset", "name": selected_asset}, - {"_id": 1} - ) - subset_doc = io.find_one( - { - "type": "subset", - "name": selected_subset, - "parent": asset_doc["_id"] - }, - {"_id": 1} - ) - subset_id = subset_doc["_id"] - last_versions_by_subset_id = self.find_last_versions([subset_id]) - version_doc = last_versions_by_subset_id.get(subset_id) - repre_docs = io.find( - { - "type": "representation", - "parent": version_doc["_id"] - }, - { - "name": 1 - } - ) - return [ - repre_doc["name"] - for repre_doc in repre_docs - ] - - # [x] [ ] [?] - # If asset only is selected - if selected_asset: - asset_doc = io.find_one( - {"type": "asset", "name": selected_asset}, - {"_id": 1} - ) - if not asset_doc: - return list() - - # Filter subsets by subset names from content - subset_names = set() - for subset_doc in self.content_subsets.values(): - subset_names.add(subset_doc["name"]) - subset_docs = io.find( - { - "type": "subset", - "parent": asset_doc["_id"], - "name": {"$in": list(subset_names)} - }, - {"_id": 1} - ) - subset_ids = [ - subset_doc["_id"] - for subset_doc in subset_docs - ] - if not subset_ids: - return list() - - last_versions_by_subset_id = self.find_last_versions(subset_ids) - subset_id_by_version_id = {} - for subset_id, last_version in last_versions_by_subset_id.items(): - version_id = last_version["_id"] - subset_id_by_version_id[version_id] = subset_id - - if not subset_id_by_version_id: - return list() - - repre_docs = list(io.find( - { - "type": "representation", - "parent": {"$in": list(subset_id_by_version_id.keys())} - }, - { - "name": 1, - "parent": 1 - } - )) - if not repre_docs: - return list() - - repre_names_by_parent = collections.defaultdict(set) - for repre_doc in repre_docs: - repre_names_by_parent[repre_doc["parent"]].add( - repre_doc["name"] - ) - - available_repres = None - for repre_names in repre_names_by_parent.values(): - if available_repres is None: - available_repres = repre_names - continue - - available_repres = available_repres.intersection(repre_names) - - return list(available_repres) - - # [ ] [x] [?] - subset_docs = list(io.find( - { - "type": "subset", - "parent": {"$in": list(self.content_assets.keys())}, - "name": selected_subset - }, - {"_id": 1, "parent": 1} - )) - if not subset_docs: - return list() - - subset_docs_by_id = { - subset_doc["_id"]: subset_doc - for subset_doc in subset_docs - } - last_versions_by_subset_id = self.find_last_versions( - subset_docs_by_id.keys() - ) - - subset_id_by_version_id = {} - for subset_id, last_version in last_versions_by_subset_id.items(): - version_id = last_version["_id"] - subset_id_by_version_id[version_id] = subset_id - - if not subset_id_by_version_id: - return list() - - repre_docs = list(io.find( - { - "type": "representation", - "parent": {"$in": list(subset_id_by_version_id.keys())} - }, - { - "name": 1, - "parent": 1 - } - )) - if not repre_docs: - return list() - - repre_names_by_asset_id = {} - for repre_doc in repre_docs: - subset_id = subset_id_by_version_id[repre_doc["parent"]] - asset_id = subset_docs_by_id[subset_id]["parent"] - if asset_id not in repre_names_by_asset_id: - repre_names_by_asset_id[asset_id] = set() - repre_names_by_asset_id[asset_id].add(repre_doc["name"]) - - available_repres = None - for repre_names in repre_names_by_asset_id.values(): - if available_repres is None: - available_repres = repre_names - continue - - available_repres = available_repres.intersection(repre_names) - - return list(available_repres) - - def _is_asset_ok(self, validation_state): - selected_asset = self._assets_box.get_valid_value() - if ( - selected_asset is None - and (self.missing_docs or self.archived_assets) - ): - validation_state.asset_ok = False - - def _is_subset_ok(self, validation_state): - selected_asset = self._assets_box.get_valid_value() - selected_subset = self._subsets_box.get_valid_value() - - # [?] [x] [?] - # If subset is selected then must be ok - if selected_subset is not None: - return - - # [ ] [ ] [?] - if selected_asset is None: - # If there were archived subsets and asset is not selected - if self.archived_subsets: - validation_state.subset_ok = False - return - - # [x] [ ] [?] - asset_doc = io.find_one( - {"type": "asset", "name": selected_asset}, - {"_id": 1} - ) - subset_docs = io.find( - {"type": "subset", "parent": asset_doc["_id"]}, - {"name": 1} - ) - subset_names = set( - subset_doc["name"] - for subset_doc in subset_docs - ) - - for subset_doc in self.content_subsets.values(): - if subset_doc["name"] not in subset_names: - validation_state.subset_ok = False - break - - def find_last_versions(self, subset_ids): - _pipeline = [ - # Find all versions of those subsets - {"$match": { - "type": "version", - "parent": {"$in": list(subset_ids)} - }}, - # Sorting versions all together - {"$sort": {"name": 1}}, - # Group them by "parent", but only take the last - {"$group": { - "_id": "$parent", - "_version_id": {"$last": "$_id"}, - "type": {"$last": "$type"} - }} - ] - last_versions_by_subset_id = dict() - for doc in io.aggregate(_pipeline): - doc["parent"] = doc["_id"] - doc["_id"] = doc.pop("_version_id") - last_versions_by_subset_id[doc["parent"]] = doc - return last_versions_by_subset_id - - def _is_repre_ok(self, validation_state): - selected_asset = self._assets_box.get_valid_value() - selected_subset = self._subsets_box.get_valid_value() - selected_repre = self._representations_box.get_valid_value() - - # [?] [?] [x] - # If subset is selected then must be ok - if selected_repre is not None: - return - - # [ ] [ ] [ ] - if selected_asset is None and selected_subset is None: - if ( - self.archived_repres - or self.missing_versions - or self.missing_repres - ): - validation_state.repre_ok = False - return - - # [x] [x] [ ] - if selected_asset is not None and selected_subset is not None: - asset_doc = io.find_one( - {"type": "asset", "name": selected_asset}, - {"_id": 1} - ) - subset_doc = io.find_one( - { - "type": "subset", - "parent": asset_doc["_id"], - "name": selected_subset - }, - {"_id": 1} - ) - last_versions_by_subset_id = self.find_last_versions( - [subset_doc["_id"]] - ) - last_version = last_versions_by_subset_id.get(subset_doc["_id"]) - if not last_version: - validation_state.repre_ok = False - return - - repre_docs = io.find( - { - "type": "representation", - "parent": last_version["_id"] - }, - {"name": 1} - ) - - repre_names = set( - repre_doc["name"] - for repre_doc in repre_docs - ) - for repre_doc in self.content_repres.values(): - if repre_doc["name"] not in repre_names: - validation_state.repre_ok = False - break - return - - # [x] [ ] [ ] - if selected_asset is not None: - asset_doc = io.find_one( - {"type": "asset", "name": selected_asset}, - {"_id": 1} - ) - subset_docs = list(io.find( - { - "type": "subset", - "parent": asset_doc["_id"] - }, - {"_id": 1, "name": 1} - )) - - subset_name_by_id = {} - subset_ids = set() - for subset_doc in subset_docs: - subset_id = subset_doc["_id"] - subset_ids.add(subset_id) - subset_name_by_id[subset_id] = subset_doc["name"] - - last_versions_by_subset_id = self.find_last_versions(subset_ids) - - subset_id_by_version_id = {} - for subset_id, last_version in last_versions_by_subset_id.items(): - version_id = last_version["_id"] - subset_id_by_version_id[version_id] = subset_id - - repre_docs = io.find( - { - "type": "representation", - "parent": {"$in": list(subset_id_by_version_id.keys())} - }, - { - "name": 1, - "parent": 1 - } - ) - repres_by_subset_name = {} - for repre_doc in repre_docs: - subset_id = subset_id_by_version_id[repre_doc["parent"]] - subset_name = subset_name_by_id[subset_id] - if subset_name not in repres_by_subset_name: - repres_by_subset_name[subset_name] = set() - repres_by_subset_name[subset_name].add(repre_doc["name"]) - - for repre_doc in self.content_repres.values(): - version_doc = self.content_versions[repre_doc["parent"]] - subset_doc = self.content_subsets[version_doc["parent"]] - repre_names = ( - repres_by_subset_name.get(subset_doc["name"]) or [] - ) - if repre_doc["name"] not in repre_names: - validation_state.repre_ok = False - break - return - - # [ ] [x] [ ] - # Subset documents - subset_docs = io.find( - { - "type": "subset", - "parent": {"$in": list(self.content_assets.keys())}, - "name": selected_subset - }, - {"_id": 1, "name": 1, "parent": 1} - ) - - subset_docs_by_id = {} - for subset_doc in subset_docs: - subset_docs_by_id[subset_doc["_id"]] = subset_doc - - last_versions_by_subset_id = self.find_last_versions( - subset_docs_by_id.keys() - ) - subset_id_by_version_id = {} - for subset_id, last_version in last_versions_by_subset_id.items(): - version_id = last_version["_id"] - subset_id_by_version_id[version_id] = subset_id - - repre_docs = io.find( - { - "type": "representation", - "parent": {"$in": list(subset_id_by_version_id.keys())} - }, - { - "name": 1, - "parent": 1 - } - ) - repres_by_asset_id = {} - for repre_doc in repre_docs: - subset_id = subset_id_by_version_id[repre_doc["parent"]] - asset_id = subset_docs_by_id[subset_id]["parent"] - if asset_id not in repres_by_asset_id: - repres_by_asset_id[asset_id] = set() - repres_by_asset_id[asset_id].add(repre_doc["name"]) - - for repre_doc in self.content_repres.values(): - version_doc = self.content_versions[repre_doc["parent"]] - subset_doc = self.content_subsets[version_doc["parent"]] - asset_id = subset_doc["parent"] - repre_names = ( - repres_by_asset_id.get(asset_id) or [] - ) - if repre_doc["name"] not in repre_names: - validation_state.repre_ok = False - break - - def _on_current_asset(self): - # Set initial asset as current. - asset_name = api.Session["AVALON_ASSET"] - index = self._assets_box.findText( - asset_name, QtCore.Qt.MatchFixedString - ) - if index >= 0: - print("Setting asset to {}".format(asset_name)) - self._assets_box.setCurrentIndex(index) - - def _on_accept(self): - # Use None when not a valid value or when placeholder value - selected_asset = self._assets_box.get_valid_value() - selected_subset = self._subsets_box.get_valid_value() - selected_representation = self._representations_box.get_valid_value() - - if selected_asset: - asset_doc = io.find_one({"type": "asset", "name": selected_asset}) - asset_docs_by_id = {asset_doc["_id"]: asset_doc} - else: - asset_docs_by_id = self.content_assets - - asset_docs_by_name = { - asset_doc["name"]: asset_doc - for asset_doc in asset_docs_by_id.values() - } - - asset_ids = list(asset_docs_by_id.keys()) - - subset_query = { - "type": "subset", - "parent": {"$in": asset_ids} - } - if selected_subset: - subset_query["name"] = selected_subset - - subset_docs = list(io.find(subset_query)) - subset_ids = [] - subset_docs_by_parent_and_name = collections.defaultdict(dict) - for subset in subset_docs: - subset_ids.append(subset["_id"]) - parent_id = subset["parent"] - name = subset["name"] - subset_docs_by_parent_and_name[parent_id][name] = subset - - # versions - version_docs = list(io.find({ - "type": "version", - "parent": {"$in": subset_ids} - }, sort=[("name", -1)])) - - hero_version_docs = list(io.find({ - "type": "hero_version", - "parent": {"$in": subset_ids} - })) - - version_ids = list() - - version_docs_by_parent_id = {} - for version_doc in version_docs: - parent_id = version_doc["parent"] - if parent_id not in version_docs_by_parent_id: - version_ids.append(version_doc["_id"]) - version_docs_by_parent_id[parent_id] = version_doc - - hero_version_docs_by_parent_id = {} - for hero_version_doc in hero_version_docs: - version_ids.append(hero_version_doc["_id"]) - parent_id = hero_version_doc["parent"] - hero_version_docs_by_parent_id[parent_id] = hero_version_doc - - repre_docs = io.find({ - "type": "representation", - "parent": {"$in": version_ids} - }) - repre_docs_by_parent_id_by_name = collections.defaultdict(dict) - for repre_doc in repre_docs: - parent_id = repre_doc["parent"] - name = repre_doc["name"] - repre_docs_by_parent_id_by_name[parent_id][name] = repre_doc - - for container in self._items: - container_repre_id = io.ObjectId(container["representation"]) - container_repre = self.content_repres[container_repre_id] - container_repre_name = container_repre["name"] - - container_version_id = container_repre["parent"] - container_version = self.content_versions[container_version_id] - - container_subset_id = container_version["parent"] - container_subset = self.content_subsets[container_subset_id] - container_subset_name = container_subset["name"] - - container_asset_id = container_subset["parent"] - container_asset = self.content_assets[container_asset_id] - container_asset_name = container_asset["name"] - - if selected_asset: - asset_doc = asset_docs_by_name[selected_asset] - else: - asset_doc = asset_docs_by_name[container_asset_name] - - subsets_by_name = subset_docs_by_parent_and_name[asset_doc["_id"]] - if selected_subset: - subset_doc = subsets_by_name[selected_subset] - else: - subset_doc = subsets_by_name[container_subset_name] - - repre_doc = None - subset_id = subset_doc["_id"] - if container_version["type"] == "hero_version": - hero_version = hero_version_docs_by_parent_id.get( - subset_id - ) - if hero_version: - _repres = repre_docs_by_parent_id_by_name.get( - hero_version["_id"] - ) - if selected_representation: - repre_doc = _repres.get(selected_representation) - else: - repre_doc = _repres.get(container_repre_name) - - if not repre_doc: - version_doc = version_docs_by_parent_id[subset_id] - version_id = version_doc["_id"] - repres_by_name = repre_docs_by_parent_id_by_name[version_id] - if selected_representation: - repre_doc = repres_by_name[selected_representation] - else: - repre_doc = repres_by_name[container_repre_name] - - try: - api.switch(container, repre_doc) - except Exception: - log.warning( - ( - "Couldn't switch asset." - "See traceback for more information." - ), - exc_info=True - ) - dialog = QtWidgets.QMessageBox() - dialog.setStyleSheet(style.load_stylesheet()) - dialog.setWindowTitle("Switch asset failed") - msg = "Switch asset failed. "\ - "Search console log for more details" - dialog.setText(msg) - dialog.exec_() - - self.switched.emit() - - self.close() - - class SceneInventoryWindow(QtWidgets.QDialog): """Scene Inventory window""" From 87cdaa829c33550e995ac10f5ebf42153adf2a39 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 15 Nov 2021 19:11:11 +0100 Subject: [PATCH 04/16] moved View to separated file --- openpype/tools/sceneinventory/view.py | 774 +++++++++++++++++++++++ openpype/tools/sceneinventory/window.py | 776 +----------------------- 2 files changed, 781 insertions(+), 769 deletions(-) create mode 100644 openpype/tools/sceneinventory/view.py diff --git a/openpype/tools/sceneinventory/view.py b/openpype/tools/sceneinventory/view.py new file mode 100644 index 0000000000..512c65e143 --- /dev/null +++ b/openpype/tools/sceneinventory/view.py @@ -0,0 +1,774 @@ +import collections +import logging +from functools import partial + +from Qt import QtWidgets, QtCore + +from avalon import io, api, style +from avalon.vendor import qtawesome +from avalon.lib import HeroVersionType +from avalon.tools import lib as tools_lib + +from openpype.modules import ModulesManager + +from .switch_dialog import SwitchAssetDialog +from .model import InventoryModel + + +DEFAULT_COLOR = "#fb9c15" + +log = logging.getLogger("SceneInventory") + + +class View(QtWidgets.QTreeView): + data_changed = QtCore.Signal() + hierarchy_view = QtCore.Signal(bool) + + def __init__(self, parent=None): + super(View, self).__init__(parent=parent) + + if not parent: + self.setWindowFlags( + self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint + ) + # view settings + self.setIndentation(12) + self.setAlternatingRowColors(True) + self.setSortingEnabled(True) + self.setSelectionMode(self.ExtendedSelection) + self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.customContextMenuRequested.connect(self.show_right_mouse_menu) + self._hierarchy_view = False + self._selected = None + + manager = ModulesManager() + self.sync_server = manager.modules_by_name["sync_server"] + self.sync_enabled = self.sync_server.enabled + + def enter_hierarchy(self, items): + self._selected = set(i["objectName"] for i in items) + self._hierarchy_view = True + self.hierarchy_view.emit(True) + self.data_changed.emit() + self.expandToDepth(1) + self.setStyleSheet(""" + QTreeView { + border-color: #fb9c15; + } + """) + + def leave_hierarchy(self): + self._hierarchy_view = False + self.hierarchy_view.emit(False) + self.data_changed.emit() + self.setStyleSheet("QTreeView {}") + + def build_item_menu_for_selection(self, items, menu): + if not items: + return + + repre_ids = [] + for item in items: + item_id = io.ObjectId(item["representation"]) + if item_id not in repre_ids: + repre_ids.append(item_id) + + repre_docs = io.find( + { + "type": "representation", + "_id": {"$in": repre_ids} + }, + {"parent": 1} + ) + + version_ids = [] + for repre_doc in repre_docs: + version_id = repre_doc["parent"] + if version_id not in version_ids: + version_ids.append(version_id) + + loaded_versions = io.find({ + "_id": {"$in": version_ids}, + "type": {"$in": ["version", "hero_version"]} + }) + + loaded_hero_versions = [] + versions_by_parent_id = collections.defaultdict(list) + version_parents = [] + for version in loaded_versions: + if version["type"] == "hero_version": + loaded_hero_versions.append(version) + else: + parent_id = version["parent"] + versions_by_parent_id[parent_id].append(version) + if parent_id not in version_parents: + version_parents.append(parent_id) + + all_versions = io.find({ + "type": {"$in": ["hero_version", "version"]}, + "parent": {"$in": version_parents} + }) + hero_versions = [] + versions = [] + for version in all_versions: + if version["type"] == "hero_version": + hero_versions.append(version) + else: + versions.append(version) + + has_loaded_hero_versions = len(loaded_hero_versions) > 0 + has_available_hero_version = len(hero_versions) > 0 + has_outdated = False + + for version in versions: + parent_id = version["parent"] + current_versions = versions_by_parent_id[parent_id] + for current_version in current_versions: + if current_version["name"] < version["name"]: + has_outdated = True + break + + if has_outdated: + break + + switch_to_versioned = None + if has_loaded_hero_versions: + def _on_switch_to_versioned(items): + repre_ids = [] + for item in items: + item_id = io.ObjectId(item["representation"]) + if item_id not in repre_ids: + repre_ids.append(item_id) + + repre_docs = io.find( + { + "type": "representation", + "_id": {"$in": repre_ids} + }, + {"parent": 1} + ) + + version_ids = [] + version_id_by_repre_id = {} + for repre_doc in repre_docs: + version_id = repre_doc["parent"] + version_id_by_repre_id[repre_doc["_id"]] = version_id + if version_id not in version_ids: + version_ids.append(version_id) + hero_versions = io.find( + { + "_id": {"$in": version_ids}, + "type": "hero_version" + }, + {"version_id": 1} + ) + version_ids = set() + for hero_version in hero_versions: + version_id = hero_version["version_id"] + version_ids.add(version_id) + hero_version_id = hero_version["_id"] + for _repre_id, current_version_id in ( + version_id_by_repre_id.items() + ): + if current_version_id == hero_version_id: + version_id_by_repre_id[_repre_id] = version_id + + version_docs = io.find( + { + "_id": {"$in": list(version_ids)}, + "type": "version" + }, + {"name": 1} + ) + version_name_by_id = {} + for version_doc in version_docs: + version_name_by_id[version_doc["_id"]] = \ + version_doc["name"] + + for item in items: + repre_id = io.ObjectId(item["representation"]) + version_id = version_id_by_repre_id.get(repre_id) + version_name = version_name_by_id.get(version_id) + if version_name is not None: + try: + api.update(item, version_name) + except AssertionError: + self._show_version_error_dialog(version_name, + [item]) + log.warning("Update failed", exc_info=True) + + self.data_changed.emit() + + update_icon = qtawesome.icon( + "fa.asterisk", + color=DEFAULT_COLOR + ) + switch_to_versioned = QtWidgets.QAction( + update_icon, + "Switch to versioned", + menu + ) + switch_to_versioned.triggered.connect( + lambda: _on_switch_to_versioned(items) + ) + + update_to_latest_action = None + if has_outdated or has_loaded_hero_versions: + # update to latest version + def _on_update_to_latest(items): + for item in items: + try: + api.update(item, -1) + except AssertionError: + self._show_version_error_dialog(None, [item]) + log.warning("Update failed", exc_info=True) + self.data_changed.emit() + + update_icon = qtawesome.icon( + "fa.angle-double-up", + color=DEFAULT_COLOR + ) + update_to_latest_action = QtWidgets.QAction( + update_icon, + "Update to latest", + menu + ) + update_to_latest_action.triggered.connect( + lambda: _on_update_to_latest(items) + ) + + change_to_hero = None + if has_available_hero_version: + # change to hero version + def _on_update_to_hero(items): + for item in items: + try: + api.update(item, HeroVersionType(-1)) + except AssertionError: + self._show_version_error_dialog('hero', [item]) + log.warning("Update failed", exc_info=True) + self.data_changed.emit() + + # TODO change icon + change_icon = qtawesome.icon( + "fa.asterisk", + color="#00b359" + ) + change_to_hero = QtWidgets.QAction( + change_icon, + "Change to hero", + menu + ) + change_to_hero.triggered.connect( + lambda: _on_update_to_hero(items) + ) + + # set version + set_version_icon = qtawesome.icon("fa.hashtag", color=DEFAULT_COLOR) + set_version_action = QtWidgets.QAction( + set_version_icon, + "Set version", + menu + ) + set_version_action.triggered.connect( + lambda: self.show_version_dialog(items)) + + # switch asset + switch_asset_icon = qtawesome.icon("fa.sitemap", color=DEFAULT_COLOR) + switch_asset_action = QtWidgets.QAction( + switch_asset_icon, + "Switch Asset", + menu + ) + switch_asset_action.triggered.connect( + lambda: self.show_switch_dialog(items)) + + # remove + remove_icon = qtawesome.icon("fa.remove", color=DEFAULT_COLOR) + remove_action = QtWidgets.QAction(remove_icon, "Remove items", menu) + remove_action.triggered.connect( + lambda: self.show_remove_warning_dialog(items)) + + # add the actions + if switch_to_versioned: + menu.addAction(switch_to_versioned) + + if update_to_latest_action: + menu.addAction(update_to_latest_action) + + if change_to_hero: + menu.addAction(change_to_hero) + + menu.addAction(set_version_action) + menu.addAction(switch_asset_action) + + menu.addSeparator() + + menu.addAction(remove_action) + + menu.addSeparator() + + if self.sync_enabled: + menu = self.handle_sync_server(menu, repre_ids) + + def handle_sync_server(self, menu, repre_ids): + """ + Adds actions for download/upload when SyncServer is enabled + + Args: + menu (OptionMenu) + repre_ids (list) of object_ids + Returns: + (OptionMenu) + """ + download_icon = qtawesome.icon("fa.download", color=DEFAULT_COLOR) + download_active_action = QtWidgets.QAction( + download_icon, + "Download", + menu + ) + download_active_action.triggered.connect( + lambda: self._add_sites(repre_ids, 'active_site')) + + upload_icon = qtawesome.icon("fa.upload", color=DEFAULT_COLOR) + upload_remote_action = QtWidgets.QAction( + upload_icon, + "Upload", + menu + ) + upload_remote_action.triggered.connect( + lambda: self._add_sites(repre_ids, 'remote_site')) + + menu.addAction(download_active_action) + menu.addAction(upload_remote_action) + + return menu + + def _add_sites(self, repre_ids, side): + """ + (Re)sync all 'repre_ids' to specific site. + + It checks if opposite site has fully available content to limit + accidents. (ReSync active when no remote >> losing active content) + + Args: + repre_ids (list) + side (str): 'active_site'|'remote_site' + """ + project = io.Session["AVALON_PROJECT"] + active_site = self.sync_server.get_active_site(project) + remote_site = self.sync_server.get_remote_site(project) + + for repre_id in repre_ids: + representation = io.find_one({"type": "representation", + "_id": repre_id}) + if not representation: + continue + + progress = tools_lib.get_progress_for_repre(representation, + active_site, + remote_site) + if side == 'active_site': + # check opposite from added site, must be 1 or unable to sync + check_progress = progress[remote_site] + site = active_site + else: + check_progress = progress[active_site] + site = remote_site + + if check_progress == 1: + self.sync_server.add_site(project, repre_id, site, force=True) + + self.data_changed.emit() + + def build_item_menu(self, items): + """Create menu for the selected items""" + + menu = QtWidgets.QMenu(self) + + # add the actions + self.build_item_menu_for_selection(items, menu) + + # These two actions should be able to work without selection + # expand all items + expandall_action = QtWidgets.QAction(menu, text="Expand all items") + expandall_action.triggered.connect(self.expandAll) + + # collapse all items + collapse_action = QtWidgets.QAction(menu, text="Collapse all items") + collapse_action.triggered.connect(self.collapseAll) + + menu.addAction(expandall_action) + menu.addAction(collapse_action) + + custom_actions = self.get_custom_actions(containers=items) + if custom_actions: + submenu = QtWidgets.QMenu("Actions", self) + for action in custom_actions: + + color = action.color or DEFAULT_COLOR + icon = qtawesome.icon("fa.%s" % action.icon, color=color) + action_item = QtWidgets.QAction(icon, action.label, submenu) + action_item.triggered.connect( + partial(self.process_custom_action, action, items)) + + submenu.addAction(action_item) + + menu.addMenu(submenu) + + # go back to flat view + if self._hierarchy_view: + back_to_flat_icon = qtawesome.icon("fa.list", color=DEFAULT_COLOR) + back_to_flat_action = QtWidgets.QAction( + back_to_flat_icon, + "Back to Full-View", + menu + ) + back_to_flat_action.triggered.connect(self.leave_hierarchy) + + # send items to hierarchy view + enter_hierarchy_icon = qtawesome.icon("fa.indent", color="#d8d8d8") + enter_hierarchy_action = QtWidgets.QAction( + enter_hierarchy_icon, + "Cherry-Pick (Hierarchy)", + menu + ) + enter_hierarchy_action.triggered.connect( + lambda: self.enter_hierarchy(items)) + + if items: + menu.addAction(enter_hierarchy_action) + + if self._hierarchy_view: + menu.addAction(back_to_flat_action) + + return menu + + def get_custom_actions(self, containers): + """Get the registered Inventory Actions + + Args: + containers(list): collection of containers + + Returns: + list: collection of filter and initialized actions + """ + + def sorter(Plugin): + """Sort based on order attribute of the plugin""" + return Plugin.order + + # Fedd an empty dict if no selection, this will ensure the compat + # lookup always work, so plugin can interact with Scene Inventory + # reversely. + containers = containers or [dict()] + + # Check which action will be available in the menu + Plugins = api.discover(api.InventoryAction) + compatible = [p() for p in Plugins if + any(p.is_compatible(c) for c in containers)] + + return sorted(compatible, key=sorter) + + def process_custom_action(self, action, containers): + """Run action and if results are returned positive update the view + + If the result is list or dict, will select view items by the result. + + Args: + action (InventoryAction): Inventory Action instance + containers (list): Data of currently selected items + + Returns: + None + """ + + result = action.process(containers) + if result: + self.data_changed.emit() + + if isinstance(result, (list, set)): + self.select_items_by_action(result) + + if isinstance(result, dict): + self.select_items_by_action(result["objectNames"], + result["options"]) + + def select_items_by_action(self, object_names, options=None): + """Select view items by the result of action + + Args: + object_names (list or set): A list/set of container object name + options (dict): GUI operation options. + + Returns: + None + + """ + options = options or dict() + + if options.get("clear", True): + self.clearSelection() + + object_names = set(object_names) + if (self._hierarchy_view and + not self._selected.issuperset(object_names)): + # If any container not in current cherry-picked view, update + # view before selecting them. + self._selected.update(object_names) + self.data_changed.emit() + + model = self.model() + selection_model = self.selectionModel() + + select_mode = { + "select": selection_model.Select, + "deselect": selection_model.Deselect, + "toggle": selection_model.Toggle, + }[options.get("mode", "select")] + + for item in tools_lib.iter_model_rows(model, 0): + item = item.data(InventoryModel.ItemRole) + if item.get("isGroupNode"): + continue + + name = item.get("objectName") + if name in object_names: + self.scrollTo(item) # Ensure item is visible + flags = select_mode | selection_model.Rows + selection_model.select(item, flags) + + object_names.remove(name) + + if len(object_names) == 0: + break + + def show_right_mouse_menu(self, pos): + """Display the menu when at the position of the item clicked""" + + globalpos = self.viewport().mapToGlobal(pos) + + if not self.selectionModel().hasSelection(): + print("No selection") + # Build menu without selection, feed an empty list + menu = self.build_item_menu([]) + menu.exec_(globalpos) + return + + active = self.currentIndex() # index under mouse + active = active.sibling(active.row(), 0) # get first column + + # move index under mouse + indices = self.get_indices() + if active in indices: + indices.remove(active) + + indices.append(active) + + # Extend to the sub-items + all_indices = self.extend_to_children(indices) + items = [dict(i.data(InventoryModel.ItemRole)) for i in all_indices + if i.parent().isValid()] + + if self._hierarchy_view: + # Ensure no group item + items = [n for n in items if not n.get("isGroupNode")] + + menu = self.build_item_menu(items) + menu.exec_(globalpos) + + def get_indices(self): + """Get the selected rows""" + selection_model = self.selectionModel() + return selection_model.selectedRows() + + def extend_to_children(self, indices): + """Extend the indices to the children indices. + + Top-level indices are extended to its children indices. Sub-items + are kept as is. + + Args: + indices (list): The indices to extend. + + Returns: + list: The children indices + + """ + def get_children(i): + model = i.model() + rows = model.rowCount(parent=i) + for row in range(rows): + child = model.index(row, 0, parent=i) + yield child + + subitems = set() + for i in indices: + valid_parent = i.parent().isValid() + if valid_parent and i not in subitems: + subitems.add(i) + + if self._hierarchy_view: + # Assume this is a group item + for child in get_children(i): + subitems.add(child) + else: + # is top level item + for child in get_children(i): + subitems.add(child) + + return list(subitems) + + def show_version_dialog(self, items): + """Create a dialog with the available versions for the selected file + + Args: + items (list): list of items to run the "set_version" for + + Returns: + None + """ + + active = items[-1] + + # Get available versions for active representation + representation_id = io.ObjectId(active["representation"]) + representation = io.find_one({"_id": representation_id}) + version = io.find_one({ + "_id": representation["parent"] + }) + + versions = list(io.find( + { + "parent": version["parent"], + "type": "version" + }, + sort=[("name", 1)] + )) + + hero_version = io.find_one({ + "parent": version["parent"], + "type": "hero_version" + }) + if hero_version: + _version_id = hero_version["version_id"] + for _version in versions: + if _version["_id"] != _version_id: + continue + + hero_version["name"] = HeroVersionType( + _version["name"] + ) + hero_version["data"] = _version["data"] + break + + # Get index among the listed versions + current_item = None + current_version = active["version"] + if isinstance(current_version, HeroVersionType): + current_item = hero_version + else: + for version in versions: + if version["name"] == current_version: + current_item = version + break + + all_versions = [] + if hero_version: + all_versions.append(hero_version) + all_versions.extend(reversed(versions)) + + if current_item: + index = all_versions.index(current_item) + else: + index = 0 + + versions_by_label = dict() + labels = [] + for version in all_versions: + is_hero = version["type"] == "hero_version" + label = tools_lib.format_version(version["name"], is_hero) + labels.append(label) + versions_by_label[label] = version["name"] + + label, state = QtWidgets.QInputDialog.getItem( + self, + "Set version..", + "Set version number to", + labels, + current=index, + editable=False + ) + if not state: + return + + if label: + version = versions_by_label[label] + for item in items: + try: + api.update(item, version) + except AssertionError: + self._show_version_error_dialog(version, [item]) + log.warning("Update failed", exc_info=True) + # refresh model when done + self.data_changed.emit() + + def show_switch_dialog(self, items): + """Display Switch dialog""" + dialog = SwitchAssetDialog(self, items) + dialog.switched.connect(self.data_changed.emit) + dialog.show() + + def show_remove_warning_dialog(self, items): + """Prompt a dialog to inform the user the action will remove items""" + + accept = QtWidgets.QMessageBox.Ok + buttons = accept | QtWidgets.QMessageBox.Cancel + + message = ("Are you sure you want to remove " + "{} item(s)".format(len(items))) + state = QtWidgets.QMessageBox.question(self, "Are you sure?", + message, + buttons=buttons, + defaultButton=accept) + + if state != accept: + return + + for item in items: + api.remove(item) + self.data_changed.emit() + + def _show_version_error_dialog(self, version, items): + """Shows QMessageBox when version switch doesn't work + + Args: + version: str or int or None + """ + if not version: + version_str = "latest" + elif version == "hero": + version_str = "hero" + elif isinstance(version, int): + version_str = "v{:03d}".format(version) + else: + version_str = version + + dialog = QtWidgets.QMessageBox() + dialog.setIcon(QtWidgets.QMessageBox.Warning) + dialog.setStyleSheet(style.load_stylesheet()) + dialog.setWindowTitle("Update failed") + + switch_btn = dialog.addButton("Switch Asset", + QtWidgets.QMessageBox.ActionRole) + switch_btn.clicked.connect(lambda: self.show_switch_dialog(items)) + + dialog.addButton(QtWidgets.QMessageBox.Cancel) + + msg = "Version update to '{}' ".format(version_str) + \ + "failed as representation doesn't exist.\n\n" \ + "Please update to version with a valid " \ + "representation OR \n use 'Switch Asset' button " \ + "to change asset." + dialog.setText(msg) + dialog.exec_() diff --git a/openpype/tools/sceneinventory/window.py b/openpype/tools/sceneinventory/window.py index 1bd96ef85e..e0bbedf297 100644 --- a/openpype/tools/sceneinventory/window.py +++ b/openpype/tools/sceneinventory/window.py @@ -1,784 +1,22 @@ import os import sys -import logging -import collections -from functools import partial -from ...vendor.Qt import QtWidgets, QtCore -from ...vendor import qtawesome -from ... import io, api, style -from ...lib import HeroVersionType +from Qt import QtWidgets, QtCore +from avalon.vendor import qtawesome +from avalon import io, api, style -from .. import lib as tools_lib -from ..delegates import VersionDelegate + +from avalon.tools import lib as tools_lib +from avalon.tools.delegates import VersionDelegate from .proxy import FilterProxyModel from .model import InventoryModel -from .switch_dialog import SwitchAssetDialog +from .view import View -from openpype.modules import ModulesManager - -DEFAULT_COLOR = "#fb9c15" module = sys.modules[__name__] module.window = None -log = logging.getLogger("SceneInventory") - - -class View(QtWidgets.QTreeView): - data_changed = QtCore.Signal() - hierarchy_view = QtCore.Signal(bool) - - def __init__(self, parent=None): - super(View, self).__init__(parent=parent) - - if not parent: - self.setWindowFlags( - self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint - ) - # view settings - self.setIndentation(12) - self.setAlternatingRowColors(True) - self.setSortingEnabled(True) - self.setSelectionMode(self.ExtendedSelection) - self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - self.customContextMenuRequested.connect(self.show_right_mouse_menu) - self._hierarchy_view = False - self._selected = None - - manager = ModulesManager() - self.sync_server = manager.modules_by_name["sync_server"] - self.sync_enabled = self.sync_server.enabled - - def enter_hierarchy(self, items): - self._selected = set(i["objectName"] for i in items) - self._hierarchy_view = True - self.hierarchy_view.emit(True) - self.data_changed.emit() - self.expandToDepth(1) - self.setStyleSheet(""" - QTreeView { - border-color: #fb9c15; - } - """) - - def leave_hierarchy(self): - self._hierarchy_view = False - self.hierarchy_view.emit(False) - self.data_changed.emit() - self.setStyleSheet("QTreeView {}") - - def build_item_menu_for_selection(self, items, menu): - if not items: - return - - repre_ids = [] - for item in items: - item_id = io.ObjectId(item["representation"]) - if item_id not in repre_ids: - repre_ids.append(item_id) - - repre_docs = io.find( - { - "type": "representation", - "_id": {"$in": repre_ids} - }, - {"parent": 1} - ) - - version_ids = [] - for repre_doc in repre_docs: - version_id = repre_doc["parent"] - if version_id not in version_ids: - version_ids.append(version_id) - - loaded_versions = io.find({ - "_id": {"$in": version_ids}, - "type": {"$in": ["version", "hero_version"]} - }) - - loaded_hero_versions = [] - versions_by_parent_id = collections.defaultdict(list) - version_parents = [] - for version in loaded_versions: - if version["type"] == "hero_version": - loaded_hero_versions.append(version) - else: - parent_id = version["parent"] - versions_by_parent_id[parent_id].append(version) - if parent_id not in version_parents: - version_parents.append(parent_id) - - all_versions = io.find({ - "type": {"$in": ["hero_version", "version"]}, - "parent": {"$in": version_parents} - }) - hero_versions = [] - versions = [] - for version in all_versions: - if version["type"] == "hero_version": - hero_versions.append(version) - else: - versions.append(version) - - has_loaded_hero_versions = len(loaded_hero_versions) > 0 - has_available_hero_version = len(hero_versions) > 0 - has_outdated = False - - for version in versions: - parent_id = version["parent"] - current_versions = versions_by_parent_id[parent_id] - for current_version in current_versions: - if current_version["name"] < version["name"]: - has_outdated = True - break - - if has_outdated: - break - - switch_to_versioned = None - if has_loaded_hero_versions: - def _on_switch_to_versioned(items): - repre_ids = [] - for item in items: - item_id = io.ObjectId(item["representation"]) - if item_id not in repre_ids: - repre_ids.append(item_id) - - repre_docs = io.find( - { - "type": "representation", - "_id": {"$in": repre_ids} - }, - {"parent": 1} - ) - - version_ids = [] - version_id_by_repre_id = {} - for repre_doc in repre_docs: - version_id = repre_doc["parent"] - version_id_by_repre_id[repre_doc["_id"]] = version_id - if version_id not in version_ids: - version_ids.append(version_id) - hero_versions = io.find( - { - "_id": {"$in": version_ids}, - "type": "hero_version" - }, - {"version_id": 1} - ) - version_ids = set() - for hero_version in hero_versions: - version_id = hero_version["version_id"] - version_ids.add(version_id) - hero_version_id = hero_version["_id"] - for _repre_id, current_version_id in ( - version_id_by_repre_id.items() - ): - if current_version_id == hero_version_id: - version_id_by_repre_id[_repre_id] = version_id - - version_docs = io.find( - { - "_id": {"$in": list(version_ids)}, - "type": "version" - }, - {"name": 1} - ) - version_name_by_id = {} - for version_doc in version_docs: - version_name_by_id[version_doc["_id"]] = \ - version_doc["name"] - - for item in items: - repre_id = io.ObjectId(item["representation"]) - version_id = version_id_by_repre_id.get(repre_id) - version_name = version_name_by_id.get(version_id) - if version_name is not None: - try: - api.update(item, version_name) - except AssertionError: - self._show_version_error_dialog(version_name, - [item]) - log.warning("Update failed", exc_info=True) - - self.data_changed.emit() - - update_icon = qtawesome.icon( - "fa.asterisk", - color=DEFAULT_COLOR - ) - switch_to_versioned = QtWidgets.QAction( - update_icon, - "Switch to versioned", - menu - ) - switch_to_versioned.triggered.connect( - lambda: _on_switch_to_versioned(items) - ) - - update_to_latest_action = None - if has_outdated or has_loaded_hero_versions: - # update to latest version - def _on_update_to_latest(items): - for item in items: - try: - api.update(item, -1) - except AssertionError: - self._show_version_error_dialog(None, [item]) - log.warning("Update failed", exc_info=True) - self.data_changed.emit() - - update_icon = qtawesome.icon( - "fa.angle-double-up", - color=DEFAULT_COLOR - ) - update_to_latest_action = QtWidgets.QAction( - update_icon, - "Update to latest", - menu - ) - update_to_latest_action.triggered.connect( - lambda: _on_update_to_latest(items) - ) - - change_to_hero = None - if has_available_hero_version: - # change to hero version - def _on_update_to_hero(items): - for item in items: - try: - api.update(item, HeroVersionType(-1)) - except AssertionError: - self._show_version_error_dialog('hero', [item]) - log.warning("Update failed", exc_info=True) - self.data_changed.emit() - - # TODO change icon - change_icon = qtawesome.icon( - "fa.asterisk", - color="#00b359" - ) - change_to_hero = QtWidgets.QAction( - change_icon, - "Change to hero", - menu - ) - change_to_hero.triggered.connect( - lambda: _on_update_to_hero(items) - ) - - # set version - set_version_icon = qtawesome.icon("fa.hashtag", color=DEFAULT_COLOR) - set_version_action = QtWidgets.QAction( - set_version_icon, - "Set version", - menu - ) - set_version_action.triggered.connect( - lambda: self.show_version_dialog(items)) - - # switch asset - switch_asset_icon = qtawesome.icon("fa.sitemap", color=DEFAULT_COLOR) - switch_asset_action = QtWidgets.QAction( - switch_asset_icon, - "Switch Asset", - menu - ) - switch_asset_action.triggered.connect( - lambda: self.show_switch_dialog(items)) - - # remove - remove_icon = qtawesome.icon("fa.remove", color=DEFAULT_COLOR) - remove_action = QtWidgets.QAction(remove_icon, "Remove items", menu) - remove_action.triggered.connect( - lambda: self.show_remove_warning_dialog(items)) - - # add the actions - if switch_to_versioned: - menu.addAction(switch_to_versioned) - - if update_to_latest_action: - menu.addAction(update_to_latest_action) - - if change_to_hero: - menu.addAction(change_to_hero) - - menu.addAction(set_version_action) - menu.addAction(switch_asset_action) - - menu.addSeparator() - - menu.addAction(remove_action) - - menu.addSeparator() - - if self.sync_enabled: - menu = self.handle_sync_server(menu, repre_ids) - - def handle_sync_server(self, menu, repre_ids): - """ - Adds actions for download/upload when SyncServer is enabled - - Args: - menu (OptionMenu) - repre_ids (list) of object_ids - Returns: - (OptionMenu) - """ - download_icon = qtawesome.icon("fa.download", color=DEFAULT_COLOR) - download_active_action = QtWidgets.QAction( - download_icon, - "Download", - menu - ) - download_active_action.triggered.connect( - lambda: self._add_sites(repre_ids, 'active_site')) - - upload_icon = qtawesome.icon("fa.upload", color=DEFAULT_COLOR) - upload_remote_action = QtWidgets.QAction( - upload_icon, - "Upload", - menu - ) - upload_remote_action.triggered.connect( - lambda: self._add_sites(repre_ids, 'remote_site')) - - menu.addAction(download_active_action) - menu.addAction(upload_remote_action) - - return menu - - def _add_sites(self, repre_ids, side): - """ - (Re)sync all 'repre_ids' to specific site. - - It checks if opposite site has fully available content to limit - accidents. (ReSync active when no remote >> losing active content) - - Args: - repre_ids (list) - side (str): 'active_site'|'remote_site' - """ - project = io.Session["AVALON_PROJECT"] - active_site = self.sync_server.get_active_site(project) - remote_site = self.sync_server.get_remote_site(project) - - for repre_id in repre_ids: - representation = io.find_one({"type": "representation", - "_id": repre_id}) - if not representation: - continue - - progress = tools_lib.get_progress_for_repre(representation, - active_site, - remote_site) - if side == 'active_site': - # check opposite from added site, must be 1 or unable to sync - check_progress = progress[remote_site] - site = active_site - else: - check_progress = progress[active_site] - site = remote_site - - if check_progress == 1: - self.sync_server.add_site(project, repre_id, site, force=True) - - self.data_changed.emit() - - def build_item_menu(self, items): - """Create menu for the selected items""" - - menu = QtWidgets.QMenu(self) - - # add the actions - self.build_item_menu_for_selection(items, menu) - - # These two actions should be able to work without selection - # expand all items - expandall_action = QtWidgets.QAction(menu, text="Expand all items") - expandall_action.triggered.connect(self.expandAll) - - # collapse all items - collapse_action = QtWidgets.QAction(menu, text="Collapse all items") - collapse_action.triggered.connect(self.collapseAll) - - menu.addAction(expandall_action) - menu.addAction(collapse_action) - - custom_actions = self.get_custom_actions(containers=items) - if custom_actions: - submenu = QtWidgets.QMenu("Actions", self) - for action in custom_actions: - - color = action.color or DEFAULT_COLOR - icon = qtawesome.icon("fa.%s" % action.icon, color=color) - action_item = QtWidgets.QAction(icon, action.label, submenu) - action_item.triggered.connect( - partial(self.process_custom_action, action, items)) - - submenu.addAction(action_item) - - menu.addMenu(submenu) - - # go back to flat view - if self._hierarchy_view: - back_to_flat_icon = qtawesome.icon("fa.list", color=DEFAULT_COLOR) - back_to_flat_action = QtWidgets.QAction( - back_to_flat_icon, - "Back to Full-View", - menu - ) - back_to_flat_action.triggered.connect(self.leave_hierarchy) - - # send items to hierarchy view - enter_hierarchy_icon = qtawesome.icon("fa.indent", color="#d8d8d8") - enter_hierarchy_action = QtWidgets.QAction( - enter_hierarchy_icon, - "Cherry-Pick (Hierarchy)", - menu - ) - enter_hierarchy_action.triggered.connect( - lambda: self.enter_hierarchy(items)) - - if items: - menu.addAction(enter_hierarchy_action) - - if self._hierarchy_view: - menu.addAction(back_to_flat_action) - - return menu - - def get_custom_actions(self, containers): - """Get the registered Inventory Actions - - Args: - containers(list): collection of containers - - Returns: - list: collection of filter and initialized actions - """ - - def sorter(Plugin): - """Sort based on order attribute of the plugin""" - return Plugin.order - - # Fedd an empty dict if no selection, this will ensure the compat - # lookup always work, so plugin can interact with Scene Inventory - # reversely. - containers = containers or [dict()] - - # Check which action will be available in the menu - Plugins = api.discover(api.InventoryAction) - compatible = [p() for p in Plugins if - any(p.is_compatible(c) for c in containers)] - - return sorted(compatible, key=sorter) - - def process_custom_action(self, action, containers): - """Run action and if results are returned positive update the view - - If the result is list or dict, will select view items by the result. - - Args: - action (InventoryAction): Inventory Action instance - containers (list): Data of currently selected items - - Returns: - None - """ - - result = action.process(containers) - if result: - self.data_changed.emit() - - if isinstance(result, (list, set)): - self.select_items_by_action(result) - - if isinstance(result, dict): - self.select_items_by_action(result["objectNames"], - result["options"]) - - def select_items_by_action(self, object_names, options=None): - """Select view items by the result of action - - Args: - object_names (list or set): A list/set of container object name - options (dict): GUI operation options. - - Returns: - None - - """ - options = options or dict() - - if options.get("clear", True): - self.clearSelection() - - object_names = set(object_names) - if (self._hierarchy_view and - not self._selected.issuperset(object_names)): - # If any container not in current cherry-picked view, update - # view before selecting them. - self._selected.update(object_names) - self.data_changed.emit() - - model = self.model() - selection_model = self.selectionModel() - - select_mode = { - "select": selection_model.Select, - "deselect": selection_model.Deselect, - "toggle": selection_model.Toggle, - }[options.get("mode", "select")] - - for item in tools_lib.iter_model_rows(model, 0): - item = item.data(InventoryModel.ItemRole) - if item.get("isGroupNode"): - continue - - name = item.get("objectName") - if name in object_names: - self.scrollTo(item) # Ensure item is visible - flags = select_mode | selection_model.Rows - selection_model.select(item, flags) - - object_names.remove(name) - - if len(object_names) == 0: - break - - def show_right_mouse_menu(self, pos): - """Display the menu when at the position of the item clicked""" - - globalpos = self.viewport().mapToGlobal(pos) - - if not self.selectionModel().hasSelection(): - print("No selection") - # Build menu without selection, feed an empty list - menu = self.build_item_menu([]) - menu.exec_(globalpos) - return - - active = self.currentIndex() # index under mouse - active = active.sibling(active.row(), 0) # get first column - - # move index under mouse - indices = self.get_indices() - if active in indices: - indices.remove(active) - - indices.append(active) - - # Extend to the sub-items - all_indices = self.extend_to_children(indices) - items = [dict(i.data(InventoryModel.ItemRole)) for i in all_indices - if i.parent().isValid()] - - if self._hierarchy_view: - # Ensure no group item - items = [n for n in items if not n.get("isGroupNode")] - - menu = self.build_item_menu(items) - menu.exec_(globalpos) - - def get_indices(self): - """Get the selected rows""" - selection_model = self.selectionModel() - return selection_model.selectedRows() - - def extend_to_children(self, indices): - """Extend the indices to the children indices. - - Top-level indices are extended to its children indices. Sub-items - are kept as is. - - Args: - indices (list): The indices to extend. - - Returns: - list: The children indices - - """ - def get_children(i): - model = i.model() - rows = model.rowCount(parent=i) - for row in range(rows): - child = model.index(row, 0, parent=i) - yield child - - subitems = set() - for i in indices: - valid_parent = i.parent().isValid() - if valid_parent and i not in subitems: - subitems.add(i) - - if self._hierarchy_view: - # Assume this is a group item - for child in get_children(i): - subitems.add(child) - else: - # is top level item - for child in get_children(i): - subitems.add(child) - - return list(subitems) - - def show_version_dialog(self, items): - """Create a dialog with the available versions for the selected file - - Args: - items (list): list of items to run the "set_version" for - - Returns: - None - """ - - active = items[-1] - - # Get available versions for active representation - representation_id = io.ObjectId(active["representation"]) - representation = io.find_one({"_id": representation_id}) - version = io.find_one({ - "_id": representation["parent"] - }) - - versions = list(io.find( - { - "parent": version["parent"], - "type": "version" - }, - sort=[("name", 1)] - )) - - hero_version = io.find_one({ - "parent": version["parent"], - "type": "hero_version" - }) - if hero_version: - _version_id = hero_version["version_id"] - for _version in versions: - if _version["_id"] != _version_id: - continue - - hero_version["name"] = HeroVersionType( - _version["name"] - ) - hero_version["data"] = _version["data"] - break - - # Get index among the listed versions - current_item = None - current_version = active["version"] - if isinstance(current_version, HeroVersionType): - current_item = hero_version - else: - for version in versions: - if version["name"] == current_version: - current_item = version - break - - all_versions = [] - if hero_version: - all_versions.append(hero_version) - all_versions.extend(reversed(versions)) - - if current_item: - index = all_versions.index(current_item) - else: - index = 0 - - versions_by_label = dict() - labels = [] - for version in all_versions: - is_hero = version["type"] == "hero_version" - label = tools_lib.format_version(version["name"], is_hero) - labels.append(label) - versions_by_label[label] = version["name"] - - label, state = QtWidgets.QInputDialog.getItem( - self, - "Set version..", - "Set version number to", - labels, - current=index, - editable=False - ) - if not state: - return - - if label: - version = versions_by_label[label] - for item in items: - try: - api.update(item, version) - except AssertionError: - self._show_version_error_dialog(version, [item]) - log.warning("Update failed", exc_info=True) - # refresh model when done - self.data_changed.emit() - - def show_switch_dialog(self, items): - """Display Switch dialog""" - dialog = SwitchAssetDialog(self, items) - dialog.switched.connect(self.data_changed.emit) - dialog.show() - - def show_remove_warning_dialog(self, items): - """Prompt a dialog to inform the user the action will remove items""" - - accept = QtWidgets.QMessageBox.Ok - buttons = accept | QtWidgets.QMessageBox.Cancel - - message = ("Are you sure you want to remove " - "{} item(s)".format(len(items))) - state = QtWidgets.QMessageBox.question(self, "Are you sure?", - message, - buttons=buttons, - defaultButton=accept) - - if state != accept: - return - - for item in items: - api.remove(item) - self.data_changed.emit() - - def _show_version_error_dialog(self, version, items): - """Shows QMessageBox when version switch doesn't work - - Args: - version: str or int or None - """ - if not version: - version_str = "latest" - elif version == "hero": - version_str = "hero" - elif isinstance(version, int): - version_str = "v{:03d}".format(version) - else: - version_str = version - - dialog = QtWidgets.QMessageBox() - dialog.setIcon(QtWidgets.QMessageBox.Warning) - dialog.setStyleSheet(style.load_stylesheet()) - dialog.setWindowTitle("Update failed") - - switch_btn = dialog.addButton("Switch Asset", - QtWidgets.QMessageBox.ActionRole) - switch_btn.clicked.connect(lambda: self.show_switch_dialog(items)) - - dialog.addButton(QtWidgets.QMessageBox.Cancel) - - msg = "Version update to '{}' ".format(version_str) + \ - "failed as representation doesn't exist.\n\n" \ - "Please update to version with a valid " \ - "representation OR \n use 'Switch Asset' button " \ - "to change asset." - dialog.setText(msg) - dialog.exec_() - class SceneInventoryWindow(QtWidgets.QDialog): """Scene Inventory window""" From 0fcdbabeb112eb8e826ce0314d69d4a9682e7466 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 15 Nov 2021 19:12:14 +0100 Subject: [PATCH 05/16] fixed imports in models --- openpype/tools/sceneinventory/model.py | 12 ++++++------ openpype/tools/sceneinventory/proxy.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/openpype/tools/sceneinventory/model.py b/openpype/tools/sceneinventory/model.py index 7b4e051b36..59c38ca553 100644 --- a/openpype/tools/sceneinventory/model.py +++ b/openpype/tools/sceneinventory/model.py @@ -2,13 +2,13 @@ import logging from collections import defaultdict -from ... import api, io, style, schema -from ...vendor.Qt import QtCore, QtGui -from ...vendor import qtawesome +from Qt import QtCore, QtGui +from avalon import api, io, style, schema +from avalon.vendor import qtawesome -from .. import lib as tools_lib -from ...lib import HeroVersionType -from ..models import TreeModel, Item +from avalon.tools import lib as tools_lib +from avalon.lib import HeroVersionType +from avalon.tools.models import TreeModel, Item from . import lib diff --git a/openpype/tools/sceneinventory/proxy.py b/openpype/tools/sceneinventory/proxy.py index 307e032eb6..0f92942ad5 100644 --- a/openpype/tools/sceneinventory/proxy.py +++ b/openpype/tools/sceneinventory/proxy.py @@ -1,6 +1,6 @@ import re -from ...vendor.Qt import QtCore +from Qt import QtCore from . import lib From 150eb6a29c090d3cbfac7e52fb71a09109f8ca4a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 15 Nov 2021 19:14:38 +0100 Subject: [PATCH 06/16] use openpype style on main window --- openpype/tools/sceneinventory/window.py | 15 +++++++++++---- openpype/tools/utils/host_tools.py | 7 ++----- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/openpype/tools/sceneinventory/window.py b/openpype/tools/sceneinventory/window.py index e0bbedf297..99e2228bb7 100644 --- a/openpype/tools/sceneinventory/window.py +++ b/openpype/tools/sceneinventory/window.py @@ -3,12 +3,13 @@ import sys from Qt import QtWidgets, QtCore from avalon.vendor import qtawesome -from avalon import io, api, style - +from avalon import io, api from avalon.tools import lib as tools_lib from avalon.tools.delegates import VersionDelegate +from openpype import style + from .proxy import FilterProxyModel from .model import InventoryModel from .view import View @@ -94,7 +95,6 @@ class SceneInventoryWindow(QtWidgets.QDialog): "version": version_delegate } } - # set some nice default widths for the view self.view.setColumnWidth(0, 250) # name self.view.setColumnWidth(1, 55) # version @@ -104,6 +104,14 @@ class SceneInventoryWindow(QtWidgets.QDialog): self.family_config_cache.refresh() + self._first_show = True + + def showEvent(self, event): + super(SceneInventoryWindow, self).showEvent(event) + if self._first_show: + self._first_show = False + self.setStyleSheet(style.load_stylesheet()) + def keyPressEvent(self, event): """Custom keyPressEvent. @@ -161,7 +169,6 @@ def show(root=None, debug=False, parent=None, items=None): with tools_lib.application(): window = SceneInventoryWindow(parent) - window.setStyleSheet(style.load_stylesheet()) window.show() window.refresh(items=items) diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py index d5e4792c94..8011410ce9 100644 --- a/openpype/tools/utils/host_tools.py +++ b/openpype/tools/utils/host_tools.py @@ -154,21 +154,18 @@ class HostToolsHelper: def get_scene_inventory_tool(self, parent): """Create, cache and return scene inventory tool window.""" if self._scene_inventory_tool is None: - from avalon.tools.sceneinventory.app import Window + from openpype.tools.sceneinventory import SceneInventoryWindow - scene_inventory_window = Window(parent=parent or self._parent) + scene_inventory_window = SceneInventoryWindow(parent=parent or self._parent) self._scene_inventory_tool = scene_inventory_window return self._scene_inventory_tool def show_scene_inventory(self, parent=None): """Show tool maintain loaded containers.""" - from avalon import style - scene_inventory_tool = self.get_scene_inventory_tool(parent) scene_inventory_tool.show() scene_inventory_tool.refresh() - scene_inventory_tool.setStyleSheet(style.load_stylesheet()) # Pull window to the front. scene_inventory_tool.raise_() From a10fc7e492f67154f21587f365b02a3fae5adc45 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 15 Nov 2021 19:21:56 +0100 Subject: [PATCH 07/16] reorganized initialization --- openpype/tools/sceneinventory/proxy.py | 12 ++++------ openpype/tools/sceneinventory/view.py | 4 ---- openpype/tools/sceneinventory/window.py | 29 ++++++++++++++----------- 3 files changed, 20 insertions(+), 25 deletions(-) diff --git a/openpype/tools/sceneinventory/proxy.py b/openpype/tools/sceneinventory/proxy.py index 0f92942ad5..7d4e6fdb4c 100644 --- a/openpype/tools/sceneinventory/proxy.py +++ b/openpype/tools/sceneinventory/proxy.py @@ -14,11 +14,8 @@ class FilterProxyModel(QtCore.QSortFilterProxyModel): self._hierarchy_view = False def filterAcceptsRow(self, row, parent): - model = self.sourceModel() - source_index = model.index(row, - self.filterKeyColumn(), - parent) + source_index = model.index(row, self.filterKeyColumn(), parent) # Always allow bottom entries (individual containers), since their # parent group hidden if it wouldn't have been validated. @@ -97,13 +94,12 @@ class FilterProxyModel(QtCore.QSortFilterProxyModel): if is_outdated: return True - elif self._hierarchy_view: + if self._hierarchy_view: for _node in lib.walk_hierarchy(node): if outdated(_node): return True - return False - else: - return False + + return False def _matches(self, row, parent, pattern): """Return whether row matches regex pattern. diff --git a/openpype/tools/sceneinventory/view.py b/openpype/tools/sceneinventory/view.py index 512c65e143..88914fd0af 100644 --- a/openpype/tools/sceneinventory/view.py +++ b/openpype/tools/sceneinventory/view.py @@ -27,10 +27,6 @@ class View(QtWidgets.QTreeView): def __init__(self, parent=None): super(View, self).__init__(parent=parent) - if not parent: - self.setWindowFlags( - self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint - ) # view settings self.setIndentation(12) self.setAlternatingRowColors(True) diff --git a/openpype/tools/sceneinventory/window.py b/openpype/tools/sceneinventory/window.py index 99e2228bb7..ed2b848481 100644 --- a/openpype/tools/sceneinventory/window.py +++ b/openpype/tools/sceneinventory/window.py @@ -25,6 +25,11 @@ class SceneInventoryWindow(QtWidgets.QDialog): def __init__(self, parent=None): super(SceneInventoryWindow, self).__init__(parent) + if not parent: + self.setWindowFlags( + self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint + ) + self.resize(1100, 480) self.setWindowTitle( "Scene Inventory 1.0 - {}".format( @@ -34,21 +39,19 @@ class SceneInventoryWindow(QtWidgets.QDialog): self.setObjectName("SceneInventory") self.setProperty("saveWindowPref", True) # Maya only property! - layout = QtWidgets.QVBoxLayout(self) - # region control - control_layout = QtWidgets.QHBoxLayout() - filter_label = QtWidgets.QLabel("Search") - text_filter = QtWidgets.QLineEdit() + filter_label = QtWidgets.QLabel("Search", self) + text_filter = QtWidgets.QLineEdit(self) - outdated_only = QtWidgets.QCheckBox("Filter to outdated") + outdated_only = QtWidgets.QCheckBox("Filter to outdated", self) outdated_only.setToolTip("Show outdated files only") outdated_only.setChecked(False) icon = qtawesome.icon("fa.refresh", color="white") - refresh_button = QtWidgets.QPushButton() + refresh_button = QtWidgets.QPushButton(self) refresh_button.setIcon(icon) + control_layout = QtWidgets.QHBoxLayout() control_layout.addWidget(filter_label) control_layout.addWidget(text_filter) control_layout.addWidget(outdated_only) @@ -59,7 +62,11 @@ class SceneInventoryWindow(QtWidgets.QDialog): model = InventoryModel(self.family_config_cache) proxy = FilterProxyModel() - view = View() + proxy.setSourceModel(model) + proxy.setDynamicSortFilter(True) + proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) + + view = View(self) view.setModel(proxy) # apply delegates @@ -67,6 +74,7 @@ class SceneInventoryWindow(QtWidgets.QDialog): column = model.Columns.index("version") view.setItemDelegateForColumn(column, version_delegate) + layout = QtWidgets.QVBoxLayout(self) layout.addLayout(control_layout) layout.addWidget(view) @@ -85,11 +93,6 @@ class SceneInventoryWindow(QtWidgets.QDialog): view.hierarchy_view.connect(self.model.set_hierarchy_view) view.hierarchy_view.connect(self.proxy.set_hierarchy_view) - # proxy settings - proxy.setSourceModel(self.model) - proxy.setDynamicSortFilter(True) - proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) - self.data = { "delegates": { "version": version_delegate From f4d070bce00dc3ba9da7c8c678f35ad261875e80 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 16 Nov 2021 10:37:59 +0100 Subject: [PATCH 08/16] minor reorganizations and renaming --- openpype/tools/sceneinventory/model.py | 64 ++++++----- openpype/tools/sceneinventory/proxy.py | 34 +++--- openpype/tools/sceneinventory/view.py | 147 +++++++++++++----------- openpype/tools/sceneinventory/window.py | 40 +++---- 4 files changed, 154 insertions(+), 131 deletions(-) diff --git a/openpype/tools/sceneinventory/model.py b/openpype/tools/sceneinventory/model.py index 59c38ca553..bf7b296703 100644 --- a/openpype/tools/sceneinventory/model.py +++ b/openpype/tools/sceneinventory/model.py @@ -41,33 +41,36 @@ class InventoryModel(TreeModel): self.active_site = self.remote_site = None self.active_provider = self.remote_provider = None - if self.sync_enabled: - project = io.Session['AVALON_PROJECT'] - active_site = sync_server.get_active_site(project) - remote_site = sync_server.get_remote_site(project) + if not self.sync_enabled: + return - # TODO refactor - active_provider = \ - sync_server.get_provider_for_site(project, - active_site) - if active_site == 'studio': - active_provider = 'studio' # sanitized for icon + project_name = io.Session["AVALON_PROJECT"] + active_site = sync_server.get_active_site(project_name) + remote_site = sync_server.get_remote_site(project_name) - remote_provider = \ - sync_server.get_provider_for_site(project, - remote_site) - if remote_site == 'studio': - remote_provider = 'studio' + active_provider = "studio" + remote_provider = "studio" + if active_site != "studio": + # sanitized for icon + active_provider = sync_server.get_provider_for_site( + project_name, active_site + ) - # self.sync_server = sync_server - self.active_site = active_site - self.active_provider = active_provider - self.remote_site = remote_site - self.remote_provider = remote_provider - self._icons = tools_lib.get_repre_icons() - if 'active_site' not in self.Columns and \ - 'remote_site' not in self.Columns: - self.Columns.extend(['active_site', 'remote_site']) + if remote_site != "studio": + remote_provider = sync_server.get_provider_for_site( + project_name, remote_site + ) + + # self.sync_server = sync_server + self.active_site = active_site + self.active_provider = active_provider + self.remote_site = remote_site + self.remote_provider = remote_provider + self._icons = tools_lib.get_repre_icons() + if "active_site" not in self.Columns: + self.Columns.append("active_site") + if "remote_site" not in self.Columns: + self.Columns.extend("remote_site") def outdated(self, item): value = item.get("version") @@ -79,7 +82,6 @@ class InventoryModel(TreeModel): return True def data(self, index, role): - if not index.isValid(): return @@ -128,10 +130,10 @@ class InventoryModel(TreeModel): color = item.get("color", style.colors.default) if item.get("isGroupNode"): # group-item return qtawesome.icon("fa.folder", color=color) - elif item.get("isNotSet"): + if item.get("isNotSet"): return qtawesome.icon("fa.exclamation-circle", color=color) - else: - return qtawesome.icon("fa.file-o", color=color) + + return qtawesome.icon("fa.file-o", color=color) if index.column() == 3: # Family icon @@ -393,9 +395,9 @@ class InventoryModel(TreeModel): group_node["isGroupNode"] = True if self.sync_enabled: - progress = tools_lib.get_progress_for_repre(representation, - self.active_site, - self.remote_site) + progress = tools_lib.get_progress_for_repre( + representation, self.active_site, self.remote_site + ) group_node["active_site"] = self.active_site group_node["active_site_provider"] = self.active_provider group_node["remote_site"] = self.remote_site diff --git a/openpype/tools/sceneinventory/proxy.py b/openpype/tools/sceneinventory/proxy.py index 7d4e6fdb4c..3c4295c446 100644 --- a/openpype/tools/sceneinventory/proxy.py +++ b/openpype/tools/sceneinventory/proxy.py @@ -123,22 +123,28 @@ class FilterProxyModel(QtCore.QSortFilterProxyModel): if re.search(pattern, key, re.IGNORECASE): return True - if not matches(row, parent, pattern): - # Also allow if any of the children matches - source_index = model.index(row, column, parent) - rows = model.rowCount(source_index) + if matches(row, parent, pattern): + return True - if not any(matches(i, source_index, pattern) - for i in range(rows)): + # Also allow if any of the children matches + source_index = model.index(row, column, parent) + rows = model.rowCount(source_index) - if self._hierarchy_view: - for i in range(rows): - child_i = model.index(i, column, source_index) - child_rows = model.rowCount(child_i) - return any(self._matches(ch_i, child_i, pattern) - for ch_i in range(child_rows)) + if any( + matches(idx, source_index, pattern) + for idx in range(rows) + ): + return True - else: - return False + if not self._hierarchy_view: + return False + + for i in range(rows): + child_i = model.index(i, column, source_index) + child_rows = model.rowCount(child_i) + return any( + self._matches(ch_i, child_i, pattern) + for ch_i in range(child_rows) + ) return True diff --git a/openpype/tools/sceneinventory/view.py b/openpype/tools/sceneinventory/view.py index 88914fd0af..08d5499355 100644 --- a/openpype/tools/sceneinventory/view.py +++ b/openpype/tools/sceneinventory/view.py @@ -20,12 +20,12 @@ DEFAULT_COLOR = "#fb9c15" log = logging.getLogger("SceneInventory") -class View(QtWidgets.QTreeView): +class SceneInvetoryView(QtWidgets.QTreeView): data_changed = QtCore.Signal() hierarchy_view = QtCore.Signal(bool) def __init__(self, parent=None): - super(View, self).__init__(parent=parent) + super(SceneInvetoryView, self).__init__(parent=parent) # view settings self.setIndentation(12) @@ -33,7 +33,7 @@ class View(QtWidgets.QTreeView): self.setSortingEnabled(True) self.setSelectionMode(self.ExtendedSelection) self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - self.customContextMenuRequested.connect(self.show_right_mouse_menu) + self.customContextMenuRequested.connect(self._show_right_mouse_menu) self._hierarchy_view = False self._selected = None @@ -41,7 +41,7 @@ class View(QtWidgets.QTreeView): self.sync_server = manager.modules_by_name["sync_server"] self.sync_enabled = self.sync_server.enabled - def enter_hierarchy(self, items): + def _enter_hierarchy(self, items): self._selected = set(i["objectName"] for i in items) self._hierarchy_view = True self.hierarchy_view.emit(True) @@ -53,13 +53,13 @@ class View(QtWidgets.QTreeView): } """) - def leave_hierarchy(self): + def _leave_hierarchy(self): self._hierarchy_view = False self.hierarchy_view.emit(False) self.data_changed.emit() self.setStyleSheet("QTreeView {}") - def build_item_menu_for_selection(self, items, menu): + def _build_item_menu_for_selection(self, items, menu): if not items: return @@ -267,7 +267,7 @@ class View(QtWidgets.QTreeView): menu ) set_version_action.triggered.connect( - lambda: self.show_version_dialog(items)) + lambda: self._show_version_dialog(items)) # switch asset switch_asset_icon = qtawesome.icon("fa.sitemap", color=DEFAULT_COLOR) @@ -277,13 +277,13 @@ class View(QtWidgets.QTreeView): menu ) switch_asset_action.triggered.connect( - lambda: self.show_switch_dialog(items)) + lambda: self._show_switch_dialog(items)) # remove remove_icon = qtawesome.icon("fa.remove", color=DEFAULT_COLOR) remove_action = QtWidgets.QAction(remove_icon, "Remove items", menu) remove_action.triggered.connect( - lambda: self.show_remove_warning_dialog(items)) + lambda: self._show_remove_warning_dialog(items)) # add the actions if switch_to_versioned: @@ -302,12 +302,9 @@ class View(QtWidgets.QTreeView): menu.addAction(remove_action) - menu.addSeparator() + self._handle_sync_server(menu, repre_ids) - if self.sync_enabled: - menu = self.handle_sync_server(menu, repre_ids) - - def handle_sync_server(self, menu, repre_ids): + def _handle_sync_server(self, menu, repre_ids): """ Adds actions for download/upload when SyncServer is enabled @@ -317,6 +314,11 @@ class View(QtWidgets.QTreeView): Returns: (OptionMenu) """ + if not self.sync_enabled: + return + + menu.addSeparator() + download_icon = qtawesome.icon("fa.download", color=DEFAULT_COLOR) download_active_action = QtWidgets.QAction( download_icon, @@ -338,8 +340,6 @@ class View(QtWidgets.QTreeView): menu.addAction(download_active_action) menu.addAction(upload_remote_action) - return menu - def _add_sites(self, repre_ids, side): """ (Re)sync all 'repre_ids' to specific site. @@ -351,20 +351,29 @@ class View(QtWidgets.QTreeView): repre_ids (list) side (str): 'active_site'|'remote_site' """ - project = io.Session["AVALON_PROJECT"] - active_site = self.sync_server.get_active_site(project) - remote_site = self.sync_server.get_remote_site(project) + project_name = io.Session["AVALON_PROJECT"] + active_site = self.sync_server.get_active_site(project_name) + remote_site = self.sync_server.get_remote_site(project_name) + repre_docs = io.find({ + "type": "representation", + "_id": {"$in": repre_ids} + }) + repre_docs_by_id = { + repre_doc["_id"]: repre_doc + for repre_doc in repre_docs + } for repre_id in repre_ids: - representation = io.find_one({"type": "representation", - "_id": repre_id}) - if not representation: + repre_doc = repre_docs_by_id.get(repre_id) + if not repre_doc: continue - progress = tools_lib.get_progress_for_repre(representation, - active_site, - remote_site) - if side == 'active_site': + progress = tools_lib.get_progress_for_repre( + repre_doc, + active_site, + remote_site + ) + if side == "active_site": # check opposite from added site, must be 1 or unable to sync check_progress = progress[remote_site] site = active_site @@ -373,17 +382,22 @@ class View(QtWidgets.QTreeView): site = remote_site if check_progress == 1: - self.sync_server.add_site(project, repre_id, site, force=True) + self.sync_server.add_site( + project_name, repre_id, site, force=True + ) self.data_changed.emit() - def build_item_menu(self, items): + def _build_item_menu(self, items=None): """Create menu for the selected items""" + if not items: + items = [] + menu = QtWidgets.QMenu(self) # add the actions - self.build_item_menu_for_selection(items, menu) + self._build_item_menu_for_selection(items, menu) # These two actions should be able to work without selection # expand all items @@ -397,16 +411,15 @@ class View(QtWidgets.QTreeView): menu.addAction(expandall_action) menu.addAction(collapse_action) - custom_actions = self.get_custom_actions(containers=items) + custom_actions = self._get_custom_actions(containers=items) if custom_actions: submenu = QtWidgets.QMenu("Actions", self) for action in custom_actions: - color = action.color or DEFAULT_COLOR icon = qtawesome.icon("fa.%s" % action.icon, color=color) action_item = QtWidgets.QAction(icon, action.label, submenu) action_item.triggered.connect( - partial(self.process_custom_action, action, items)) + partial(self._process_custom_action, action, items)) submenu.addAction(action_item) @@ -420,7 +433,7 @@ class View(QtWidgets.QTreeView): "Back to Full-View", menu ) - back_to_flat_action.triggered.connect(self.leave_hierarchy) + back_to_flat_action.triggered.connect(self._leave_hierarchy) # send items to hierarchy view enter_hierarchy_icon = qtawesome.icon("fa.indent", color="#d8d8d8") @@ -430,7 +443,7 @@ class View(QtWidgets.QTreeView): menu ) enter_hierarchy_action.triggered.connect( - lambda: self.enter_hierarchy(items)) + lambda: self._enter_hierarchy(items)) if items: menu.addAction(enter_hierarchy_action) @@ -440,7 +453,7 @@ class View(QtWidgets.QTreeView): return menu - def get_custom_actions(self, containers): + def _get_custom_actions(self, containers): """Get the registered Inventory Actions Args: @@ -466,7 +479,7 @@ class View(QtWidgets.QTreeView): return sorted(compatible, key=sorter) - def process_custom_action(self, action, containers): + def _process_custom_action(self, action, containers): """Run action and if results are returned positive update the view If the result is list or dict, will select view items by the result. @@ -484,13 +497,14 @@ class View(QtWidgets.QTreeView): self.data_changed.emit() if isinstance(result, (list, set)): - self.select_items_by_action(result) + self._select_items_by_action(result) if isinstance(result, dict): - self.select_items_by_action(result["objectNames"], - result["options"]) + self._select_items_by_action( + result["objectNames"], result["options"] + ) - def select_items_by_action(self, object_names, options=None): + def _select_items_by_action(self, object_names, options=None): """Select view items by the result of action Args: @@ -507,8 +521,10 @@ class View(QtWidgets.QTreeView): self.clearSelection() object_names = set(object_names) - if (self._hierarchy_view and - not self._selected.issuperset(object_names)): + if ( + self._hierarchy_view + and not self._selected.issuperset(object_names) + ): # If any container not in current cherry-picked view, update # view before selecting them. self._selected.update(object_names) @@ -539,7 +555,7 @@ class View(QtWidgets.QTreeView): if len(object_names) == 0: break - def show_right_mouse_menu(self, pos): + def _show_right_mouse_menu(self, pos): """Display the menu when at the position of the item clicked""" globalpos = self.viewport().mapToGlobal(pos) @@ -547,7 +563,7 @@ class View(QtWidgets.QTreeView): if not self.selectionModel().hasSelection(): print("No selection") # Build menu without selection, feed an empty list - menu = self.build_item_menu([]) + menu = self._build_item_menu() menu.exec_(globalpos) return @@ -562,7 +578,7 @@ class View(QtWidgets.QTreeView): indices.append(active) # Extend to the sub-items - all_indices = self.extend_to_children(indices) + all_indices = self._extend_to_children(indices) items = [dict(i.data(InventoryModel.ItemRole)) for i in all_indices if i.parent().isValid()] @@ -570,7 +586,7 @@ class View(QtWidgets.QTreeView): # Ensure no group item items = [n for n in items if not n.get("isGroupNode")] - menu = self.build_item_menu(items) + menu = self._build_item_menu(items) menu.exec_(globalpos) def get_indices(self): @@ -578,7 +594,7 @@ class View(QtWidgets.QTreeView): selection_model = self.selectionModel() return selection_model.selectedRows() - def extend_to_children(self, indices): + def _extend_to_children(self, indices): """Extend the indices to the children indices. Top-level indices are extended to its children indices. Sub-items @@ -615,7 +631,7 @@ class View(QtWidgets.QTreeView): return list(subitems) - def show_version_dialog(self, items): + def _show_version_dialog(self, items): """Create a dialog with the available versions for the selected file Args: @@ -709,24 +725,25 @@ class View(QtWidgets.QTreeView): # refresh model when done self.data_changed.emit() - def show_switch_dialog(self, items): + def _show_switch_dialog(self, items): """Display Switch dialog""" dialog = SwitchAssetDialog(self, items) dialog.switched.connect(self.data_changed.emit) dialog.show() - def show_remove_warning_dialog(self, items): + def _show_remove_warning_dialog(self, items): """Prompt a dialog to inform the user the action will remove items""" accept = QtWidgets.QMessageBox.Ok buttons = accept | QtWidgets.QMessageBox.Cancel - message = ("Are you sure you want to remove " - "{} item(s)".format(len(items))) - state = QtWidgets.QMessageBox.question(self, "Are you sure?", - message, - buttons=buttons, - defaultButton=accept) + state = QtWidgets.QMessageBox.question( + self, + "Are you sure?", + "Are you sure you want to remove {} item(s)".format(len(items)), + buttons=buttons, + defaultButton=accept + ) if state != accept: return @@ -755,16 +772,18 @@ class View(QtWidgets.QTreeView): dialog.setStyleSheet(style.load_stylesheet()) dialog.setWindowTitle("Update failed") - switch_btn = dialog.addButton("Switch Asset", - QtWidgets.QMessageBox.ActionRole) - switch_btn.clicked.connect(lambda: self.show_switch_dialog(items)) + switch_btn = dialog.addButton( + "Switch Asset", + QtWidgets.QMessageBox.ActionRole + ) + switch_btn.clicked.connect(lambda: self._show_switch_dialog(items)) dialog.addButton(QtWidgets.QMessageBox.Cancel) - msg = "Version update to '{}' ".format(version_str) + \ - "failed as representation doesn't exist.\n\n" \ - "Please update to version with a valid " \ - "representation OR \n use 'Switch Asset' button " \ - "to change asset." + msg = ( + "Version update to '{}' failed as representation doesn't exist." + "\n\nPlease update to version with a valid representation" + " OR \n use 'Switch Asset' button to change asset." + ).format(version_str) dialog.setText(msg) dialog.exec_() diff --git a/openpype/tools/sceneinventory/window.py b/openpype/tools/sceneinventory/window.py index ed2b848481..18d2c971d8 100644 --- a/openpype/tools/sceneinventory/window.py +++ b/openpype/tools/sceneinventory/window.py @@ -12,7 +12,7 @@ from openpype import style from .proxy import FilterProxyModel from .model import InventoryModel -from .view import View +from .view import SceneInvetoryView module = sys.modules[__name__] @@ -66,9 +66,16 @@ class SceneInventoryWindow(QtWidgets.QDialog): proxy.setDynamicSortFilter(True) proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) - view = View(self) + view = SceneInvetoryView(self) view.setModel(proxy) + # set some nice default widths for the view + view.setColumnWidth(0, 250) # name + view.setColumnWidth(1, 55) # version + view.setColumnWidth(2, 55) # count + view.setColumnWidth(3, 150) # family + view.setColumnWidth(4, 100) # namespace + # apply delegates version_delegate = VersionDelegate(io, self) column = model.Columns.index("version") @@ -78,32 +85,21 @@ class SceneInventoryWindow(QtWidgets.QDialog): layout.addLayout(control_layout) layout.addWidget(view) + # signals + text_filter.textChanged.connect(proxy.setFilterRegExp) + outdated_only.stateChanged.connect(proxy.set_filter_outdated) + refresh_button.clicked.connect(self.refresh) + view.data_changed.connect(self.refresh) + view.hierarchy_view.connect(model.set_hierarchy_view) + view.hierarchy_view.connect(proxy.set_hierarchy_view) + self.filter = text_filter self.outdated_only = outdated_only self.view = view self.refresh_button = refresh_button self.model = model self.proxy = proxy - - # signals - text_filter.textChanged.connect(self.proxy.setFilterRegExp) - outdated_only.stateChanged.connect(self.proxy.set_filter_outdated) - refresh_button.clicked.connect(self.refresh) - view.data_changed.connect(self.refresh) - view.hierarchy_view.connect(self.model.set_hierarchy_view) - view.hierarchy_view.connect(self.proxy.set_hierarchy_view) - - self.data = { - "delegates": { - "version": version_delegate - } - } - # set some nice default widths for the view - self.view.setColumnWidth(0, 250) # name - self.view.setColumnWidth(1, 55) # version - self.view.setColumnWidth(2, 55) # count - self.view.setColumnWidth(3, 150) # family - self.view.setColumnWidth(4, 100) # namespace + self._version_delegate = version_delegate self.family_config_cache.refresh() From 6c75aa2fd765924e592ebcc28e4c0a8cee9ac35d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 16 Nov 2021 10:38:26 +0100 Subject: [PATCH 09/16] moved sync server lib function to scene inventory lib --- openpype/tools/sceneinventory/lib.py | 74 ++++++++++++++++++++++++++ openpype/tools/sceneinventory/model.py | 23 ++++---- 2 files changed, 87 insertions(+), 10 deletions(-) diff --git a/openpype/tools/sceneinventory/lib.py b/openpype/tools/sceneinventory/lib.py index 0ac7622d65..7653e1da89 100644 --- a/openpype/tools/sceneinventory/lib.py +++ b/openpype/tools/sceneinventory/lib.py @@ -1,3 +1,9 @@ +import os +from openpype_modules import sync_server + +from Qt import QtGui + + def walk_hierarchy(node): """Recursively yield group node.""" for child in node.children(): @@ -6,3 +12,71 @@ def walk_hierarchy(node): for _child in walk_hierarchy(child): yield _child + + +def get_site_icons(): + resource_path = os.path.join( + os.path.dirname(sync_server.sync_server_module.__file__), + "providers", + "resources" + ) + icons = {} + # TODO get from sync module + for provider in ["studio", "local_drive", "gdrive"]: + pix_url = "{}/{}.png".format(resource_path, provider) + icons[provider] = QtGui.QIcon(pix_url) + + return icons + + +def get_progress_for_repre(repre_doc, active_site, remote_site): + """ + Calculates average progress for representation. + + If site has created_dt >> fully available >> progress == 1 + + Could be calculated in aggregate if it would be too slow + Args: + repre_doc(dict): representation dict + Returns: + (dict) with active and remote sites progress + {'studio': 1.0, 'gdrive': -1} - gdrive site is not present + -1 is used to highlight the site should be added + {'studio': 1.0, 'gdrive': 0.0} - gdrive site is present, not + uploaded yet + """ + progress = {active_site: -1, remote_site: -1} + if not repre_doc: + return progress + + files = {active_site: 0, remote_site: 0} + doc_files = repre_doc.get("files") or [] + for doc_file in doc_files: + if not isinstance(doc_file, dict): + continue + + sites = doc_file.get("sites") or [] + for site in sites: + if ( + # Pype 2 compatibility + not isinstance(site, dict) + # Check if site name is one of progress sites + or site["name"] not in progress + ): + continue + + files[site["name"]] += 1 + norm_progress = max(progress[site["name"]], 0) + if site.get("created_dt"): + progress[site["name"]] = norm_progress + 1 + elif site.get("progress"): + progress[site["name"]] = norm_progress + site["progress"] + else: # site exists, might be failed, do not add again + progress[site["name"]] = 0 + + # for example 13 fully avail. files out of 26 >> 13/26 = 0.5 + avg_progress = { + active_site: progress[active_site] / max(files[active_site], 1), + remote_site: progress[remote_site] / max(files[remote_site], 1) + } + return avg_progress diff --git a/openpype/tools/sceneinventory/model.py b/openpype/tools/sceneinventory/model.py index bf7b296703..5962802c30 100644 --- a/openpype/tools/sceneinventory/model.py +++ b/openpype/tools/sceneinventory/model.py @@ -6,11 +6,14 @@ from Qt import QtCore, QtGui from avalon import api, io, style, schema from avalon.vendor import qtawesome -from avalon.tools import lib as tools_lib from avalon.lib import HeroVersionType from avalon.tools.models import TreeModel, Item -from . import lib +from .lib import ( + get_site_icons, + walk_hierarchy, + get_progress_for_repre +) from openpype.modules import ModulesManager @@ -37,7 +40,7 @@ class InventoryModel(TreeModel): manager = ModulesManager() sync_server = manager.modules_by_name["sync_server"] self.sync_enabled = sync_server.enabled - self._icons = {} + self._site_icons = {} self.active_site = self.remote_site = None self.active_provider = self.remote_provider = None @@ -66,11 +69,11 @@ class InventoryModel(TreeModel): self.active_provider = active_provider self.remote_site = remote_site self.remote_provider = remote_provider - self._icons = tools_lib.get_repre_icons() + self._site_icons = get_site_icons() if "active_site" not in self.Columns: self.Columns.append("active_site") if "remote_site" not in self.Columns: - self.Columns.extend("remote_site") + self.Columns.append("remote_site") def outdated(self, item): value = item.get("version") @@ -106,7 +109,7 @@ class InventoryModel(TreeModel): if self._hierarchy_view: # If current group is not outdated, check if any # outdated children. - for _node in lib.walk_hierarchy(item): + for _node in walk_hierarchy(item): if self.outdated(_node): return self.CHILD_OUTDATED_COLOR else: @@ -114,7 +117,7 @@ class InventoryModel(TreeModel): if self._hierarchy_view: # Although this is not a group item, we still need # to distinguish which one contain outdated child. - for _node in lib.walk_hierarchy(item): + for _node in walk_hierarchy(item): if self.outdated(_node): return self.CHILD_OUTDATED_COLOR.darker(150) @@ -142,9 +145,9 @@ class InventoryModel(TreeModel): if item.get("isGroupNode"): column_name = self.Columns[index.column()] if column_name == 'active_site': - return self._icons.get(item.get('active_site_provider')) + return self._site_icons.get(item.get('active_site_provider')) if column_name == 'remote_site': - return self._icons.get(item.get('remote_site_provider')) + return self._site_icons.get(item.get('remote_site_provider')) if role == QtCore.Qt.DisplayRole and item.get("isGroupNode"): column_name = self.Columns[index.column()] @@ -395,7 +398,7 @@ class InventoryModel(TreeModel): group_node["isGroupNode"] = True if self.sync_enabled: - progress = tools_lib.get_progress_for_repre( + progress = get_progress_for_repre( representation, self.active_site, self.remote_site ) group_node["active_site"] = self.active_site From 0c7a0a04c40333db08f531ae56639dd8c7d38075 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 16 Nov 2021 10:48:07 +0100 Subject: [PATCH 10/16] removed avalon tools import --- openpype/tools/sceneinventory/window.py | 31 ++++++++++++++++--------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/openpype/tools/sceneinventory/window.py b/openpype/tools/sceneinventory/window.py index 18d2c971d8..3583624a4a 100644 --- a/openpype/tools/sceneinventory/window.py +++ b/openpype/tools/sceneinventory/window.py @@ -12,6 +12,11 @@ from openpype import style from .proxy import FilterProxyModel from .model import InventoryModel +from openpype.tools.utils.lib import ( + qt_app_context, + preserve_expanded_rows, + preserve_selection +) from .view import SceneInvetoryView @@ -95,7 +100,7 @@ class SceneInventoryWindow(QtWidgets.QDialog): self.filter = text_filter self.outdated_only = outdated_only - self.view = view + self._view = view self.refresh_button = refresh_button self.model = model self.proxy = proxy @@ -122,16 +127,20 @@ class SceneInventoryWindow(QtWidgets.QDialog): """ def refresh(self, items=None): - with tools_lib.preserve_expanded_rows(tree_view=self.view, - role=self.model.UniqueRole): - with tools_lib.preserve_selection(tree_view=self.view, - role=self.model.UniqueRole, - current_index=False): + with preserve_expanded_rows( + tree_view=self._view, + role=self.model.UniqueRole + ): + with preserve_selection( + tree_view=self._view, + role=self.model.UniqueRole, + current_index=False + ): + kwargs = {"items": items} if self.view._hierarchy_view: - self.model.refresh(selected=self.view._selected, - items=items) - else: - self.model.refresh(items=items) + # TODO do not touch view's inner attribute + kwargs["selected"] = self.view._selected + self.model.refresh(**kwargs) def show(root=None, debug=False, parent=None, items=None): @@ -166,7 +175,7 @@ def show(root=None, debug=False, parent=None, items=None): else: api.Session["AVALON_PROJECT"] = os.environ.get("AVALON_PROJECT") - with tools_lib.application(): + with qt_app_context(): window = SceneInventoryWindow(parent) window.show() window.refresh(items=items) From fadfcacc364de5fd1048689596deed900c38bd14 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 16 Nov 2021 10:48:18 +0100 Subject: [PATCH 11/16] renamed checkbox variable --- openpype/tools/sceneinventory/window.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/openpype/tools/sceneinventory/window.py b/openpype/tools/sceneinventory/window.py index 3583624a4a..e9cbfa6670 100644 --- a/openpype/tools/sceneinventory/window.py +++ b/openpype/tools/sceneinventory/window.py @@ -48,9 +48,11 @@ class SceneInventoryWindow(QtWidgets.QDialog): filter_label = QtWidgets.QLabel("Search", self) text_filter = QtWidgets.QLineEdit(self) - outdated_only = QtWidgets.QCheckBox("Filter to outdated", self) - outdated_only.setToolTip("Show outdated files only") - outdated_only.setChecked(False) + outdated_only_checkbox = QtWidgets.QCheckBox( + "Filter to outdated", self + ) + outdated_only_checkbox.setToolTip("Show outdated files only") + outdated_only_checkbox.setChecked(False) icon = qtawesome.icon("fa.refresh", color="white") refresh_button = QtWidgets.QPushButton(self) @@ -59,7 +61,7 @@ class SceneInventoryWindow(QtWidgets.QDialog): control_layout = QtWidgets.QHBoxLayout() control_layout.addWidget(filter_label) control_layout.addWidget(text_filter) - control_layout.addWidget(outdated_only) + control_layout.addWidget(outdated_only_checkbox) control_layout.addWidget(refresh_button) # endregion control @@ -92,14 +94,12 @@ class SceneInventoryWindow(QtWidgets.QDialog): # signals text_filter.textChanged.connect(proxy.setFilterRegExp) - outdated_only.stateChanged.connect(proxy.set_filter_outdated) + outdated_only_checkbox.stateChanged.connect(proxy.set_filter_outdated) refresh_button.clicked.connect(self.refresh) view.data_changed.connect(self.refresh) view.hierarchy_view.connect(model.set_hierarchy_view) view.hierarchy_view.connect(proxy.set_hierarchy_view) - self.filter = text_filter - self.outdated_only = outdated_only self._view = view self.refresh_button = refresh_button self.model = model From 3a3e83e58976be38a5625f5277a426a3027d3642 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 16 Nov 2021 10:48:32 +0100 Subject: [PATCH 12/16] moved proxy into model.py --- openpype/tools/sceneinventory/model.py | 148 ++++++++++++++++++++++- openpype/tools/sceneinventory/proxy.py | 150 ------------------------ openpype/tools/sceneinventory/window.py | 8 +- 3 files changed, 151 insertions(+), 155 deletions(-) delete mode 100644 openpype/tools/sceneinventory/proxy.py diff --git a/openpype/tools/sceneinventory/model.py b/openpype/tools/sceneinventory/model.py index 5962802c30..3a4e5d5a4b 100644 --- a/openpype/tools/sceneinventory/model.py +++ b/openpype/tools/sceneinventory/model.py @@ -1,3 +1,4 @@ +import re import logging from collections import defaultdict @@ -346,10 +347,10 @@ class InventoryModel(TreeModel): self.add_child(group_node, parent=parent) - for items in group_items: + for _group_items in group_items: item_node = Item() item_node["Name"] = ", ".join( - [item["objectName"] for item in items] + [item["objectName"] for item in _group_items] ) self.add_child(item_node, parent=group_node) @@ -427,3 +428,146 @@ class InventoryModel(TreeModel): self.endResetModel() return self._root_item + + +class FilterProxyModel(QtCore.QSortFilterProxyModel): + """Filter model to where key column's value is in the filtered tags""" + + def __init__(self, *args, **kwargs): + super(FilterProxyModel, self).__init__(*args, **kwargs) + self._filter_outdated = False + self._hierarchy_view = False + + def filterAcceptsRow(self, row, parent): + model = self.sourceModel() + source_index = model.index(row, self.filterKeyColumn(), parent) + + # Always allow bottom entries (individual containers), since their + # parent group hidden if it wouldn't have been validated. + rows = model.rowCount(source_index) + if not rows: + return True + + # Filter by regex + if not self.filterRegExp().isEmpty(): + pattern = re.escape(self.filterRegExp().pattern()) + + if not self._matches(row, parent, pattern): + return False + + if self._filter_outdated: + # When filtering to outdated we filter the up to date entries + # thus we "allow" them when they are outdated + if not self._is_outdated(row, parent): + return False + + return True + + def set_filter_outdated(self, state): + """Set whether to show the outdated entries only.""" + state = bool(state) + + if state != self._filter_outdated: + self._filter_outdated = bool(state) + self.invalidateFilter() + + def set_hierarchy_view(self, state): + state = bool(state) + + if state != self._hierarchy_view: + self._hierarchy_view = state + + def _is_outdated(self, row, parent): + """Return whether row is outdated. + + A row is considered outdated if it has "version" and "highest_version" + data and in the internal data structure, and they are not of an + equal value. + + """ + def outdated(node): + version = node.get("version", None) + highest = node.get("highest_version", None) + + # Always allow indices that have no version data at all + if version is None and highest is None: + return True + + # If either a version or highest is present but not the other + # consider the item invalid. + if not self._hierarchy_view: + # Skip this check if in hierarchy view, or the child item + # node will be hidden even it's actually outdated. + if version is None or highest is None: + return False + return version != highest + + index = self.sourceModel().index(row, self.filterKeyColumn(), parent) + + # The scene contents are grouped by "representation", e.g. the same + # "representation" loaded twice is grouped under the same header. + # Since the version check filters these parent groups we skip that + # check for the individual children. + has_parent = index.parent().isValid() + if has_parent and not self._hierarchy_view: + return True + + # Filter to those that have the different version numbers + node = index.internalPointer() + if outdated(node): + return True + + if self._hierarchy_view: + for _node in walk_hierarchy(node): + if outdated(_node): + return True + + return False + + def _matches(self, row, parent, pattern): + """Return whether row matches regex pattern. + + Args: + row (int): row number in model + parent (QtCore.QModelIndex): parent index + pattern (regex.pattern): pattern to check for in key + + Returns: + bool + + """ + model = self.sourceModel() + column = self.filterKeyColumn() + role = self.filterRole() + + def matches(row, parent, pattern): + index = model.index(row, column, parent) + key = model.data(index, role) + if re.search(pattern, key, re.IGNORECASE): + return True + + if matches(row, parent, pattern): + return True + + # Also allow if any of the children matches + source_index = model.index(row, column, parent) + rows = model.rowCount(source_index) + + if any( + matches(idx, source_index, pattern) + for idx in range(rows) + ): + return True + + if not self._hierarchy_view: + return False + + for idx in range(rows): + child_index = model.index(idx, column, source_index) + child_rows = model.rowCount(child_index) + return any( + self._matches(child_idx, child_index, pattern) + for child_idx in range(child_rows) + ) + + return True diff --git a/openpype/tools/sceneinventory/proxy.py b/openpype/tools/sceneinventory/proxy.py deleted file mode 100644 index 3c4295c446..0000000000 --- a/openpype/tools/sceneinventory/proxy.py +++ /dev/null @@ -1,150 +0,0 @@ -import re - -from Qt import QtCore - -from . import lib - - -class FilterProxyModel(QtCore.QSortFilterProxyModel): - """Filter model to where key column's value is in the filtered tags""" - - def __init__(self, *args, **kwargs): - super(FilterProxyModel, self).__init__(*args, **kwargs) - self._filter_outdated = False - self._hierarchy_view = False - - def filterAcceptsRow(self, row, parent): - model = self.sourceModel() - source_index = model.index(row, self.filterKeyColumn(), parent) - - # Always allow bottom entries (individual containers), since their - # parent group hidden if it wouldn't have been validated. - rows = model.rowCount(source_index) - if not rows: - return True - - # Filter by regex - if not self.filterRegExp().isEmpty(): - pattern = re.escape(self.filterRegExp().pattern()) - - if not self._matches(row, parent, pattern): - return False - - if self._filter_outdated: - # When filtering to outdated we filter the up to date entries - # thus we "allow" them when they are outdated - if not self._is_outdated(row, parent): - return False - - return True - - def set_filter_outdated(self, state): - """Set whether to show the outdated entries only.""" - state = bool(state) - - if state != self._filter_outdated: - self._filter_outdated = bool(state) - self.invalidateFilter() - - def set_hierarchy_view(self, state): - state = bool(state) - - if state != self._hierarchy_view: - self._hierarchy_view = state - - def _is_outdated(self, row, parent): - """Return whether row is outdated. - - A row is considered outdated if it has "version" and "highest_version" - data and in the internal data structure, and they are not of an - equal value. - - """ - def outdated(node): - version = node.get("version", None) - highest = node.get("highest_version", None) - - # Always allow indices that have no version data at all - if version is None and highest is None: - return True - - # If either a version or highest is present but not the other - # consider the item invalid. - if not self._hierarchy_view: - # Skip this check if in hierarchy view, or the child item - # node will be hidden even it's actually outdated. - if version is None or highest is None: - return False - return version != highest - - index = self.sourceModel().index(row, self.filterKeyColumn(), parent) - - # The scene contents are grouped by "representation", e.g. the same - # "representation" loaded twice is grouped under the same header. - # Since the version check filters these parent groups we skip that - # check for the individual children. - has_parent = index.parent().isValid() - if has_parent and not self._hierarchy_view: - return True - - # Filter to those that have the different version numbers - node = index.internalPointer() - is_outdated = outdated(node) - - if is_outdated: - return True - - if self._hierarchy_view: - for _node in lib.walk_hierarchy(node): - if outdated(_node): - return True - - return False - - def _matches(self, row, parent, pattern): - """Return whether row matches regex pattern. - - Args: - row (int): row number in model - parent (QtCore.QModelIndex): parent index - pattern (regex.pattern): pattern to check for in key - - Returns: - bool - - """ - model = self.sourceModel() - column = self.filterKeyColumn() - role = self.filterRole() - - def matches(row, parent, pattern): - index = model.index(row, column, parent) - key = model.data(index, role) - if re.search(pattern, key, re.IGNORECASE): - return True - - if matches(row, parent, pattern): - return True - - # Also allow if any of the children matches - source_index = model.index(row, column, parent) - rows = model.rowCount(source_index) - - if any( - matches(idx, source_index, pattern) - for idx in range(rows) - ): - return True - - if not self._hierarchy_view: - return False - - for i in range(rows): - child_i = model.index(i, column, source_index) - child_rows = model.rowCount(child_i) - return any( - self._matches(ch_i, child_i, pattern) - for ch_i in range(child_rows) - ) - - return True diff --git a/openpype/tools/sceneinventory/window.py b/openpype/tools/sceneinventory/window.py index e9cbfa6670..35ff2b5a55 100644 --- a/openpype/tools/sceneinventory/window.py +++ b/openpype/tools/sceneinventory/window.py @@ -9,14 +9,16 @@ from avalon.tools import lib as tools_lib from avalon.tools.delegates import VersionDelegate from openpype import style - -from .proxy import FilterProxyModel -from .model import InventoryModel from openpype.tools.utils.lib import ( qt_app_context, preserve_expanded_rows, preserve_selection ) + +from .model import ( + InventoryModel, + FilterProxyModel +) from .view import SceneInvetoryView From 280560652d4e9cae6e9fb2985da5d8bf5651e1aa Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 16 Nov 2021 11:07:33 +0100 Subject: [PATCH 13/16] more cleanup in code and imports --- .../tools/sceneinventory/switch_dialog.py | 24 +++---- openpype/tools/sceneinventory/view.py | 19 +++-- openpype/tools/sceneinventory/window.py | 72 +++++++++++-------- 3 files changed, 67 insertions(+), 48 deletions(-) diff --git a/openpype/tools/sceneinventory/switch_dialog.py b/openpype/tools/sceneinventory/switch_dialog.py index 37659b2370..f539294ded 100644 --- a/openpype/tools/sceneinventory/switch_dialog.py +++ b/openpype/tools/sceneinventory/switch_dialog.py @@ -1,11 +1,14 @@ import collections +import logging from Qt import QtWidgets, QtCore -from avalon import io, api, style +from avalon import io, api from avalon.vendor import qtawesome from .widgets import SearchComboBox +log = logging.getLogger("SwitchAssetDialog") + class ValidationState: def __init__(self): @@ -969,19 +972,16 @@ class SwitchAssetDialog(QtWidgets.QDialog): try: api.switch(container, repre_doc) except Exception: - log.warning( - ( - "Couldn't switch asset." - "See traceback for more information." - ), - exc_info=True + msg = ( + "Couldn't switch asset." + "See traceback for more information." ) - dialog = QtWidgets.QMessageBox() - dialog.setStyleSheet(style.load_stylesheet()) + log.warning(msg, exc_info=True) + dialog = QtWidgets.QMessageBox(self) dialog.setWindowTitle("Switch asset failed") - msg = "Switch asset failed. "\ - "Search console log for more details" - dialog.setText(msg) + dialog.setText( + "Switch asset failed. Search console log for more details" + ) dialog.exec_() self.switched.emit() diff --git a/openpype/tools/sceneinventory/view.py b/openpype/tools/sceneinventory/view.py index 08d5499355..80f26a881d 100644 --- a/openpype/tools/sceneinventory/view.py +++ b/openpype/tools/sceneinventory/view.py @@ -22,7 +22,7 @@ log = logging.getLogger("SceneInventory") class SceneInvetoryView(QtWidgets.QTreeView): data_changed = QtCore.Signal() - hierarchy_view = QtCore.Signal(bool) + hierarchy_view_changed = QtCore.Signal(bool) def __init__(self, parent=None): super(SceneInvetoryView, self).__init__(parent=parent) @@ -41,10 +41,15 @@ class SceneInvetoryView(QtWidgets.QTreeView): self.sync_server = manager.modules_by_name["sync_server"] self.sync_enabled = self.sync_server.enabled + def _set_hierarchy_view(self, enabled): + if enabled == self._hierarchy_view: + return + self._hierarchy_view = enabled + self.hierarchy_view_changed.emit(enabled) + def _enter_hierarchy(self, items): self._selected = set(i["objectName"] for i in items) - self._hierarchy_view = True - self.hierarchy_view.emit(True) + self._set_hierarchy_view(True) self.data_changed.emit() self.expandToDepth(1) self.setStyleSheet(""" @@ -54,8 +59,7 @@ class SceneInvetoryView(QtWidgets.QTreeView): """) def _leave_hierarchy(self): - self._hierarchy_view = False - self.hierarchy_view.emit(False) + self._set_hierarchy_view(False) self.data_changed.emit() self.setStyleSheet("QTreeView {}") @@ -189,8 +193,9 @@ class SceneInvetoryView(QtWidgets.QTreeView): try: api.update(item, version_name) except AssertionError: - self._show_version_error_dialog(version_name, - [item]) + self._show_version_error_dialog( + version_name, [item] + ) log.warning("Update failed", exc_info=True) self.data_changed.emit() diff --git a/openpype/tools/sceneinventory/window.py b/openpype/tools/sceneinventory/window.py index 35ff2b5a55..e71af6a93d 100644 --- a/openpype/tools/sceneinventory/window.py +++ b/openpype/tools/sceneinventory/window.py @@ -5,14 +5,13 @@ from Qt import QtWidgets, QtCore from avalon.vendor import qtawesome from avalon import io, api -from avalon.tools import lib as tools_lib -from avalon.tools.delegates import VersionDelegate - from openpype import style +from openpype.tools.utils.delegates import VersionDelegate from openpype.tools.utils.lib import ( qt_app_context, preserve_expanded_rows, - preserve_selection + preserve_selection, + FamilyConfigCache ) from .model import ( @@ -37,14 +36,13 @@ class SceneInventoryWindow(QtWidgets.QDialog): self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint ) - self.resize(1100, 480) - self.setWindowTitle( - "Scene Inventory 1.0 - {}".format( - os.getenv("AVALON_PROJECT") or "" - ) - ) + project_name = os.getenv("AVALON_PROJECT") or "" + self.setWindowTitle("Scene Inventory 1.0 - {}".format(project_name)) self.setObjectName("SceneInventory") - self.setProperty("saveWindowPref", True) # Maya only property! + # Maya only property + self.setProperty("saveWindowPref", True) + + self.resize(1100, 480) # region control filter_label = QtWidgets.QLabel("Search", self) @@ -67,9 +65,9 @@ class SceneInventoryWindow(QtWidgets.QDialog): control_layout.addWidget(refresh_button) # endregion control - self.family_config_cache = tools_lib.global_family_cache() + family_config_cache = FamilyConfigCache(io) - model = InventoryModel(self.family_config_cache) + model = InventoryModel(family_config_cache) proxy = FilterProxyModel() proxy.setSourceModel(model) proxy.setDynamicSortFilter(True) @@ -95,23 +93,27 @@ class SceneInventoryWindow(QtWidgets.QDialog): layout.addWidget(view) # signals - text_filter.textChanged.connect(proxy.setFilterRegExp) - outdated_only_checkbox.stateChanged.connect(proxy.set_filter_outdated) - refresh_button.clicked.connect(self.refresh) + text_filter.textChanged.connect(self._on_text_filter_change) + outdated_only_checkbox.stateChanged.connect( + self._on_outdated_state_change + ) + view.hierarchy_view_changed.connect( + self._on_hiearchy_view_change + ) view.data_changed.connect(self.refresh) - view.hierarchy_view.connect(model.set_hierarchy_view) - view.hierarchy_view.connect(proxy.set_hierarchy_view) + refresh_button.clicked.connect(self.refresh) + self._outdated_only_checkbox = outdated_only_checkbox self._view = view - self.refresh_button = refresh_button - self.model = model - self.proxy = proxy + self._model = model + self._proxy = proxy self._version_delegate = version_delegate - - self.family_config_cache.refresh() + self._family_config_cache = family_config_cache self._first_show = True + family_config_cache.refresh() + def showEvent(self, event): super(SceneInventoryWindow, self).showEvent(event) if self._first_show: @@ -131,18 +133,30 @@ class SceneInventoryWindow(QtWidgets.QDialog): def refresh(self, items=None): with preserve_expanded_rows( tree_view=self._view, - role=self.model.UniqueRole + role=self._model.UniqueRole ): with preserve_selection( tree_view=self._view, - role=self.model.UniqueRole, + role=self._model.UniqueRole, current_index=False ): kwargs = {"items": items} - if self.view._hierarchy_view: - # TODO do not touch view's inner attribute - kwargs["selected"] = self.view._selected - self.model.refresh(**kwargs) + # TODO do not touch view's inner attribute + if self._view._hierarchy_view: + kwargs["selected"] = self._view._selected + self._model.refresh(**kwargs) + + def _on_hiearchy_view_change(self, enabled): + self._proxy.set_hierarchy_view(enabled) + self._model.set_hierarchy_view(enabled) + + def _on_text_filter_change(self, text_filter): + self._proxy.setFilterRegExp(text_filter) + + def _on_outdated_state_change(self): + self._proxy.set_filter_outdated( + self._outdated_only_checkbox.isChecked() + ) def show(root=None, debug=False, parent=None, items=None): From 797d5d2d9af88b40002c726e6d3855cd47cf2d35 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 16 Nov 2021 11:53:37 +0100 Subject: [PATCH 14/16] set stretch of switch dialog layout --- openpype/tools/sceneinventory/switch_dialog.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/tools/sceneinventory/switch_dialog.py b/openpype/tools/sceneinventory/switch_dialog.py index f539294ded..ecad8eac0a 100644 --- a/openpype/tools/sceneinventory/switch_dialog.py +++ b/openpype/tools/sceneinventory/switch_dialog.py @@ -71,6 +71,10 @@ class SwitchAssetDialog(QtWidgets.QDialog): main_layout.addWidget(repre_label, 2, 2) # Btn column main_layout.addWidget(accept_btn, 1, 3) + main_layout.setColumnStretch(0, 1) + main_layout.setColumnStretch(1, 1) + main_layout.setColumnStretch(2, 1) + main_layout.setColumnStretch(3, 0) assets_combox.currentIndexChanged.connect( self._combobox_value_changed From 215bfd7c47d62504f0fe9b0d26cdecab7b338133 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 16 Nov 2021 12:03:59 +0100 Subject: [PATCH 15/16] fixed too long line --- openpype/tools/sceneinventory/model.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/openpype/tools/sceneinventory/model.py b/openpype/tools/sceneinventory/model.py index 3a4e5d5a4b..d2b7f8b70f 100644 --- a/openpype/tools/sceneinventory/model.py +++ b/openpype/tools/sceneinventory/model.py @@ -145,10 +145,13 @@ class InventoryModel(TreeModel): if item.get("isGroupNode"): column_name = self.Columns[index.column()] - if column_name == 'active_site': - return self._site_icons.get(item.get('active_site_provider')) - if column_name == 'remote_site': - return self._site_icons.get(item.get('remote_site_provider')) + if column_name == "active_site": + provider = item.get("active_site_provider") + return self._site_icons.get(provider) + + if column_name == "remote_site": + provider = item.get("remote_site_provider") + return self._site_icons.get(provider) if role == QtCore.Qt.DisplayRole and item.get("isGroupNode"): column_name = self.Columns[index.column()] From d6c608199d871fe48f8c3820b2ea8e0cd7dc57e6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 16 Nov 2021 12:04:59 +0100 Subject: [PATCH 16/16] fixed too long line --- openpype/tools/utils/host_tools.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py index 8011410ce9..e87da7f0b4 100644 --- a/openpype/tools/utils/host_tools.py +++ b/openpype/tools/utils/host_tools.py @@ -156,7 +156,9 @@ class HostToolsHelper: if self._scene_inventory_tool is None: from openpype.tools.sceneinventory import SceneInventoryWindow - scene_inventory_window = SceneInventoryWindow(parent=parent or self._parent) + scene_inventory_window = SceneInventoryWindow( + parent=parent or self._parent + ) self._scene_inventory_tool = scene_inventory_window return self._scene_inventory_tool