diff --git a/client/ayon_core/modules/avalon_apps/avalon_app.py b/client/ayon_core/modules/avalon_apps/avalon_app.py index efae0c8a9f..041bd770a8 100644 --- a/client/ayon_core/modules/avalon_apps/avalon_app.py +++ b/client/ayon_core/modules/avalon_apps/avalon_app.py @@ -1,6 +1,5 @@ import os -from ayon_core import AYON_SERVER_ENABLED from ayon_core.modules import OpenPypeModule, ITrayModule @@ -77,10 +76,8 @@ class AvalonModule(OpenPypeModule, ITrayModule): def show_library_loader(self): if self._library_loader_window is None: from ayon_core.pipeline import install_openpype_plugins - if AYON_SERVER_ENABLED: - self._init_ayon_loader() - else: - self._init_library_loader() + + self._init_library_loader() install_openpype_plugins() @@ -100,22 +97,7 @@ class AvalonModule(OpenPypeModule, ITrayModule): self.rest_api_obj = AvalonRestApiResource(self, server_manager) def _init_library_loader(self): - from qtpy import QtCore - from ayon_core.tools.libraryloader import LibraryLoaderWindow - - libraryloader = LibraryLoaderWindow( - show_projects=True, - show_libraries=True - ) - # Remove always on top flag for tray - window_flags = libraryloader.windowFlags() - if window_flags | QtCore.Qt.WindowStaysOnTopHint: - window_flags ^= QtCore.Qt.WindowStaysOnTopHint - libraryloader.setWindowFlags(window_flags) - self._library_loader_window = libraryloader - - def _init_ayon_loader(self): - from ayon_core.tools.ayon_loader.ui import LoaderWindow + from ayon_core.tools.loader.ui import LoaderWindow libraryloader = LoaderWindow() diff --git a/client/ayon_core/modules/launcher_action.py b/client/ayon_core/modules/launcher_action.py index f7ea6fc63c..2b5c76a272 100644 --- a/client/ayon_core/modules/launcher_action.py +++ b/client/ayon_core/modules/launcher_action.py @@ -1,6 +1,6 @@ import os -from ayon_core import AYON_CORE_ROOT, AYON_SERVER_ENABLED +from ayon_core import AYON_CORE_ROOT from ayon_core.modules import ( OpenPypeModule, ITrayAction, @@ -67,10 +67,7 @@ class LauncherAction(OpenPypeModule, ITrayAction): def _create_window(self): if self._window: return - if AYON_SERVER_ENABLED: - from ayon_core.tools.ayon_launcher.ui import LauncherWindow - else: - from ayon_core.tools.launcher import LauncherWindow + from ayon_core.tools.launcher.ui import LauncherWindow self._window = LauncherWindow() def _show_launcher(self): diff --git a/client/ayon_core/plugins/load/push_to_library.py b/client/ayon_core/plugins/load/push_to_library.py index 421a1e0c37..1ac2a3fb3b 100644 --- a/client/ayon_core/plugins/load/push_to_library.py +++ b/client/ayon_core/plugins/load/push_to_library.py @@ -1,6 +1,6 @@ import os -from ayon_core import AYON_CORE_ROOT, AYON_SERVER_ENABLED +from ayon_core import AYON_CORE_ROOT from ayon_core.lib import get_openpype_execute_args, run_detached_process from ayon_core.pipeline import load from ayon_core.pipeline.load import LoadError @@ -33,20 +33,12 @@ class PushToLibraryProject(load.SubsetLoaderPlugin): context = tuple(filtered_contexts)[0] - if AYON_SERVER_ENABLED: - push_tool_script_path = os.path.join( - AYON_CORE_ROOT, - "tools", - "ayon_push_to_project", - "main.py" - ) - else: - push_tool_script_path = os.path.join( - AYON_CORE_ROOT, - "tools", - "push_to_project", - "app.py" - ) + push_tool_script_path = os.path.join( + AYON_CORE_ROOT, + "tools", + "push_to_project", + "main.py" + ) project_doc = context["project"] version_doc = context["version"] diff --git a/client/ayon_core/tools/ayon_loader/__init__.py b/client/ayon_core/tools/ayon_loader/__init__.py deleted file mode 100644 index 09ecf65f3a..0000000000 --- a/client/ayon_core/tools/ayon_loader/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .control import LoaderController - - -__all__ = ( - "LoaderController", -) diff --git a/client/ayon_core/tools/ayon_push_to_project/__init__.py b/client/ayon_core/tools/ayon_push_to_project/__init__.py deleted file mode 100644 index 83df110c96..0000000000 --- a/client/ayon_core/tools/ayon_push_to_project/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .control import PushToContextController - - -__all__ = ( - "PushToContextController", -) diff --git a/client/ayon_core/tools/ayon_sceneinventory/__init__.py b/client/ayon_core/tools/ayon_sceneinventory/__init__.py deleted file mode 100644 index 5412e2fea2..0000000000 --- a/client/ayon_core/tools/ayon_sceneinventory/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .control import SceneInventoryController - - -__all__ = ( - "SceneInventoryController", -) diff --git a/client/ayon_core/tools/ayon_sceneinventory/model.py b/client/ayon_core/tools/ayon_sceneinventory/model.py deleted file mode 100644 index 05ecfd442d..0000000000 --- a/client/ayon_core/tools/ayon_sceneinventory/model.py +++ /dev/null @@ -1,623 +0,0 @@ -import collections -import re -import logging -import uuid -import copy - -from collections import defaultdict - -from qtpy import QtCore, QtGui -import qtawesome - -from ayon_core.client import ( - get_assets, - get_subsets, - get_versions, - get_last_version_by_subset_id, - get_representations, -) -from ayon_core.pipeline import ( - get_current_project_name, - schema, - HeroVersionType, -) -from ayon_core.style import get_default_entity_icon_color -from ayon_core.tools.utils.models import TreeModel, Item -from ayon_core.tools.ayon_utils.widgets import get_qt_icon - - -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 - - -class InventoryModel(TreeModel): - """The model for the inventory""" - - Columns = [ - "Name", - "version", - "count", - "family", - "group", - "loader", - "objectName", - "active_site", - "remote_site", - ] - active_site_col = Columns.index("active_site") - remote_site_col = Columns.index("remote_site") - - 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, controller, parent=None): - super(InventoryModel, self).__init__(parent) - self.log = logging.getLogger(self.__class__.__name__) - - self._controller = controller - - self._hierarchy_view = False - - self._default_icon_color = get_default_entity_icon_color() - - site_icons = self._controller.get_site_provider_icons() - - self._site_icons = { - provider: get_qt_icon(icon_def) - for provider, icon_def in site_icons.items() - } - - 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 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 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", self._default_icon_color) - if item.get("isGroupNode"): # group-item - return qtawesome.icon("fa.folder", color=color) - if item.get("isNotSet"): - return qtawesome.icon("fa.exclamation-circle", color=color) - - return qtawesome.icon("fa.file-o", color=color) - - if index.column() == 3: - # Family icon - return item.get("familyIcon", None) - - column_name = self.Columns[index.column()] - - if column_name == "group" and item.get("group"): - return qtawesome.icon("fa.object-group", - color=get_default_entity_icon_color()) - - if item.get("isGroupNode"): - 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()] - 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, containers=None): - """Refresh the model""" - - # for debugging or testing, injecting items from outside - if containers is None: - containers = self._controller.get_containers() - - self.clear() - if not selected or not self._hierarchy_view: - self._add_containers(containers) - return - - # Filter by cherry-picked items - self._add_containers(( - container - for container in containers - if container["objectName"] in selected - )) - - def _add_containers(self, containers, 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: - containers (generator): Container items. - 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 - """ - - project_name = get_current_project_name() - - self.beginResetModel() - - # Group by representation - grouped = defaultdict(lambda: {"containers": list()}) - for container in containers: - repre_id = container["representation"] - grouped[repre_id]["containers"].append(container) - - ( - repres_by_id, - versions_by_id, - products_by_id, - folders_by_id, - ) = self._query_entities(project_name, set(grouped.keys())) - # Add to model - not_found = defaultdict(list) - not_found_ids = [] - for repre_id, group_dict in sorted(grouped.items()): - group_containers = group_dict["containers"] - representation = repres_by_id.get(repre_id) - if not representation: - not_found["representation"].extend(group_containers) - not_found_ids.append(repre_id) - continue - - version = versions_by_id.get(representation["parent"]) - if not version: - not_found["version"].extend(group_containers) - not_found_ids.append(repre_id) - continue - - product = products_by_id.get(version["parent"]) - if not product: - not_found["product"].extend(group_containers) - not_found_ids.append(repre_id) - continue - - folder = folders_by_id.get(product["parent"]) - if not folder: - not_found["folder"].extend(group_containers) - not_found_ids.append(repre_id) - continue - - group_dict.update({ - "representation": representation, - "version": version, - "subset": product, - "asset": folder - }) - - for _repre_id in not_found_ids: - grouped.pop(_repre_id) - - for where, group_containers 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_containers) - group_node["isGroupNode"] = False - group_node["isNotSet"] = True - - self.add_child(group_node, parent=parent) - - for container in group_containers: - item_node = Item() - item_node.update(container) - item_node["Name"] = container.get("objectName", "NO NAME") - item_node["isNotFound"] = True - self.add_child(item_node, parent=group_node) - - # TODO Use product icons - family_icon = qtawesome.icon( - "fa.folder", color="#0091B2" - ) - # Prepare site sync specific data - progress_by_id = self._controller.get_representations_site_progress( - set(grouped.keys()) - ) - sites_info = self._controller.get_sites_information() - - for repre_id, group_dict in sorted(grouped.items()): - group_containers = group_dict["containers"] - representation = group_dict["representation"] - version = group_dict["version"] - subset = group_dict["subset"] - asset = group_dict["asset"] - - # Get the primary family - maj_version, _ = schema.get_schema_version(subset["schema"]) - if maj_version < 3: - src_doc = version - else: - src_doc = subset - - prim_family = src_doc["data"].get("family") - if not prim_family: - families = src_doc["data"].get("families") - if families: - prim_family = families[0] - - # Store the highest available version so the model can know - # whether current version is currently up-to-date. - highest_version = get_last_version_by_subset_id( - project_name, version["parent"] - ) - - # create the group header - group_node = Item() - group_node["Name"] = "{}_{}: ({})".format( - 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"] = prim_family or "" - group_node["familyIcon"] = family_icon - group_node["count"] = len(group_containers) - group_node["isGroupNode"] = True - group_node["group"] = subset["data"].get("subsetGroup") - - # Site sync specific data - progress = progress_by_id[repre_id] - group_node.update(sites_info) - group_node["active_site_progress"] = progress["active_site"] - group_node["remote_site_progress"] = progress["remote_site"] - - self.add_child(group_node, parent=parent) - - for container in group_containers: - item_node = Item() - item_node.update(container) - - # 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"] = container["namespace"] - - self.add_child(item_node, parent=group_node) - - self.endResetModel() - - return self._root_item - - def _query_entities(self, project_name, repre_ids): - """Query entities for representations from containers. - - Returns: - tuple[dict, dict, dict, dict]: Representation, version, product - and folder documents by id. - """ - - repres_by_id = {} - versions_by_id = {} - products_by_id = {} - folders_by_id = {} - output = ( - repres_by_id, - versions_by_id, - products_by_id, - folders_by_id, - ) - - filtered_repre_ids = set() - for repre_id in repre_ids: - # Filter out invalid representation ids - # NOTE: This is added because scenes from OpenPype did contain - # ObjectId from mongo. - try: - uuid.UUID(repre_id) - filtered_repre_ids.add(repre_id) - except ValueError: - continue - if not filtered_repre_ids: - return output - - repre_docs = get_representations(project_name, repre_ids) - repres_by_id.update({ - repre_doc["_id"]: repre_doc - for repre_doc in repre_docs - }) - version_ids = { - repre_doc["parent"] for repre_doc in repres_by_id.values() - } - if not version_ids: - return output - - version_docs = get_versions(project_name, version_ids, hero=True) - versions_by_id.update({ - version_doc["_id"]: version_doc - for version_doc in version_docs - }) - hero_versions_by_subversion_id = collections.defaultdict(list) - for version_doc in versions_by_id.values(): - if version_doc["type"] != "hero_version": - continue - subversion = version_doc["version_id"] - hero_versions_by_subversion_id[subversion].append(version_doc) - - if hero_versions_by_subversion_id: - subversion_ids = set( - hero_versions_by_subversion_id.keys() - ) - subversion_docs = get_versions(project_name, subversion_ids) - for subversion_doc in subversion_docs: - subversion_id = subversion_doc["_id"] - subversion_ids.discard(subversion_id) - h_version_docs = hero_versions_by_subversion_id[subversion_id] - for version_doc in h_version_docs: - version_doc["name"] = HeroVersionType( - subversion_doc["name"] - ) - version_doc["data"] = copy.deepcopy( - subversion_doc["data"] - ) - - for subversion_id in subversion_ids: - h_version_docs = hero_versions_by_subversion_id[subversion_id] - for version_doc in h_version_docs: - versions_by_id.pop(version_doc["_id"]) - - product_ids = { - version_doc["parent"] - for version_doc in versions_by_id.values() - } - if not product_ids: - return output - product_docs = get_subsets(project_name, product_ids) - products_by_id.update({ - product_doc["_id"]: product_doc - for product_doc in product_docs - }) - folder_ids = { - product_doc["parent"] - for product_doc in products_by_id.values() - } - if not folder_ids: - return output - - folder_docs = get_assets(project_name, folder_ids) - folders_by_id.update({ - folder_doc["_id"]: folder_doc - for folder_doc in folder_docs - }) - return output - - -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 hasattr(self, "filterRegExp"): - regex = self.filterRegExp() - else: - regex = self.filterRegularExpression() - pattern = regex.pattern() - if pattern: - pattern = re.escape(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/client/ayon_core/tools/ayon_sceneinventory/view.py b/client/ayon_core/tools/ayon_sceneinventory/view.py deleted file mode 100644 index 214be68ae0..0000000000 --- a/client/ayon_core/tools/ayon_sceneinventory/view.py +++ /dev/null @@ -1,825 +0,0 @@ -import uuid -import collections -import logging -import itertools -from functools import partial - -from qtpy import QtWidgets, QtCore -import qtawesome - -from ayon_core.client import ( - get_version_by_id, - get_versions, - get_hero_versions, - get_representation_by_id, - get_representations, -) -from ayon_core import style -from ayon_core.pipeline import ( - HeroVersionType, - update_container, - remove_container, - discover_inventory_actions, -) -from ayon_core.tools.utils.lib import ( - iter_model_rows, - format_version -) - -from .switch_dialog import SwitchAssetDialog -from .model import InventoryModel - - -DEFAULT_COLOR = "#fb9c15" - -log = logging.getLogger("SceneInventory") - - -class SceneInventoryView(QtWidgets.QTreeView): - data_changed = QtCore.Signal() - hierarchy_view_changed = QtCore.Signal(bool) - - def __init__(self, controller, parent): - super(SceneInventoryView, self).__init__(parent=parent) - - # view settings - self.setIndentation(12) - self.setAlternatingRowColors(True) - self.setSortingEnabled(True) - self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) - self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - - self.customContextMenuRequested.connect(self._show_right_mouse_menu) - - self._hierarchy_view = False - self._selected = None - - self._controller = controller - - 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._set_hierarchy_view(True) - self.data_changed.emit() - self.expandToDepth(1) - self.setStyleSheet(""" - QTreeView { - border-color: #fb9c15; - } - """) - - def _leave_hierarchy(self): - self._set_hierarchy_view(False) - self.data_changed.emit() - self.setStyleSheet("QTreeView {}") - - def _build_item_menu_for_selection(self, items, menu): - # Exclude items that are "NOT FOUND" since setting versions, updating - # and removal won't work for those items. - items = [item for item in items if not item.get("isNotFound")] - if not items: - return - - # An item might not have a representation, for example when an item - # is listed as "NOT FOUND" - repre_ids = set() - for item in items: - repre_id = item["representation"] - try: - uuid.UUID(repre_id) - repre_ids.add(repre_id) - except ValueError: - pass - - project_name = self._controller.get_current_project_name() - repre_docs = get_representations( - project_name, representation_ids=repre_ids, fields=["parent"] - ) - - version_ids = { - repre_doc["parent"] - for repre_doc in repre_docs - } - - loaded_versions = get_versions( - project_name, version_ids=version_ids, hero=True - ) - - loaded_hero_versions = [] - versions_by_parent_id = collections.defaultdict(list) - subset_ids = set() - 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) - subset_ids.add(parent_id) - - all_versions = get_versions( - project_name, subset_ids=subset_ids, hero=True - ) - 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 = { - item["representation"] - for item in items - } - - repre_docs = get_representations( - project_name, - representation_ids=repre_ids, - fields=["parent"] - ) - - version_ids = set() - version_id_by_repre_id = {} - for repre_doc in repre_docs: - version_id = repre_doc["parent"] - repre_id = str(repre_doc["_id"]) - version_id_by_repre_id[repre_id] = version_id - version_ids.add(version_id) - - hero_versions = get_hero_versions( - project_name, - version_ids=version_ids, - fields=["version_id"] - ) - - hero_src_version_ids = set() - for hero_version in hero_versions: - version_id = hero_version["version_id"] - hero_src_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 = get_versions( - project_name, - version_ids=hero_src_version_ids, - fields=["name"] - ) - version_name_by_id = {} - for version_doc in version_docs: - version_name_by_id[version_doc["_id"]] = \ - version_doc["name"] - - # Specify version per item to update to - update_items = [] - update_versions = [] - for item in items: - repre_id = 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: - update_items.append(item) - update_versions.append(version_name) - self._update_containers(update_items, update_versions) - - 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_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: self._update_containers(items, version=-1) - ) - - change_to_hero = None - if has_available_hero_version: - # 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: self._update_containers(items, - version=HeroVersionType(-1)) - ) - - # 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 folder - switch_folder_icon = qtawesome.icon("fa.sitemap", color=DEFAULT_COLOR) - switch_folder_action = QtWidgets.QAction( - switch_folder_icon, - "Switch Folder", - menu - ) - switch_folder_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_folder_action) - - menu.addSeparator() - - menu.addAction(remove_action) - - 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) - """ - - if not self._controller.is_sync_server_enabled(): - return - - menu.addSeparator() - - 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) - - def _add_sites(self, repre_ids, site_type): - """(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) - site_type (Literal[active_site, remote_site]): Site type. - """ - - self._controller.resync_representations(repre_ids, site_type) - - self.data_changed.emit() - - 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) - - # 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 - back_to_flat_action = None - 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 back_to_flat_action is not None: - 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 = discover_inventory_actions() - 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": QtCore.QItemSelectionModel.Select, - "deselect": QtCore.QItemSelectionModel.Deselect, - "toggle": QtCore.QItemSelectionModel.Toggle, - }[options.get("mode", "select")] - - for index in iter_model_rows(model, 0): - item = index.data(InventoryModel.ItemRole) - if item.get("isGroupNode"): - continue - - name = item.get("objectName") - if name in object_names: - self.scrollTo(index) # Ensure item is visible - flags = select_mode | QtCore.QItemSelectionModel.Rows - selection_model.select(index, 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] - - project_name = self._controller.get_current_project_name() - # Get available versions for active representation - repre_doc = get_representation_by_id( - project_name, - active["representation"], - fields=["parent"] - ) - - repre_version_doc = get_version_by_id( - project_name, - repre_doc["parent"], - fields=["parent"] - ) - - version_docs = list(get_versions( - project_name, - subset_ids=[repre_version_doc["parent"]], - hero=True - )) - hero_version = None - standard_versions = [] - for version_doc in version_docs: - if version_doc["type"] == "hero_version": - hero_version = version_doc - else: - standard_versions.append(version_doc) - versions = list(reversed( - sorted(standard_versions, key=lambda item: item["name"]) - )) - 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(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 = 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] - self._update_containers(items, version) - - def _show_switch_dialog(self, items): - """Display Switch dialog""" - dialog = SwitchAssetDialog(self._controller, 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 - - 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 - - for item in items: - remove_container(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 version == -1: - version_str = "latest" - elif isinstance(version, HeroVersionType): - version_str = "hero" - elif isinstance(version, int): - version_str = "v{:03d}".format(version) - else: - version_str = version - - dialog = QtWidgets.QMessageBox(self) - dialog.setIcon(QtWidgets.QMessageBox.Warning) - dialog.setStyleSheet(style.load_stylesheet()) - dialog.setWindowTitle("Update failed") - - switch_btn = dialog.addButton( - "Switch Folder", - QtWidgets.QMessageBox.ActionRole - ) - switch_btn.clicked.connect(lambda: self._show_switch_dialog(items)) - - dialog.addButton(QtWidgets.QMessageBox.Cancel) - - msg = ( - "Version update to '{}' failed as representation doesn't exist." - "\n\nPlease update to version with a valid representation" - " OR \n use 'Switch Folder' button to change folder." - ).format(version_str) - dialog.setText(msg) - dialog.exec_() - - def update_all(self): - """Update all items that are currently 'outdated' in the view""" - # Get the source model through the proxy model - model = self.model().sourceModel() - - # Get all items from outdated groups - outdated_items = [] - for index in iter_model_rows(model, - column=0, - include_root=False): - item = index.data(model.ItemRole) - - if not item.get("isGroupNode"): - continue - - # Only the group nodes contain the "highest_version" data and as - # such we find only the groups and take its children. - if not model.outdated(item): - continue - - # Collect all children which we want to update - children = item.children() - outdated_items.extend(children) - - if not outdated_items: - log.info("Nothing to update.") - return - - # Trigger update to latest - self._update_containers(outdated_items, version=-1) - - def _update_containers(self, items, version): - """Helper to update items to given version (or version per item) - - If at least one item is specified this will always try to refresh - the inventory even if errors occurred on any of the items. - - Arguments: - items (list): Items to update - version (int or list): Version to set to. - This can be a list specifying a version for each item. - Like `update_container` version -1 sets the latest version - and HeroTypeVersion instances set the hero version. - - """ - - if isinstance(version, (list, tuple)): - # We allow a unique version to be specified per item. In that case - # the length must match with the items - assert len(items) == len(version), ( - "Number of items mismatches number of versions: " - "{} items - {} versions".format(len(items), len(version)) - ) - versions = version - else: - # Repeat the same version infinitely - versions = itertools.repeat(version) - - # Trigger update to latest - try: - for item, item_version in zip(items, versions): - try: - update_container(item, item_version) - except AssertionError: - self._show_version_error_dialog(item_version, [item]) - log.warning("Update failed", exc_info=True) - finally: - # Always update the scene inventory view, even if errors occurred - self.data_changed.emit() diff --git a/client/ayon_core/tools/ayon_sceneinventory/window.py b/client/ayon_core/tools/ayon_sceneinventory/window.py deleted file mode 100644 index d0ccf1d5d2..0000000000 --- a/client/ayon_core/tools/ayon_sceneinventory/window.py +++ /dev/null @@ -1,200 +0,0 @@ -from qtpy import QtWidgets, QtCore, QtGui -import qtawesome - -from ayon_core import style, resources -from ayon_core.tools.utils.delegates import VersionDelegate -from ayon_core.tools.utils.lib import ( - preserve_expanded_rows, - preserve_selection, -) -from ayon_core.tools.ayon_sceneinventory import SceneInventoryController - -from .model import ( - InventoryModel, - FilterProxyModel -) -from .view import SceneInventoryView - - -class ControllerVersionDelegate(VersionDelegate): - """Version delegate that uses controller to get project. - - Original VersionDelegate is using 'AvalonMongoDB' object instead. Don't - worry about the variable name, object is stored to '_dbcon' attribute. - """ - - def get_project_name(self): - self._dbcon.get_current_project_name() - - -class SceneInventoryWindow(QtWidgets.QDialog): - """Scene Inventory window""" - - def __init__(self, controller=None, parent=None): - super(SceneInventoryWindow, self).__init__(parent) - - if controller is None: - controller = SceneInventoryController() - - project_name = controller.get_current_project_name() - icon = QtGui.QIcon(resources.get_openpype_icon_filepath()) - self.setWindowIcon(icon) - self.setWindowTitle("Scene Inventory - {}".format(project_name)) - self.setObjectName("SceneInventory") - - self.resize(1100, 480) - - # region control - - filter_label = QtWidgets.QLabel("Search", self) - text_filter = QtWidgets.QLineEdit(self) - - 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.arrow-up", color="white") - update_all_button = QtWidgets.QPushButton(self) - update_all_button.setToolTip("Update all outdated to latest version") - update_all_button.setIcon(icon) - - icon = qtawesome.icon("fa.refresh", color="white") - refresh_button = QtWidgets.QPushButton(self) - refresh_button.setToolTip("Refresh") - refresh_button.setIcon(icon) - - control_layout = QtWidgets.QHBoxLayout() - control_layout.addWidget(filter_label) - control_layout.addWidget(text_filter) - control_layout.addWidget(outdated_only_checkbox) - control_layout.addWidget(update_all_button) - control_layout.addWidget(refresh_button) - - model = InventoryModel(controller) - proxy = FilterProxyModel() - proxy.setSourceModel(model) - proxy.setDynamicSortFilter(True) - proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) - - view = SceneInventoryView(controller, self) - view.setModel(proxy) - - sync_enabled = controller.is_sync_server_enabled() - view.setColumnHidden(model.active_site_col, not sync_enabled) - view.setColumnHidden(model.remote_site_col, not sync_enabled) - - # 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, 120) # group - view.setColumnWidth(5, 150) # loader - - # apply delegates - version_delegate = ControllerVersionDelegate(controller, self) - column = model.Columns.index("version") - view.setItemDelegateForColumn(column, version_delegate) - - layout = QtWidgets.QVBoxLayout(self) - layout.addLayout(control_layout) - layout.addWidget(view) - - show_timer = QtCore.QTimer() - show_timer.setInterval(0) - show_timer.setSingleShot(False) - - # signals - show_timer.timeout.connect(self._on_show_timer) - 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_hierarchy_view_change - ) - view.data_changed.connect(self._on_refresh_request) - refresh_button.clicked.connect(self._on_refresh_request) - update_all_button.clicked.connect(self._on_update_all) - - self._show_timer = show_timer - self._show_counter = 0 - self._controller = controller - self._update_all_button = update_all_button - self._outdated_only_checkbox = outdated_only_checkbox - self._view = view - self._model = model - self._proxy = proxy - self._version_delegate = version_delegate - - self._first_show = True - self._first_refresh = True - - def showEvent(self, event): - super(SceneInventoryWindow, self).showEvent(event) - if self._first_show: - self._first_show = False - self.setStyleSheet(style.load_stylesheet()) - - self._show_counter = 0 - self._show_timer.start() - - 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 accidentally perform Maya commands - whilst trying to name an instance. - - """ - - def _on_refresh_request(self): - """Signal callback to trigger 'refresh' without any arguments.""" - - self.refresh() - - def refresh(self, containers=None): - self._first_refresh = False - self._controller.reset() - 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 = {"containers": containers} - # TODO do not touch view's inner attribute - if self._view._hierarchy_view: - kwargs["selected"] = self._view._selected - self._model.refresh(**kwargs) - - def _on_show_timer(self): - if self._show_counter < 3: - self._show_counter += 1 - return - self._show_timer.stop() - self.refresh() - - def _on_hierarchy_view_change(self, enabled): - self._proxy.set_hierarchy_view(enabled) - self._model.set_hierarchy_view(enabled) - - def _on_text_filter_change(self, text_filter): - if hasattr(self._proxy, "setFilterRegExp"): - self._proxy.setFilterRegExp(text_filter) - else: - self._proxy.setFilterRegularExpression(text_filter) - - def _on_outdated_state_change(self): - self._proxy.set_filter_outdated( - self._outdated_only_checkbox.isChecked() - ) - - def _on_update_all(self): - self._view.update_all() diff --git a/client/ayon_core/tools/ayon_workfiles/__init__.py b/client/ayon_core/tools/ayon_workfiles/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/client/ayon_core/tools/context_dialog/__init__.py b/client/ayon_core/tools/context_dialog/__init__.py index 220b19dac5..4fb912fb62 100644 --- a/client/ayon_core/tools/context_dialog/__init__.py +++ b/client/ayon_core/tools/context_dialog/__init__.py @@ -1,9 +1,4 @@ -from ayon_core import AYON_SERVER_ENABLED - -if AYON_SERVER_ENABLED: - from ._ayon_window import ContextDialog, main -else: - from ._openpype_window import ContextDialog, main +from .window import ContextDialog, main __all__ = ( diff --git a/client/ayon_core/tools/context_dialog/_openpype_window.py b/client/ayon_core/tools/context_dialog/_openpype_window.py deleted file mode 100644 index 85464c7b74..0000000000 --- a/client/ayon_core/tools/context_dialog/_openpype_window.py +++ /dev/null @@ -1,396 +0,0 @@ -import os -import json - -from qtpy import QtWidgets, QtCore, QtGui - -from ayon_core import style -from ayon_core.pipeline import AvalonMongoDB -from ayon_core.tools.utils.lib import center_window, get_openpype_qt_app -from ayon_core.tools.utils.assets_widget import SingleSelectAssetsWidget -from ayon_core.tools.utils.constants import ( - PROJECT_NAME_ROLE -) -from ayon_core.tools.utils.tasks_widget import TasksWidget -from ayon_core.tools.utils.models import ( - ProjectModel, - ProjectSortFilterProxy -) - - -class ContextDialog(QtWidgets.QDialog): - """Dialog to select a context. - - Context has 3 parts: - - Project - - Asset - - Task - - It is possible to predefine project and asset. In that case their widgets - will have passed preselected values and will be disabled. - """ - def __init__(self, parent=None): - super(ContextDialog, self).__init__(parent) - - self.setWindowTitle("Select Context") - self.setWindowIcon(QtGui.QIcon(style.app_icon_path())) - - # Enable minimize and maximize for app - window_flags = QtCore.Qt.Window - if not parent: - window_flags |= QtCore.Qt.WindowStaysOnTopHint - self.setWindowFlags(window_flags) - self.setFocusPolicy(QtCore.Qt.StrongFocus) - - dbcon = AvalonMongoDB() - - # UI initialization - main_splitter = QtWidgets.QSplitter(self) - - # Left side widget contains project combobox and asset widget - left_side_widget = QtWidgets.QWidget(main_splitter) - - project_combobox = QtWidgets.QComboBox(left_side_widget) - # Styled delegate to propagate stylessheet - project_delegate = QtWidgets.QStyledItemDelegate(project_combobox) - project_combobox.setItemDelegate(project_delegate) - # Project model with only active projects without default item - project_model = ProjectModel( - dbcon, - only_active=True, - add_default_project=False - ) - # Sorting proxy model - project_proxy = ProjectSortFilterProxy() - project_proxy.setSourceModel(project_model) - project_combobox.setModel(project_proxy) - - # Assets widget - assets_widget = SingleSelectAssetsWidget( - dbcon, parent=left_side_widget - ) - - left_side_layout = QtWidgets.QVBoxLayout(left_side_widget) - left_side_layout.setContentsMargins(0, 0, 0, 0) - left_side_layout.addWidget(project_combobox) - left_side_layout.addWidget(assets_widget) - - # Right side of window contains only tasks - tasks_widget = TasksWidget(dbcon, main_splitter) - - # Add widgets to main splitter - main_splitter.addWidget(left_side_widget) - main_splitter.addWidget(tasks_widget) - - # Set stretch of both sides - main_splitter.setStretchFactor(0, 7) - main_splitter.setStretchFactor(1, 3) - - # Add confimation button to bottom right - ok_btn = QtWidgets.QPushButton("OK", self) - - buttons_layout = QtWidgets.QHBoxLayout() - buttons_layout.setContentsMargins(0, 0, 0, 0) - buttons_layout.addStretch(1) - buttons_layout.addWidget(ok_btn, 0) - - main_layout = QtWidgets.QVBoxLayout(self) - main_layout.addWidget(main_splitter, 1) - main_layout.addLayout(buttons_layout, 0) - - # Timer which will trigger asset refresh - # - this is needed because asset widget triggers - # finished refresh before hides spin box so we need to trigger - # refreshing in small offset if we want re-refresh asset widget - assets_timer = QtCore.QTimer() - assets_timer.setInterval(50) - assets_timer.setSingleShot(True) - - assets_timer.timeout.connect(self._on_asset_refresh_timer) - - project_combobox.currentIndexChanged.connect( - self._on_project_combo_change - ) - assets_widget.selection_changed.connect(self._on_asset_change) - assets_widget.refresh_triggered.connect(self._on_asset_refresh_trigger) - assets_widget.refreshed.connect(self._on_asset_widget_refresh_finished) - tasks_widget.task_changed.connect(self._on_task_change) - ok_btn.clicked.connect(self._on_ok_click) - - self._dbcon = dbcon - - self._project_combobox = project_combobox - self._project_model = project_model - self._project_proxy = project_proxy - self._project_delegate = project_delegate - - self._assets_widget = assets_widget - - self._tasks_widget = tasks_widget - - self._ok_btn = ok_btn - - self._strict = False - - # Values set by `set_context` method - self._set_context_project = None - self._set_context_asset = None - - # Requirements for asset widget refresh - self._assets_timer = assets_timer - self._rerefresh_assets = True - self._assets_refreshing = False - - # Set stylehseet and resize window on first show - self._first_show = True - - # Helper attributes for handling of refresh - self._ignore_value_changes = False - self._refresh_on_next_show = True - - # Output of dialog - self._context_to_store = { - "project": None, - "asset": None, - "task": None - } - - def closeEvent(self, event): - """Ignore close event if is in strict state and context is not done.""" - if self._strict and not self._ok_btn.isEnabled(): - event.ignore() - return - - if self._strict: - self._confirm_values() - super(ContextDialog, self).closeEvent(event) - - def set_strict(self, strict): - """Change strictness of dialog.""" - self._strict = strict - self._validate_strict() - - def _set_refresh_on_next_show(self): - """Refresh will be called on next showEvent. - - If window is already visible then just execute refresh. - """ - self._refresh_on_next_show = True - if self.isVisible(): - self.refresh() - - def _refresh_assets(self): - """Trigger refreshing of asset widget. - - This will set mart to rerefresh asset when current refreshing is done - or do it immidietely if asset widget is not refreshing at the time. - """ - if self._assets_refreshing: - self._rerefresh_assets = True - else: - self._on_asset_refresh_timer() - - def showEvent(self, event): - """Override show event to do some callbacks.""" - super(ContextDialog, self).showEvent(event) - if self._first_show: - self._first_show = False - # Set stylesheet and resize - self.setStyleSheet(style.load_stylesheet()) - self.resize(600, 700) - center_window(self) - - if self._refresh_on_next_show: - self.refresh() - - def refresh(self): - """Refresh all widget one by one. - - When asset refresh is triggered we have to wait when is done so - this method continues with `_on_asset_widget_refresh_finished`. - """ - # Change state of refreshing (no matter how refresh was called) - self._refresh_on_next_show = False - - # Ignore changes of combobox and asset widget - self._ignore_value_changes = True - - # Get current project name to be able set it afterwards - select_project_name = self._dbcon.Session.get("AVALON_PROJECT") - # Trigger project refresh - self._project_model.refresh() - # Sort projects - self._project_proxy.sort(0) - - # Disable combobox if project was passed to `set_context` - if self._set_context_project: - select_project_name = self._set_context_project - self._project_combobox.setEnabled(False) - else: - # Find new project to select - self._project_combobox.setEnabled(True) - if ( - select_project_name is None - and self._project_proxy.rowCount() > 0 - ): - index = self._project_proxy.index(0, 0) - select_project_name = index.data(PROJECT_NAME_ROLE) - - self._ignore_value_changes = False - - idx = self._project_combobox.findText(select_project_name) - if idx >= 0: - self._project_combobox.setCurrentIndex(idx) - self._dbcon.Session["AVALON_PROJECT"] = ( - self._project_combobox.currentText() - ) - - # Trigger asset refresh - self._refresh_assets() - - def _on_asset_refresh_timer(self): - """This is only way how to trigger refresh asset widget. - - Use `_refresh_assets` method to refresh asset widget. - """ - self._assets_widget.refresh() - - def _on_asset_widget_refresh_finished(self): - """Catch when asset widget finished refreshing.""" - # If should refresh again then skip all other callbacks and trigger - # assets timer directly. - self._assets_refreshing = False - if self._rerefresh_assets: - self._rerefresh_assets = False - self._assets_timer.start() - return - - self._ignore_value_changes = True - if self._set_context_asset: - self._dbcon.Session["AVALON_ASSET"] = self._set_context_asset - self._assets_widget.setEnabled(False) - self._assets_widget.select_asset_by_name(self._set_context_asset) - self._set_asset_to_tasks_widget() - else: - self._assets_widget.setEnabled(True) - self._assets_widget.set_current_asset_btn_visibility(False) - - # Refresh tasks - self._tasks_widget.refresh() - - self._ignore_value_changes = False - - self._validate_strict() - - def _on_project_combo_change(self): - if self._ignore_value_changes: - return - project_name = self._project_combobox.currentText() - - if self._dbcon.Session.get("AVALON_PROJECT") == project_name: - return - - self._dbcon.Session["AVALON_PROJECT"] = project_name - - self._refresh_assets() - self._validate_strict() - - def _on_asset_refresh_trigger(self): - self._assets_refreshing = True - self._on_asset_change() - - def _on_asset_change(self): - """Selected assets have changed""" - if self._ignore_value_changes: - return - self._set_asset_to_tasks_widget() - - def _on_task_change(self): - self._validate_strict() - - def _set_asset_to_tasks_widget(self): - asset_id = self._assets_widget.get_selected_asset_id() - - self._tasks_widget.set_asset_id(asset_id) - - def _confirm_values(self): - """Store values to output.""" - self._context_to_store["project"] = self.get_selected_project() - self._context_to_store["asset"] = self.get_selected_asset() - self._context_to_store["task"] = self.get_selected_task() - - def _on_ok_click(self): - # Store values to output - self._confirm_values() - # Close dialog - self.accept() - - def get_selected_project(self): - """Get selected project.""" - return self._project_combobox.currentText() - - def get_selected_asset(self): - """Currently selected asset in asset widget.""" - return self._assets_widget.get_selected_asset_name() - - def get_selected_task(self): - """Currently selected task.""" - return self._tasks_widget.get_selected_task_name() - - def _validate_strict(self): - if not self._strict: - if not self._ok_btn.isEnabled(): - self._ok_btn.setEnabled(True) - return - - enabled = True - if not self._set_context_project and not self.get_selected_project(): - enabled = False - elif not self._set_context_asset and not self.get_selected_asset(): - enabled = False - elif not self.get_selected_task(): - enabled = False - self._ok_btn.setEnabled(enabled) - - def set_context(self, project_name=None, asset_name=None): - """Set context which will be used and locked in dialog.""" - if project_name is None: - asset_name = None - - self._set_context_project = project_name - self._set_context_asset = asset_name - - self._context_to_store["project"] = project_name - self._context_to_store["asset"] = asset_name - - self._set_refresh_on_next_show() - - def get_context(self): - """Result of dialog.""" - return self._context_to_store - - -def main( - path_to_store, - project_name=None, - asset_name=None, - strict=True -): - # Run Qt application - app = get_openpype_qt_app() - window = ContextDialog() - window.set_strict(strict) - window.set_context(project_name, asset_name) - window.show() - app.exec_() - - # Get result from window - data = window.get_context() - - # Make sure json filepath directory exists - file_dir = os.path.dirname(path_to_store) - if not os.path.exists(file_dir): - os.makedirs(file_dir) - - # Store result into json file - with open(path_to_store, "w") as stream: - json.dump(data, stream) diff --git a/client/ayon_core/tools/context_dialog/_ayon_window.py b/client/ayon_core/tools/context_dialog/window.py similarity index 100% rename from client/ayon_core/tools/context_dialog/_ayon_window.py rename to client/ayon_core/tools/context_dialog/window.py diff --git a/client/ayon_core/tools/creator/widgets.py b/client/ayon_core/tools/creator/widgets.py index 34e5fc538e..05b5469151 100644 --- a/client/ayon_core/tools/creator/widgets.py +++ b/client/ayon_core/tools/creator/widgets.py @@ -5,7 +5,6 @@ from qtpy import QtWidgets, QtCore, QtGui import qtawesome -from ayon_core import AYON_SERVER_ENABLED from ayon_core.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS from ayon_core.tools.utils import ErrorMessageBox @@ -43,13 +42,11 @@ class CreateErrorMessageBox(ErrorMessageBox): def _get_report_data(self): report_message = ( - "Failed to create {subset_label}: \"{subset}\"" - " {family_label}: \"{family}\"" + "Failed to create Product: \"{subset}\"" + " Type: \"{family}\"" " in Asset: \"{asset}\"" "\n\nError: {message}" ).format( - subset_label="Product" if AYON_SERVER_ENABLED else "Subset", - family_label="Type" if AYON_SERVER_ENABLED else "Family", subset=self._subset_name, family=self._family, asset=self._asset_name, @@ -65,9 +62,9 @@ class CreateErrorMessageBox(ErrorMessageBox): "{}: {{}}
" "{}: {{}}
" ).format( - "Product type" if AYON_SERVER_ENABLED else "Family", - "Product name" if AYON_SERVER_ENABLED else "Subset", - "Folder" if AYON_SERVER_ENABLED else "Asset" + "Product type", + "Product name", + "Folder" ) exc_msg_template = "{}" @@ -159,21 +156,15 @@ class VariantLineEdit(QtWidgets.QLineEdit): def as_empty(self): self._set_border("empty") - self.report.emit("Empty {} name ..".format( - "product" if AYON_SERVER_ENABLED else "subset" - )) + self.report.emit("Empty product name ..") def as_exists(self): self._set_border("exists") - self.report.emit("Existing {}, appending next version.".format( - "product" if AYON_SERVER_ENABLED else "subset" - )) + self.report.emit("Existing product, appending next version.") def as_new(self): self._set_border("new") - self.report.emit("New {}, creating first version.".format( - "product" if AYON_SERVER_ENABLED else "subset" - )) + self.report.emit("New product, creating first version.") def _set_border(self, status): qcolor, style = self.colors[status] diff --git a/client/ayon_core/tools/experimental_tools/dialog.py b/client/ayon_core/tools/experimental_tools/dialog.py index 5c282a89ae..39789c859e 100644 --- a/client/ayon_core/tools/experimental_tools/dialog.py +++ b/client/ayon_core/tools/experimental_tools/dialog.py @@ -1,6 +1,5 @@ from qtpy import QtWidgets, QtCore, QtGui -from ayon_core import AYON_SERVER_ENABLED from ayon_core.style import ( load_stylesheet, app_icon_path @@ -27,8 +26,7 @@ class ExperimentalToolsDialog(QtWidgets.QDialog): def __init__(self, parent=None): super(ExperimentalToolsDialog, self).__init__(parent) - app_label = "AYON" if AYON_SERVER_ENABLED else "OpenPype" - self.setWindowTitle("{} Experimental tools".format(app_label)) + self.setWindowTitle("AYON Experimental tools") icon = QtGui.QIcon(app_icon_path()) self.setWindowIcon(icon) self.setStyleSheet(load_stylesheet()) @@ -70,8 +68,8 @@ class ExperimentalToolsDialog(QtWidgets.QDialog): tool_btns_label = QtWidgets.QLabel( ( "You can enable these features in" - "
{} tray -> Settings -> Experimental tools" - ).format(app_label), + "
AYON tray -> Settings -> Experimental tools" + ), tool_btns_widget ) tool_btns_label.setAlignment(QtCore.Qt.AlignCenter) @@ -115,7 +113,6 @@ class ExperimentalToolsDialog(QtWidgets.QDialog): self._window_is_active = False def refresh(self): - app_label = "AYON" if AYON_SERVER_ENABLED else "OpenPype" self._experimental_tools.refresh_availability() buttons_to_remove = set(self._buttons_by_tool_identifier.keys()) @@ -142,8 +139,8 @@ class ExperimentalToolsDialog(QtWidgets.QDialog): elif is_new or button.isEnabled(): button.setToolTip(( "You can enable this tool in local settings." - "\n\n{} Tray > Settings > Experimental Tools" - ).format(app_label)) + "\n\nAYON Tray > Settings > Experimental Tools" + )) if tool.enabled != button.isEnabled(): button.setEnabled(tool.enabled) diff --git a/client/ayon_core/tools/launcher/__init__.py b/client/ayon_core/tools/launcher/__init__.py deleted file mode 100644 index 109d642e86..0000000000 --- a/client/ayon_core/tools/launcher/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .window import LauncherWindow -from . import actions - -__all__ = [ - "LauncherWindow", - "actions" -] diff --git a/client/ayon_core/tools/ayon_launcher/abstract.py b/client/ayon_core/tools/launcher/abstract.py similarity index 100% rename from client/ayon_core/tools/ayon_launcher/abstract.py rename to client/ayon_core/tools/launcher/abstract.py diff --git a/client/ayon_core/tools/launcher/actions.py b/client/ayon_core/tools/launcher/actions.py deleted file mode 100644 index ac246bf8df..0000000000 --- a/client/ayon_core/tools/launcher/actions.py +++ /dev/null @@ -1,91 +0,0 @@ -from qtpy import QtWidgets, QtGui - -from ayon_core import style -from ayon_core import resources -from ayon_core.lib import ( - Logger, - ApplictionExecutableNotFound, - ApplicationLaunchFailed -) -from ayon_core.pipeline import LauncherAction - - -# TODO move to 'ayon_core.pipeline.actions' -# - remove Qt related stuff and implement exceptions to show error in launcher -class ApplicationAction(LauncherAction): - """Pype's application launcher - - Application action based on pype's ApplicationManager system. - """ - - # Application object - application = None - # Action attributes - name = None - label = None - label_variant = None - group = None - icon = None - color = None - order = 0 - data = {} - - _log = None - required_session_keys = ( - "AVALON_PROJECT", - "AVALON_ASSET", - "AVALON_TASK" - ) - - @property - def log(self): - if self._log is None: - self._log = Logger.get_logger(self.__class__.__name__) - return self._log - - def is_compatible(self, session): - for key in self.required_session_keys: - if key not in session: - return False - return True - - def _show_message_box(self, title, message, details=None): - dialog = QtWidgets.QMessageBox() - icon = QtGui.QIcon(resources.get_openpype_icon_filepath()) - dialog.setWindowIcon(icon) - dialog.setStyleSheet(style.load_stylesheet()) - dialog.setWindowTitle(title) - dialog.setText(message) - if details: - dialog.setDetailedText(details) - dialog.exec_() - - def process(self, session, **kwargs): - """Process the full Application action""" - - project_name = session["AVALON_PROJECT"] - asset_name = session["AVALON_ASSET"] - task_name = session["AVALON_TASK"] - try: - self.application.launch( - project_name=project_name, - asset_name=asset_name, - task_name=task_name, - **self.data - ) - - except ApplictionExecutableNotFound as exc: - details = exc.details - msg = exc.msg - log_msg = str(msg) - if details: - log_msg += "\n" + details - self.log.warning(log_msg) - self._show_message_box( - "Application executable not found", msg, details - ) - - except ApplicationLaunchFailed as exc: - msg = str(exc) - self.log.warning(msg, exc_info=True) - self._show_message_box("Application launch failed", msg) diff --git a/client/ayon_core/tools/launcher/constants.py b/client/ayon_core/tools/launcher/constants.py deleted file mode 100644 index cb0049055c..0000000000 --- a/client/ayon_core/tools/launcher/constants.py +++ /dev/null @@ -1,14 +0,0 @@ -from qtpy import QtCore - - -ACTION_ROLE = QtCore.Qt.UserRole -GROUP_ROLE = QtCore.Qt.UserRole + 1 -VARIANT_GROUP_ROLE = QtCore.Qt.UserRole + 2 -ACTION_ID_ROLE = QtCore.Qt.UserRole + 3 -ANIMATION_START_ROLE = QtCore.Qt.UserRole + 4 -ANIMATION_STATE_ROLE = QtCore.Qt.UserRole + 5 -FORCE_NOT_OPEN_WORKFILE_ROLE = QtCore.Qt.UserRole + 6 -ACTION_TOOLTIP_ROLE = QtCore.Qt.UserRole + 7 - -# Animation length in seconds -ANIMATION_LEN = 7 diff --git a/client/ayon_core/tools/ayon_launcher/control.py b/client/ayon_core/tools/launcher/control.py similarity index 100% rename from client/ayon_core/tools/ayon_launcher/control.py rename to client/ayon_core/tools/launcher/control.py diff --git a/client/ayon_core/tools/launcher/delegates.py b/client/ayon_core/tools/launcher/delegates.py deleted file mode 100644 index 02a40861d2..0000000000 --- a/client/ayon_core/tools/launcher/delegates.py +++ /dev/null @@ -1,129 +0,0 @@ -import time -from qtpy import QtCore, QtWidgets, QtGui -from .constants import ( - ANIMATION_START_ROLE, - ANIMATION_STATE_ROLE, - FORCE_NOT_OPEN_WORKFILE_ROLE -) - - -class ActionDelegate(QtWidgets.QStyledItemDelegate): - extender_lines = 2 - extender_bg_brush = QtGui.QBrush(QtGui.QColor(100, 100, 100, 160)) - extender_fg = QtGui.QColor(255, 255, 255, 160) - - def __init__(self, group_roles, *args, **kwargs): - super(ActionDelegate, self).__init__(*args, **kwargs) - self.group_roles = group_roles - self._anim_start_color = QtGui.QColor(178, 255, 246) - self._anim_end_color = QtGui.QColor(5, 44, 50) - - def _draw_animation(self, painter, option, index): - grid_size = option.widget.gridSize() - x_offset = int( - (grid_size.width() / 2) - - (option.rect.width() / 2) - ) - item_x = option.rect.x() - x_offset - rect_offset = grid_size.width() / 20 - size = grid_size.width() - (rect_offset * 2) - anim_rect = QtCore.QRect( - item_x + rect_offset, - option.rect.y() + rect_offset, - size, - size - ) - - painter.save() - - painter.setBrush(QtCore.Qt.transparent) - painter.setRenderHint(QtGui.QPainter.Antialiasing) - - gradient = QtGui.QConicalGradient() - gradient.setCenter(anim_rect.center()) - gradient.setColorAt(0, self._anim_start_color) - gradient.setColorAt(1, self._anim_end_color) - - time_diff = time.time() - index.data(ANIMATION_START_ROLE) - - # Repeat 4 times - part_anim = 2.5 - part_time = time_diff % part_anim - offset = (part_time / part_anim) * 360 - angle = (offset + 90) % 360 - - gradient.setAngle(-angle) - - pen = QtGui.QPen(QtGui.QBrush(gradient), rect_offset) - pen.setCapStyle(QtCore.Qt.RoundCap) - painter.setPen(pen) - painter.drawArc( - anim_rect, - -16 * (angle + 10), - -16 * offset - ) - - painter.restore() - - def paint(self, painter, option, index): - if index.data(ANIMATION_STATE_ROLE): - self._draw_animation(painter, option, index) - - super(ActionDelegate, self).paint(painter, option, index) - - if index.data(FORCE_NOT_OPEN_WORKFILE_ROLE): - rect = QtCore.QRectF(option.rect.x(), option.rect.height(), - 5, 5) - painter.setPen(QtCore.Qt.transparent) - painter.setBrush(QtGui.QColor(200, 0, 0)) - painter.drawEllipse(rect) - - painter.setBrush(self.extender_bg_brush) - - is_group = False - for group_role in self.group_roles: - is_group = index.data(group_role) - if is_group: - break - if not is_group: - return - - grid_size = option.widget.gridSize() - x_offset = int( - (grid_size.width() / 2) - - (option.rect.width() / 2) - ) - item_x = option.rect.x() - x_offset - - tenth_width = int(grid_size.width() / 10) - tenth_height = int(grid_size.height() / 10) - - extender_width = tenth_width * 2 - extender_height = tenth_height * 2 - - exteder_rect = QtCore.QRectF( - item_x + tenth_width, - option.rect.y() + tenth_height, - extender_width, - extender_height - ) - path = QtGui.QPainterPath() - path.addRoundedRect(exteder_rect, 2, 2) - - painter.fillPath(path, self.extender_bg_brush) - - painter.setPen(self.extender_fg) - painter.drawPath(path) - - divider = (2 * self.extender_lines) + 1 - extender_offset = int(extender_width / 6) - line_height = round(extender_height / divider) - line_width = extender_width - (extender_offset * 2) + 1 - pos_x = exteder_rect.x() + extender_offset - pos_y = exteder_rect.y() + line_height - for _ in range(self.extender_lines): - line_rect = QtCore.QRectF( - pos_x, pos_y, line_width, line_height - ) - painter.fillRect(line_rect, self.extender_fg) - pos_y += 2 * line_height diff --git a/client/ayon_core/tools/launcher/lib.py b/client/ayon_core/tools/launcher/lib.py deleted file mode 100644 index 5ce45676cf..0000000000 --- a/client/ayon_core/tools/launcher/lib.py +++ /dev/null @@ -1,53 +0,0 @@ -import os -from qtpy import QtGui -import qtawesome -from ayon_core import resources - -ICON_CACHE = {} -NOT_FOUND = type("NotFound", (object, ), {}) - - -def get_action_icon(action): - icon_name = action.icon - if not icon_name: - return None - - global ICON_CACHE - - icon = ICON_CACHE.get(icon_name) - if icon is NOT_FOUND: - return None - elif icon: - return icon - - icon_path = resources.get_resource(icon_name) - if not os.path.exists(icon_path): - icon_path = icon_name.format(resources.RESOURCES_DIR) - - if os.path.exists(icon_path): - icon = QtGui.QIcon(icon_path) - ICON_CACHE[icon_name] = icon - return icon - - try: - icon_color = getattr(action, "color", None) or "white" - icon = qtawesome.icon( - "fa.{}".format(icon_name), color=icon_color - ) - - except Exception: - ICON_CACHE[icon_name] = NOT_FOUND - print("Can't load icon \"{}\"".format(icon_name)) - - return icon - - -def get_action_label(action): - label = getattr(action, "label", None) - if not label: - return action.name - - label_variant = getattr(action, "label_variant", None) - if not label_variant: - return label - return " ".join([label, label_variant]) diff --git a/client/ayon_core/tools/launcher/models.py b/client/ayon_core/tools/launcher/models.py deleted file mode 100644 index 31955f25c8..0000000000 --- a/client/ayon_core/tools/launcher/models.py +++ /dev/null @@ -1,906 +0,0 @@ -import re -import uuid -import copy -import logging -import collections -import time - -import appdirs -from qtpy import QtCore, QtGui -import qtawesome - -from ayon_core.client import ( - get_projects, - get_project, - get_assets, -) -from ayon_core.lib import JSONSettingRegistry -from ayon_core.lib.applications import ( - CUSTOM_LAUNCH_APP_GROUPS, - ApplicationManager -) -from ayon_core.settings import get_project_settings -from ayon_core.pipeline import discover_launcher_actions -from ayon_core.tools.utils.lib import ( - DynamicQThread, - get_project_icon, -) -from ayon_core.tools.utils.assets_widget import ( - AssetModel, - ASSET_NAME_ROLE -) -from ayon_core.tools.utils.tasks_widget import ( - TasksModel, - TasksProxyModel, - TASK_TYPE_ROLE, - TASK_ASSIGNEE_ROLE -) - -from . import lib -from .constants import ( - ACTION_ROLE, - GROUP_ROLE, - VARIANT_GROUP_ROLE, - ACTION_ID_ROLE, - FORCE_NOT_OPEN_WORKFILE_ROLE -) -from .actions import ApplicationAction - -log = logging.getLogger(__name__) - -# Must be different than roles in default asset model -ASSET_TASK_TYPES_ROLE = QtCore.Qt.UserRole + 10 -ASSET_ASSIGNEE_ROLE = QtCore.Qt.UserRole + 11 - - -class ActionModel(QtGui.QStandardItemModel): - def __init__(self, dbcon, parent=None): - super(ActionModel, self).__init__(parent=parent) - self.dbcon = dbcon - - self.application_manager = ApplicationManager() - - self.default_icon = qtawesome.icon("fa.cube", color="white") - # Cache of available actions - self._registered_actions = list() - self.items_by_id = {} - path = appdirs.user_data_dir("openpype", "pypeclub") - self.launcher_registry = JSONSettingRegistry("launcher", path) - - try: - _ = self.launcher_registry.get_item("force_not_open_workfile") - except ValueError: - self.launcher_registry.set_item("force_not_open_workfile", []) - - def discover(self): - """Set up Actions cache. Run this for each new project.""" - # Discover all registered actions - actions = discover_launcher_actions() - - # Get available project actions and the application actions - app_actions = self.get_application_actions() - actions.extend(app_actions) - - self._registered_actions = actions - - self.filter_actions() - - def get_application_actions(self): - actions = [] - if not self.dbcon.current_project(): - return actions - - project_name = self.dbcon.active_project() - project_doc = get_project(project_name, fields=["config.apps"]) - if not project_doc: - return actions - - project_settings = get_project_settings(project_name) - only_available = project_settings["applications"]["only_available"] - self.application_manager.refresh() - for app_def in project_doc["config"]["apps"]: - app_name = app_def["name"] - app = self.application_manager.applications.get(app_name) - if not app or not app.enabled: - continue - - if app.group.name in CUSTOM_LAUNCH_APP_GROUPS: - continue - - if only_available and not app.find_executable(): - continue - - # Get from app definition, if not there from app in project - action = type( - "app_{}".format(app_name), - (ApplicationAction,), - { - "application": app, - "name": app.name, - "label": app.group.label, - "label_variant": app.label, - "group": None, - "icon": app.icon, - "color": getattr(app, "color", None), - "order": getattr(app, "order", None) or 0, - "data": {} - } - ) - - actions.append(action) - return actions - - def get_icon(self, action, skip_default=False): - icon = lib.get_action_icon(action) - if not icon and not skip_default: - return self.default_icon - return icon - - def filter_actions(self): - self.items_by_id.clear() - # Validate actions based on compatibility - self.clear() - - actions = self.filter_compatible_actions(self._registered_actions) - - single_actions = [] - varianted_actions = collections.defaultdict(list) - grouped_actions = collections.defaultdict(list) - for action in actions: - # Groups - group_name = getattr(action, "group", None) - - # Label variants - label = getattr(action, "label", None) - label_variant = getattr(action, "label_variant", None) - if label_variant and not label: - print(( - "Invalid action \"{}\" has set `label_variant` to \"{}\"" - ", but doesn't have set `label` attribute" - ).format(action.name, label_variant)) - action.label_variant = None - label_variant = None - - if group_name: - grouped_actions[group_name].append(action) - - elif label_variant: - varianted_actions[label].append(action) - else: - single_actions.append(action) - - items_by_order = collections.defaultdict(list) - for label, actions in tuple(varianted_actions.items()): - if len(actions) == 1: - varianted_actions.pop(label) - single_actions.append(actions[0]) - continue - - icon = None - order = None - for action in actions: - if icon is None: - _icon = lib.get_action_icon(action) - if _icon: - icon = _icon - - if order is None or action.order < order: - order = action.order - - if icon is None: - icon = self.default_icon - - item = QtGui.QStandardItem(icon, label) - item.setData(label, QtCore.Qt.ToolTipRole) - item.setData(actions, ACTION_ROLE) - item.setData(True, VARIANT_GROUP_ROLE) - items_by_order[order].append(item) - - for action in single_actions: - icon = self.get_icon(action) - label = lib.get_action_label(action) - item = QtGui.QStandardItem(icon, label) - item.setData(label, QtCore.Qt.ToolTipRole) - item.setData(action, ACTION_ROLE) - items_by_order[action.order].append(item) - - for group_name, actions in grouped_actions.items(): - icon = None - order = None - for action in actions: - if order is None or action.order < order: - order = action.order - - if icon is None: - _icon = lib.get_action_icon(action) - if _icon: - icon = _icon - - if icon is None: - icon = self.default_icon - - item = QtGui.QStandardItem(icon, group_name) - item.setData(actions, ACTION_ROLE) - item.setData(True, GROUP_ROLE) - - items_by_order[order].append(item) - - self.beginResetModel() - - stored = self.launcher_registry.get_item("force_not_open_workfile") - items = [] - for order in sorted(items_by_order.keys()): - for item in items_by_order[order]: - item_id = str(uuid.uuid4()) - item.setData(item_id, ACTION_ID_ROLE) - - if self.is_force_not_open_workfile(item, - stored): - self.change_action_item(item, True) - - self.items_by_id[item_id] = item - items.append(item) - - self.invisibleRootItem().appendRows(items) - - self.endResetModel() - - def filter_compatible_actions(self, actions): - """Collect all actions which are compatible with the environment - - Each compatible action will be translated to a dictionary to ensure - the action can be visualized in the launcher. - - Args: - actions (list): list of classes - - Returns: - list: collection of dictionaries sorted on order int he - """ - - compatible = [] - _session = copy.deepcopy(self.dbcon.Session) - session = { - key: value - for key, value in _session.items() - if value - } - - for action in actions: - if action().is_compatible(session): - compatible.append(action) - - # Sort by order and name - return sorted( - compatible, - key=lambda action: (action.order, lib.get_action_label(action)) - ) - - def update_force_not_open_workfile_settings(self, is_checked, action_id): - """Store/remove config for forcing to skip opening last workfile. - - Args: - is_checked (bool): True to add, False to remove - action_id (str) - """ - action_item = self.items_by_id.get(action_id) - if not action_item: - return - - actions = action_item.data(ACTION_ROLE) - if not isinstance(actions, list): - actions = [actions] - - action_actions_data = [ - self._prepare_compare_data(action) - for action in actions - ] - - stored = self.launcher_registry.get_item("force_not_open_workfile") - for actual_data in action_actions_data: - if is_checked: - stored.append(actual_data) - else: - final_values = [] - for config in stored: - if config != actual_data: - final_values.append(config) - stored = final_values - - self.launcher_registry.set_item("force_not_open_workfile", stored) - self.launcher_registry._get_item.cache_clear() - self.change_action_item(action_item, is_checked) - - def change_action_item(self, item, checked): - """Modifies tooltip and sets if opening of last workfile forbidden""" - tooltip = item.data(QtCore.Qt.ToolTipRole) - if checked: - tooltip += " (Not opening last workfile)" - - item.setData(tooltip, QtCore.Qt.ToolTipRole) - item.setData(checked, FORCE_NOT_OPEN_WORKFILE_ROLE) - - def is_application_action(self, action): - """Checks if item is of a ApplicationAction type - - Args: - action (action) - """ - if isinstance(action, list) and action: - action = action[0] - - return ApplicationAction in action.__bases__ - - def is_force_not_open_workfile(self, item, stored): - """Checks if application for task is marked to not open workfile - - There might be specific tasks where is unwanted to open workfile right - always (broken file, low performance). This allows artist to mark to - skip opening for combination (project, asset, task_name, app) - - Args: - item (QStandardItem) - stored (list) of dict - """ - - actions = item.data(ACTION_ROLE) - if not isinstance(actions, list): - actions = [actions] - - if not self.is_application_action(actions[0]): - return False - - action_actions_data = [ - self._prepare_compare_data(action) - for action in actions - ] - for config in stored: - if config in action_actions_data: - return True - return False - - def _prepare_compare_data(self, action): - compare_data = {} - if action and action.label: - compare_data = { - "app_label": action.label.lower(), - "project_name": self.dbcon.Session["AVALON_PROJECT"], - "asset": self.dbcon.Session["AVALON_ASSET"], - "task_name": self.dbcon.Session["AVALON_TASK"] - } - return compare_data - - -class LauncherModel(QtCore.QObject): - # Refresh interval of projects - refresh_interval = 10000 - - # Signals - # Current project has changed - project_changed = QtCore.Signal(str) - # Filters has changed (any) - filters_changed = QtCore.Signal() - - # Projects were refreshed - projects_refreshed = QtCore.Signal() - - # Signals ONLY for assets model! - # - other objects should listen to asset model signals - # Asset refresh started - assets_refresh_started = QtCore.Signal() - # Assets refresh finished - assets_refreshed = QtCore.Signal() - - # Refresh timer timeout - # - give ability to tell parent window that this timer still runs - timer_timeout = QtCore.Signal() - - # Duplication from AssetsModel with "data.tasks" - _asset_projection = { - "name": 1, - "parent": 1, - "data.visualParent": 1, - "data.label": 1, - "data.icon": 1, - "data.color": 1, - "data.tasks": 1 - } - - def __init__(self, dbcon): - super(LauncherModel, self).__init__() - # Refresh timer - # - should affect only projects - refresh_timer = QtCore.QTimer() - refresh_timer.setInterval(self.refresh_interval) - refresh_timer.timeout.connect(self._on_timeout) - - self._refresh_timer = refresh_timer - - # Launcher is active - self._active = False - - # Global data - self._dbcon = dbcon - # Available project names - self._project_names = set() - self._project_docs_by_name = {} - - # Context data - self._asset_docs = [] - self._asset_docs_by_id = {} - self._asset_filter_data_by_id = {} - self._assignees = set() - self._task_types = set() - - # Filters - self._asset_name_filter = "" - self._assignee_filters = set() - self._task_type_filters = set() - - # Last project for which were assets queried - self._last_project_name = None - # Asset refresh thread is running - self._refreshing_assets = False - # Asset refresh thread - self._asset_refresh_thread = None - - def _on_timeout(self): - """Refresh timer timeout.""" - if self._active: - self.timer_timeout.emit() - self.refresh_projects() - - def set_active(self, active): - """Window change active state.""" - self._active = active - - def start_refresh_timer(self, trigger=False): - """Start refresh timer.""" - self._refresh_timer.start() - if trigger: - self._on_timeout() - - def stop_refresh_timer(self): - """Stop refresh timer.""" - self._refresh_timer.stop() - - @property - def project_name(self): - """Current project name.""" - return self._dbcon.current_project() - - @property - def refreshing_assets(self): - """Refreshing thread is running.""" - return self._refreshing_assets - - @property - def asset_docs(self): - """Access to asset docs.""" - return self._asset_docs - - @property - def project_names(self): - """Available project names.""" - return self._project_names - - def get_project_doc(self, project_name): - return self._project_docs_by_name.get(project_name) - - @property - def asset_filter_data_by_id(self): - """Prepared filter data by asset id.""" - return self._asset_filter_data_by_id - - @property - def assignees(self): - """All assignees for all assets in current project.""" - return self._assignees - - @property - def task_types(self): - """All task types for all assets in current project. - - TODO: This could be maybe taken from project document where are all - task types... - """ - return self._task_types - - @property - def task_type_filters(self): - """Currently set task type filters.""" - return self._task_type_filters - - @property - def assignee_filters(self): - """Currently set assignee filters.""" - return self._assignee_filters - - @property - def asset_name_filter(self): - """Asset name filter (can be used as regex filter).""" - return self._asset_name_filter - - def get_asset_doc(self, asset_id): - """Get single asset document by id.""" - return self._asset_docs_by_id.get(asset_id) - - def set_project_name(self, project_name): - """Change project name and refresh asset documents.""" - if project_name == self.project_name: - return - self._dbcon.Session["AVALON_PROJECT"] = project_name - self.project_changed.emit(project_name) - - self.refresh_assets(force=True) - - def refresh(self): - """Trigger refresh of whole model.""" - self.refresh_projects() - self.refresh_assets(force=False) - - def refresh_projects(self): - """Refresh projects.""" - current_project = self.project_name - project_names = set() - project_docs_by_name = {} - for project_doc in get_projects(): - project_name = project_doc["name"] - project_names.add(project_name) - project_docs_by_name[project_name] = project_doc - - self._project_docs_by_name = project_docs_by_name - self._project_names = project_names - self.projects_refreshed.emit() - if ( - current_project is not None - and current_project not in project_names - ): - self.set_project_name(None) - - def _set_asset_docs(self, asset_docs=None): - """Set asset documents and all related data. - - Method extract and prepare data needed for assets and tasks widget and - prepare filtering data. - """ - if asset_docs is None: - asset_docs = [] - - all_task_types = set() - all_assignees = set() - asset_docs_by_id = {} - asset_filter_data_by_id = {} - for asset_doc in asset_docs: - task_types = set() - assignees = set() - asset_id = asset_doc["_id"] - asset_docs_by_id[asset_id] = asset_doc - asset_tasks = asset_doc.get("data", {}).get("tasks") - asset_filter_data_by_id[asset_id] = { - "assignees": assignees, - "task_types": task_types - } - if not asset_tasks: - continue - - for task_data in asset_tasks.values(): - task_assignees = set() - _task_assignees = task_data.get("assignees") - if _task_assignees: - for assignee in _task_assignees: - task_assignees.add(assignee["username"]) - - task_type = task_data.get("type") - if task_assignees: - assignees |= set(task_assignees) - if task_type: - task_types.add(task_type) - - all_task_types |= task_types - all_assignees |= assignees - - self._asset_docs_by_id = asset_docs_by_id - self._asset_docs = asset_docs - self._asset_filter_data_by_id = asset_filter_data_by_id - self._assignees = all_assignees - self._task_types = all_task_types - - self.assets_refreshed.emit() - - def set_task_type_filter(self, task_types): - """Change task type filter. - - Args: - task_types (set): Set of task types that should be visible. - Pass empty set to turn filter off. - """ - self._task_type_filters = task_types - self.filters_changed.emit() - - def set_assignee_filter(self, assignees): - """Change assignees filter. - - Args: - assignees (set): Set of assignees that should be visible. - Pass empty set to turn filter off. - """ - self._assignee_filters = assignees - self.filters_changed.emit() - - def set_asset_name_filter(self, text_filter): - """Change asset name filter. - - Args: - text_filter (str): Asset name filter. Pass empty string to - turn filter off. - """ - self._asset_name_filter = text_filter - self.filters_changed.emit() - - def refresh_assets(self, force=True): - """Refresh assets.""" - self.assets_refresh_started.emit() - - if self.project_name is None: - self._set_asset_docs() - return - - if ( - not force - and self._last_project_name == self.project_name - ): - return - - self._stop_fetch_thread() - - self._refreshing_assets = True - self._last_project_name = self.project_name - self._asset_refresh_thread = DynamicQThread(self._refresh_assets) - self._asset_refresh_thread.start() - - def _stop_fetch_thread(self): - self._refreshing_assets = False - if self._asset_refresh_thread is not None: - while self._asset_refresh_thread.isRunning(): - # TODO this is blocking UI should be done in a different way - time.sleep(0.01) - self._asset_refresh_thread = None - - def _refresh_assets(self): - asset_docs = list(get_assets( - self._last_project_name, fields=self._asset_projection.keys() - )) - if not self._refreshing_assets: - return - self._refreshing_assets = False - self._set_asset_docs(asset_docs) - - -class LauncherTasksProxyModel(TasksProxyModel): - """Tasks proxy model with more filtering. - - TODO: - This can be (with few modifications) used in default tasks widget too. - """ - def __init__(self, launcher_model, *args, **kwargs): - self._launcher_model = launcher_model - super(LauncherTasksProxyModel, self).__init__(*args, **kwargs) - - launcher_model.filters_changed.connect(self._on_filter_change) - - self._task_types_filter = set() - self._assignee_filter = set() - - def _on_filter_change(self): - self._task_types_filter = self._launcher_model.task_type_filters - self._assignee_filter = self._launcher_model.assignee_filters - self.invalidateFilter() - - def filterAcceptsRow(self, row, parent): - if not self._task_types_filter and not self._assignee_filter: - return True - - model = self.sourceModel() - source_index = model.index(row, self.filterKeyColumn(), parent) - if not source_index.isValid(): - return False - - # Check current index itself - if self._task_types_filter: - task_type = model.data(source_index, TASK_TYPE_ROLE) - if task_type not in self._task_types_filter: - return False - - if self._assignee_filter: - assignee = model.data(source_index, TASK_ASSIGNEE_ROLE) - if not self._assignee_filter.intersection(assignee): - return False - return True - - -class LauncherTaskModel(TasksModel): - def __init__(self, launcher_model, *args, **kwargs): - self._launcher_model = launcher_model - super(LauncherTaskModel, self).__init__(*args, **kwargs) - - def _refresh_project_doc(self): - self._project_doc = self._launcher_model.get_project_doc( - self._launcher_model.project_name - ) - - def set_asset_id(self, asset_id): - asset_doc = None - if self._context_is_valid(): - asset_doc = self._launcher_model.get_asset_doc(asset_id) - self._set_asset(asset_doc) - - -class AssetRecursiveSortFilterModel(QtCore.QSortFilterProxyModel): - def __init__(self, launcher_model, *args, **kwargs): - self._launcher_model = launcher_model - - super(AssetRecursiveSortFilterModel, self).__init__(*args, **kwargs) - - launcher_model.filters_changed.connect(self._on_filter_change) - self._name_filter = "" - self._task_types_filter = set() - self._assignee_filter = set() - - def _on_filter_change(self): - self._name_filter = self._launcher_model.asset_name_filter - self._task_types_filter = self._launcher_model.task_type_filters - self._assignee_filter = self._launcher_model.assignee_filters - self.invalidateFilter() - - def filterAcceptsRow(self, row, parent): - if ( - not self._name_filter - and not self._task_types_filter - and not self._assignee_filter - ): - return True - - model = self.sourceModel() - source_index = model.index(row, self.filterKeyColumn(), parent) - if not source_index.isValid(): - return False - - # Check current index itself - valid = True - if self._name_filter: - name = model.data(source_index, ASSET_NAME_ROLE) - if ( - name is None - or not re.search(self._name_filter, name, re.IGNORECASE) - ): - valid = False - - if valid and self._task_types_filter: - task_types = model.data(source_index, ASSET_TASK_TYPES_ROLE) - if not self._task_types_filter.intersection(task_types): - valid = False - - if valid and self._assignee_filter: - assignee = model.data(source_index, ASSET_ASSIGNEE_ROLE) - if not self._assignee_filter.intersection(assignee): - valid = False - - if valid: - return True - - # Check children - rows = model.rowCount(source_index) - for child_row in range(rows): - if self.filterAcceptsRow(child_row, source_index): - return True - return False - - -class LauncherAssetsModel(AssetModel): - def __init__(self, launcher_model, dbcon, parent=None): - self._launcher_model = launcher_model - # Make sure that variable is available (even if is in AssetModel) - self._last_project_name = None - - super(LauncherAssetsModel, self).__init__(dbcon, parent) - - launcher_model.project_changed.connect(self._on_project_change) - launcher_model.assets_refresh_started.connect( - self._on_launcher_refresh_start - ) - launcher_model.assets_refreshed.connect(self._on_launcher_refresh) - - def _on_launcher_refresh_start(self): - self._refreshing = True - project_name = self._launcher_model.project_name - if self._last_project_name != project_name: - self._clear_items() - self._last_project_name = project_name - - def _on_launcher_refresh(self): - self._fill_assets(self._launcher_model.asset_docs) - self._refreshing = False - self.refreshed.emit(bool(self._items_by_asset_id)) - - def _fill_assets(self, *args, **kwargs): - super(LauncherAssetsModel, self)._fill_assets(*args, **kwargs) - asset_filter_data_by_id = self._launcher_model.asset_filter_data_by_id - for asset_id, item in self._items_by_asset_id.items(): - filter_data = asset_filter_data_by_id.get(asset_id) - - assignees = filter_data["assignees"] - task_types = filter_data["task_types"] - - item.setData(assignees, ASSET_ASSIGNEE_ROLE) - item.setData(task_types, ASSET_TASK_TYPES_ROLE) - - def _on_project_change(self): - self._clear_items() - - def refresh(self, *args, **kwargs): - raise ValueError("This is a bug!") - - def stop_refresh(self, *args, **kwargs): - raise ValueError("This is a bug!") - - -class ProjectModel(QtGui.QStandardItemModel): - """List of projects""" - - def __init__(self, launcher_model, parent=None): - super(ProjectModel, self).__init__(parent=parent) - - self._launcher_model = launcher_model - self._project_names = set() - - launcher_model.projects_refreshed.connect(self._on_refresh) - - def _on_refresh(self): - project_names = set(self._launcher_model.project_names) - origin_project_names = set(self._project_names) - self._project_names = project_names - - project_names_to_remove = origin_project_names - project_names - if project_names_to_remove: - row_counts = {} - continuous = None - for row in range(self.rowCount()): - index = self.index(row, 0) - index_name = index.data(QtCore.Qt.DisplayRole) - if index_name in project_names_to_remove: - if continuous is None: - continuous = row - row_counts[continuous] = 0 - row_counts[continuous] += 1 - else: - continuous = None - - for row in reversed(sorted(row_counts.keys())): - count = row_counts[row] - self.removeRows(row, count) - - continuous = None - row_counts = {} - for idx, project_name in enumerate(sorted(project_names)): - if project_name in origin_project_names: - continuous = None - continue - - if continuous is None: - continuous = idx - row_counts[continuous] = [] - - row_counts[continuous].append(project_name) - - for row in reversed(sorted(row_counts.keys())): - items = [] - for project_name in row_counts[row]: - project_doc = self._launcher_model.get_project_doc( - project_name - ) - icon = get_project_icon(project_doc) - item = QtGui.QStandardItem(icon, project_name) - items.append(item) - - self.invisibleRootItem().insertRows(row, items) diff --git a/client/ayon_core/tools/ayon_launcher/models/__init__.py b/client/ayon_core/tools/launcher/models/__init__.py similarity index 100% rename from client/ayon_core/tools/ayon_launcher/models/__init__.py rename to client/ayon_core/tools/launcher/models/__init__.py diff --git a/client/ayon_core/tools/ayon_launcher/models/actions.py b/client/ayon_core/tools/launcher/models/actions.py similarity index 100% rename from client/ayon_core/tools/ayon_launcher/models/actions.py rename to client/ayon_core/tools/launcher/models/actions.py diff --git a/client/ayon_core/tools/ayon_launcher/models/selection.py b/client/ayon_core/tools/launcher/models/selection.py similarity index 100% rename from client/ayon_core/tools/ayon_launcher/models/selection.py rename to client/ayon_core/tools/launcher/models/selection.py diff --git a/client/ayon_core/tools/ayon_launcher/ui/__init__.py b/client/ayon_core/tools/launcher/ui/__init__.py similarity index 100% rename from client/ayon_core/tools/ayon_launcher/ui/__init__.py rename to client/ayon_core/tools/launcher/ui/__init__.py diff --git a/client/ayon_core/tools/ayon_launcher/ui/actions_widget.py b/client/ayon_core/tools/launcher/ui/actions_widget.py similarity index 100% rename from client/ayon_core/tools/ayon_launcher/ui/actions_widget.py rename to client/ayon_core/tools/launcher/ui/actions_widget.py diff --git a/client/ayon_core/tools/ayon_launcher/ui/hierarchy_page.py b/client/ayon_core/tools/launcher/ui/hierarchy_page.py similarity index 100% rename from client/ayon_core/tools/ayon_launcher/ui/hierarchy_page.py rename to client/ayon_core/tools/launcher/ui/hierarchy_page.py diff --git a/client/ayon_core/tools/ayon_launcher/ui/projects_widget.py b/client/ayon_core/tools/launcher/ui/projects_widget.py similarity index 100% rename from client/ayon_core/tools/ayon_launcher/ui/projects_widget.py rename to client/ayon_core/tools/launcher/ui/projects_widget.py diff --git a/client/ayon_core/tools/ayon_launcher/ui/resources/__init__.py b/client/ayon_core/tools/launcher/ui/resources/__init__.py similarity index 100% rename from client/ayon_core/tools/ayon_launcher/ui/resources/__init__.py rename to client/ayon_core/tools/launcher/ui/resources/__init__.py diff --git a/client/ayon_core/tools/ayon_launcher/ui/resources/options.png b/client/ayon_core/tools/launcher/ui/resources/options.png similarity index 100% rename from client/ayon_core/tools/ayon_launcher/ui/resources/options.png rename to client/ayon_core/tools/launcher/ui/resources/options.png diff --git a/client/ayon_core/tools/ayon_launcher/ui/window.py b/client/ayon_core/tools/launcher/ui/window.py similarity index 99% rename from client/ayon_core/tools/ayon_launcher/ui/window.py rename to client/ayon_core/tools/launcher/ui/window.py index 45788fe2f6..a5c255c691 100644 --- a/client/ayon_core/tools/ayon_launcher/ui/window.py +++ b/client/ayon_core/tools/launcher/ui/window.py @@ -3,7 +3,7 @@ from qtpy import QtWidgets, QtCore, QtGui from ayon_core import style from ayon_core import resources -from ayon_core.tools.ayon_launcher.control import BaseLauncherController +from ayon_core.tools.launcher.control import BaseLauncherController from .projects_widget import ProjectsWidget from .hierarchy_page import HierarchyPage diff --git a/client/ayon_core/tools/launcher/widgets.py b/client/ayon_core/tools/launcher/widgets.py deleted file mode 100644 index a1f8c133c4..0000000000 --- a/client/ayon_core/tools/launcher/widgets.py +++ /dev/null @@ -1,566 +0,0 @@ -import copy -import time -import collections -from qtpy import QtWidgets, QtCore, QtGui -import qtawesome - -from ayon_core.tools.flickcharm import FlickCharm -from ayon_core.tools.utils.assets_widget import SingleSelectAssetsWidget -from ayon_core.tools.utils.tasks_widget import TasksWidget - -from .delegates import ActionDelegate -from . import lib -from .models import ( - ActionModel, - ProjectModel, - LauncherAssetsModel, - AssetRecursiveSortFilterModel, - LauncherTaskModel, - LauncherTasksProxyModel -) -from .actions import ApplicationAction -from .constants import ( - ACTION_ROLE, - GROUP_ROLE, - VARIANT_GROUP_ROLE, - ACTION_ID_ROLE, - ANIMATION_START_ROLE, - ANIMATION_STATE_ROLE, - ANIMATION_LEN, - FORCE_NOT_OPEN_WORKFILE_ROLE -) - - -class ProjectBar(QtWidgets.QWidget): - def __init__(self, launcher_model, parent=None): - super(ProjectBar, self).__init__(parent) - - project_combobox = QtWidgets.QComboBox(self) - # Change delegate so stylysheets are applied - project_delegate = QtWidgets.QStyledItemDelegate(project_combobox) - project_combobox.setItemDelegate(project_delegate) - model = ProjectModel(launcher_model) - project_combobox.setModel(model) - project_combobox.setRootModelIndex(QtCore.QModelIndex()) - - layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(project_combobox) - - self.setSizePolicy( - QtWidgets.QSizePolicy.MinimumExpanding, - QtWidgets.QSizePolicy.Maximum - ) - - self._launcher_model = launcher_model - self.project_delegate = project_delegate - self.project_combobox = project_combobox - self._model = model - - # Signals - self.project_combobox.currentIndexChanged.connect(self.on_index_change) - launcher_model.project_changed.connect(self._on_project_change) - - # Set current project by default if it's set. - project_name = launcher_model.project_name - if project_name: - self.set_project(project_name) - - def _on_project_change(self, project_name): - if self.get_current_project() == project_name: - return - self.set_project(project_name) - - def get_current_project(self): - return self.project_combobox.currentText() - - def set_project(self, project_name): - index = self.project_combobox.findText(project_name) - if index < 0: - # Try refresh combobox model - self._launcher_model.refresh_projects() - index = self.project_combobox.findText(project_name) - - if index >= 0: - self.project_combobox.setCurrentIndex(index) - - def on_index_change(self, idx): - if not self.isVisible(): - return - - project_name = self.get_current_project() - self._launcher_model.set_project_name(project_name) - - -class LauncherTaskWidget(TasksWidget): - def __init__(self, launcher_model, *args, **kwargs): - self._launcher_model = launcher_model - - super(LauncherTaskWidget, self).__init__(*args, **kwargs) - - def _create_source_model(self): - return LauncherTaskModel(self._launcher_model, self._dbcon) - - def _create_proxy_model(self, source_model): - proxy = LauncherTasksProxyModel(self._launcher_model) - proxy.setSourceModel(source_model) - return proxy - - -class LauncherAssetsWidget(SingleSelectAssetsWidget): - def __init__(self, launcher_model, *args, **kwargs): - self._launcher_model = launcher_model - - super(LauncherAssetsWidget, self).__init__(*args, **kwargs) - - launcher_model.assets_refresh_started.connect(self._on_refresh_start) - - self.set_current_asset_btn_visibility(False) - - def _on_refresh_start(self): - self._set_loading_state(loading=True, empty=True) - self.refresh_triggered.emit() - - @property - def refreshing(self): - return self._model.refreshing - - def refresh(self): - self._launcher_model.refresh_assets(force=True) - - def stop_refresh(self): - raise ValueError("bug stop_refresh called") - - def _refresh_model(self, clear=False): - raise ValueError("bug _refresh_model called") - - def _create_source_model(self): - model = LauncherAssetsModel(self._launcher_model, self.dbcon) - model.refreshed.connect(self._on_model_refresh) - return model - - def _create_proxy_model(self, source_model): - proxy = AssetRecursiveSortFilterModel(self._launcher_model) - proxy.setSourceModel(source_model) - proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) - proxy.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive) - return proxy - - def _on_model_refresh(self, has_item): - self._proxy.sort(0) - self._set_loading_state(loading=False, empty=not has_item) - self.refreshed.emit() - - def _on_filter_text_change(self, new_text): - self._launcher_model.set_asset_name_filter(new_text) - - -class ActionBar(QtWidgets.QWidget): - """Launcher interface""" - - action_clicked = QtCore.Signal(object) - - def __init__(self, launcher_model, dbcon, parent=None): - super(ActionBar, self).__init__(parent) - - self._launcher_model = launcher_model - self.dbcon = dbcon - - view = QtWidgets.QListView(self) - view.setProperty("mode", "icon") - view.setObjectName("IconView") - view.setViewMode(QtWidgets.QListView.IconMode) - view.setResizeMode(QtWidgets.QListView.Adjust) - view.setSelectionMode(QtWidgets.QListView.NoSelection) - view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - view.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) - view.setWrapping(True) - view.setGridSize(QtCore.QSize(70, 75)) - view.setIconSize(QtCore.QSize(30, 30)) - view.setSpacing(0) - view.setWordWrap(True) - - model = ActionModel(self.dbcon, self) - view.setModel(model) - - # TODO better group delegate - delegate = ActionDelegate( - [GROUP_ROLE, VARIANT_GROUP_ROLE], - self - ) - view.setItemDelegate(delegate) - - layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(view) - - self.model = model - self.view = view - - self._animated_items = set() - - animation_timer = QtCore.QTimer() - animation_timer.setInterval(50) - animation_timer.timeout.connect(self._on_animation) - self._animation_timer = animation_timer - - # Make view flickable - flick = FlickCharm(parent=view) - flick.activateOn(view) - - self.set_row_height(1) - - launcher_model.projects_refreshed.connect(self._on_projects_refresh) - view.clicked.connect(self.on_clicked) - view.customContextMenuRequested.connect(self.on_context_menu) - - self._context_menu = None - self._discover_on_menu = False - - def discover_actions(self): - if self._context_menu is not None: - self._discover_on_menu = True - return - - if self._animation_timer.isActive(): - self._animation_timer.stop() - self.model.discover() - - def filter_actions(self): - if self._animation_timer.isActive(): - self._animation_timer.stop() - self.model.filter_actions() - - def set_row_height(self, rows): - self.setMinimumHeight(rows * 75) - - def _on_projects_refresh(self): - self.discover_actions() - - def _on_animation(self): - time_now = time.time() - for action_id in tuple(self._animated_items): - item = self.model.items_by_id.get(action_id) - if not item: - self._animated_items.remove(action_id) - continue - - start_time = item.data(ANIMATION_START_ROLE) - if (time_now - start_time) > ANIMATION_LEN: - item.setData(0, ANIMATION_STATE_ROLE) - self._animated_items.remove(action_id) - - if not self._animated_items: - self._animation_timer.stop() - - self.update() - - def _start_animation(self, index): - # Offset refresh timout - self._launcher_model.start_refresh_timer() - action_id = index.data(ACTION_ID_ROLE) - item = self.model.items_by_id.get(action_id) - if item: - item.setData(time.time(), ANIMATION_START_ROLE) - item.setData(1, ANIMATION_STATE_ROLE) - self._animated_items.add(action_id) - self._animation_timer.start() - - def on_context_menu(self, point): - """Creates menu to force skip opening last workfile.""" - index = self.view.indexAt(point) - if not index.isValid(): - return - - action_item = index.data(ACTION_ROLE) - if not self.model.is_application_action(action_item): - return - - menu = QtWidgets.QMenu(self.view) - checkbox = QtWidgets.QCheckBox("Skip opening last workfile.", - menu) - if index.data(FORCE_NOT_OPEN_WORKFILE_ROLE): - checkbox.setChecked(True) - - action_id = index.data(ACTION_ID_ROLE) - checkbox.stateChanged.connect( - lambda: self.on_checkbox_changed(checkbox.isChecked(), - action_id)) - action = QtWidgets.QWidgetAction(menu) - action.setDefaultWidget(checkbox) - - menu.addAction(action) - - self._context_menu = menu - global_point = self.mapToGlobal(point) - menu.exec_(global_point) - self._context_menu = None - if self._discover_on_menu: - self._discover_on_menu = False - self.discover_actions() - - def on_checkbox_changed(self, is_checked, action_id): - self.model.update_force_not_open_workfile_settings(is_checked, - action_id) - self.view.update() - if self._context_menu is not None: - self._context_menu.close() - - def on_clicked(self, index): - if not index or not index.isValid(): - return - - is_group = index.data(GROUP_ROLE) - is_variant_group = index.data(VARIANT_GROUP_ROLE) - force_not_open_workfile = index.data(FORCE_NOT_OPEN_WORKFILE_ROLE) - if not is_group and not is_variant_group: - action = index.data(ACTION_ROLE) - # Change data of application action - if issubclass(action, ApplicationAction): - if force_not_open_workfile: - action.data["start_last_workfile"] = False - else: - action.data.pop("start_last_workfile", None) - self._start_animation(index) - self.action_clicked.emit(action) - return - - # Offset refresh timout - self._launcher_model.start_refresh_timer() - - actions = index.data(ACTION_ROLE) - - menu = QtWidgets.QMenu(self) - actions_mapping = {} - - if is_variant_group: - for action in actions: - menu_action = QtWidgets.QAction( - lib.get_action_label(action) - ) - menu.addAction(menu_action) - actions_mapping[menu_action] = action - else: - by_variant_label = collections.defaultdict(list) - orders = [] - for action in actions: - # Label variants - label = getattr(action, "label", None) - label_variant = getattr(action, "label_variant", None) - if label_variant and not label: - label_variant = None - - if not label_variant: - orders.append(action) - continue - - if label not in orders: - orders.append(label) - by_variant_label[label].append(action) - - for action_item in orders: - actions = by_variant_label.get(action_item) - if not actions: - action = action_item - elif len(actions) == 1: - action = actions[0] - else: - action = None - - if action: - menu_action = QtWidgets.QAction( - lib.get_action_label(action) - ) - menu.addAction(menu_action) - actions_mapping[menu_action] = action - continue - - sub_menu = QtWidgets.QMenu(label, menu) - for action in actions: - menu_action = QtWidgets.QAction( - lib.get_action_label(action) - ) - sub_menu.addAction(menu_action) - actions_mapping[menu_action] = action - - menu.addMenu(sub_menu) - - result = menu.exec_(QtGui.QCursor.pos()) - if not result: - return - - action = actions_mapping[result] - if issubclass(action, ApplicationAction): - if force_not_open_workfile: - action.data["start_last_workfile"] = False - else: - action.data.pop("start_last_workfile", None) - - self._start_animation(index) - self.action_clicked.emit(action) - - -class ActionHistory(QtWidgets.QPushButton): - trigger_history = QtCore.Signal(tuple) - - def __init__(self, parent=None): - super(ActionHistory, self).__init__(parent=parent) - - self.max_history = 15 - - self.setFixedWidth(25) - self.setFixedHeight(25) - - self.setIcon(qtawesome.icon("fa.history", color="#CCCCCC")) - self.setIconSize(QtCore.QSize(15, 15)) - - self._history = [] - self.clicked.connect(self.show_history) - - def show_history(self): - # Show history popup - if not self._history: - return - - widget = QtWidgets.QListWidget() - widget.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) - widget.setStyleSheet(""" - * { - font-family: "Courier New"; - } - """) - - largest_label_num_chars = 0 - largest_action_label = max(len(x[0].label) for x in self._history) - action_session_role = QtCore.Qt.UserRole + 1 - - for action, session in reversed(self._history): - project = session.get("AVALON_PROJECT") - asset = session.get("AVALON_ASSET") - task = session.get("AVALON_TASK") - breadcrumb = " > ".join(x for x in [project, asset, task] if x) - - m = "{{action:{0}}} | {{breadcrumb}}".format(largest_action_label) - label = m.format(action=action.label, breadcrumb=breadcrumb) - - icon = lib.get_action_icon(action) - item = QtWidgets.QListWidgetItem(icon, label) - item.setData(action_session_role, (action, session)) - - largest_label_num_chars = max(largest_label_num_chars, len(label)) - - widget.addItem(item) - - # Show history - dialog = QtWidgets.QDialog(parent=self) - dialog.setWindowTitle("Action History") - dialog.setWindowFlags( - QtCore.Qt.FramelessWindowHint | QtCore.Qt.Popup - ) - dialog.setSizePolicy( - QtWidgets.QSizePolicy.Ignored, - QtWidgets.QSizePolicy.Ignored - ) - - layout = QtWidgets.QVBoxLayout(dialog) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(widget) - - def on_clicked(index): - data = index.data(action_session_role) - self.trigger_history.emit(data) - dialog.close() - - widget.clicked.connect(on_clicked) - - # padding + icon + text - width = 40 + (largest_label_num_chars * 7) - entry_height = 21 - height = entry_height * len(self._history) - - point = QtGui.QCursor().pos() - dialog.setGeometry( - point.x() - width, - point.y() - height, - width, - height - ) - dialog.exec_() - - self.widget_popup = widget - - def add_action(self, action, session): - key = (action, copy.deepcopy(session)) - - # Remove entry if already exists - if key in self._history: - self._history.remove(key) - - self._history.append(key) - - # Slice the end of the list if we exceed the max history - if len(self._history) > self.max_history: - self._history = self._history[-self.max_history:] - - def clear_history(self): - self._history.clear() - - -class SlidePageWidget(QtWidgets.QStackedWidget): - """Stacked widget that nicely slides between its pages""" - - directions = { - "left": QtCore.QPoint(-1, 0), - "right": QtCore.QPoint(1, 0), - "up": QtCore.QPoint(0, 1), - "down": QtCore.QPoint(0, -1) - } - - def slide_view(self, index, direction="right"): - if self.currentIndex() == index: - return - - offset_direction = self.directions.get(direction) - if offset_direction is None: - print("BUG: invalid slide direction: {}".format(direction)) - return - - width = self.frameRect().width() - height = self.frameRect().height() - offset = QtCore.QPoint( - offset_direction.x() * width, - offset_direction.y() * height - ) - - new_page = self.widget(index) - new_page.setGeometry(0, 0, width, height) - curr_pos = new_page.pos() - new_page.move(curr_pos + offset) - new_page.show() - new_page.raise_() - - current_page = self.currentWidget() - - b_pos = QtCore.QByteArray(b"pos") - - anim_old = QtCore.QPropertyAnimation(current_page, b_pos, self) - anim_old.setDuration(250) - anim_old.setStartValue(curr_pos) - anim_old.setEndValue(curr_pos - offset) - anim_old.setEasingCurve(QtCore.QEasingCurve.OutQuad) - - anim_new = QtCore.QPropertyAnimation(new_page, b_pos, self) - anim_new.setDuration(250) - anim_new.setStartValue(curr_pos + offset) - anim_new.setEndValue(curr_pos) - anim_new.setEasingCurve(QtCore.QEasingCurve.OutQuad) - - anim_group = QtCore.QParallelAnimationGroup(self) - anim_group.addAnimation(anim_old) - anim_group.addAnimation(anim_new) - - def slide_finished(): - self.setCurrentWidget(new_page) - - anim_group.finished.connect(slide_finished) - anim_group.start() diff --git a/client/ayon_core/tools/launcher/window.py b/client/ayon_core/tools/launcher/window.py deleted file mode 100644 index 5c9198ea4a..0000000000 --- a/client/ayon_core/tools/launcher/window.py +++ /dev/null @@ -1,437 +0,0 @@ -import copy -import logging - -from qtpy import QtWidgets, QtCore, QtGui - -from ayon_core import style -from ayon_core import resources -from ayon_core.pipeline import AvalonMongoDB - -import qtawesome -from .models import ( - LauncherModel, - ProjectModel -) -from .lib import get_action_label -from .widgets import ( - ProjectBar, - ActionBar, - ActionHistory, - SlidePageWidget, - LauncherAssetsWidget, - LauncherTaskWidget -) - -from ayon_core.tools.flickcharm import FlickCharm - - -class ProjectIconView(QtWidgets.QListView): - """Styled ListView that allows to toggle between icon and list mode. - - Toggling between the two modes is done by Right Mouse Click. - - """ - - IconMode = 0 - ListMode = 1 - - def __init__(self, parent=None, mode=ListMode): - super(ProjectIconView, self).__init__(parent=parent) - - # Workaround for scrolling being super slow or fast when - # toggling between the two visual modes - self.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) - self.setObjectName("IconView") - - self._mode = None - self.set_mode(mode) - - def set_mode(self, mode): - if mode == self._mode: - return - - self._mode = mode - - if mode == self.IconMode: - self.setViewMode(QtWidgets.QListView.IconMode) - self.setResizeMode(QtWidgets.QListView.Adjust) - self.setWrapping(True) - self.setWordWrap(True) - self.setGridSize(QtCore.QSize(151, 90)) - self.setIconSize(QtCore.QSize(50, 50)) - self.setSpacing(0) - self.setAlternatingRowColors(False) - - self.setProperty("mode", "icon") - self.style().polish(self) - - self.verticalScrollBar().setSingleStep(30) - - elif self.ListMode: - self.setProperty("mode", "list") - self.style().polish(self) - - self.setViewMode(QtWidgets.QListView.ListMode) - self.setResizeMode(QtWidgets.QListView.Adjust) - self.setWrapping(False) - self.setWordWrap(False) - self.setIconSize(QtCore.QSize(20, 20)) - self.setGridSize(QtCore.QSize(100, 25)) - self.setSpacing(0) - self.setAlternatingRowColors(False) - - self.verticalScrollBar().setSingleStep(33.33) - - def mousePressEvent(self, event): - if event.button() == QtCore.Qt.RightButton: - self.set_mode(int(not self._mode)) - return super(ProjectIconView, self).mousePressEvent(event) - - -class ProjectsPanel(QtWidgets.QWidget): - """Projects Page""" - def __init__(self, launcher_model, parent=None): - super(ProjectsPanel, self).__init__(parent=parent) - - view = ProjectIconView(parent=self) - view.setSelectionMode(QtWidgets.QListView.NoSelection) - flick = FlickCharm(parent=self) - flick.activateOn(view) - model = ProjectModel(launcher_model) - view.setModel(model) - - layout = QtWidgets.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(view) - - view.clicked.connect(self.on_clicked) - - self._model = model - self.view = view - self._launcher_model = launcher_model - - def on_clicked(self, index): - if index.isValid(): - project_name = index.data(QtCore.Qt.DisplayRole) - self._launcher_model.set_project_name(project_name) - - -class AssetsPanel(QtWidgets.QWidget): - """Assets page""" - back_clicked = QtCore.Signal() - session_changed = QtCore.Signal() - - def __init__(self, launcher_model, dbcon, parent=None): - super(AssetsPanel, self).__init__(parent=parent) - - self.dbcon = dbcon - - # Project bar - btn_back_icon = qtawesome.icon("fa.angle-left", color="white") - btn_back = QtWidgets.QPushButton(self) - btn_back.setIcon(btn_back_icon) - - project_bar = ProjectBar(launcher_model, self) - - project_bar_layout = QtWidgets.QHBoxLayout() - project_bar_layout.setContentsMargins(0, 0, 0, 0) - project_bar_layout.setSpacing(4) - project_bar_layout.addWidget(btn_back) - project_bar_layout.addWidget(project_bar) - - # Assets widget - assets_widget = LauncherAssetsWidget( - launcher_model, dbcon=self.dbcon, parent=self - ) - # Make assets view flickable - assets_widget.activate_flick_charm() - - # Tasks widget - tasks_widget = LauncherTaskWidget(launcher_model, self.dbcon, self) - - # Body - body = QtWidgets.QSplitter(self) - body.setContentsMargins(0, 0, 0, 0) - body.setSizePolicy( - QtWidgets.QSizePolicy.Expanding, - QtWidgets.QSizePolicy.Expanding - ) - body.setOrientation(QtCore.Qt.Horizontal) - body.addWidget(assets_widget) - body.addWidget(tasks_widget) - body.setStretchFactor(0, 100) - body.setStretchFactor(1, 65) - - # main layout - layout = QtWidgets.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.addLayout(project_bar_layout) - layout.addWidget(body) - - # signals - launcher_model.project_changed.connect(self._on_project_changed) - assets_widget.selection_changed.connect(self._on_asset_changed) - assets_widget.refreshed.connect(self._on_asset_changed) - tasks_widget.task_changed.connect(self._on_task_change) - - btn_back.clicked.connect(self.back_clicked) - - self.project_bar = project_bar - self.assets_widget = assets_widget - self._tasks_widget = tasks_widget - self._btn_back = btn_back - - self._launcher_model = launcher_model - - def select_asset(self, asset_name): - self.assets_widget.select_asset_by_name(asset_name) - - def showEvent(self, event): - super(AssetsPanel, self).showEvent(event) - - # Change size of a btn - # WARNING does not handle situation if combobox is bigger - btn_size = self.project_bar.height() - self._btn_back.setFixedSize(QtCore.QSize(btn_size, btn_size)) - - def select_task_name(self, task_name): - self._on_asset_changed() - self._tasks_widget.select_task_name(task_name) - - def _on_project_changed(self): - self.session_changed.emit() - - def _on_asset_changed(self): - """Callback on asset selection changed - - This updates the task view. - """ - - # Check asset on current index and selected assets - asset_id = self.assets_widget.get_selected_asset_id() - asset_name = self.assets_widget.get_selected_asset_name() - - self.dbcon.Session["AVALON_TASK"] = None - self.dbcon.Session["AVALON_ASSET"] = asset_name - - self.session_changed.emit() - - self._tasks_widget.set_asset_id(asset_id) - - def _on_task_change(self): - task_name = self._tasks_widget.get_selected_task_name() - self.dbcon.Session["AVALON_TASK"] = task_name - self.session_changed.emit() - - -class LauncherWindow(QtWidgets.QDialog): - """Launcher interface""" - message_timeout = 5000 - - def __init__(self, parent=None): - super(LauncherWindow, self).__init__(parent) - - self.log = logging.getLogger( - ".".join([__name__, self.__class__.__name__]) - ) - self.dbcon = AvalonMongoDB() - - self.setWindowTitle("Launcher") - self.setFocusPolicy(QtCore.Qt.StrongFocus) - self.setAttribute(QtCore.Qt.WA_DeleteOnClose, False) - - icon = QtGui.QIcon(resources.get_openpype_icon_filepath()) - self.setWindowIcon(icon) - self.setStyleSheet(style.load_stylesheet()) - - # Allow minimize - self.setWindowFlags( - QtCore.Qt.Window - | QtCore.Qt.CustomizeWindowHint - | QtCore.Qt.WindowTitleHint - | QtCore.Qt.WindowMinimizeButtonHint - | QtCore.Qt.WindowCloseButtonHint - ) - - launcher_model = LauncherModel(self.dbcon) - - project_panel = ProjectsPanel(launcher_model) - asset_panel = AssetsPanel(launcher_model, self.dbcon) - - page_slider = SlidePageWidget() - page_slider.addWidget(project_panel) - page_slider.addWidget(asset_panel) - - # actions - actions_bar = ActionBar(launcher_model, self.dbcon, self) - - # statusbar - message_label = QtWidgets.QLabel(self) - - action_history = ActionHistory(self) - action_history.setStatusTip("Show Action History") - - status_layout = QtWidgets.QHBoxLayout() - status_layout.addWidget(message_label, 1) - status_layout.addWidget(action_history, 0) - - # Vertically split Pages and Actions - body = QtWidgets.QSplitter(self) - body.setContentsMargins(0, 0, 0, 0) - body.setSizePolicy( - QtWidgets.QSizePolicy.Expanding, - QtWidgets.QSizePolicy.Expanding - ) - body.setOrientation(QtCore.Qt.Vertical) - body.addWidget(page_slider) - body.addWidget(actions_bar) - - # Set useful default sizes and set stretch - # for the pages so that is the only one that - # stretches on UI resize. - body.setStretchFactor(0, 10) - body.setSizes([580, 160]) - - layout = QtWidgets.QVBoxLayout(self) - layout.addWidget(body) - layout.addLayout(status_layout) - - message_timer = QtCore.QTimer() - message_timer.setInterval(self.message_timeout) - message_timer.setSingleShot(True) - - message_timer.timeout.connect(self._on_message_timeout) - - # signals - actions_bar.action_clicked.connect(self.on_action_clicked) - action_history.trigger_history.connect(self.on_history_action) - launcher_model.project_changed.connect(self.on_project_change) - launcher_model.timer_timeout.connect(self._on_refresh_timeout) - asset_panel.back_clicked.connect(self.on_back_clicked) - asset_panel.session_changed.connect(self.on_session_changed) - - self.resize(520, 740) - - self._page = 0 - - self._message_timer = message_timer - - self._launcher_model = launcher_model - - self._message_label = message_label - self.project_panel = project_panel - self.asset_panel = asset_panel - self.actions_bar = actions_bar - self.action_history = action_history - self.page_slider = page_slider - - def showEvent(self, event): - self._launcher_model.set_active(True) - self._launcher_model.start_refresh_timer(True) - - super(LauncherWindow, self).showEvent(event) - - def _on_refresh_timeout(self): - # Stop timer if widget is not visible - if not self.isVisible(): - self._launcher_model.stop_refresh_timer() - - def changeEvent(self, event): - if event.type() == QtCore.QEvent.ActivationChange: - self._launcher_model.set_active(self.isActiveWindow()) - super(LauncherWindow, self).changeEvent(event) - - def set_page(self, page): - current = self.page_slider.currentIndex() - if current == page and self._page == page: - return - - direction = "right" if page > current else "left" - self._page = page - self.page_slider.slide_view(page, direction=direction) - - def _on_message_timeout(self): - self._message_label.setText("") - - def echo(self, message): - self._message_label.setText(str(message)) - self._message_timer.start() - self.log.debug(message) - - def on_session_changed(self): - self.filter_actions() - - def discover_actions(self): - self.actions_bar.discover_actions() - - def filter_actions(self): - self.actions_bar.filter_actions() - - def on_project_change(self, project_name): - # Update the Action plug-ins available for the current project - self.set_page(1) - self.discover_actions() - - def on_back_clicked(self): - self._launcher_model.set_project_name(None) - self.set_page(0) - self.discover_actions() - - def on_action_clicked(self, action): - self.echo("Running action: {}".format(get_action_label(action))) - self.run_action(action) - - def on_history_action(self, history_data): - action, session = history_data - app = QtWidgets.QApplication.instance() - modifiers = app.keyboardModifiers() - - is_control_down = QtCore.Qt.ControlModifier & modifiers - if is_control_down: - # Revert to that "session" location - self.set_session(session) - else: - # User is holding control, rerun the action - self.run_action(action, session=session) - - def run_action(self, action, session=None): - if session is None: - session = copy.deepcopy(self.dbcon.Session) - - filtered_session = { - key: value - for key, value in session.items() - if value - } - # Add to history - self.action_history.add_action(action, filtered_session) - - # Process the Action - try: - action().process(filtered_session) - except Exception as exc: - self.log.warning("Action launch failed.", exc_info=True) - self.echo("Failed: {}".format(str(exc))) - - def set_session(self, session): - project_name = session.get("AVALON_PROJECT") - asset_name = session.get("AVALON_ASSET") - task_name = session.get("AVALON_TASK") - - if project_name: - # Force the "in project" view. - self.page_slider.slide_view(1, direction="right") - index = self.asset_panel.project_bar.project_combobox.findText( - project_name - ) - if index >= 0: - self.asset_panel.project_bar.project_combobox.setCurrentIndex( - index - ) - - if asset_name: - self.asset_panel.select_asset(asset_name) - - if task_name: - # requires a forced refresh first - self.asset_panel.select_task_name(task_name) diff --git a/client/ayon_core/tools/libraryloader/__init__.py b/client/ayon_core/tools/libraryloader/__init__.py deleted file mode 100644 index bbf4a1087d..0000000000 --- a/client/ayon_core/tools/libraryloader/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -from .app import ( - LibraryLoaderWindow, - show, - cli -) - -__all__ = [ - "LibraryLoaderWindow", - "show", - "cli", -] diff --git a/client/ayon_core/tools/libraryloader/__main__.py b/client/ayon_core/tools/libraryloader/__main__.py deleted file mode 100644 index d77bc585c5..0000000000 --- a/client/ayon_core/tools/libraryloader/__main__.py +++ /dev/null @@ -1,5 +0,0 @@ -from . import cli - -if __name__ == '__main__': - import sys - sys.exit(cli(sys.argv[1:])) diff --git a/client/ayon_core/tools/libraryloader/app.py b/client/ayon_core/tools/libraryloader/app.py deleted file mode 100644 index abfff2a20d..0000000000 --- a/client/ayon_core/tools/libraryloader/app.py +++ /dev/null @@ -1,523 +0,0 @@ -import sys - -from qtpy import QtWidgets, QtCore, QtGui - -from ayon_core import style -from ayon_core.client import get_projects, get_project -from ayon_core.pipeline import AvalonMongoDB -from ayon_core.tools.utils import lib as tools_lib -from ayon_core.tools.loader.widgets import ( - ThumbnailWidget, - VersionWidget, - FamilyListView, - RepresentationWidget, - SubsetWidget -) -from ayon_core.tools.utils.assets_widget import MultiSelectAssetsWidget - -from ayon_core.modules import ModulesManager - -module = sys.modules[__name__] -module.window = None - - -class LibraryLoaderWindow(QtWidgets.QDialog): - """Asset library loader interface""" - - tool_title = "Library Loader 0.5" - tool_name = "library_loader" - - message_timeout = 5000 - - def __init__( - self, parent=None, show_projects=False, show_libraries=True - ): - super(LibraryLoaderWindow, self).__init__(parent) - - # Window modifications - self.setWindowTitle(self.tool_title) - window_flags = QtCore.Qt.Window - if not parent: - window_flags |= QtCore.Qt.WindowStaysOnTopHint - self.setWindowFlags(window_flags) - self.setFocusPolicy(QtCore.Qt.StrongFocus) - - icon = QtGui.QIcon(style.app_icon_path()) - self.setWindowIcon(icon) - - self._first_show = True - self._initial_refresh = False - self._ignore_project_change = False - - dbcon = AvalonMongoDB() - dbcon.install() - dbcon.Session["AVALON_PROJECT"] = None - - self.dbcon = dbcon - - self.show_projects = show_projects - self.show_libraries = show_libraries - - # Groups config - self.groups_config = tools_lib.GroupsConfig(dbcon) - self.family_config_cache = tools_lib.FamilyConfigCache(dbcon) - - # UI initialization - main_splitter = QtWidgets.QSplitter(self) - - # --- Left part --- - left_side_splitter = QtWidgets.QSplitter(main_splitter) - left_side_splitter.setOrientation(QtCore.Qt.Vertical) - - # Project combobox - projects_combobox = QtWidgets.QComboBox(left_side_splitter) - combobox_delegate = QtWidgets.QStyledItemDelegate(self) - projects_combobox.setItemDelegate(combobox_delegate) - - # Assets widget - assets_widget = MultiSelectAssetsWidget( - dbcon, parent=left_side_splitter - ) - - # Families widget - families_filter_view = FamilyListView( - dbcon, self.family_config_cache, left_side_splitter - ) - left_side_splitter.addWidget(projects_combobox) - left_side_splitter.addWidget(assets_widget) - left_side_splitter.addWidget(families_filter_view) - left_side_splitter.setStretchFactor(1, 65) - left_side_splitter.setStretchFactor(2, 35) - - # --- Middle part --- - # Subsets widget - subsets_widget = SubsetWidget( - dbcon, - self.groups_config, - self.family_config_cache, - tool_name=self.tool_name, - parent=self - ) - - # --- Right part --- - thumb_ver_splitter = QtWidgets.QSplitter(main_splitter) - thumb_ver_splitter.setOrientation(QtCore.Qt.Vertical) - - thumbnail_widget = ThumbnailWidget(dbcon, parent=thumb_ver_splitter) - version_info_widget = VersionWidget(dbcon, parent=thumb_ver_splitter) - - thumb_ver_splitter.addWidget(thumbnail_widget) - thumb_ver_splitter.addWidget(version_info_widget) - - thumb_ver_splitter.setStretchFactor(0, 30) - thumb_ver_splitter.setStretchFactor(1, 35) - - manager = ModulesManager() - sync_server = manager.modules_by_name.get("sync_server") - sync_server_enabled = ( - sync_server is not None - and sync_server.enabled - ) - - repres_widget = None - if sync_server_enabled: - repres_widget = RepresentationWidget( - dbcon, self.tool_name, parent=thumb_ver_splitter - ) - thumb_ver_splitter.addWidget(repres_widget) - - main_splitter.addWidget(left_side_splitter) - main_splitter.addWidget(subsets_widget) - main_splitter.addWidget(thumb_ver_splitter) - if sync_server_enabled: - main_splitter.setSizes([250, 1000, 550]) - else: - main_splitter.setSizes([250, 850, 200]) - - # --- Footer --- - footer_widget = QtWidgets.QWidget(self) - footer_widget.setFixedHeight(20) - - message_label = QtWidgets.QLabel(footer_widget) - - footer_layout = QtWidgets.QVBoxLayout(footer_widget) - footer_layout.setContentsMargins(0, 0, 0, 0) - footer_layout.addWidget(message_label) - - layout = QtWidgets.QVBoxLayout(self) - layout.addWidget(main_splitter) - layout.addWidget(footer_widget) - - self.data = { - "state": { - "assetIds": None - } - } - - message_timer = QtCore.QTimer() - message_timer.setInterval(self.message_timeout) - message_timer.setSingleShot(True) - - message_timer.timeout.connect(self._on_message_timeout) - - families_filter_view.active_changed.connect( - self._on_family_filter_change - ) - assets_widget.selection_changed.connect(self.on_assetschanged) - assets_widget.refresh_triggered.connect(self.on_assetschanged) - subsets_widget.active_changed.connect(self.on_subsetschanged) - subsets_widget.version_changed.connect(self.on_versionschanged) - subsets_widget.refreshed.connect(self._on_subset_refresh) - projects_combobox.currentTextChanged.connect(self.on_project_change) - - self.sync_server = sync_server - self._sync_server_enabled = sync_server_enabled - - self._combobox_delegate = combobox_delegate - self._projects_combobox = projects_combobox - self._assets_widget = assets_widget - self._families_filter_view = families_filter_view - - self._subsets_widget = subsets_widget - - self._version_info_widget = version_info_widget - self._thumbnail_widget = thumbnail_widget - self._repres_widget = repres_widget - - self._message_label = message_label - self._message_timer = message_timer - - def showEvent(self, event): - super(LibraryLoaderWindow, self).showEvent(event) - if self._first_show: - self._first_show = False - self.setStyleSheet(style.load_stylesheet()) - if self._sync_server_enabled: - self.resize(1800, 900) - else: - self.resize(1300, 700) - - tools_lib.center_window(self) - - if not self._initial_refresh: - self._initial_refresh = True - self.refresh() - - def _set_projects(self): - # Store current project - old_project_name = self.current_project - - self._ignore_project_change = True - - # Cleanup - self._projects_combobox.clear() - - # Fill combobox with projects - select_project_item = QtGui.QStandardItem("< Select project >") - select_project_item.setData(None, QtCore.Qt.UserRole + 1) - - combobox_items = [select_project_item] - - project_names = self.get_filtered_projects() - - for project_name in sorted(project_names): - item = QtGui.QStandardItem(project_name) - item.setData(project_name, QtCore.Qt.UserRole + 1) - combobox_items.append(item) - - root_item = self._projects_combobox.model().invisibleRootItem() - root_item.appendRows(combobox_items) - - index = 0 - self._ignore_project_change = False - - if old_project_name: - index = self._projects_combobox.findText( - old_project_name, QtCore.Qt.MatchFixedString - ) - - self._projects_combobox.setCurrentIndex(index) - - def get_filtered_projects(self): - projects = list() - for project in get_projects(fields=["name", "data.library_project"]): - is_library = project.get("data", {}).get("library_project", False) - if ( - (is_library and self.show_libraries) or - (not is_library and self.show_projects) - ): - projects.append(project["name"]) - - return projects - - def on_project_change(self): - if self._ignore_project_change: - return - - row = self._projects_combobox.currentIndex() - index = self._projects_combobox.model().index(row, 0) - project_name = index.data(QtCore.Qt.UserRole + 1) - - self.dbcon.Session["AVALON_PROJECT"] = project_name - - self._subsets_widget.on_project_change(project_name) - if self._repres_widget: - self._repres_widget.on_project_change(project_name) - - self.family_config_cache.refresh() - self.groups_config.refresh() - - self._refresh_assets() - self._assetschanged() - - project_name = self.dbcon.active_project() or "No project selected" - title = "{} - {}".format(self.tool_title, project_name) - self.setWindowTitle(title) - - @property - def current_project(self): - return self.dbcon.active_project() or None - - # ------------------------------- - # Delay calling blocking methods - # ------------------------------- - - def refresh(self): - self.echo("Fetching results..") - tools_lib.schedule(self._refresh, 50, channel="mongo") - - def on_assetschanged(self, *args): - self.echo("Fetching asset..") - tools_lib.schedule(self._assetschanged, 50, channel="mongo") - - def on_subsetschanged(self, *args): - self.echo("Fetching subset..") - tools_lib.schedule(self._subsetschanged, 50, channel="mongo") - - def on_versionschanged(self, *args): - self.echo("Fetching version..") - tools_lib.schedule(self._versionschanged, 150, channel="mongo") - - def _on_subset_refresh(self, has_item): - self._subsets_widget.set_loading_state( - loading=False, empty=not has_item - ) - families = self._subsets_widget.get_subsets_families() - self._families_filter_view.set_enabled_families(families) - - # ------------------------------ - def set_context(self, context, refresh=True): - """Set the selection in the interface using a context. - The context must contain `asset` data by name. - - Args: - context (dict): The context to apply. - Returns: - None - """ - - asset_name = context.get("asset", None) - if asset_name is None: - return - - if refresh: - self._refresh_assets() - - self._assets_widget.select_asset_by_name(asset_name) - - def _on_family_filter_change(self, families): - self._subsets_widget.set_family_filters(families) - - def _refresh(self): - if not self._initial_refresh: - self._initial_refresh = True - self._set_projects() - - def _refresh_assets(self): - """Load assets from database""" - if self.current_project is not None: - # Ensure a project is loaded - project_doc = get_project(self.current_project, fields=["_id"]) - assert project_doc, "This is a bug" - - self._families_filter_view.set_enabled_families(set()) - self._families_filter_view.refresh() - - self._assets_widget.stop_refresh() - self._assets_widget.refresh() - self._assets_widget.setFocus() - - def clear_assets_underlines(self): - last_asset_ids = self.data["state"]["assetIds"] - if last_asset_ids: - self._assets_widget.clear_underlines() - - def _assetschanged(self): - """Selected assets have changed""" - subsets_model = self._subsets_widget.model - - subsets_model.clear() - self.clear_assets_underlines() - - if not self.dbcon.Session.get("AVALON_PROJECT"): - self._subsets_widget.set_loading_state( - loading=False, - empty=True - ) - return - - asset_ids = self._assets_widget.get_selected_asset_ids() - - # Start loading - self._subsets_widget.set_loading_state( - loading=bool(asset_ids), - empty=True - ) - - subsets_model.set_assets(asset_ids) - self._subsets_widget.view.setColumnHidden( - subsets_model.Columns.index("asset"), - len(asset_ids) < 2 - ) - - # Clear the version information on asset change - self._version_info_widget.set_version(None) - self._thumbnail_widget.set_thumbnail("asset", asset_ids) - - self.data["state"]["assetIds"] = asset_ids - - # reset repre list - if self._repres_widget: - self._repres_widget.set_version_ids([]) - - def _subsetschanged(self): - asset_ids = self.data["state"]["assetIds"] - # Skip setting colors if not asset multiselection - if not asset_ids or len(asset_ids) < 2: - self._versionschanged() - return - - selected_subsets = self._subsets_widget.get_selected_merge_items() - - asset_colors = {} - asset_ids = [] - for subset_node in selected_subsets: - asset_ids.extend(subset_node.get("assetIds", [])) - asset_ids = set(asset_ids) - - for subset_node in selected_subsets: - for asset_id in asset_ids: - if asset_id not in asset_colors: - asset_colors[asset_id] = [] - - color = None - if asset_id in subset_node.get("assetIds", []): - color = subset_node["subsetColor"] - - asset_colors[asset_id].append(color) - - self._assets_widget.set_underline_colors(asset_colors) - - # Set version in Version Widget - self._versionschanged() - - def _versionschanged(self): - items = self._subsets_widget.get_selected_subsets() - version_doc = None - version_docs = [] - for item in items: - doc = item["version_document"] - version_docs.append(doc) - if version_doc is None: - version_doc = doc - - self._version_info_widget.set_version(version_doc) - - thumbnail_src_ids = [ - version_doc["_id"] - for version_doc in version_docs - ] - src_type = "version" - if not thumbnail_src_ids: - src_type = "asset" - thumbnail_src_ids = self._assets_widget.get_selected_asset_ids() - - self._thumbnail_widget.set_thumbnail(src_type, thumbnail_src_ids) - - version_ids = [doc["_id"] for doc in version_docs or []] - if self._repres_widget: - self._repres_widget.set_version_ids(version_ids) - - def _on_message_timeout(self): - self._message_label.setText("") - - def echo(self, message): - self._message_label.setText(str(message)) - print(message) - self._message_timer.start() - - def closeEvent(self, event): - # Kill on holding SHIFT - modifiers = QtWidgets.QApplication.queryKeyboardModifiers() - shift_pressed = QtCore.Qt.ShiftModifier & modifiers - - if shift_pressed: - print("Force quitted..") - self.setAttribute(QtCore.Qt.WA_DeleteOnClose) - - print("Good bye") - return super(LibraryLoaderWindow, self).closeEvent(event) - - -def show(debug=False, parent=None, show_projects=False, show_libraries=True): - """Display Loader GUI - - Arguments: - debug (bool, optional): Run loader in debug-mode, - defaults to False - parent (QtCore.QObject, optional): The Qt object to parent to. - use_context (bool): Whether to apply the current context upon launch - - """ - # Remember window - if module.window is not None: - try: - module.window.show() - - # If the window is minimized then unminimize it. - if module.window.windowState() & QtCore.Qt.WindowMinimized: - module.window.setWindowState(QtCore.Qt.WindowActive) - - # Raise and activate the window - module.window.raise_() # for MacOS - module.window.activateWindow() # for Windows - module.window.refresh() - return - except RuntimeError as e: - if not e.message.rstrip().endswith("already deleted."): - raise - - # Garbage collected - module.window = None - - if debug: - import traceback - sys.excepthook = lambda typ, val, tb: traceback.print_last() - - with tools_lib.qt_app_context(): - window = LibraryLoaderWindow( - parent, show_projects, show_libraries - ) - window.show() - - module.window = window - - -def cli(args): - - import argparse - - parser = argparse.ArgumentParser() - parser.add_argument("project") - - show(show_projects=True, show_libraries=True) diff --git a/client/ayon_core/tools/loader/__init__.py b/client/ayon_core/tools/loader/__init__.py index a5fda8f018..09ecf65f3a 100644 --- a/client/ayon_core/tools/loader/__init__.py +++ b/client/ayon_core/tools/loader/__init__.py @@ -1,11 +1,6 @@ -from .app import ( - LoaderWindow, - show, - cli, -) +from .control import LoaderController + __all__ = ( - "LoaderWindow", - "show", - "cli", + "LoaderController", ) diff --git a/client/ayon_core/tools/loader/__main__.py b/client/ayon_core/tools/loader/__main__.py deleted file mode 100644 index acf357aa97..0000000000 --- a/client/ayon_core/tools/loader/__main__.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Main entrypoint for standalone debugging - - Used for running 'avalon.tool.loader.__main__' as a module (-m), useful for - debugging without need to start host. - - Modify AVALON_MONGO accordingly -""" -import os -import sys -from . import cli - - -def my_exception_hook(exctype, value, traceback): - # Print the error and traceback - print(exctype, value, traceback) - # Call the normal Exception hook after - sys._excepthook(exctype, value, traceback) - sys.exit(1) - - -if __name__ == '__main__': - os.environ["OPENPYPE_MONGO"] = "mongodb://localhost:27017" - os.environ["AVALON_DB"] = "avalon" - os.environ["AVALON_TIMEOUT"] = "1000" - os.environ["OPENPYPE_DEBUG"] = "1" - os.environ["AVALON_ASSET"] = "Jungle" - - # Set the exception hook to our wrapping function - sys.excepthook = my_exception_hook - - sys.exit(cli(sys.argv[1:])) diff --git a/client/ayon_core/tools/ayon_loader/abstract.py b/client/ayon_core/tools/loader/abstract.py similarity index 100% rename from client/ayon_core/tools/ayon_loader/abstract.py rename to client/ayon_core/tools/loader/abstract.py diff --git a/client/ayon_core/tools/loader/app.py b/client/ayon_core/tools/loader/app.py deleted file mode 100644 index 96ce46db2f..0000000000 --- a/client/ayon_core/tools/loader/app.py +++ /dev/null @@ -1,625 +0,0 @@ -import sys -import traceback - -from qtpy import QtWidgets, QtCore - -from ayon_core.client import get_projects, get_project -from ayon_core import style -from ayon_core.lib import register_event_callback -from ayon_core.pipeline import ( - install_openpype_plugins, - legacy_io, -) -from ayon_core.tools.utils import ( - lib, - PlaceholderLineEdit -) -from ayon_core.tools.utils.assets_widget import MultiSelectAssetsWidget - -from .widgets import ( - SubsetWidget, - VersionWidget, - FamilyListView, - ThumbnailWidget, - RepresentationWidget, - OverlayFrame -) - -from ayon_core.modules import ModulesManager - -module = sys.modules[__name__] -module.window = None - - -class LoaderWindow(QtWidgets.QDialog): - """Asset loader interface""" - - tool_name = "loader" - message_timeout = 5000 - - def __init__(self, parent=None): - super(LoaderWindow, self).__init__(parent) - title = "Asset Loader 2.1" - project_name = legacy_io.active_project() - if project_name: - title += " - {}".format(project_name) - self.setWindowTitle(title) - - # Groups config - self.groups_config = lib.GroupsConfig(legacy_io) - self.family_config_cache = lib.FamilyConfigCache(legacy_io) - - # Enable minimize and maximize for app - window_flags = QtCore.Qt.Window - if not parent: - window_flags |= QtCore.Qt.WindowStaysOnTopHint - self.setWindowFlags(window_flags) - self.setFocusPolicy(QtCore.Qt.StrongFocus) - - main_splitter = QtWidgets.QSplitter(self) - - # --- Left part --- - left_side_splitter = QtWidgets.QSplitter(main_splitter) - left_side_splitter.setOrientation(QtCore.Qt.Vertical) - - # Assets widget - assets_widget = MultiSelectAssetsWidget( - legacy_io, parent=left_side_splitter - ) - assets_widget.set_current_asset_btn_visibility(True) - - # Families widget - families_filter_view = FamilyListView( - legacy_io, self.family_config_cache, left_side_splitter - ) - left_side_splitter.addWidget(assets_widget) - left_side_splitter.addWidget(families_filter_view) - left_side_splitter.setStretchFactor(0, 65) - left_side_splitter.setStretchFactor(1, 35) - - # --- Middle part --- - # Subsets widget - subsets_widget = SubsetWidget( - legacy_io, - self.groups_config, - self.family_config_cache, - tool_name=self.tool_name, - parent=main_splitter - ) - - # --- Right part --- - thumb_ver_splitter = QtWidgets.QSplitter(main_splitter) - thumb_ver_splitter.setOrientation(QtCore.Qt.Vertical) - - thumbnail_widget = ThumbnailWidget( - legacy_io, parent=thumb_ver_splitter - ) - version_info_widget = VersionWidget( - legacy_io, parent=thumb_ver_splitter - ) - - thumb_ver_splitter.addWidget(thumbnail_widget) - thumb_ver_splitter.addWidget(version_info_widget) - - thumb_ver_splitter.setStretchFactor(0, 30) - thumb_ver_splitter.setStretchFactor(1, 35) - - manager = ModulesManager() - sync_server = manager.modules_by_name.get("sync_server") - sync_server_enabled = False - if sync_server is not None: - sync_server_enabled = sync_server.enabled - - repres_widget = None - if sync_server_enabled: - repres_widget = RepresentationWidget( - legacy_io, self.tool_name, parent=thumb_ver_splitter - ) - thumb_ver_splitter.addWidget(repres_widget) - - main_splitter.addWidget(left_side_splitter) - main_splitter.addWidget(subsets_widget) - main_splitter.addWidget(thumb_ver_splitter) - - if sync_server_enabled: - main_splitter.setSizes([250, 1000, 550]) - else: - main_splitter.setSizes([250, 850, 200]) - - footer_widget = QtWidgets.QWidget(self) - - message_label = QtWidgets.QLabel(footer_widget) - - footer_layout = QtWidgets.QHBoxLayout(footer_widget) - footer_layout.setContentsMargins(0, 0, 0, 0) - footer_layout.addWidget(message_label, 1) - - layout = QtWidgets.QVBoxLayout(self) - layout.addWidget(main_splitter, 1) - layout.addWidget(footer_widget, 0) - - self.data = { - "state": { - "assetIds": None - } - } - - overlay_frame = OverlayFrame("Loading...", self) - overlay_frame.setVisible(False) - - message_timer = QtCore.QTimer() - message_timer.setInterval(self.message_timeout) - message_timer.setSingleShot(True) - - message_timer.timeout.connect(self._on_message_timeout) - - families_filter_view.active_changed.connect( - self._on_family_filter_change - ) - assets_widget.selection_changed.connect(self.on_assetschanged) - assets_widget.refresh_triggered.connect(self.on_assetschanged) - subsets_widget.active_changed.connect(self.on_subsetschanged) - subsets_widget.version_changed.connect(self.on_versionschanged) - subsets_widget.refreshed.connect(self._on_subset_refresh) - - subsets_widget.load_started.connect(self._on_load_start) - subsets_widget.load_ended.connect(self._on_load_end) - if repres_widget: - repres_widget.load_started.connect(self._on_load_start) - repres_widget.load_ended.connect(self._on_load_end) - - self._sync_server_enabled = sync_server_enabled - - self._assets_widget = assets_widget - self._families_filter_view = families_filter_view - - self._subsets_widget = subsets_widget - - self._version_info_widget = version_info_widget - self._thumbnail_widget = thumbnail_widget - self._repres_widget = repres_widget - - self._message_label = message_label - self._message_timer = message_timer - - # TODO add overlay using stack widget - self._overlay_frame = overlay_frame - - self.family_config_cache.refresh() - self.groups_config.refresh() - - self._refresh() - self._assetschanged() - - self._first_show = True - - register_event_callback("taskChanged", self.on_context_task_change) - - def resizeEvent(self, event): - super(LoaderWindow, self).resizeEvent(event) - self._overlay_frame.resize(self.size()) - - def moveEvent(self, event): - super(LoaderWindow, self).moveEvent(event) - self._overlay_frame.move(0, 0) - - def showEvent(self, event): - super(LoaderWindow, self).showEvent(event) - if self._first_show: - self._first_show = False - self.setStyleSheet(style.load_stylesheet()) - if self._sync_server_enabled: - self.resize(1800, 900) - else: - self.resize(1300, 700) - lib.center_window(self) - - # ------------------------------- - # Delay calling blocking methods - # ------------------------------- - - def refresh(self): - self.echo("Fetching results..") - lib.schedule(self._refresh, 50, channel="mongo") - - def on_assetschanged(self, *args): - self.echo("Fetching hierarchy..") - lib.schedule(self._assetschanged, 50, channel="mongo") - - def on_subsetschanged(self, *args): - self.echo("Fetching subset..") - lib.schedule(self._subsetschanged, 50, channel="mongo") - - def on_versionschanged(self, *args): - self.echo("Fetching version..") - lib.schedule(self._versionschanged, 150, channel="mongo") - - def set_context(self, context, refresh=True): - self.echo("Setting context: {}".format(context)) - lib.schedule(lambda: self._set_context(context, refresh=refresh), - 50, channel="mongo") - - def _on_load_start(self): - # Show overlay and process events so it's repainted - self._overlay_frame.setVisible(True) - QtWidgets.QApplication.processEvents() - - def _hide_overlay(self): - self._overlay_frame.setVisible(False) - - def _on_subset_refresh(self, has_item): - self._subsets_widget.set_loading_state( - loading=False, empty=not has_item - ) - families = self._subsets_widget.get_subsets_families() - self._families_filter_view.set_enabled_families(families) - - def _on_load_end(self): - # Delay hiding as click events happened during loading should be - # blocked - QtCore.QTimer.singleShot(100, self._hide_overlay) - - # ------------------------------ - def _on_family_filter_change(self, families): - self._subsets_widget.set_family_filters(families) - - def on_context_task_change(self, *args, **kwargs): - # Refresh families config - self._families_filter_view.refresh() - # Change to context asset on context change - self._assets_widget.select_asset_by_name( - legacy_io.Session["AVALON_ASSET"] - ) - - def _refresh(self): - """Load assets from database""" - - # Ensure a project is loaded - project_name = legacy_io.active_project() - project_doc = get_project(project_name, fields=["_id"]) - assert project_doc, "Project was not found! This is a bug" - - self._assets_widget.refresh() - self._assets_widget.setFocus() - - self._families_filter_view.refresh() - - def clear_assets_underlines(self): - """Clear colors from asset data to remove colored underlines - When multiple assets are selected colored underlines mark which asset - own selected subsets. These colors must be cleared from asset data - on selection change so they match current selection. - """ - # TODO do not touch inner attributes of asset widget - self._assets_widget.clear_underlines() - - def _assetschanged(self): - """Selected assets have changed""" - subsets_widget = self._subsets_widget - # TODO do not touch subset widget inner attributes - subsets_model = subsets_widget.model - - subsets_model.clear() - self.clear_assets_underlines() - - asset_ids = self._assets_widget.get_selected_asset_ids() - # Start loading - subsets_widget.set_loading_state( - loading=bool(asset_ids), - empty=True - ) - - subsets_model.set_assets(asset_ids) - subsets_widget.view.setColumnHidden( - subsets_model.Columns.index("asset"), - len(asset_ids) < 2 - ) - - # Clear the version information on asset change - self._thumbnail_widget.set_thumbnail("asset", asset_ids) - self._version_info_widget.set_version(None) - - self.data["state"]["assetIds"] = asset_ids - - # reset repre list - if self._repres_widget is not None: - self._repres_widget.set_version_ids([]) - - def _subsetschanged(self): - asset_ids = self.data["state"]["assetIds"] - # Skip setting colors if not asset multiselection - if not asset_ids or len(asset_ids) < 2: - self.clear_assets_underlines() - self._versionschanged() - return - - selected_subsets = self._subsets_widget.get_selected_merge_items() - - asset_colors = {} - asset_ids = [] - for subset_node in selected_subsets: - asset_ids.extend(subset_node.get("assetIds", [])) - asset_ids = set(asset_ids) - - for subset_node in selected_subsets: - for asset_id in asset_ids: - if asset_id not in asset_colors: - asset_colors[asset_id] = [] - - color = None - if asset_id in subset_node.get("assetIds", []): - color = subset_node["subsetColor"] - - asset_colors[asset_id].append(color) - - self._assets_widget.set_underline_colors(asset_colors) - - # Set version in Version Widget - self._versionschanged() - - def _versionschanged(self): - items = self._subsets_widget.get_selected_subsets() - version_doc = None - version_docs = [] - for item in items: - doc = item["version_document"] - version_docs.append(doc) - if version_doc is None: - version_doc = doc - - self._version_info_widget.set_version(version_doc) - - thumbnail_src_ids = [ - version_doc["_id"] - for version_doc in version_docs - ] - source_type = "version" - if not thumbnail_src_ids: - source_type = "asset" - thumbnail_src_ids = self._assets_widget.get_selected_asset_ids() - - self._thumbnail_widget.set_thumbnail(source_type, thumbnail_src_ids) - - if self._repres_widget is not None: - version_ids = [doc["_id"] for doc in version_docs] - self._repres_widget.set_version_ids(version_ids) - - # self._repres_widget.change_visibility("subset", len(rows) > 1) - # self._repres_widget.change_visibility( - # "asset", len(asset_docs) > 1 - # ) - - def _set_context(self, context, refresh=True): - """Set the selection in the interface using a context. - - The context must contain `asset` data by name. - - Args: - context (dict): The context to apply. - refrest (bool): Trigger refresh on context set. - """ - - asset = context.get("asset", None) - if asset is None: - return - - if refresh: - self._refresh() - - self._assets_widget.select_asset_by_name(asset) - - def _on_message_timeout(self): - self._message_label.setText("") - - def echo(self, message): - self._message_label.setText(str(message)) - print(message) - self._message_timer.start() - - def closeEvent(self, event): - # Kill on holding SHIFT - modifiers = QtWidgets.QApplication.queryKeyboardModifiers() - shift_pressed = QtCore.Qt.ShiftModifier & modifiers - - if shift_pressed: - print("Force quit..") - self.setAttribute(QtCore.Qt.WA_DeleteOnClose) - - print("Good bye") - return super(LoaderWindow, self).closeEvent(event) - - def keyPressEvent(self, event): - modifiers = event.modifiers() - ctrl_pressed = QtCore.Qt.ControlModifier & modifiers - - # Grouping subsets on pressing Ctrl + G - if (ctrl_pressed and event.key() == QtCore.Qt.Key_G and - not event.isAutoRepeat()): - self.show_grouping_dialog() - return - - super(LoaderWindow, self).keyPressEvent(event) - event.setAccepted(True) # Avoid interfering other widgets - - def show_grouping_dialog(self): - subsets = self._subsets_widget - if not subsets.is_groupable(): - self.echo("Grouping not enabled.") - return - - selected = self._subsets_widget.get_selected_subsets() - if not selected: - self.echo("No selected subset.") - return - - dialog = SubsetGroupingDialog( - items=selected, groups_config=self.groups_config, parent=self - ) - dialog.grouped.connect(self._assetschanged) - dialog.show() - - -class SubsetGroupingDialog(QtWidgets.QDialog): - grouped = QtCore.Signal() - - def __init__(self, items, groups_config, parent=None): - super(SubsetGroupingDialog, self).__init__(parent=parent) - self.setWindowTitle("Grouping Subsets") - self.setMinimumWidth(250) - self.setModal(True) - - self.items = items - self.groups_config = groups_config - # TODO do not touch inner attributes - self.subsets = parent._subsets_widget - self.asset_ids = parent.data["state"]["assetIds"] - - name = PlaceholderLineEdit(self) - name.setPlaceholderText("Remain blank to ungroup..") - - # Menu for pre-defined subset groups - name_button = QtWidgets.QPushButton() - name_button.setFixedWidth(18) - name_button.setFixedHeight(20) - name_menu = QtWidgets.QMenu(name_button) - name_button.setMenu(name_menu) - - name_layout = QtWidgets.QHBoxLayout() - name_layout.addWidget(name) - name_layout.addWidget(name_button) - name_layout.setContentsMargins(0, 0, 0, 0) - - group_btn = QtWidgets.QPushButton("Apply") - - layout = QtWidgets.QVBoxLayout(self) - layout.addWidget(QtWidgets.QLabel("Group Name")) - layout.addLayout(name_layout) - layout.addWidget(group_btn) - - group_btn.clicked.connect(self.on_group) - group_btn.setAutoDefault(True) - group_btn.setDefault(True) - - self.name = name - self.name_menu = name_menu - - self._build_menu() - - def _build_menu(self): - menu = self.name_menu - button = menu.parent() - # Get and destroy the action group - group = button.findChild(QtWidgets.QActionGroup) - if group: - group.deleteLater() - - active_groups = self.groups_config.active_groups(self.asset_ids) - - # Build new action group - group = QtWidgets.QActionGroup(button) - group_names = list() - for data in sorted(active_groups, key=lambda x: x["order"]): - name = data["name"] - if name in group_names: - continue - group_names.append(name) - icon = data["icon"] - - action = group.addAction(name) - action.setIcon(icon) - menu.addAction(action) - - group.triggered.connect(self._on_action_clicked) - button.setEnabled(not menu.isEmpty()) - - def _on_action_clicked(self, action): - self.name.setText(action.text()) - - def on_group(self): - name = self.name.text().strip() - self.subsets.group_subsets(name, self.asset_ids, self.items) - - with lib.preserve_selection(tree_view=self.subsets.view, - current_index=False): - self.grouped.emit() - self.close() - - -def show(debug=False, parent=None, use_context=False): - """Display Loader GUI - - Arguments: - debug (bool, optional): Run loader in debug-mode, - defaults to False - parent (QtCore.QObject, optional): The Qt object to parent to. - use_context (bool): Whether to apply the current context upon launch - - """ - - # Remember window - if module.window is not None: - try: - module.window.show() - - # If the window is minimized then unminimize it. - if module.window.windowState() & QtCore.Qt.WindowMinimized: - module.window.setWindowState(QtCore.Qt.WindowActive) - - # Raise and activate the window - module.window.raise_() # for MacOS - module.window.activateWindow() # for Windows - module.window.refresh() - return - except (AttributeError, RuntimeError): - # Garbage collected - module.window = None - - if debug: - sys.excepthook = lambda typ, val, tb: traceback.print_last() - - legacy_io.install() - - any_project = next( - project for project in get_projects(fields=["name"]) - ) - - legacy_io.Session["AVALON_PROJECT"] = any_project["name"] - module.project = any_project["name"] - - with lib.qt_app_context(): - window = LoaderWindow(parent) - window.show() - - if use_context: - context = {"asset": legacy_io.Session["AVALON_ASSET"]} - window.set_context(context, refresh=True) - else: - window.refresh() - - module.window = window - - # Pull window to the front. - module.window.raise_() - module.window.activateWindow() - - -def cli(args): - - import argparse - - parser = argparse.ArgumentParser() - parser.add_argument("project") - - args = parser.parse_args(args) - project = args.project - - print("Entering Project: %s" % project) - - legacy_io.install() - - # Store settings - legacy_io.Session["AVALON_PROJECT"] = project - - install_openpype_plugins(project) - - show() diff --git a/client/ayon_core/tools/ayon_loader/control.py b/client/ayon_core/tools/loader/control.py similarity index 100% rename from client/ayon_core/tools/ayon_loader/control.py rename to client/ayon_core/tools/loader/control.py diff --git a/client/ayon_core/tools/loader/delegates.py b/client/ayon_core/tools/loader/delegates.py deleted file mode 100644 index 0686fe78cd..0000000000 --- a/client/ayon_core/tools/loader/delegates.py +++ /dev/null @@ -1,28 +0,0 @@ -from qtpy import QtWidgets, QtGui, QtCore - - -class LoadedInSceneDelegate(QtWidgets.QStyledItemDelegate): - """Delegate for Loaded in Scene state columns. - - Shows "yes" or "no" for True or False values - Colorizes green or dark grey based on True or False values - - """ - - def __init__(self, *args, **kwargs): - super(LoadedInSceneDelegate, self).__init__(*args, **kwargs) - self._colors = { - True: QtGui.QColor(80, 170, 80), - False: QtGui.QColor(90, 90, 90) - } - - def displayText(self, value, locale): - return "yes" if value else "no" - - def initStyleOption(self, option, index): - super(LoadedInSceneDelegate, self).initStyleOption(option, index) - - # Colorize based on value - value = index.data(QtCore.Qt.DisplayRole) - color = self._colors[bool(value)] - option.palette.setBrush(QtGui.QPalette.Text, color) diff --git a/client/ayon_core/tools/loader/images/default_thumbnail.png b/client/ayon_core/tools/loader/images/default_thumbnail.png deleted file mode 100644 index adea862e5b..0000000000 Binary files a/client/ayon_core/tools/loader/images/default_thumbnail.png and /dev/null differ diff --git a/client/ayon_core/tools/loader/lib.py b/client/ayon_core/tools/loader/lib.py deleted file mode 100644 index 654172b551..0000000000 --- a/client/ayon_core/tools/loader/lib.py +++ /dev/null @@ -1,181 +0,0 @@ -import inspect -from qtpy import QtGui -import qtawesome - -from ayon_core.lib.attribute_definitions import AbstractAttrDef -from ayon_core.tools.attribute_defs import AttributeDefinitionsDialog -from ayon_core.tools.utils.widgets import ( - OptionalAction, - OptionDialog -) - - -def change_visibility(model, view, column_name, visible): - """ - Hides or shows particular 'column_name'. - - "asset" and "subset" columns should be visible only in multiselect - """ - index = model.Columns.index(column_name) - view.setColumnHidden(index, not visible) - - -def get_options(action, loader, parent, repre_contexts): - """Provides dialog to select value from loader provided options. - - Loader can provide static or dynamically created options based on - qargparse variants. - - Args: - action (OptionalAction) - action in menu - loader (cls of api.Loader) - not initialized yet - parent (Qt element to parent dialog to) - repre_contexts (list) of dict with full info about selected repres - Returns: - (dict) - selected value from OptionDialog - None when dialog was closed or cancelled, in all other cases {} - if no options - """ - - # Pop option dialog - options = {} - loader_options = loader.get_options(repre_contexts) - if not getattr(action, "optioned", False) or not loader_options: - return options - - if isinstance(loader_options[0], AbstractAttrDef): - qargparse_options = False - dialog = AttributeDefinitionsDialog(loader_options, parent) - else: - qargparse_options = True - dialog = OptionDialog(parent) - dialog.create(loader_options) - - dialog.setWindowTitle(action.label + " Options") - - if not dialog.exec_(): - return None - - # Get option - if qargparse_options: - return dialog.parse() - return dialog.get_values() - - -def add_representation_loaders_to_menu(loaders, menu, repre_contexts): - """ - Loops through provider loaders and adds them to 'menu'. - - Expects loaders sorted in requested order. - Expects loaders de-duplicated if wanted. - - Args: - loaders(tuple): representation - loader - menu (OptionalMenu): - repre_contexts (dict): full info about representations (contains - their repre_doc, asset_doc, subset_doc, version_doc), - keys are repre_ids - - Returns: - menu (OptionalMenu): with new items - """ - # List the available loaders - for representation, loader in loaders: - label = None - repre_context = None - if representation: - label = representation.get("custom_label") - repre_context = repre_contexts[representation["_id"]] - - if not label: - label = get_label_from_loader(loader, representation) - - icon = get_icon_from_loader(loader) - - loader_options = loader.get_options([repre_context]) - - use_option = bool(loader_options) - action = OptionalAction(label, icon, use_option, menu) - if use_option: - # Add option box tip - action.set_option_tip(loader_options) - - action.setData((representation, loader)) - - # Add tooltip and statustip from Loader docstring - tip = inspect.getdoc(loader) - if tip: - action.setToolTip(tip) - action.setStatusTip(tip) - - menu.addAction(action) - - return menu - - -def remove_tool_name_from_loaders(available_loaders, tool_name): - if not tool_name: - return available_loaders - filtered_loaders = [] - for loader in available_loaders: - if hasattr(loader, "tool_names"): - if not ("*" in loader.tool_names or - tool_name in loader.tool_names): - continue - filtered_loaders.append(loader) - return filtered_loaders - - -def get_icon_from_loader(loader): - """Pull icon info from loader class""" - # Support font-awesome icons using the `.icon` and `.color` - # attributes on plug-ins. - icon = getattr(loader, "icon", None) - if icon is not None: - try: - key = "fa.{0}".format(icon) - color = getattr(loader, "color", "white") - icon = qtawesome.icon(key, color=color) - except Exception as e: - print("Unable to set icon for loader " - "{}: {}".format(loader, e)) - icon = None - return icon - - -def get_label_from_loader(loader, representation=None): - """Pull label info from loader class""" - label = getattr(loader, "label", None) - if label is None: - label = loader.__name__ - if representation: - # Add the representation as suffix - label = "{0} ({1})".format(label, representation['name']) - return label - - -def get_no_loader_action(menu, one_item_selected=False): - """Creates dummy no loader option in 'menu'""" - submsg = "your selection." - if one_item_selected: - submsg = "this version." - msg = "No compatible loaders for {}".format(submsg) - print(msg) - icon = qtawesome.icon( - "fa.exclamation", - color=QtGui.QColor(255, 51, 0) - ) - action = OptionalAction(("*" + msg), icon, False, menu) - return action - - -def sort_loaders(loaders, custom_sorter=None): - def sorter(value): - """Sort the Loaders by their order and then their name""" - Plugin = value[1] - return Plugin.order, Plugin.__name__ - - if not custom_sorter: - custom_sorter = sorter - - return sorted(loaders, key=custom_sorter) diff --git a/client/ayon_core/tools/loader/model.py b/client/ayon_core/tools/loader/model.py deleted file mode 100644 index d1e93345d4..0000000000 --- a/client/ayon_core/tools/loader/model.py +++ /dev/null @@ -1,1292 +0,0 @@ -import copy -import re -import math -import time -from uuid import uuid4 - -from qtpy import QtCore, QtGui -import qtawesome - -from ayon_core import AYON_SERVER_ENABLED -from ayon_core.client import ( - get_assets, - get_subsets, - get_last_versions, - get_versions, - get_hero_versions, - get_version_by_name, - get_representations -) -from ayon_core.pipeline import ( - registered_host, - HeroVersionType, - schema, -) - -from ayon_core.style import get_default_entity_icon_color -from ayon_core.tools.utils.models import TreeModel, Item -from ayon_core.tools.utils import lib -from ayon_core.host import ILoadHost - -from ayon_core.modules import ModulesManager -from ayon_core.tools.utils.constants import ( - LOCAL_PROVIDER_ROLE, - REMOTE_PROVIDER_ROLE, - LOCAL_AVAILABILITY_ROLE, - REMOTE_AVAILABILITY_ROLE -) - -ITEM_ID_ROLE = QtCore.Qt.UserRole + 90 - - -def is_filtering_recursible(): - """Does Qt binding support recursive filtering for QSortFilterProxyModel? - - (NOTE) Recursive filtering was introduced in Qt 5.10. - - """ - return hasattr(QtCore.QSortFilterProxyModel, - "setRecursiveFilteringEnabled") - - -class BaseRepresentationModel(object): - """Methods for SyncServer useful in multiple models""" - # Cheap & hackish way how to avoid refreshing of whole sync server module - # on each selection change - _last_project = None - _modules_manager = None - _last_project_cache = 0 - _last_manager_cache = 0 - _max_project_cache_time = 30 - _max_manager_cache_time = 60 - - def reset_sync_server(self, project_name=None): - """Sets/Resets sync server vars after every change (refresh.)""" - repre_icons = {} - sync_server = None - sync_server_enabled = False - active_site = active_provider = None - remote_site = remote_provider = None - - if not project_name: - project_name = self.dbcon.active_project() - else: - self.dbcon.Session["AVALON_PROJECT"] = project_name - - if not project_name: - self.repre_icons = repre_icons - self.sync_server = sync_server - self.sync_server_enabled = sync_server_enabled - self.active_site = active_site - self.active_provider = active_provider - self.remote_site = remote_site - self.remote_provider = remote_provider - return - - now_time = time.time() - project_cache_diff = now_time - self._last_project_cache - if project_cache_diff > self._max_project_cache_time: - self._last_project = None - - if project_name == self._last_project: - return - - self._last_project = project_name - self._last_project_cache = now_time - - manager_cache_diff = now_time - self._last_manager_cache - if manager_cache_diff > self._max_manager_cache_time: - self._modules_manager = None - - if self._modules_manager is None: - self._modules_manager = ModulesManager() - self._last_manager_cache = now_time - - sync_server = self._modules_manager.modules_by_name.get("sync_server") - if ( - sync_server is not None - and sync_server.enabled - and sync_server.is_project_enabled(project_name, single=True) - ): - sync_server_enabled = True - active_site = sync_server.get_active_site(project_name) - active_provider = sync_server.get_provider_for_site( - project_name, active_site) - if active_site == 'studio': # for studio use explicit icon - active_provider = 'studio' - - remote_site = sync_server.get_remote_site(project_name) - remote_provider = sync_server.get_provider_for_site( - project_name, remote_site) - if remote_site == 'studio': # for studio use explicit icon - remote_provider = 'studio' - - repre_icons = lib.get_repre_icons() - - self.repre_icons = repre_icons - self.sync_server = sync_server - self.sync_server_enabled = sync_server_enabled - self.active_site = active_site - self.active_provider = active_provider - self.remote_site = remote_site - self.remote_provider = remote_provider - - -class SubsetsModel(BaseRepresentationModel, TreeModel): - doc_fetched = QtCore.Signal() - refreshed = QtCore.Signal(bool) - - Columns = [ - "subset", - "asset", - "family", - "version", - "time", - "author", - "frames", - "duration", - "handles", - "step", - "loaded_in_scene", - "repre_info" - ] - - column_labels_mapping = { - "subset": "Product" if AYON_SERVER_ENABLED else "Subset", - "asset": "Folder" if AYON_SERVER_ENABLED else "Asset", - "family": "Product type" if AYON_SERVER_ENABLED else "Family", - "version": "Version", - "time": "Time", - "author": "Author", - "frames": "Frames", - "duration": "Duration", - "handles": "Handles", - "step": "Step", - "loaded_in_scene": "In scene", - "repre_info": "Availability" - } - - SortAscendingRole = QtCore.Qt.UserRole + 2 - SortDescendingRole = QtCore.Qt.UserRole + 3 - merged_subset_colors = [ - (55, 161, 222), # Light Blue - (231, 176, 0), # Yellow - (154, 13, 255), # Purple - (130, 184, 30), # Light Green - (211, 79, 63), # Light Red - (179, 181, 182), # Grey - (194, 57, 179), # Pink - (0, 120, 215), # Dark Blue - (0, 204, 106), # Dark Green - (247, 99, 12), # Orange - ] - not_last_hero_brush = QtGui.QBrush(QtGui.QColor(254, 121, 121)) - - # Should be minimum of required asset document keys - asset_doc_projection = { - "name": 1, - "label": 1 - } - # Should be minimum of required subset document keys - subset_doc_projection = { - "name": 1, - "parent": 1, - "schema": 1, - "data.families": 1, - "data.subsetGroup": 1 - } - - def __init__( - self, - dbcon, - groups_config, - family_config_cache, - grouping=True, - parent=None, - asset_doc_projection=None, - subset_doc_projection=None - ): - super(SubsetsModel, self).__init__(parent=parent) - - self.dbcon = dbcon - - # Projections for Mongo queries - # - let ability to modify them if used in tools that require more than - # defaults - if asset_doc_projection: - self.asset_doc_projection = asset_doc_projection - - if subset_doc_projection: - self.subset_doc_projection = subset_doc_projection - - self.repre_icons = {} - self.sync_server = None - self.sync_server_enabled = False - self.active_site = self.active_provider = None - - self.columns_index = dict( - (key, idx) for idx, key in enumerate(self.Columns) - ) - self._asset_ids = None - - self.groups_config = groups_config - self.family_config_cache = family_config_cache - self._sorter = None - self._grouping = grouping - self._icons = { - "subset": qtawesome.icon( - "fa.file-o", - color=get_default_entity_icon_color() - ) - } - self._items_by_id = {} - - self._doc_fetching_thread = None - self._doc_fetching_stop = False - self._doc_payload = {} - - self._host = registered_host() - self._loaded_representation_ids = set() - - # Refresh loaded scene containers only every 3 seconds at most - self._host_loaded_refresh_timeout = 3 - self._host_loaded_refresh_time = 0 - - self.doc_fetched.connect(self._on_doc_fetched) - self.refresh() - - def get_item_by_id(self, item_id): - return self._items_by_id.get(item_id) - - def add_child(self, new_item, *args, **kwargs): - item_id = str(uuid4()) - new_item["id"] = item_id - self._items_by_id[item_id] = new_item - super(SubsetsModel, self).add_child(new_item, *args, **kwargs) - - def set_assets(self, asset_ids): - self._asset_ids = asset_ids - self.refresh() - - def set_grouping(self, state): - self._grouping = state - self._on_doc_fetched() - - def get_subsets_families(self): - return self._doc_payload.get("subset_families") or set() - - def setData(self, index, value, role=QtCore.Qt.EditRole): - # Trigger additional edit when `version` column changed - # because it also updates the information in other columns - if index.column() == self.columns_index["version"]: - item = index.internalPointer() - subset_id = item["_id"] - if isinstance(value, HeroVersionType): - version_doc = self._get_hero_version(subset_id) - - else: - project_name = self.dbcon.active_project() - version_doc = get_version_by_name( - project_name, value, subset_id - ) - - # update availability on active site when version changes - if self.sync_server_enabled and version_doc: - repres_info = list( - self.sync_server.get_repre_info_for_versions( - project_name, - [version_doc["_id"]], - self.active_site, - self.remote_site - ) - ) - if repres_info: - version_doc["data"].update( - self._get_repre_dict(repres_info[0])) - - self.set_version(index, version_doc) - - return super(SubsetsModel, self).setData(index, value, role) - - def _get_hero_version(self, subset_id): - project_name = self.dbcon.active_project() - version_docs = get_versions( - project_name, subset_ids=[subset_id], hero=True - ) - standard_versions = [] - hero_version_doc = None - for version_doc in version_docs: - if version_doc["type"] == "hero_version": - hero_version_doc = version_doc - continue - standard_versions.append(version_doc) - - src_version_id = hero_version_doc["version_id"] - src_version = None - is_from_latest = True - for version_doc in reversed(sorted( - standard_versions, key=lambda item: item["name"] - )): - if version_doc["_id"] == src_version_id: - src_version = version_doc - break - is_from_latest = False - - hero_version_doc["data"] = src_version["data"] - hero_version_doc["name"] = src_version["name"] - hero_version_doc["is_from_latest"] = is_from_latest - return hero_version_doc - - def set_version(self, index, version): - """Update the version data of the given index. - - Arguments: - index (QtCore.QModelIndex): The model index. - version (dict) Version document in the database. - - """ - - assert isinstance(index, QtCore.QModelIndex) - if not index.isValid(): - return - - item = index.internalPointer() - - assert version["parent"] == item["_id"], ( - "Version does not belong to subset" - ) - - # Get the data from the version - version_data = version.get("data", dict()) - - # Compute frame ranges (if data is present) - frame_start = version_data.get( - "frameStart", - # backwards compatibility - version_data.get("startFrame", None) - ) - frame_end = version_data.get( - "frameEnd", - # backwards compatibility - version_data.get("endFrame", None) - ) - - handles_label = None - handle_start = version_data.get("handleStart", None) - handle_end = version_data.get("handleEnd", None) - if handle_start is not None and handle_end is not None: - handles_label = "{}-{}".format(str(handle_start), str(handle_end)) - - if frame_start is not None and frame_end is not None: - # Remove superfluous zeros from numbers (3.0 -> 3) to improve - # readability for most frame ranges - start_clean = ("%f" % frame_start).rstrip("0").rstrip(".") - end_clean = ("%f" % frame_end).rstrip("0").rstrip(".") - frames = "{0}-{1}".format(start_clean, end_clean) - duration = frame_end - frame_start + 1 - else: - frames = None - duration = None - - schema_maj_version, _ = schema.get_schema_version(item["schema"]) - if schema_maj_version < 3: - families = version_data.get("families", [None]) - else: - families = item["data"]["families"] - - family = None - if families: - family = families[0] - - family_config = self.family_config_cache.family_config(family) - - item.update({ - "version": version["name"], - "version_document": version, - "author": version_data.get("author", None), - "time": version_data.get("time", None), - "family": family, - "familyLabel": family_config.get("label", family), - "familyIcon": family_config.get("icon", None), - "families": set(families), - "frameStart": frame_start, - "frameEnd": frame_end, - "duration": duration, - "handles": handles_label, - "frames": frames, - "step": version_data.get("step", None), - }) - - repre_info = version_data.get("repre_info") - if repre_info: - item["repre_info"] = repre_info - - def _fetch(self): - project_name = self.dbcon.active_project() - asset_docs = get_assets( - project_name, - asset_ids=self._asset_ids, - fields=self.asset_doc_projection.keys() - ) - - asset_docs_by_id = { - asset_doc["_id"]: asset_doc - for asset_doc in asset_docs - } - - subset_docs_by_id = {} - subset_docs = get_subsets( - project_name, - asset_ids=self._asset_ids, - fields=self.subset_doc_projection.keys() - ) - - subset_families = set() - for subset_doc in subset_docs: - if self._doc_fetching_stop: - return - - families = subset_doc.get("data", {}).get("families") - if families: - subset_families.add(families[0]) - - subset_docs_by_id[subset_doc["_id"]] = subset_doc - - subset_ids = list(subset_docs_by_id.keys()) - last_versions_by_subset_id = get_last_versions( - project_name, - subset_ids, - active=True, - fields=["_id", "parent", "name", "type", "data", "schema"] - ) - - hero_versions = get_hero_versions(project_name, subset_ids=subset_ids) - missing_versions = [] - for hero_version in hero_versions: - version_id = hero_version["version_id"] - if version_id not in last_versions_by_subset_id: - missing_versions.append(version_id) - - missing_versions_by_id = {} - if missing_versions: - missing_version_docs = get_versions( - project_name, version_ids=missing_versions - ) - missing_versions_by_id = { - missing_version_doc["_id"]: missing_version_doc - for missing_version_doc in missing_version_docs - } - - for hero_version in hero_versions: - version_id = hero_version["version_id"] - subset_id = hero_version["parent"] - - version_doc = last_versions_by_subset_id.get(subset_id) - if version_doc is None: - version_doc = missing_versions_by_id.get(version_id) - if version_doc is None: - continue - - hero_version["data"] = version_doc["data"] - hero_version["name"] = HeroVersionType(version_doc["name"]) - # Add information if hero version is from latest version - hero_version["is_from_latest"] = version_id == version_doc["_id"] - - last_versions_by_subset_id[subset_id] = hero_version - - # Check loaded subsets - loaded_subset_ids = set() - ids = self._loaded_representation_ids - if ids: - if self._doc_fetching_stop: - return - - # Get subset ids from loaded representations in workfile - # todo: optimize with aggregation query to distinct subset id - representations = get_representations(project_name, - representation_ids=ids, - fields=["parent"]) - version_ids = set(repre["parent"] for repre in representations) - versions = get_versions(project_name, - version_ids=version_ids, - fields=["parent"]) - loaded_subset_ids = set(version["parent"] for version in versions) - - if self._doc_fetching_stop: - return - - repre_info_by_version_id = {} - if self.sync_server_enabled: - versions_by_id = {} - for _subset_id, doc in last_versions_by_subset_id.items(): - versions_by_id[doc["_id"]] = doc - - repres_info = self.sync_server.get_repre_info_for_versions( - project_name, - list(versions_by_id.keys()), - self.active_site, - self.remote_site - ) - for repre_info in repres_info: - if self._doc_fetching_stop: - return - - version_id = repre_info["_id"] - doc = versions_by_id[version_id] - doc["active_provider"] = self.active_provider - doc["remote_provider"] = self.remote_provider - repre_info_by_version_id[version_id] = repre_info - - self._doc_payload = { - "asset_docs_by_id": asset_docs_by_id, - "subset_docs_by_id": subset_docs_by_id, - "subset_families": subset_families, - "last_versions_by_subset_id": last_versions_by_subset_id, - "repre_info_by_version_id": repre_info_by_version_id, - "subsets_loaded_by_id": loaded_subset_ids - } - - self.doc_fetched.emit() - - def fetch_subset_and_version(self): - """Query all subsets and latest versions from aggregation - (NOTE) The returned version documents are NOT the real version - document, it's generated from the MongoDB's aggregation so - some of the first level field may not be presented. - """ - self._doc_payload = {} - self._doc_fetching_stop = False - self._doc_fetching_thread = lib.create_qthread(self._fetch) - self._doc_fetching_thread.start() - - def stop_fetch_thread(self): - if self._doc_fetching_thread is not None: - self._doc_fetching_stop = True - while self._doc_fetching_thread.isRunning(): - pass - - def refresh(self): - self.stop_fetch_thread() - self.clear() - self._items_by_id = {} - self.reset_sync_server() - - if not self._asset_ids: - self.doc_fetched.emit() - return - - # Collect scene container representations to compare loaded state - # This runs in the main thread because it involves the host DCC - if self._host: - time_since_refresh = time.time() - self._host_loaded_refresh_time - if time_since_refresh > self._host_loaded_refresh_timeout: - if isinstance(self._host, ILoadHost): - containers = self._host.get_containers() - else: - containers = self._host.ls() - - repre_ids = {con.get("representation") for con in containers} - self._loaded_representation_ids = repre_ids - self._host_loaded_refresh_time = time.time() - - self.fetch_subset_and_version() - - def _on_doc_fetched(self): - self.clear() - self._items_by_id = {} - self.beginResetModel() - - asset_docs_by_id = self._doc_payload.get( - "asset_docs_by_id" - ) - subset_docs_by_id = self._doc_payload.get( - "subset_docs_by_id" - ) - last_versions_by_subset_id = self._doc_payload.get( - "last_versions_by_subset_id" - ) - - repre_info_by_version_id = self._doc_payload.get( - "repre_info_by_version_id" - ) - - subsets_loaded_by_id = self._doc_payload.get( - "subsets_loaded_by_id" - ) - - if ( - asset_docs_by_id is None - or subset_docs_by_id is None - or last_versions_by_subset_id is None - or len(self._asset_ids) == 0 - ): - self.endResetModel() - self.refreshed.emit(False) - return - - self._fill_subset_items( - asset_docs_by_id, - subset_docs_by_id, - last_versions_by_subset_id, - repre_info_by_version_id, - subsets_loaded_by_id - ) - self.endResetModel() - self.refreshed.emit(True) - - def create_multiasset_group( - self, subset_name, asset_ids, subset_counter, parent_item=None - ): - subset_color = self.merged_subset_colors[ - subset_counter % len(self.merged_subset_colors) - ] - merge_group = Item() - merge_group.update({ - "subset": "{} ({})".format(subset_name, len(asset_ids)), - "isMerged": True, - "subsetColor": subset_color, - "assetIds": list(asset_ids), - "icon": qtawesome.icon( - "fa.circle", - color="#{0:02x}{1:02x}{2:02x}".format(*subset_color) - ) - }) - - self.add_child(merge_group, parent_item) - - return merge_group - - def _fill_subset_items( - self, - asset_docs_by_id, - subset_docs_by_id, - last_versions_by_subset_id, - repre_info_by_version_id, - subsets_loaded_by_id - ): - _groups_tuple = self.groups_config.split_subsets_for_groups( - subset_docs_by_id.values(), self._grouping - ) - groups, subset_docs_without_group, subset_docs_by_group = _groups_tuple - - group_item_by_name = {} - for group_data in groups: - group_name = group_data["name"] - group_item = Item() - group_item.update({ - "subset": group_name, - "isGroup": True - }) - group_item.update(group_data) - - self.add_child(group_item) - - group_item_by_name[group_name] = { - "item": group_item, - "index": self.index(group_item.row(), 0) - } - - def _add_subset_item(subset_doc, parent_item, parent_index): - last_version = last_versions_by_subset_id.get( - subset_doc["_id"] - ) - # do not show subset without version - if not last_version: - return - - data = copy.deepcopy(subset_doc) - data["subset"] = subset_doc["name"] - - asset_id = subset_doc["parent"] - data["asset"] = asset_docs_by_id[asset_id]["name"] - - data["last_version"] = last_version - data["loaded_in_scene"] = subset_doc["_id"] in subsets_loaded_by_id - - # Sync server data - data.update( - self._get_last_repre_info(repre_info_by_version_id, - last_version["_id"])) - - item = Item() - item.update(data) - self.add_child(item, parent_item) - - index = self.index(item.row(), 0, parent_index) - self.set_version(index, last_version) - - subset_counter = 0 - for group_name, subset_docs_by_name in subset_docs_by_group.items(): - parent_item = group_item_by_name[group_name]["item"] - parent_index = group_item_by_name[group_name]["index"] - for subset_name in sorted(subset_docs_by_name.keys()): - subset_docs = subset_docs_by_name[subset_name] - asset_ids = [ - subset_doc["parent"] for subset_doc in subset_docs - ] - if len(subset_docs) > 1: - _parent_item = self.create_multiasset_group( - subset_name, asset_ids, subset_counter, parent_item - ) - _parent_index = self.index( - _parent_item.row(), 0, parent_index - ) - subset_counter += 1 - else: - _parent_item = parent_item - _parent_index = parent_index - - for subset_doc in subset_docs: - _add_subset_item(subset_doc, - parent_item=_parent_item, - parent_index=_parent_index) - - for subset_name in sorted(subset_docs_without_group.keys()): - subset_docs = subset_docs_without_group[subset_name] - asset_ids = [subset_doc["parent"] for subset_doc in subset_docs] - parent_item = None - parent_index = None - if len(subset_docs) > 1: - parent_item = self.create_multiasset_group( - subset_name, asset_ids, subset_counter - ) - parent_index = self.index(parent_item.row(), 0) - subset_counter += 1 - - for subset_doc in subset_docs: - _add_subset_item(subset_doc, - parent_item=parent_item, - parent_index=parent_index) - - def data(self, index, role): - if not index.isValid(): - return - - item = index.internalPointer() - if role == ITEM_ID_ROLE: - return item["id"] - - if role == self.SortDescendingRole: - if item.get("isGroup"): - # Ensure groups be on top when sorting by descending order - prefix = "2" - order = item["order"] - else: - if item.get("isMerged"): - prefix = "1" - else: - prefix = "0" - order = str(super(SubsetsModel, self).data( - index, QtCore.Qt.DisplayRole - )) - return prefix + order - - if role == self.SortAscendingRole: - if item.get("isGroup"): - # Ensure groups be on top when sorting by ascending order - prefix = "0" - order = item["order"] - else: - if item.get("isMerged"): - prefix = "1" - else: - prefix = "2" - order = str(super(SubsetsModel, self).data( - index, QtCore.Qt.DisplayRole - )) - return prefix + order - - if role == QtCore.Qt.DisplayRole: - if index.column() == self.columns_index["family"]: - # Show familyLabel instead of family - return item.get("familyLabel", None) - - elif role == QtCore.Qt.DecorationRole: - - # Add icon to subset column - if index.column() == self.columns_index["subset"]: - if item.get("isGroup") or item.get("isMerged"): - return item["icon"] - else: - return self._icons["subset"] - - # Add icon to family column - if index.column() == self.columns_index["family"]: - return item.get("familyIcon", None) - - elif role == QtCore.Qt.ForegroundRole: - version_doc = item.get("version_document") - if version_doc and version_doc.get("type") == "hero_version": - if not version_doc["is_from_latest"]: - return self.not_last_hero_brush - - elif role == LOCAL_AVAILABILITY_ROLE: - if not item.get("isGroup"): - return item.get("repre_info_local") - else: - return None - - elif role == REMOTE_AVAILABILITY_ROLE: - if not item.get("isGroup"): - return item.get("repre_info_remote") - else: - return None - - elif role == LOCAL_PROVIDER_ROLE: - return self.active_provider - - elif role == REMOTE_PROVIDER_ROLE: - return self.remote_provider - - return super(SubsetsModel, self).data(index, role) - - def flags(self, index): - flags = QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable - - # Make the version column editable - if index.column() == self.columns_index["version"]: - flags |= QtCore.Qt.ItemIsEditable - - return flags - - def headerData(self, section, orientation, role): - """Remap column names to labels""" - if role == QtCore.Qt.DisplayRole: - if section < len(self.Columns): - key = self.Columns[section] - return self.column_labels_mapping.get(key) or key - - super(TreeModel, self).headerData(section, orientation, role) - - def _get_last_repre_info(self, repre_info_by_version_id, last_version_id): - data = {} - if repre_info_by_version_id: - repre_info = repre_info_by_version_id.get(last_version_id) - return self._get_repre_dict(repre_info) - - return data - - def _get_repre_dict(self, repre_info): - """Returns str representation of availability""" - data = {} - if repre_info: - repres_str = "{}/{}".format( - int(math.floor(float(repre_info['avail_repre_local']))), - int(math.floor(float(repre_info['repre_count'])))) - - data["repre_info_local"] = repres_str - - repres_str = "{}/{}".format( - int(math.floor(float(repre_info['avail_repre_remote']))), - int(math.floor(float(repre_info['repre_count'])))) - - data["repre_info_remote"] = repres_str - - return data - - -class GroupMemberFilterProxyModel(QtCore.QSortFilterProxyModel): - """Provide the feature of filtering group by the acceptance of members - - The subset group nodes will not be filtered directly, the group node's - acceptance depends on it's child subsets' acceptance. - - """ - - if is_filtering_recursible(): - def _is_group_acceptable(self, index, node): - # (NOTE) With the help of `RecursiveFiltering` feature from - # Qt 5.10, group always not be accepted by default. - return False - filter_accepts_group = _is_group_acceptable - - else: - # Patch future function - setRecursiveFilteringEnabled = (lambda *args: None) - - def _is_group_acceptable(self, index, model): - # (NOTE) This is not recursive. - for child_row in range(model.rowCount(index)): - if self.filterAcceptsRow(child_row, index): - return True - return False - filter_accepts_group = _is_group_acceptable - - def __init__(self, *args, **kwargs): - super(GroupMemberFilterProxyModel, self).__init__(*args, **kwargs) - self.setRecursiveFilteringEnabled(True) - - -class SubsetFilterProxyModel(GroupMemberFilterProxyModel): - def filterAcceptsRow(self, row, parent): - model = self.sourceModel() - index = model.index(row, self.filterKeyColumn(), parent) - item = index.internalPointer() - if item.get("isGroup"): - return self.filter_accepts_group(index, model) - return super( - SubsetFilterProxyModel, self - ).filterAcceptsRow(row, parent) - - -class FamiliesFilterProxyModel(GroupMemberFilterProxyModel): - """Filters to specified families""" - - def __init__(self, *args, **kwargs): - super(FamiliesFilterProxyModel, self).__init__(*args, **kwargs) - self._families = set() - - def familyFilter(self): - return self._families - - def setFamiliesFilter(self, values): - """Set the families to include""" - assert isinstance(values, (tuple, list, set)) - self._families = set(values) - self.invalidateFilter() - - def filterAcceptsRow(self, row=0, parent=None): - if not self._families: - return False - - model = self.sourceModel() - index = model.index(row, 0, parent=parent or QtCore.QModelIndex()) - - # Ensure index is valid - if not index.isValid() or index is None: - return True - - # Get the item data and validate - item = model.data(index, TreeModel.ItemRole) - - if item.get("isGroup"): - return self.filter_accepts_group(index, model) - - family = item.get("family") - if not family: - return True - - # We want to keep the families which are not in the list - return family in self._families - - def sort(self, column, order): - proxy = self.sourceModel() - model = proxy.sourceModel() - # We need to know the sorting direction for pinning groups on top - if order == QtCore.Qt.AscendingOrder: - self.setSortRole(model.SortAscendingRole) - else: - self.setSortRole(model.SortDescendingRole) - - super(FamiliesFilterProxyModel, self).sort(column, order) - - -class RepresentationSortProxyModel(GroupMemberFilterProxyModel): - """To properly sort progress string""" - def lessThan(self, left, right): - source_model = self.sourceModel() - progress_indexes = [source_model.Columns.index("active_site"), - source_model.Columns.index("remote_site")] - if left.column() in progress_indexes: - left_data = self.sourceModel().data(left, QtCore.Qt.DisplayRole) - right_data = self.sourceModel().data(right, QtCore.Qt.DisplayRole) - left_val = re.sub("[^0-9]", '', left_data) - right_val = re.sub("[^0-9]", '', right_data) - - return int(left_val) < int(right_val) - - return super(RepresentationSortProxyModel, self).lessThan(left, right) - - -class RepresentationModel(TreeModel, BaseRepresentationModel): - doc_fetched = QtCore.Signal() - refreshed = QtCore.Signal(bool) - - SiteNameRole = QtCore.Qt.UserRole + 2 - ProgressRole = QtCore.Qt.UserRole + 3 - SiteSideRole = QtCore.Qt.UserRole + 4 - IdRole = QtCore.Qt.UserRole + 5 - ContextRole = QtCore.Qt.UserRole + 6 - - Columns = [ - "name", - "subset", - "asset", - "active_site", - "remote_site" - ] - - column_labels_mapping = { - "name": "Name", - "subset": "Subset", - "asset": "Asset", - "active_site": "Active", - "remote_site": "Remote" - } - - repre_projection = { - "_id": 1, - "name": 1, - "context.subset": 1, - "context.asset": 1, - "context.version": 1, - "context.representation": 1, - 'files.sites': 1 - } - - def __init__(self, dbcon, header): - super(RepresentationModel, self).__init__() - self.dbcon = dbcon - self._data = [] - self._header = header - self._version_ids = [] - - manager = ModulesManager() - active_site = remote_site = None - active_provider = remote_provider = None - sync_server = manager.modules_by_name.get("sync_server") - sync_server_enabled = ( - sync_server is not None - and sync_server.enabled - ) - - project_name = dbcon.current_project() - if sync_server_enabled and project_name: - active_site = sync_server.get_active_site(project_name) - remote_site = sync_server.get_remote_site(project_name) - - # TODO refactor - active_provider = sync_server.get_provider_for_site( - project_name, active_site - ) - if active_site == 'studio': - active_provider = 'studio' - - remote_provider = sync_server.get_provider_for_site( - project_name, remote_site - ) - - if remote_site == 'studio': - remote_provider = 'studio' - - self.sync_server = sync_server - self.sync_server_enabled = sync_server_enabled - self.active_site = active_site - self.active_provider = active_provider - self.remote_site = remote_site - self.remote_provider = remote_provider - - self.doc_fetched.connect(self._on_doc_fetched) - - self._docs = {} - self._icons = lib.get_repre_icons() - self._icons["repre"] = qtawesome.icon( - "fa.file-o", - color=get_default_entity_icon_color() - ) - self._items_by_id = {} - - def set_version_ids(self, version_ids): - self._version_ids = version_ids - self.refresh() - - def data(self, index, role): - item = index.internalPointer() - - if role == ITEM_ID_ROLE: - return item["id"] - - if role == self.IdRole: - return item.get("_id") - - if role == QtCore.Qt.DecorationRole: - # Add icon to subset column - if index.column() == self.Columns.index("name"): - if item.get("isMerged"): - return item["icon"] - return self._icons["repre"] - - active_index = self.Columns.index("active_site") - remote_index = self.Columns.index("remote_site") - if role == QtCore.Qt.DisplayRole: - progress = None - label = '' - if index.column() == active_index: - progress = item.get("active_site_progress", 0) - elif index.column() == remote_index: - progress = item.get("remote_site_progress", 0) - - if progress is not None: - # site added, sync in progress - progress_str = "not avail." - if progress >= 0: - if progress == 0 and item.get("isMerged"): - progress_str = "not avail." - else: - progress_str = "{}% {}".format( - int(progress * 100), label - ) - - return progress_str - - if role == QtCore.Qt.DecorationRole: - if index.column() == active_index: - return item.get("active_site_icon", None) - if index.column() == remote_index: - return item.get("remote_site_icon", None) - - if role == self.SiteNameRole: - if index.column() == active_index: - return item.get("active_site_name", None) - if index.column() == remote_index: - return item.get("remote_site_name", None) - - if role == self.SiteSideRole: - if index.column() == active_index: - return "active" - if index.column() == remote_index: - return "remote" - - if role == self.ProgressRole: - if index.column() == active_index: - return item.get("active_site_progress", 0) - if index.column() == remote_index: - return item.get("remote_site_progress", 0) - - return super(RepresentationModel, self).data(index, role) - - def _on_doc_fetched(self): - self.clear() - self.beginResetModel() - subsets = set() - assets = set() - repre_groups = {} - repre_groups_items = {} - group = None - self._items_by_id = {} - for doc in self._docs: - if len(self._version_ids) > 1: - group = repre_groups.get(doc["name"]) - if not group: - group_item = Item() - item_id = str(uuid4()) - group_item.update({ - "id": item_id, - "_id": doc["_id"], - "name": doc["name"], - "isMerged": True, - "active_site_name": self.active_site, - "remote_site_name": self.remote_site, - "icon": qtawesome.icon( - "fa.folder", - color=get_default_entity_icon_color() - ) - }) - self._items_by_id[item_id] = group_item - self.add_child(group_item, None) - repre_groups[doc["name"]] = group_item - repre_groups_items[doc["name"]] = 0 - group = group_item - - progress = { - self.active_site: 0, - self.remote_site: 0, - } - if self.sync_server_enabled: - progress = self.sync_server.get_progress_for_repre( - doc, - self.active_site, - self.remote_site) - - active_site_icon = self._icons.get(self.active_provider) - remote_site_icon = self._icons.get(self.remote_provider) - - item_id = str(uuid4()) - data = { - "id": item_id, - "_id": doc["_id"], - "name": doc["name"], - "subset": doc["context"]["subset"], - "asset": doc["context"]["asset"], - "isMerged": False, - - "active_site_icon": active_site_icon, - "remote_site_icon": remote_site_icon, - "active_site_name": self.active_site, - "remote_site_name": self.remote_site, - "active_site_progress": progress[self.active_site], - "remote_site_progress": progress[self.remote_site] - } - subsets.add(doc["context"]["subset"]) - assets.add(doc["context"]["subset"]) - - item = Item() - item.update(data) - self._items_by_id[item_id] = item - - current_progress = { - 'active_site_progress': progress[self.active_site], - 'remote_site_progress': progress[self.remote_site] - } - if group: - group = self._sum_group_progress( - doc["name"], group, current_progress, repre_groups_items - ) - - self.add_child(item, group) - - # finalize group average progress - for group_name, group in repre_groups.items(): - items_cnt = repre_groups_items[group_name] - active_progress = group.get("active_site_progress", 0) - group["active_site_progress"] = active_progress / items_cnt - remote_progress = group.get("remote_site_progress", 0) - group["remote_site_progress"] = remote_progress / items_cnt - - self.endResetModel() - self.refreshed.emit(False) - - def get_item_by_id(self, item_id): - return self._items_by_id.get(item_id) - - def refresh(self): - project_name = self.dbcon.current_project() - if not project_name: - return - - repre_docs = [] - if self._version_ids: - # Simple find here for now, expected to receive lower number of - # representations and logic could be in Python - repre_docs = list(get_representations( - project_name, - version_ids=self._version_ids, - fields=self.repre_projection.keys() - )) - - self._docs = repre_docs - - self.doc_fetched.emit() - - def _sum_group_progress( - self, repre_name, group, current_item_progress, repre_groups_items - ): - """Update final group progress - - Called after every item in group is added - - Args: - repre_name(string) - group(dict): info about group of selected items - current_item_progress(dict): {'active_site_progress': XX, - 'remote_site_progress': YY} - repre_groups_items(dict) - Returns: - (dict): updated group info - """ - repre_groups_items[repre_name] += 1 - - for key, progress in current_item_progress.items(): - group[key] = (group.get(key, 0) + max(progress, 0)) - - return group diff --git a/client/ayon_core/tools/ayon_loader/models/__init__.py b/client/ayon_core/tools/loader/models/__init__.py similarity index 100% rename from client/ayon_core/tools/ayon_loader/models/__init__.py rename to client/ayon_core/tools/loader/models/__init__.py diff --git a/client/ayon_core/tools/ayon_loader/models/actions.py b/client/ayon_core/tools/loader/models/actions.py similarity index 99% rename from client/ayon_core/tools/ayon_loader/models/actions.py rename to client/ayon_core/tools/loader/models/actions.py index 9b51a2e4e8..8d6234f978 100644 --- a/client/ayon_core/tools/ayon_loader/models/actions.py +++ b/client/ayon_core/tools/loader/models/actions.py @@ -24,7 +24,7 @@ from ayon_core.pipeline.load import ( IncompatibleLoaderError, ) from ayon_core.tools.ayon_utils.models import NestedCacheItem -from ayon_core.tools.ayon_loader.abstract import ActionItem +from ayon_core.tools.loader.abstract import ActionItem ACTIONS_MODEL_SENDER = "actions.model" NOT_SET = object() diff --git a/client/ayon_core/tools/ayon_loader/models/products.py b/client/ayon_core/tools/loader/models/products.py similarity index 99% rename from client/ayon_core/tools/ayon_loader/models/products.py rename to client/ayon_core/tools/loader/models/products.py index 90792d0623..63547bef8b 100644 --- a/client/ayon_core/tools/ayon_loader/models/products.py +++ b/client/ayon_core/tools/loader/models/products.py @@ -7,7 +7,7 @@ from ayon_api.operations import OperationsSession from ayon_core.style import get_default_entity_icon_color from ayon_core.tools.ayon_utils.models import NestedCacheItem -from ayon_core.tools.ayon_loader.abstract import ( +from ayon_core.tools.loader.abstract import ( ProductTypeItem, ProductItem, VersionItem, diff --git a/client/ayon_core/tools/ayon_loader/models/selection.py b/client/ayon_core/tools/loader/models/selection.py similarity index 100% rename from client/ayon_core/tools/ayon_loader/models/selection.py rename to client/ayon_core/tools/loader/models/selection.py diff --git a/client/ayon_core/tools/ayon_loader/models/site_sync.py b/client/ayon_core/tools/loader/models/site_sync.py similarity index 99% rename from client/ayon_core/tools/ayon_loader/models/site_sync.py rename to client/ayon_core/tools/loader/models/site_sync.py index 2acea9645c..9e9fdc5313 100644 --- a/client/ayon_core/tools/ayon_loader/models/site_sync.py +++ b/client/ayon_core/tools/loader/models/site_sync.py @@ -5,7 +5,7 @@ from ayon_core.client.entities import get_representations from ayon_core.client import get_linked_representation_id from ayon_core.modules import ModulesManager from ayon_core.tools.ayon_utils.models import NestedCacheItem -from ayon_core.tools.ayon_loader.abstract import ActionItem +from ayon_core.tools.loader.abstract import ActionItem DOWNLOAD_IDENTIFIER = "sitesync.download" UPLOAD_IDENTIFIER = "sitesync.upload" diff --git a/client/ayon_core/tools/ayon_loader/ui/__init__.py b/client/ayon_core/tools/loader/ui/__init__.py similarity index 100% rename from client/ayon_core/tools/ayon_loader/ui/__init__.py rename to client/ayon_core/tools/loader/ui/__init__.py diff --git a/client/ayon_core/tools/ayon_loader/ui/actions_utils.py b/client/ayon_core/tools/loader/ui/actions_utils.py similarity index 100% rename from client/ayon_core/tools/ayon_loader/ui/actions_utils.py rename to client/ayon_core/tools/loader/ui/actions_utils.py diff --git a/client/ayon_core/tools/ayon_loader/ui/folders_widget.py b/client/ayon_core/tools/loader/ui/folders_widget.py similarity index 100% rename from client/ayon_core/tools/ayon_loader/ui/folders_widget.py rename to client/ayon_core/tools/loader/ui/folders_widget.py diff --git a/client/ayon_core/tools/ayon_loader/ui/info_widget.py b/client/ayon_core/tools/loader/ui/info_widget.py similarity index 100% rename from client/ayon_core/tools/ayon_loader/ui/info_widget.py rename to client/ayon_core/tools/loader/ui/info_widget.py diff --git a/client/ayon_core/tools/ayon_loader/ui/product_group_dialog.py b/client/ayon_core/tools/loader/ui/product_group_dialog.py similarity index 100% rename from client/ayon_core/tools/ayon_loader/ui/product_group_dialog.py rename to client/ayon_core/tools/loader/ui/product_group_dialog.py diff --git a/client/ayon_core/tools/ayon_loader/ui/product_types_widget.py b/client/ayon_core/tools/loader/ui/product_types_widget.py similarity index 100% rename from client/ayon_core/tools/ayon_loader/ui/product_types_widget.py rename to client/ayon_core/tools/loader/ui/product_types_widget.py diff --git a/client/ayon_core/tools/ayon_loader/ui/products_delegates.py b/client/ayon_core/tools/loader/ui/products_delegates.py similarity index 100% rename from client/ayon_core/tools/ayon_loader/ui/products_delegates.py rename to client/ayon_core/tools/loader/ui/products_delegates.py diff --git a/client/ayon_core/tools/ayon_loader/ui/products_model.py b/client/ayon_core/tools/loader/ui/products_model.py similarity index 100% rename from client/ayon_core/tools/ayon_loader/ui/products_model.py rename to client/ayon_core/tools/loader/ui/products_model.py diff --git a/client/ayon_core/tools/ayon_loader/ui/products_widget.py b/client/ayon_core/tools/loader/ui/products_widget.py similarity index 100% rename from client/ayon_core/tools/ayon_loader/ui/products_widget.py rename to client/ayon_core/tools/loader/ui/products_widget.py diff --git a/client/ayon_core/tools/ayon_loader/ui/repres_widget.py b/client/ayon_core/tools/loader/ui/repres_widget.py similarity index 100% rename from client/ayon_core/tools/ayon_loader/ui/repres_widget.py rename to client/ayon_core/tools/loader/ui/repres_widget.py diff --git a/client/ayon_core/tools/ayon_loader/ui/window.py b/client/ayon_core/tools/loader/ui/window.py similarity index 99% rename from client/ayon_core/tools/ayon_loader/ui/window.py rename to client/ayon_core/tools/loader/ui/window.py index 357aabb2bb..8f1c1ceb72 100644 --- a/client/ayon_core/tools/ayon_loader/ui/window.py +++ b/client/ayon_core/tools/loader/ui/window.py @@ -11,7 +11,7 @@ from ayon_core.tools.utils import ( ) from ayon_core.tools.utils.lib import center_window from ayon_core.tools.ayon_utils.widgets import ProjectsCombobox -from ayon_core.tools.ayon_loader.control import LoaderController +from ayon_core.tools.loader.control import LoaderController from .folders_widget import LoaderFoldersWidget from .products_widget import ProductsWidget diff --git a/client/ayon_core/tools/loader/widgets.py b/client/ayon_core/tools/loader/widgets.py deleted file mode 100644 index 071aeeea35..0000000000 --- a/client/ayon_core/tools/loader/widgets.py +++ /dev/null @@ -1,1750 +0,0 @@ -import os -import sys -import datetime -import pprint -import traceback -import collections - -from qtpy import QtWidgets, QtCore, QtGui - -from ayon_core.client import ( - get_subset_families, - get_subset_by_id, - get_subsets, - get_version_by_id, - get_versions, - get_representations, - get_thumbnail_id_from_source, - get_thumbnail, -) -from ayon_core.client.operations import OperationsSession, REMOVED_VALUE -from ayon_core.pipeline import HeroVersionType, Anatomy -from ayon_core.pipeline.thumbnail import get_thumbnail_binary -from ayon_core.pipeline.load import ( - discover_loader_plugins, - SubsetLoaderPlugin, - loaders_from_repre_context, - get_repres_contexts, - get_subset_contexts, - load_with_repre_context, - load_with_subset_context, - load_with_subset_contexts, - LoadError, - IncompatibleLoaderError, -) -from ayon_core.tools.utils import ( - ErrorMessageBox, - lib as tools_lib -) -from ayon_core.tools.utils.lib import checkstate_int_to_enum -from ayon_core.tools.utils.delegates import ( - VersionDelegate, - PrettyTimeDelegate -) -from ayon_core.tools.utils.widgets import ( - OptionalMenu, - PlaceholderLineEdit -) -from ayon_core.tools.utils.views import ( - TreeViewSpinner, - DeselectableTreeView -) -from ayon_core.tools.utils.constants import ( - LOCAL_PROVIDER_ROLE, - REMOTE_PROVIDER_ROLE, - LOCAL_AVAILABILITY_ROLE, - REMOTE_AVAILABILITY_ROLE, -) -from ayon_core.tools.assetlinks.widgets import SimpleLinkView - -from .model import ( - SubsetsModel, - SubsetFilterProxyModel, - FamiliesFilterProxyModel, - RepresentationModel, - RepresentationSortProxyModel, - ITEM_ID_ROLE -) -from . import lib -from .delegates import LoadedInSceneDelegate - - -class OverlayFrame(QtWidgets.QFrame): - def __init__(self, label, parent): - super(OverlayFrame, self).__init__(parent) - - label_widget = QtWidgets.QLabel(label, self) - label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) - - main_layout = QtWidgets.QVBoxLayout(self) - main_layout.addWidget(label_widget, 1, QtCore.Qt.AlignCenter) - - self.label_widget = label_widget - - self.setStyleSheet(( - "background: rgba(0, 0, 0, 127);" - "font-size: 60pt;" - )) - - def set_label(self, label): - self.label_widget.setText(label) - - -class LoadErrorMessageBox(ErrorMessageBox): - def __init__(self, messages, parent=None): - self._messages = messages - super(LoadErrorMessageBox, self).__init__("Loading failed", parent) - - def _create_top_widget(self, parent_widget): - label_widget = QtWidgets.QLabel(parent_widget) - label_widget.setText( - "Failed to load items" - ) - return label_widget - - def _get_report_data(self): - report_data = [] - for exc_msg, tb_text, repre, subset, version in self._messages: - report_message = ( - "During load error happened on Subset: \"{subset}\"" - " Representation: \"{repre}\" Version: {version}" - "\n\nError message: {message}" - ).format( - subset=subset, - repre=repre, - version=version, - message=exc_msg - ) - if tb_text: - report_message += "\n\n{}".format(tb_text) - report_data.append(report_message) - return report_data - - def _create_content(self, content_layout): - item_name_template = ( - "Subset: {}
" - "Version: {}
" - "Representation: {}
" - ) - exc_msg_template = "{}" - - for exc_msg, tb_text, repre, subset, version in self._messages: - line = self._create_line() - content_layout.addWidget(line) - - item_name = item_name_template.format(subset, version, repre) - item_name_widget = QtWidgets.QLabel( - item_name.replace("\n", "
"), self - ) - item_name_widget.setWordWrap(True) - content_layout.addWidget(item_name_widget) - - exc_msg = exc_msg_template.format(exc_msg.replace("\n", "
")) - message_label_widget = QtWidgets.QLabel(exc_msg, self) - message_label_widget.setWordWrap(True) - content_layout.addWidget(message_label_widget) - - if tb_text: - line = self._create_line() - tb_widget = self._create_traceback_widget(tb_text, self) - content_layout.addWidget(line) - content_layout.addWidget(tb_widget) - - -class SubsetWidget(QtWidgets.QWidget): - """A widget that lists the published subsets for an asset""" - - active_changed = QtCore.Signal() # active index changed - version_changed = QtCore.Signal() # version state changed for a subset - load_started = QtCore.Signal() - load_ended = QtCore.Signal() - refreshed = QtCore.Signal(bool) - - default_widths = ( - ("subset", 200), - ("asset", 130), - ("family", 90), - ("version", 60), - ("time", 125), - ("author", 75), - ("frames", 75), - ("duration", 60), - ("handles", 55), - ("step", 10), - ("loaded_in_scene", 25), - ("repre_info", 65) - ) - - def __init__( - self, - dbcon, - groups_config, - family_config_cache, - enable_grouping=True, - tool_name=None, - parent=None - ): - super(SubsetWidget, self).__init__(parent=parent) - - self.dbcon = dbcon - self.tool_name = tool_name - - model = SubsetsModel( - dbcon, - groups_config, - family_config_cache, - grouping=enable_grouping - ) - proxy = SubsetFilterProxyModel() - proxy.setSourceModel(model) - proxy.setDynamicSortFilter(True) - proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) - - family_proxy = FamiliesFilterProxyModel() - family_proxy.setSourceModel(proxy) - - subset_filter = PlaceholderLineEdit(self) - subset_filter.setPlaceholderText("Filter subsets..") - - group_checkbox = QtWidgets.QCheckBox("Enable Grouping", self) - group_checkbox.setChecked(enable_grouping) - - top_bar_layout = QtWidgets.QHBoxLayout() - top_bar_layout.addWidget(subset_filter) - top_bar_layout.addWidget(group_checkbox) - - view = TreeViewSpinner(self) - view.setModel(family_proxy) - view.setObjectName("SubsetView") - view.setIndentation(20) - view.setAllColumnsShowFocus(True) - view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - view.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) - view.setSortingEnabled(True) - view.sortByColumn(1, QtCore.Qt.AscendingOrder) - view.setAlternatingRowColors(True) - - # Set view delegates - version_delegate = VersionDelegate(self.dbcon, view) - column = model.Columns.index("version") - view.setItemDelegateForColumn(column, version_delegate) - - time_delegate = PrettyTimeDelegate(view) - column = model.Columns.index("time") - view.setItemDelegateForColumn(column, time_delegate) - - avail_delegate = AvailabilityDelegate(self.dbcon, view) - column = model.Columns.index("repre_info") - view.setItemDelegateForColumn(column, avail_delegate) - - loaded_in_scene_delegate = LoadedInSceneDelegate(view) - column = model.Columns.index("loaded_in_scene") - view.setItemDelegateForColumn(column, loaded_in_scene_delegate) - - layout = QtWidgets.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.addLayout(top_bar_layout) - layout.addWidget(view) - - # settings and connections - for column_name, width in self.default_widths: - idx = model.Columns.index(column_name) - view.setColumnWidth(idx, width) - - self.model = model - self.view = view - - self.on_project_change(dbcon.current_project()) - - view.customContextMenuRequested.connect(self.on_context_menu) - - selection = view.selectionModel() - selection.selectionChanged.connect(self.active_changed) - - version_delegate.version_changed.connect(self.version_changed) - - group_checkbox.stateChanged.connect(self.set_grouping) - - subset_filter.textChanged.connect(self._subset_changed) - - model.refreshed.connect(self.refreshed) - - self.proxy = proxy - self.family_proxy = family_proxy - - self._subset_filter = subset_filter - self._group_checkbox = group_checkbox - - self._version_delegate = version_delegate - self._time_delegate = time_delegate - - self.model.refresh() - - def get_subsets_families(self): - return self.model.get_subsets_families() - - def set_family_filters(self, families): - self.family_proxy.setFamiliesFilter(families) - - def is_groupable(self): - return self._group_checkbox.isChecked() - - def set_grouping(self, state): - with tools_lib.preserve_selection(tree_view=self.view, - current_index=False): - self.model.set_grouping(state) - - def _subset_changed(self, text): - if hasattr(self.proxy, "setFilterRegExp"): - self.proxy.setFilterRegExp(text) - else: - self.proxy.setFilterRegularExpression(text) - self.view.expandAll() - - def set_loading_state(self, loading, empty): - view = self.view - - if view.is_loading != loading: - if loading: - view.spinner.repaintNeeded.connect(view.viewport().update) - else: - view.spinner.repaintNeeded.disconnect() - - view.is_loading = loading - view.is_empty = empty - - def _repre_contexts_for_loaders_filter(self, items): - version_docs_by_id = { - item["version_document"]["_id"]: item["version_document"] - for item in items - } - version_docs_by_subset_id = collections.defaultdict(list) - for item in items: - subset_id = item["version_document"]["parent"] - version_docs_by_subset_id[subset_id].append( - item["version_document"] - ) - - project_name = self.dbcon.active_project() - subset_docs = list(get_subsets( - project_name, - subset_ids=version_docs_by_subset_id.keys(), - fields=["schema", "data.families"] - )) - subset_docs_by_id = { - subset_doc["_id"]: subset_doc - for subset_doc in subset_docs - } - version_ids = list(version_docs_by_id.keys()) - repre_docs = get_representations( - project_name, - version_ids=version_ids, - fields=["name", "parent", "data", "context"] - ) - - repre_docs_by_version_id = { - version_id: [] - for version_id in version_ids - } - repre_context_by_id = {} - for repre_doc in repre_docs: - version_id = repre_doc["parent"] - repre_docs_by_version_id[version_id].append(repre_doc) - - version_doc = version_docs_by_id[version_id] - repre_context_by_id[repre_doc["_id"]] = { - "representation": repre_doc, - "version": version_doc, - "subset": subset_docs_by_id[version_doc["parent"]] - } - return repre_context_by_id, repre_docs_by_version_id - - def on_project_change(self, project_name): - """ - Called on each project change in parent widget. - - Checks if Sync Server is enabled for a project, pushes changes to - model. - """ - enabled = False - if project_name: - self.model.reset_sync_server(project_name) - sync_server = self.model.sync_server - if sync_server: - enabled = sync_server.is_project_enabled(project_name, - single=True) - - lib.change_visibility(self.model, self.view, "repre_info", enabled) - - def get_selected_items(self): - selection_model = self.view.selectionModel() - indexes = selection_model.selectedIndexes() - - item_ids = set() - for index in indexes: - item_id = index.data(ITEM_ID_ROLE) - if item_id is not None: - item_ids.add(item_id) - - output = [] - for item_id in item_ids: - item = self.model.get_item_by_id(item_id) - if item is not None: - output.append(item) - return output - - def get_selected_merge_items(self): - output = [] - items = collections.deque(self.get_selected_items()) - - item_ids = set() - while items: - item = items.popleft() - if item.get("isGroup"): - for child in item.children(): - items.appendleft(child) - - elif item.get("isMerged"): - item_id = item["id"] - if item_id not in item_ids: - item_ids.add(item_id) - output.append(item) - - return output - - def get_selected_subsets(self): - output = [] - items = collections.deque(self.get_selected_items()) - - item_ids = set() - while items: - item = items.popleft() - if item.get("isGroup") or item.get("isMerged"): - for child in item.children(): - items.appendleft(child) - else: - item_id = item["id"] - if item_id not in item_ids: - item_ids.add(item_id) - output.append(item) - return output - - def on_context_menu(self, point): - """Shows menu with loader actions on Right-click. - - Registered actions are filtered by selection and help of - `loaders_from_representation` from avalon api. Intersection of actions - is shown when more subset is selected. When there are not available - actions for selected subsets then special action is shown (works as - info message to user): "*No compatible loaders for your selection" - - """ - - point_index = self.view.indexAt(point) - if not point_index.isValid(): - return - - # Get selected subsets without groups - items = self.get_selected_subsets() - - # Get all representation->loader combinations available for the - # index under the cursor, so we can list the user the options. - project_name = self.dbcon.active_project() - available_loaders = discover_loader_plugins(project_name) - if self.tool_name: - available_loaders = lib.remove_tool_name_from_loaders( - available_loaders, self.tool_name - ) - - repre_loaders = [] - subset_loaders = [] - for loader in available_loaders: - if not loader.enabled: - continue - # Skip if its a SubsetLoader. - if issubclass(loader, SubsetLoaderPlugin): - subset_loaders.append(loader) - else: - repre_loaders.append(loader) - - loaders = list() - - # Bool if is selected only one subset - one_item_selected = (len(items) == 1) - - # Prepare variables for multiple selected subsets - first_loaders = [] - found_combinations = None - - is_first = True - repre_context_by_id, repre_docs_by_version_id = ( - self._repre_contexts_for_loaders_filter(items) - ) - for item in items: - _found_combinations = [] - version_id = item["version_document"]["_id"] - repre_docs = repre_docs_by_version_id[version_id] - for repre_doc in repre_docs: - repre_context = repre_context_by_id[repre_doc["_id"]] - for loader in loaders_from_repre_context( - repre_loaders, - repre_context - ): - # do not allow download whole repre, select specific repre - if tools_lib.is_sync_loader(loader): - continue - - # skip multiple select variant if one is selected - if one_item_selected: - loaders.append((repre_doc, loader)) - continue - - # store loaders of first subset - if is_first: - first_loaders.append((repre_doc, loader)) - - # store combinations to compare with other subsets - _found_combinations.append( - (repre_doc["name"].lower(), loader) - ) - - # skip multiple select variant if one is selected - if one_item_selected: - continue - - is_first = False - # Store first combinations to compare - if found_combinations is None: - found_combinations = _found_combinations - # Intersect found combinations with all previous subsets - else: - found_combinations = list( - set(found_combinations) & set(_found_combinations) - ) - - if not one_item_selected: - # Filter loaders from first subset by intersected combinations - for repre, loader in first_loaders: - if (repre["name"].lower(), loader) not in found_combinations: - continue - - loaders.append((repre, loader)) - - # Subset Loaders. - for loader in subset_loaders: - loaders.append((None, loader)) - - loaders = lib.sort_loaders(loaders) - - # Prepare menu content based on selected items - menu = OptionalMenu(self) - if not loaders: - action = lib.get_no_loader_action(menu, one_item_selected) - menu.addAction(action) - else: - repre_contexts = get_repres_contexts( - repre_context_by_id.keys(), self.dbcon) - - menu = lib.add_representation_loaders_to_menu( - loaders, menu, repre_contexts) - - # Show the context action menu - global_point = self.view.mapToGlobal(point) - action = menu.exec_(global_point) - if not action or not action.data(): - return - - # Find the representation name and loader to trigger - action_representation, loader = action.data() - - self.load_started.emit() - - if issubclass(loader, SubsetLoaderPlugin): - subset_ids = [] - subset_version_docs = {} - for item in items: - subset_id = item["version_document"]["parent"] - subset_ids.append(subset_id) - subset_version_docs[subset_id] = item["version_document"] - - # get contexts only for selected menu option - subset_contexts_by_id = get_subset_contexts(subset_ids, self.dbcon) - subset_contexts = list(subset_contexts_by_id.values()) - options = lib.get_options(action, loader, self, subset_contexts) - - error_info = _load_subsets_by_loader( - loader, subset_contexts, options, subset_version_docs - ) - - else: - representation_name = action_representation["name"] - - # Run the loader for all selected indices, for those that have the - # same representation available - - # Trigger - project_name = self.dbcon.active_project() - subset_name_by_version_id = dict() - for item in items: - version_id = item["version_document"]["_id"] - subset_name_by_version_id[version_id] = item["subset"] - - version_ids = set(subset_name_by_version_id.keys()) - repre_docs = get_representations( - project_name, - representation_names=[representation_name], - version_ids=version_ids, - fields=["_id", "parent"] - ) - - repre_ids = [] - for repre_doc in repre_docs: - repre_ids.append(repre_doc["_id"]) - - # keep only version ids without representation with that name - version_id = repre_doc["parent"] - version_ids.discard(version_id) - - if version_ids: - # report versions that didn't have valid representation - joined_subset_names = ", ".join([ - '"{}"'.format(subset_name_by_version_id[version_id]) - for version_id in version_ids - ]) - self.echo("Subsets {} don't have representation '{}'".format( - joined_subset_names, representation_name - )) - - # get contexts only for selected menu option - repre_contexts = get_repres_contexts(repre_ids, self.dbcon) - options = lib.get_options( - action, loader, self, list(repre_contexts.values()) - ) - error_info = _load_representations_by_loader( - loader, repre_contexts, options=options - ) - - self.load_ended.emit() - - if error_info: - box = LoadErrorMessageBox(error_info, self) - box.show() - - def group_subsets(self, name, asset_ids, items): - subset_ids = { - item["_id"] - for item in items - if item.get("_id") - } - if not subset_ids: - return - - if name: - self.echo("Group subsets to '%s'.." % name) - else: - self.echo("Ungroup subsets..") - - project_name = self.dbcon.active_project() - op_session = OperationsSession() - for subset_id in subset_ids: - op_session.update_entity( - project_name, - "subset", - subset_id, - {"data.subsetGroup": name or REMOVED_VALUE} - ) - - op_session.commit() - - def echo(self, message): - print(message) - - -class VersionTextEdit(QtWidgets.QTextEdit): - """QTextEdit that displays version specific information. - - This also overrides the context menu to add actions like copying - source path to clipboard or copying the raw data of the version - to clipboard. - - """ - def __init__(self, dbcon, parent=None): - super(VersionTextEdit, self).__init__(parent=parent) - self.dbcon = dbcon - - self.data = { - "source": None, - "raw": None - } - self._anatomy = None - - # Reset - self.set_version(None) - - def set_version(self, version_doc=None, version_id=None): - # TODO expect only filling data (do not query them here!) - if not version_doc and not version_id: - # Reset state to empty - self.data = { - "source": None, - "raw": None, - } - self.setText("") - self.setEnabled(True) - return - - self.setEnabled(True) - - print("Querying..") - - project_name = self.dbcon.active_project() - if not version_doc: - version_doc = get_version_by_id(project_name, version_id) - assert version_doc, "Not a valid version id" - - if version_doc["type"] == "hero_version": - _version_doc = get_version_by_id( - project_name, version_doc["version_id"] - ) - version_doc["data"] = _version_doc["data"] - version_doc["name"] = HeroVersionType( - _version_doc["name"] - ) - - subset = get_subset_by_id(project_name, version_doc["parent"]) - assert subset, "No valid subset parent for version" - - # Define readable creation timestamp - created = version_doc["data"]["time"] - created = datetime.datetime.strptime(created, "%Y%m%dT%H%M%SZ") - created = datetime.datetime.strftime(created, "%b %d %Y %H:%M") - - comment = version_doc["data"].get("comment", None) or "No comment" - - source = version_doc["data"].get("source", None) - source_label = source if source else "No source" - - # Store source and raw data - self.data["source"] = source - self.data["raw"] = version_doc - - if version_doc["type"] == "hero_version": - version_name = "hero" - else: - version_name = tools_lib.format_version(version_doc["name"]) - - data = { - "subset": subset["name"], - "version": version_name, - "comment": comment, - "created": created, - "source": source_label - } - - self.setHtml(( - "

{subset}

" - "

{version}

" - "Comment
" - "{comment}

" - - "Created
" - "{created}

" - - "Source
" - "{source}" - ).format(**data)) - - def contextMenuEvent(self, event): - """Context menu with additional actions""" - menu = self.createStandardContextMenu() - - # Add additional actions when any text so we can assume - # the version is set. - if self.toPlainText().strip(): - menu.addSeparator() - action = QtWidgets.QAction( - "Copy source path to clipboard", menu - ) - action.triggered.connect(self.on_copy_source) - menu.addAction(action) - - action = QtWidgets.QAction( - "Copy raw data to clipboard", menu - ) - action.triggered.connect(self.on_copy_raw) - menu.addAction(action) - - menu.exec_(event.globalPos()) - - def on_copy_source(self): - """Copy formatted source path to clipboard""" - source = self.data.get("source", None) - if not source: - return - - project_name = self.dbcon.current_project() - if self._anatomy is None or self._anatomy.project_name != project_name: - self._anatomy = Anatomy(project_name) - - path = source.format(root=self._anatomy.roots) - clipboard = QtWidgets.QApplication.clipboard() - clipboard.setText(path) - - def on_copy_raw(self): - """Copy raw version data to clipboard - - The data is string formatted with `pprint.pformat`. - - """ - raw = self.data.get("raw", None) - if not raw: - return - - raw_text = pprint.pformat(raw) - clipboard = QtWidgets.QApplication.clipboard() - clipboard.setText(raw_text) - - -class ThumbnailWidget(QtWidgets.QLabel): - aspect_ratio = (16, 9) - max_width = 300 - - def __init__(self, dbcon, parent=None): - super(ThumbnailWidget, self).__init__(parent) - self.dbcon = dbcon - - self.current_thumb_id = None - self.current_thumbnail = None - - self.setAlignment(QtCore.Qt.AlignCenter) - - # TODO get res path much better way - default_pix_path = os.path.join( - os.path.dirname(os.path.abspath(__file__)), - "images", - "default_thumbnail.png" - ) - self.default_pix = QtGui.QPixmap(default_pix_path) - self.set_pixmap() - - def height(self): - width = self.width() - asp_w, asp_h = self.aspect_ratio - - return (width / asp_w) * asp_h - - def width(self): - width = super(ThumbnailWidget, self).width() - if width > self.max_width: - width = self.max_width - return width - - def set_pixmap(self, pixmap=None): - if not pixmap: - pixmap = self.default_pix - self.current_thumb_id = None - - self.current_thumbnail = pixmap - - pixmap = self.scale_pixmap(pixmap) - self.setPixmap(pixmap) - - def resizeEvent(self, _event): - if not self.current_thumbnail: - return - cur_pix = self.scale_pixmap(self.current_thumbnail) - self.setPixmap(cur_pix) - - def scale_pixmap(self, pixmap): - return pixmap.scaled( - self.width(), - self.height(), - QtCore.Qt.KeepAspectRatio, - QtCore.Qt.SmoothTransformation - ) - - def set_thumbnail(self, src_type, doc_ids): - if not doc_ids: - self.set_pixmap() - return - - src_id = doc_ids[0] - - project_name = self.dbcon.active_project() - thumbnail_id = get_thumbnail_id_from_source( - project_name, - src_type, - src_id, - ) - if thumbnail_id == self.current_thumb_id: - if self.current_thumbnail is None: - self.set_pixmap() - return - - self.current_thumb_id = thumbnail_id - if not thumbnail_id: - self.set_pixmap() - return - - thumbnail_ent = get_thumbnail( - project_name, thumbnail_id, src_type, src_id - ) - if not thumbnail_ent: - return - - thumbnail_bin = get_thumbnail_binary( - thumbnail_ent, "thumbnail", self.dbcon - ) - if not thumbnail_bin: - self.set_pixmap() - return - - thumbnail = QtGui.QPixmap() - thumbnail.loadFromData(thumbnail_bin) - - self.set_pixmap(thumbnail) - - -class VersionWidget(QtWidgets.QWidget): - """A Widget that display information about a specific version""" - def __init__(self, dbcon, parent=None): - super(VersionWidget, self).__init__(parent=parent) - - data = VersionTextEdit(dbcon, self) - data.setReadOnly(True) - - depend_widget = SimpleLinkView(dbcon, self) - - tab = QtWidgets.QTabWidget() - tab.addTab(data, "Version Info") - tab.addTab(depend_widget, "Dependency") - - layout = QtWidgets.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(tab) - - self.data = data - self.depend_widget = depend_widget - - def set_version(self, version_doc): - self.data.set_version(version_doc) - self.depend_widget.set_version(version_doc) - - -class FamilyModel(QtGui.QStandardItemModel): - def __init__(self, dbcon, family_config_cache): - super(FamilyModel, self).__init__() - - self.dbcon = dbcon - self.family_config_cache = family_config_cache - - self._items_by_family = {} - - def refresh(self): - families = set() - project_name = self.dbcon.current_project() - if project_name: - families = get_subset_families(project_name) - - root_item = self.invisibleRootItem() - - for family in tuple(self._items_by_family.keys()): - if family not in families: - item = self._items_by_family.pop(family) - root_item.removeRow(item.row()) - - self.family_config_cache.refresh() - - new_items = [] - for family in families: - family_config = self.family_config_cache.family_config(family) - label = family_config.get("label", family) - icon = family_config.get("icon", None) - - if family_config.get("state", True): - state = QtCore.Qt.Checked - else: - state = QtCore.Qt.Unchecked - - if family not in self._items_by_family: - item = QtGui.QStandardItem(label) - item.setFlags( - QtCore.Qt.ItemIsEnabled - | QtCore.Qt.ItemIsSelectable - | QtCore.Qt.ItemIsUserCheckable - ) - new_items.append(item) - self._items_by_family[family] = item - - else: - item = self._items_by_family[label] - item.setData(label, QtCore.Qt.DisplayRole) - - item.setCheckState(state) - - if icon: - item.setIcon(icon) - - if new_items: - root_item.appendRows(new_items) - - -class FamilyProxyFiler(QtCore.QSortFilterProxyModel): - def __init__(self, *args, **kwargs): - super(FamilyProxyFiler, self).__init__(*args, **kwargs) - - self._filtering_enabled = False - self._enabled_families = set() - - def set_enabled_families(self, families): - if self._enabled_families == families: - return - - self._enabled_families = families - if self._filtering_enabled: - self.invalidateFilter() - - def is_filter_enabled(self): - return self._filtering_enabled - - def set_filter_enabled(self, enabled=None): - if enabled is None: - enabled = not self._filtering_enabled - elif self._filtering_enabled == enabled: - return - - self._filtering_enabled = enabled - self.invalidateFilter() - - def filterAcceptsRow(self, row, parent): - if not self._filtering_enabled: - return True - - if not self._enabled_families: - return False - - index = self.sourceModel().index(row, self.filterKeyColumn(), parent) - if index.data(QtCore.Qt.DisplayRole) in self._enabled_families: - return True - return False - - -class FamilyListView(QtWidgets.QListView): - active_changed = QtCore.Signal(list) - - def __init__(self, dbcon, family_config_cache, parent=None): - super(FamilyListView, self).__init__(parent=parent) - - self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) - self.setAlternatingRowColors(True) - self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - - family_model = FamilyModel(dbcon, family_config_cache) - proxy_model = FamilyProxyFiler() - proxy_model.setDynamicSortFilter(True) - proxy_model.setSourceModel(family_model) - - self.setModel(proxy_model) - - family_model.dataChanged.connect(self._on_data_change) - self.customContextMenuRequested.connect(self._on_context_menu) - - self._family_model = family_model - self._proxy_model = proxy_model - - def set_enabled_families(self, families): - self._proxy_model.set_enabled_families(families) - - self.set_enabled_family_filtering(True) - - def set_enabled_family_filtering(self, enabled=None): - self._proxy_model.set_filter_enabled(enabled) - - def refresh(self): - self._family_model.refresh() - - self.active_changed.emit(self.get_enabled_families()) - - def get_enabled_families(self): - """Return the checked family items""" - model = self._family_model - checked_families = [] - for row in range(model.rowCount()): - index = model.index(row, 0) - checked = checkstate_int_to_enum( - index.data(QtCore.Qt.CheckStateRole) - ) - if checked == QtCore.Qt.Checked: - family = index.data(QtCore.Qt.DisplayRole) - checked_families.append(family) - - return checked_families - - def set_all_unchecked(self): - self._set_checkstates(False, self._get_all_indexes()) - - def set_all_checked(self): - self._set_checkstates(True, self._get_all_indexes()) - - def _get_all_indexes(self): - indexes = [] - model = self._family_model - for row in range(model.rowCount()): - index = model.index(row, 0) - indexes.append(index) - return indexes - - def _set_checkstates(self, checked, indexes): - if not indexes: - return - - if checked is None: - state = None - elif checked: - state = QtCore.Qt.Checked - else: - state = QtCore.Qt.Unchecked - - self.blockSignals(True) - - for index in indexes: - index_state = checkstate_int_to_enum( - index.data(QtCore.Qt.CheckStateRole) - ) - if index_state == state: - continue - - new_state = state - if new_state is None: - if index_state in QtCore.Qt.Checked: - new_state = QtCore.Qt.Unchecked - else: - new_state = QtCore.Qt.Checked - - index.model().setData(index, new_state, QtCore.Qt.CheckStateRole) - - self.blockSignals(False) - - self.active_changed.emit(self.get_enabled_families()) - - def _change_selection_state(self, checked): - indexes = self.selectionModel().selectedIndexes() - self._set_checkstates(checked, indexes) - - def _on_data_change(self, *_args): - self.active_changed.emit(self.get_enabled_families()) - - def _on_context_menu(self, pos): - """Build RMB menu under mouse at current position (within widget)""" - menu = QtWidgets.QMenu(self) - - # Add enable all action - action_check_all = QtWidgets.QAction(menu) - action_check_all.setText("Enable All") - action_check_all.triggered.connect(self.set_all_checked) - # Add disable all action - action_uncheck_all = QtWidgets.QAction(menu) - action_uncheck_all.setText("Disable All") - action_uncheck_all.triggered.connect(self.set_all_unchecked) - - menu.addAction(action_check_all) - menu.addAction(action_uncheck_all) - - # Get mouse position - global_pos = self.viewport().mapToGlobal(pos) - menu.exec_(global_pos) - - def event(self, event): - if not event.type() == QtCore.QEvent.KeyPress: - pass - - elif event.key() == QtCore.Qt.Key_Space: - self._change_selection_state(None) - return True - - elif event.key() == QtCore.Qt.Key_Backspace: - self._change_selection_state(False) - return True - - elif event.key() == QtCore.Qt.Key_Return: - self._change_selection_state(True) - return True - - return super(FamilyListView, self).event(event) - - -class RepresentationWidget(QtWidgets.QWidget): - load_started = QtCore.Signal() - load_ended = QtCore.Signal() - - default_widths = ( - ("name", 120), - ("subset", 125), - ("asset", 125), - ("active_site", 85), - ("remote_site", 85) - ) - - commands = {'active': 'Download', 'remote': 'Upload'} - - def __init__(self, dbcon, tool_name=None, parent=None): - super(RepresentationWidget, self).__init__(parent=parent) - self.dbcon = dbcon - self.tool_name = tool_name - - headers = [item[0] for item in self.default_widths] - - model = RepresentationModel(self.dbcon, headers) - - proxy_model = RepresentationSortProxyModel(self) - proxy_model.setSourceModel(model) - - label = QtWidgets.QLabel("Representations", self) - - tree_view = DeselectableTreeView(parent=self) - tree_view.setObjectName("RepresentationView") - tree_view.setModel(proxy_model) - tree_view.setAllColumnsShowFocus(True) - tree_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - tree_view.setSelectionMode( - QtWidgets.QAbstractItemView.ExtendedSelection) - tree_view.setSortingEnabled(True) - tree_view.sortByColumn(1, QtCore.Qt.AscendingOrder) - tree_view.setAlternatingRowColors(True) - tree_view.setIndentation(20) - tree_view.collapseAll() - - for column_name, width in self.default_widths: - idx = model.Columns.index(column_name) - tree_view.setColumnWidth(idx, width) - - layout = QtWidgets.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(label) - layout.addWidget(tree_view) - - # self.itemChanged.connect(self._on_item_changed) - tree_view.customContextMenuRequested.connect(self.on_context_menu) - - self.tree_view = tree_view - self.model = model - self.proxy_model = proxy_model - - self.sync_server_enabled = False - - self.on_project_change(dbcon.current_project()) - - self.model.refresh() - - def on_project_change(self, project_name): - """ - Called on each project change in parent widget. - - Checks if Sync Server is enabled for a project, pushes changes to - model. - """ - enabled = False - if project_name: - self.model.reset_sync_server(project_name) - sync_server = self.model.sync_server - if sync_server: - enabled = sync_server.is_project_enabled(project_name, - single=True) - - self.sync_server_enabled = enabled - lib.change_visibility(self.model, self.tree_view, - "active_site", enabled) - lib.change_visibility(self.model, self.tree_view, - "remote_site", enabled) - - def _repre_contexts_for_loaders_filter(self, items): - repre_ids = [] - for item in items: - repre_ids.append(item["_id"]) - - project_name = self.dbcon.active_project() - repre_docs = list(get_representations( - project_name, - representation_ids=repre_ids, - fields=["name", "parent", "data", "context"] - )) - - version_ids = [ - repre_doc["parent"] - for repre_doc in repre_docs - ] - version_docs = get_versions( - project_name, - version_ids=version_ids, - hero=True - ) - - version_docs_by_id = {} - version_docs_by_subset_id = collections.defaultdict(list) - for version_doc in version_docs: - version_id = version_doc["_id"] - subset_id = version_doc["parent"] - version_docs_by_id[version_id] = version_doc - version_docs_by_subset_id[subset_id].append(version_doc) - - subset_docs = list(get_subsets( - project_name, - subset_ids=version_docs_by_subset_id.keys(), - fields=["schema", "data.families"] - )) - subset_docs_by_id = { - subset_doc["_id"]: subset_doc - for subset_doc in subset_docs - } - repre_context_by_id = {} - for repre_doc in repre_docs: - version_id = repre_doc["parent"] - - version_doc = version_docs_by_id[version_id] - repre_context_by_id[repre_doc["_id"]] = { - "representation": repre_doc, - "version": version_doc, - "subset": subset_docs_by_id[version_doc["parent"]] - } - return repre_context_by_id - - def get_selected_items(self): - selection_model = self.tree_view.selectionModel() - indexes = selection_model.selectedIndexes() - - item_ids = set() - for index in indexes: - item_id = index.data(ITEM_ID_ROLE) - if item_id is not None: - item_ids.add(item_id) - - output = [] - for item_id in item_ids: - item = self.model.get_item_by_id(item_id) - if item is not None: - output.append(item) - return output - - def get_selected_repre_items(self): - output = [] - items = collections.deque(self.get_selected_items()) - - item_ids = set() - while items: - item = items.popleft() - if item.get("isGroup") or item.get("isMerged"): - for child in item.children(): - items.appendleft(child) - else: - item_id = item["id"] - if item_id not in item_ids: - item_ids.add(item_id) - output.append(item) - return output - - def on_context_menu(self, point): - """Shows menu with loader actions on Right-click. - - Registered actions are filtered by selection and help of - `loaders_from_representation` from avalon api. Intersection of actions - is shown when more subset is selected. When there are not available - actions for selected subsets then special action is shown (works as - info message to user): "*No compatible loaders for your selection" - - """ - point_index = self.tree_view.indexAt(point) - if not point_index.isValid(): - return - - # Get selected subsets without groups - selection = self.tree_view.selectionModel() - rows = selection.selectedRows(column=0) - - items = self.get_selected_repre_items() - selected_side = self._get_selected_side(point_index, rows) - # Get all representation->loader combinations available for the - # index under the cursor, so we can list the user the options. - project_name = self.dbcon.active_project() - available_loaders = discover_loader_plugins(project_name) - - filtered_loaders = [] - for loader in available_loaders: - if not loader.enabled: - continue - # Skip subset loaders - if issubclass(loader, SubsetLoaderPlugin): - continue - - if ( - tools_lib.is_sync_loader(loader) - and not self.sync_server_enabled - ): - continue - - filtered_loaders.append(loader) - - if self.tool_name: - filtered_loaders = lib.remove_tool_name_from_loaders( - filtered_loaders, self.tool_name - ) - - loaders = list() - already_added_loaders = set() - label_already_in_menu = set() - - repre_context_by_id = ( - self._repre_contexts_for_loaders_filter(items) - ) - - for item in items: - repre_context = repre_context_by_id[item["_id"]] - for loader in loaders_from_repre_context( - filtered_loaders, - repre_context - ): - if tools_lib.is_sync_loader(loader): - both_unavailable = ( - item["active_site_progress"] <= 0 - and item["remote_site_progress"] <= 0 - ) - if both_unavailable: - continue - - for selected_side in self.commands.keys(): - item = item.copy() - item["custom_label"] = None - label = None - selected_site_progress = item.get( - "{}_site_progress".format(selected_side), -1) - - # only remove if actually present - if tools_lib.is_remove_site_loader(loader): - label = "Remove {}".format(selected_side) - if selected_site_progress < 1: - continue - - if tools_lib.is_add_site_loader(loader): - label = self.commands[selected_side] - if selected_site_progress >= 0: - label = 'Re-{} {}'.format(label, selected_side) - - if not label: - continue - - item["selected_side"] = selected_side - item["custom_label"] = label - - if label not in label_already_in_menu: - loaders.append((item, loader)) - already_added_loaders.add(loader) - label_already_in_menu.add(label) - - else: - item = item.copy() - item["custom_label"] = None - - if loader not in already_added_loaders: - loaders.append((item, loader)) - already_added_loaders.add(loader) - - loaders = lib.sort_loaders(loaders) - - menu = OptionalMenu(self) - if not loaders: - action = lib.get_no_loader_action(menu) - menu.addAction(action) - else: - repre_contexts = get_repres_contexts( - repre_context_by_id.keys(), self.dbcon) - menu = lib.add_representation_loaders_to_menu(loaders, menu, - repre_contexts) - - self._process_action(items, menu, point) - - def _process_action(self, items, menu, point): - """Show the context action menu and process selected - - Args: - items(dict): menu items - menu(OptionalMenu) - point(PointIndex) - """ - global_point = self.tree_view.mapToGlobal(point) - action = menu.exec_(global_point) - - if not action or not action.data(): - return - - self.load_started.emit() - - # Find the representation name and loader to trigger - action_representation, loader = action.data() - repre_ids = [] - data_by_repre_id = {} - selected_side = action_representation.get("selected_side") - site_name = "{}_site_name".format(selected_side) - - is_sync_loader = tools_lib.is_sync_loader(loader) - for item in items: - repre_id = item["_id"] - repre_ids.append(repre_id) - if not is_sync_loader: - continue - - data_site_name = item.get(site_name) - if not data_site_name: - continue - - data_by_repre_id[repre_id] = { - "site_name": data_site_name - } - - repre_contexts = get_repres_contexts(repre_ids, self.dbcon) - options = lib.get_options(action, loader, self, - list(repre_contexts.values())) - - errors = _load_representations_by_loader( - loader, repre_contexts, - options=options, data_by_repre_id=data_by_repre_id) - - self.model.refresh() - - self.load_ended.emit() - - if errors: - box = LoadErrorMessageBox(errors, self) - box.show() - - def _get_optional_labels(self, loaders, selected_side): - """Each loader could have specific label - - Args: - loaders (tuple of dict, dict): (item, loader) - selected_side(string): active or remote - - Returns: - (dict) {loader: string} - """ - optional_labels = {} - if selected_side: - if selected_side == 'active': - txt = "Localize" - else: - txt = "Sync to Remote" - optional_labels = {loader: txt for _, loader in loaders - if tools_lib.is_sync_loader(loader)} - return optional_labels - - def _get_selected_side(self, point_index, rows): - """Returns active/remote label according to column in 'point_index'""" - selected_side = None - if self.sync_server_enabled: - if rows: - source_index = self.proxy_model.mapToSource(point_index) - selected_side = self.model.data(source_index, - self.model.SiteSideRole) - return selected_side - - def set_version_ids(self, version_ids): - self.model.set_version_ids(version_ids) - - def _set_download(self): - pass - - def change_visibility(self, column_name, visible): - """ - Hides or shows particular 'column_name'. - - "asset" and "subset" columns should be visible only in multiselect - """ - lib.change_visibility(self.model, self.tree_view, column_name, visible) - - -def _load_representations_by_loader(loader, repre_contexts, - options, - data_by_repre_id=None): - """Loops through list of repre_contexts and loads them with one loader - - Args: - loader (cls of LoaderPlugin) - not initialized yet - repre_contexts (dicts) - full info about selected representations - (containing repre_doc, version_doc, subset_doc, project info) - options (dict) - qargparse arguments to fill OptionDialog - data_by_repre_id (dict) - additional data applicable on top of - options to provide dynamic values - """ - error_info = [] - - if options is None: # not load when cancelled - return - - for repre_context in repre_contexts.values(): - version_doc = repre_context["version"] - if version_doc["type"] == "hero_version": - version_name = "Hero" - else: - version_name = version_doc.get("name") - try: - if data_by_repre_id: - repre_id = repre_context["representation"]["_id"] - data = data_by_repre_id.get(repre_id) - options.update(data) - load_with_repre_context( - loader, - repre_context, - options=options - ) - - except IncompatibleLoaderError as exc: - print(exc) - error_info.append(( - "Incompatible Loader", - None, - repre_context["representation"]["name"], - repre_context["subset"]["name"], - version_name - )) - - except Exception as exc: - formatted_traceback = None - if not isinstance(exc, LoadError): - exc_type, exc_value, exc_traceback = sys.exc_info() - formatted_traceback = "".join(traceback.format_exception( - exc_type, exc_value, exc_traceback - )) - - error_info.append(( - str(exc), - formatted_traceback, - repre_context["representation"]["name"], - repre_context["subset"]["name"], - version_name - )) - return error_info - - -def _load_subsets_by_loader(loader, subset_contexts, options, - subset_version_docs=None): - """ - Triggers load with SubsetLoader type of loaders - - Args: - loader (SubsetLoder): - subset_contexts (list): - options (dict): - subset_version_docs (dict): {subset_id: version_doc} - """ - error_info = [] - - if options is None: # not load when cancelled - return error_info - - if loader.is_multiple_contexts_compatible: - subset_names = [] - for context in subset_contexts: - subset_name = context.get("subset", {}).get("name") or "N/A" - subset_names.append(subset_name) - - context["version"] = subset_version_docs[context["subset"]["_id"]] - try: - load_with_subset_contexts( - loader, - subset_contexts, - options=options - ) - - except Exception as exc: - formatted_traceback = None - if not isinstance(exc, LoadError): - exc_type, exc_value, exc_traceback = sys.exc_info() - formatted_traceback = "".join(traceback.format_exception( - exc_type, exc_value, exc_traceback - )) - error_info.append(( - str(exc), - formatted_traceback, - None, - ", ".join(subset_names), - None - )) - else: - for subset_context in subset_contexts: - subset_name = subset_context.get("subset", {}).get("name") or "N/A" - - version_doc = subset_version_docs[subset_context["subset"]["_id"]] - subset_context["version"] = version_doc - try: - load_with_subset_context( - loader, - subset_context, - options=options - ) - - except Exception as exc: - formatted_traceback = None - if not isinstance(exc, LoadError): - exc_type, exc_value, exc_traceback = sys.exc_info() - formatted_traceback = "".join(traceback.format_exception( - exc_type, exc_value, exc_traceback - )) - - error_info.append(( - str(exc), - formatted_traceback, - None, - subset_name, - None - )) - - return error_info - - -class AvailabilityDelegate(QtWidgets.QStyledItemDelegate): - """ - Prints icons and downloaded representation ration for both sides. - """ - - def __init__(self, dbcon, parent=None): - super(AvailabilityDelegate, self).__init__(parent) - self.icons = tools_lib.get_repre_icons() - - def paint(self, painter, option, index): - super(AvailabilityDelegate, self).paint(painter, option, index) - option = QtWidgets.QStyleOptionViewItem(option) - option.showDecorationSelected = True - - provider_active = index.data(LOCAL_PROVIDER_ROLE) - provider_remote = index.data(REMOTE_PROVIDER_ROLE) - - availability_active = index.data(LOCAL_AVAILABILITY_ROLE) - availability_remote = index.data(REMOTE_AVAILABILITY_ROLE) - - if not availability_active or not availability_remote: # group lines - return - - idx = 0 - height = width = 24 - for value, provider in [(availability_active, provider_active), - (availability_remote, provider_remote)]: - icon = self.icons.get(provider) - if not icon: - continue - - pixmap = icon.pixmap(icon.actualSize(QtCore.QSize(height, width))) - padding = 10 + (70 * idx) - point = QtCore.QPoint(option.rect.x() + padding, - option.rect.y() + - (option.rect.height() - pixmap.height()) / 2) - painter.drawPixmap(point, pixmap) - - text_rect = option.rect.translated(padding + width + 10, 0) - painter.drawText( - text_rect, - option.displayAlignment, - value - ) - - idx += 1 - - def displayText(self, value, locale): - pass diff --git a/client/ayon_core/tools/publisher/control.py b/client/ayon_core/tools/publisher/control.py index d72a8f4bee..0b9a1f1248 100644 --- a/client/ayon_core/tools/publisher/control.py +++ b/client/ayon_core/tools/publisher/control.py @@ -13,7 +13,6 @@ import six import arrow import pyblish.api -from ayon_core import AYON_SERVER_ENABLED from ayon_core.client import ( get_assets, get_asset_by_id, @@ -74,10 +73,9 @@ class AssetDocsCache: "_id": True, "name": True, "data.visualParent": True, - "data.tasks": True + "data.tasks": True, + "data.parents": True, } - if AYON_SERVER_ENABLED: - projection["data.parents"] = True def __init__(self, controller): self._controller = controller diff --git a/client/ayon_core/tools/publisher/widgets/assets_widget.py b/client/ayon_core/tools/publisher/widgets/assets_widget.py index 8fb3f04267..8a72c03e8b 100644 --- a/client/ayon_core/tools/publisher/widgets/assets_widget.py +++ b/client/ayon_core/tools/publisher/widgets/assets_widget.py @@ -2,7 +2,6 @@ import collections from qtpy import QtWidgets, QtCore, QtGui -from ayon_core import AYON_SERVER_ENABLED from ayon_core.tools.utils import ( PlaceholderLineEdit, RecursiveSortFilterProxyModel, @@ -33,13 +32,11 @@ class CreateWidgetAssetsWidget(SingleSelectAssetsWidget): self._last_filter_height = None def get_selected_asset_name(self): - if AYON_SERVER_ENABLED: - selection_model = self._view.selectionModel() - indexes = selection_model.selectedRows() - for index in indexes: - return index.data(ASSET_PATH_ROLE) - return None - return super(CreateWidgetAssetsWidget, self).get_selected_asset_name() + selection_model = self._view.selectionModel() + indexes = selection_model.selectedRows() + for index in indexes: + return index.data(ASSET_PATH_ROLE) + return None def _check_header_height(self): """Catch header height changes. @@ -177,10 +174,7 @@ class AssetsHierarchyModel(QtGui.QStandardItemModel): return QtCore.QModelIndex() def get_index_by_asset_name(self, asset_name): - item = None - if AYON_SERVER_ENABLED: - item = self._items_by_path.get(asset_name) - + item = self._items_by_path.get(asset_name) if item is None: item = self._items_by_name.get(asset_name) @@ -189,9 +183,7 @@ class AssetsHierarchyModel(QtGui.QStandardItemModel): return item.index() def name_is_valid(self, item_name): - if AYON_SERVER_ENABLED and item_name in self._items_by_path: - return True - return item_name in self._items_by_name + return item_name in self._items_by_path class AssetDialogView(QtWidgets.QTreeView): @@ -217,8 +209,7 @@ class AssetsDialog(QtWidgets.QDialog): proxy_model.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) filter_input = PlaceholderLineEdit(self) - filter_input.setPlaceholderText("Filter {}..".format( - "folders" if AYON_SERVER_ENABLED else "assets")) + filter_input.setPlaceholderText("Filter folders..") asset_view = AssetDialogView(self) asset_view.setModel(proxy_model) @@ -325,10 +316,7 @@ class AssetsDialog(QtWidgets.QDialog): index = self._asset_view.currentIndex() asset_name = None if index.isValid(): - if AYON_SERVER_ENABLED: - asset_name = index.data(ASSET_PATH_ROLE) - else: - asset_name = index.data(ASSET_NAME_ROLE) + asset_name = index.data(ASSET_PATH_ROLE) self._selected_asset = asset_name self.done(1) diff --git a/client/ayon_core/tools/publisher/widgets/create_widget.py b/client/ayon_core/tools/publisher/widgets/create_widget.py index 1bfeb572b7..12135c6891 100644 --- a/client/ayon_core/tools/publisher/widgets/create_widget.py +++ b/client/ayon_core/tools/publisher/widgets/create_widget.py @@ -2,7 +2,6 @@ import re from qtpy import QtWidgets, QtCore, QtGui -from ayon_core import AYON_SERVER_ENABLED from ayon_core.pipeline.create import ( SUBSET_NAME_ALLOWED_SYMBOLS, PRE_CREATE_THUMBNAIL_KEY, @@ -205,9 +204,7 @@ class CreateWidget(QtWidgets.QWidget): variant_subset_layout.setHorizontalSpacing(INPUTS_LAYOUT_HSPACING) variant_subset_layout.setVerticalSpacing(INPUTS_LAYOUT_VSPACING) variant_subset_layout.addRow("Variant", variant_widget) - variant_subset_layout.addRow( - "Product" if AYON_SERVER_ENABLED else "Subset", - subset_name_input) + variant_subset_layout.addRow("Product", subset_name_input) creator_basics_layout = QtWidgets.QVBoxLayout(creator_basics_widget) creator_basics_layout.setContentsMargins(0, 0, 0, 0) @@ -816,13 +813,8 @@ class CreateWidget(QtWidgets.QWidget): # Where to define these data? # - what data show be stored? - if AYON_SERVER_ENABLED: - asset_key = "folderPath" - else: - asset_key = "asset" - instance_data = { - asset_key: asset_name, + "folderPath": asset_name, "task": task_name, "variant": variant, "family": family diff --git a/client/ayon_core/tools/publisher/widgets/overview_widget.py b/client/ayon_core/tools/publisher/widgets/overview_widget.py index 31b8bb4558..f1b271850a 100644 --- a/client/ayon_core/tools/publisher/widgets/overview_widget.py +++ b/client/ayon_core/tools/publisher/widgets/overview_widget.py @@ -1,7 +1,5 @@ from qtpy import QtWidgets, QtCore -from ayon_core import AYON_SERVER_ENABLED - from .border_label_widget import BorderedLabelWidget from .card_view_widgets import InstanceCardView @@ -37,9 +35,7 @@ class OverviewWidget(QtWidgets.QFrame): # --- Created Subsets/Instances --- # Common widget for creation and overview subset_views_widget = BorderedLabelWidget( - "{} to publish".format( - "Products" if AYON_SERVER_ENABLED else "Subsets" - ), + "Products to publish", subset_content_widget ) diff --git a/client/ayon_core/tools/publisher/widgets/widgets.py b/client/ayon_core/tools/publisher/widgets/widgets.py index fd9943c566..95268a7a4b 100644 --- a/client/ayon_core/tools/publisher/widgets/widgets.py +++ b/client/ayon_core/tools/publisher/widgets/widgets.py @@ -9,7 +9,6 @@ import collections from qtpy import QtWidgets, QtCore, QtGui import qtawesome -from ayon_core import AYON_SERVER_ENABLED from ayon_core.lib.attribute_definitions import UnknownDef from ayon_core.tools.attribute_defs import create_widget_for_attr_def from ayon_core.tools import resources @@ -210,9 +209,7 @@ class CreateBtn(PublishIconBtn): def __init__(self, parent=None): icon_path = get_icon_path("create") super(CreateBtn, self).__init__(icon_path, "Create", parent) - self.setToolTip("Create new {}/s".format( - "product" if AYON_SERVER_ENABLED else "subset" - )) + self.setToolTip("Create new product/s") self.setLayoutDirection(QtCore.Qt.RightToLeft) @@ -659,9 +656,7 @@ class TasksCombobox(QtWidgets.QComboBox): if invalid: self._set_is_valid(False) self.set_text( - "< One or more {} require Task selected >".format( - "products" if AYON_SERVER_ENABLED else "subsets" - ) + "< One or more products require Task selected >" ) else: self.set_text(None) @@ -1142,16 +1137,10 @@ class GlobalAttrsWidget(QtWidgets.QWidget): main_layout.setHorizontalSpacing(INPUTS_LAYOUT_HSPACING) main_layout.setVerticalSpacing(INPUTS_LAYOUT_VSPACING) main_layout.addRow("Variant", variant_input) - main_layout.addRow( - "Folder" if AYON_SERVER_ENABLED else "Asset", - asset_value_widget) + main_layout.addRow("Folder", asset_value_widget) main_layout.addRow("Task", task_value_widget) - main_layout.addRow( - "Product type" if AYON_SERVER_ENABLED else "Family", - family_value_widget) - main_layout.addRow( - "Product name" if AYON_SERVER_ENABLED else "Subset", - subset_value_widget) + main_layout.addRow("Product type", family_value_widget) + main_layout.addRow("Product name", subset_value_widget) main_layout.addRow(btns_layout) variant_input.value_changed.connect(self._on_variant_change) @@ -1188,10 +1177,7 @@ class GlobalAttrsWidget(QtWidgets.QWidget): asset_names = [] for instance in self._current_instances: new_variant_value = instance.get("variant") - if AYON_SERVER_ENABLED: - new_asset_name = instance.get("folderPath") - else: - new_asset_name = instance.get("asset") + new_asset_name = instance.get("folderPath") new_task_name = instance.get("task") if variant_value is not None: new_variant_value = variant_value @@ -1223,11 +1209,7 @@ class GlobalAttrsWidget(QtWidgets.QWidget): instance["variant"] = variant_value if asset_name is not None: - if AYON_SERVER_ENABLED: - instance["folderPath"] = asset_name - else: - instance["asset"] = asset_name - + instance["folderPath"] = asset_name instance.set_asset_invalid(False) if task_name is not None: @@ -1325,10 +1307,7 @@ class GlobalAttrsWidget(QtWidgets.QWidget): variants.add(instance.get("variant") or self.unknown_value) families.add(instance.get("family") or self.unknown_value) - if AYON_SERVER_ENABLED: - asset_name = instance.get("folderPath") or self.unknown_value - else: - asset_name = instance.get("asset") or self.unknown_value + asset_name = instance.get("folderPath") or self.unknown_value task_name = instance.get("task") or "" asset_names.add(asset_name) asset_task_combinations.append((asset_name, task_name)) diff --git a/client/ayon_core/tools/publisher/window.py b/client/ayon_core/tools/publisher/window.py index 6772fdda80..a6d235cde7 100644 --- a/client/ayon_core/tools/publisher/window.py +++ b/client/ayon_core/tools/publisher/window.py @@ -9,7 +9,6 @@ from ayon_core import ( resources, style ) -from ayon_core import AYON_SERVER_ENABLED from ayon_core.tools.utils import ( ErrorMessageBox, PlaceholderLineEdit, @@ -54,9 +53,7 @@ class PublisherWindow(QtWidgets.QWidget): self.setObjectName("PublishWindow") - self.setWindowTitle("{} publisher".format( - "AYON" if AYON_SERVER_ENABLED else "OpenPype" - )) + self.setWindowTitle("AYON publisher") icon = QtGui.QIcon(resources.get_openpype_icon_filepath()) self.setWindowIcon(icon) diff --git a/client/ayon_core/tools/push_to_project/__init__.py b/client/ayon_core/tools/push_to_project/__init__.py index e69de29bb2..83df110c96 100644 --- a/client/ayon_core/tools/push_to_project/__init__.py +++ b/client/ayon_core/tools/push_to_project/__init__.py @@ -0,0 +1,6 @@ +from .control import PushToContextController + + +__all__ = ( + "PushToContextController", +) diff --git a/client/ayon_core/tools/push_to_project/app.py b/client/ayon_core/tools/push_to_project/app.py deleted file mode 100644 index c734063750..0000000000 --- a/client/ayon_core/tools/push_to_project/app.py +++ /dev/null @@ -1,28 +0,0 @@ -import click - -from ayon_core.tools.utils import get_openpype_qt_app -from ayon_core.tools.push_to_project.window import PushToContextSelectWindow - - -@click.command() -@click.option("--project", help="Source project name") -@click.option("--version", help="Source version id") -def main(project, version): - """Run PushToProject tool to integrate version in different project. - - Args: - project (str): Source project name. - version (str): Version id. - """ - - app = get_openpype_qt_app() - - window = PushToContextSelectWindow() - window.show() - window.controller.set_source(project, version) - - app.exec_() - - -if __name__ == "__main__": - main() diff --git a/client/ayon_core/tools/ayon_push_to_project/control.py b/client/ayon_core/tools/push_to_project/control.py similarity index 100% rename from client/ayon_core/tools/ayon_push_to_project/control.py rename to client/ayon_core/tools/push_to_project/control.py diff --git a/client/ayon_core/tools/push_to_project/control_context.py b/client/ayon_core/tools/push_to_project/control_context.py deleted file mode 100644 index 517d92159b..0000000000 --- a/client/ayon_core/tools/push_to_project/control_context.py +++ /dev/null @@ -1,678 +0,0 @@ -import re -import collections -import threading - -from ayon_core.client import ( - get_projects, - get_assets, - get_asset_by_id, - get_subset_by_id, - get_version_by_id, - get_representations, -) -from ayon_core.settings import get_project_settings -from ayon_core.lib import prepare_template_data -from ayon_core.lib.events import EventSystem -from ayon_core.pipeline.create import ( - SUBSET_NAME_ALLOWED_SYMBOLS, - get_subset_name_template, -) - -from .control_integrate import ( - ProjectPushItem, - ProjectPushItemProcess, - ProjectPushItemStatus, -) - - -class AssetItem: - def __init__( - self, - entity_id, - name, - icon_name, - icon_color, - parent_id, - has_children - ): - self.id = entity_id - self.name = name - self.icon_name = icon_name - self.icon_color = icon_color - self.parent_id = parent_id - self.has_children = has_children - - @classmethod - def from_doc(cls, asset_doc, has_children=True): - parent_id = asset_doc["data"].get("visualParent") - if parent_id is not None: - parent_id = str(parent_id) - return cls( - str(asset_doc["_id"]), - asset_doc["name"], - asset_doc["data"].get("icon"), - asset_doc["data"].get("color"), - parent_id, - has_children - ) - - -class TaskItem: - def __init__(self, asset_id, name, task_type, short_name): - self.asset_id = asset_id - self.name = name - self.task_type = task_type - self.short_name = short_name - - @classmethod - def from_asset_doc(cls, asset_doc, project_doc): - asset_tasks = asset_doc["data"].get("tasks") or {} - project_task_types = project_doc["config"]["tasks"] - output = [] - for task_name, task_info in asset_tasks.items(): - task_type = task_info.get("type") - task_type_info = project_task_types.get(task_type) or {} - output.append(cls( - asset_doc["_id"], - task_name, - task_type, - task_type_info.get("short_name") - )) - return output - - -class EntitiesModel: - def __init__(self, event_system): - self._event_system = event_system - self._project_names = None - self._project_docs_by_name = {} - self._assets_by_project = {} - self._tasks_by_asset_id = collections.defaultdict(dict) - - def has_cached_projects(self): - return self._project_names is None - - def has_cached_assets(self, project_name): - if not project_name: - return True - return project_name in self._assets_by_project - - def has_cached_tasks(self, project_name): - return self.has_cached_assets(project_name) - - def get_projects(self): - if self._project_names is None: - self.refresh_projects() - return list(self._project_names) - - def get_assets(self, project_name): - if project_name not in self._assets_by_project: - self.refresh_assets(project_name) - return dict(self._assets_by_project[project_name]) - - def get_asset_by_id(self, project_name, asset_id): - return self._assets_by_project[project_name].get(asset_id) - - def get_tasks(self, project_name, asset_id): - if not project_name or not asset_id: - return [] - - if project_name not in self._tasks_by_asset_id: - self.refresh_assets(project_name) - - all_task_items = self._tasks_by_asset_id[project_name] - asset_task_items = all_task_items.get(asset_id) - if not asset_task_items: - return [] - return list(asset_task_items) - - def refresh_projects(self, force=False): - self._event_system.emit( - "projects.refresh.started", {}, "entities.model" - ) - if force or self._project_names is None: - project_names = [] - project_docs_by_name = {} - for project_doc in get_projects(): - library_project = project_doc["data"].get("library_project") - if not library_project: - continue - project_name = project_doc["name"] - project_names.append(project_name) - project_docs_by_name[project_name] = project_doc - self._project_names = project_names - self._project_docs_by_name = project_docs_by_name - self._event_system.emit( - "projects.refresh.finished", {}, "entities.model" - ) - - def _refresh_assets(self, project_name): - asset_items_by_id = {} - task_items_by_asset_id = {} - self._assets_by_project[project_name] = asset_items_by_id - self._tasks_by_asset_id[project_name] = task_items_by_asset_id - if not project_name: - return - - project_doc = self._project_docs_by_name[project_name] - asset_docs_by_parent_id = collections.defaultdict(list) - for asset_doc in get_assets(project_name): - parent_id = asset_doc["data"].get("visualParent") - asset_docs_by_parent_id[parent_id].append(asset_doc) - - hierarchy_queue = collections.deque() - for asset_doc in asset_docs_by_parent_id[None]: - hierarchy_queue.append(asset_doc) - - while hierarchy_queue: - asset_doc = hierarchy_queue.popleft() - children = asset_docs_by_parent_id[asset_doc["_id"]] - asset_item = AssetItem.from_doc(asset_doc, len(children) > 0) - asset_items_by_id[asset_item.id] = asset_item - task_items_by_asset_id[asset_item.id] = ( - TaskItem.from_asset_doc(asset_doc, project_doc) - ) - for child in children: - hierarchy_queue.append(child) - - def refresh_assets(self, project_name, force=False): - self._event_system.emit( - "assets.refresh.started", - {"project_name": project_name}, - "entities.model" - ) - - if force or project_name not in self._assets_by_project: - self._refresh_assets(project_name) - - self._event_system.emit( - "assets.refresh.finished", - {"project_name": project_name}, - "entities.model" - ) - - -class SelectionModel: - def __init__(self, event_system): - self._event_system = event_system - - self.project_name = None - self.asset_id = None - self.task_name = None - - def select_project(self, project_name): - if self.project_name == project_name: - return - - self.project_name = project_name - self._event_system.emit( - "project.changed", - {"project_name": project_name}, - "selection.model" - ) - - def select_asset(self, asset_id): - if self.asset_id == asset_id: - return - self.asset_id = asset_id - self._event_system.emit( - "asset.changed", - { - "project_name": self.project_name, - "asset_id": asset_id - }, - "selection.model" - ) - - def select_task(self, task_name): - if self.task_name == task_name: - return - self.task_name = task_name - self._event_system.emit( - "task.changed", - { - "project_name": self.project_name, - "asset_id": self.asset_id, - "task_name": task_name - }, - "selection.model" - ) - - -class UserPublishValues: - """Helper object to validate values required for push to different project. - - Args: - event_system (EventSystem): Event system to catch and emit events. - new_asset_name (str): Name of new asset name. - variant (str): Variant for new subset name in new project. - """ - - asset_name_regex = re.compile("^[a-zA-Z0-9_.]+$") - variant_regex = re.compile("^[{}]+$".format(SUBSET_NAME_ALLOWED_SYMBOLS)) - - def __init__(self, event_system): - self._event_system = event_system - self._new_asset_name = None - self._variant = None - self._comment = None - self._is_variant_valid = False - self._is_new_asset_name_valid = False - - self.set_new_asset("") - self.set_variant("") - self.set_comment("") - - @property - def new_asset_name(self): - return self._new_asset_name - - @property - def variant(self): - return self._variant - - @property - def comment(self): - return self._comment - - @property - def is_variant_valid(self): - return self._is_variant_valid - - @property - def is_new_asset_name_valid(self): - return self._is_new_asset_name_valid - - @property - def is_valid(self): - return self.is_variant_valid and self.is_new_asset_name_valid - - def set_variant(self, variant): - if variant == self._variant: - return - - old_variant = self._variant - old_is_valid = self._is_variant_valid - - self._variant = variant - is_valid = False - if variant: - is_valid = self.variant_regex.match(variant) is not None - self._is_variant_valid = is_valid - - changes = { - key: {"new": new, "old": old} - for key, old, new in ( - ("variant", old_variant, variant), - ("is_valid", old_is_valid, is_valid) - ) - } - - self._event_system.emit( - "variant.changed", - { - "variant": variant, - "is_valid": self._is_variant_valid, - "changes": changes - }, - "user_values" - ) - - def set_new_asset(self, asset_name): - if self._new_asset_name == asset_name: - return - old_asset_name = self._new_asset_name - old_is_valid = self._is_new_asset_name_valid - self._new_asset_name = asset_name - is_valid = True - if asset_name: - is_valid = ( - self.asset_name_regex.match(asset_name) is not None - ) - self._is_new_asset_name_valid = is_valid - changes = { - key: {"new": new, "old": old} - for key, old, new in ( - ("new_asset_name", old_asset_name, asset_name), - ("is_valid", old_is_valid, is_valid) - ) - } - - self._event_system.emit( - "new_asset_name.changed", - { - "new_asset_name": self._new_asset_name, - "is_valid": self._is_new_asset_name_valid, - "changes": changes - }, - "user_values" - ) - - def set_comment(self, comment): - if comment == self._comment: - return - old_comment = self._comment - self._comment = comment - self._event_system.emit( - "comment.changed", - { - "comment": comment, - "changes": { - "comment": {"new": comment, "old": old_comment} - } - }, - "user_values" - ) - - -class PushToContextController: - def __init__(self, project_name=None, version_id=None): - self._src_project_name = None - self._src_version_id = None - self._src_asset_doc = None - self._src_subset_doc = None - self._src_version_doc = None - - event_system = EventSystem() - entities_model = EntitiesModel(event_system) - selection_model = SelectionModel(event_system) - user_values = UserPublishValues(event_system) - - self._event_system = event_system - self._entities_model = entities_model - self._selection_model = selection_model - self._user_values = user_values - - event_system.add_callback("project.changed", self._on_project_change) - event_system.add_callback("asset.changed", self._invalidate) - event_system.add_callback("variant.changed", self._invalidate) - event_system.add_callback("new_asset_name.changed", self._invalidate) - - self._submission_enabled = False - self._process_thread = None - self._process_item = None - - self.set_source(project_name, version_id) - - def _get_task_info_from_repre_docs(self, asset_doc, repre_docs): - asset_tasks = asset_doc["data"].get("tasks") or {} - found_comb = [] - for repre_doc in repre_docs: - context = repre_doc["context"] - task_info = context.get("task") - if task_info is None: - continue - - task_name = None - task_type = None - if isinstance(task_info, str): - task_name = task_info - asset_task_info = asset_tasks.get(task_info) or {} - task_type = asset_task_info.get("type") - - elif isinstance(task_info, dict): - task_name = task_info.get("name") - task_type = task_info.get("type") - - if task_name and task_type: - return task_name, task_type - - if task_name: - found_comb.append((task_name, task_type)) - - for task_name, task_type in found_comb: - return task_name, task_type - return None, None - - def _get_src_variant(self): - project_name = self._src_project_name - version_doc = self._src_version_doc - asset_doc = self._src_asset_doc - repre_docs = get_representations( - project_name, version_ids=[version_doc["_id"]] - ) - task_name, task_type = self._get_task_info_from_repre_docs( - asset_doc, repre_docs - ) - - project_settings = get_project_settings(project_name) - subset_doc = self.src_subset_doc - family = subset_doc["data"].get("family") - if not family: - family = subset_doc["data"]["families"][0] - template = get_subset_name_template( - self._src_project_name, - family, - task_name, - task_type, - None, - project_settings=project_settings - ) - template_low = template.lower() - variant_placeholder = "{variant}" - if ( - variant_placeholder not in template_low - or (not task_name and "{task" in template_low) - ): - return "" - - idx = template_low.index(variant_placeholder) - template_s = template[:idx] - template_e = template[idx + len(variant_placeholder):] - fill_data = prepare_template_data({ - "family": family, - "task": task_name - }) - try: - subset_s = template_s.format(**fill_data) - subset_e = template_e.format(**fill_data) - except Exception as exc: - print("Failed format", exc) - return "" - - subset_name = self.src_subset_doc["name"] - if ( - (subset_s and not subset_name.startswith(subset_s)) - or (subset_e and not subset_name.endswith(subset_e)) - ): - return "" - - if subset_s: - subset_name = subset_name[len(subset_s):] - if subset_e: - subset_name = subset_name[:len(subset_e)] - return subset_name - - def set_source(self, project_name, version_id): - if ( - project_name == self._src_project_name - and version_id == self._src_version_id - ): - return - - self._src_project_name = project_name - self._src_version_id = version_id - asset_doc = None - subset_doc = None - version_doc = None - if project_name and version_id: - version_doc = get_version_by_id(project_name, version_id) - - if version_doc: - subset_doc = get_subset_by_id(project_name, version_doc["parent"]) - - if subset_doc: - asset_doc = get_asset_by_id(project_name, subset_doc["parent"]) - - self._src_asset_doc = asset_doc - self._src_subset_doc = subset_doc - self._src_version_doc = version_doc - if asset_doc: - self.user_values.set_new_asset(asset_doc["name"]) - variant = self._get_src_variant() - if variant: - self.user_values.set_variant(variant) - - comment = version_doc["data"].get("comment") - if comment: - self.user_values.set_comment(comment) - - self._event_system.emit( - "source.changed", { - "project_name": project_name, - "version_id": version_id - }, - "controller" - ) - - @property - def src_project_name(self): - return self._src_project_name - - @property - def src_version_id(self): - return self._src_version_id - - @property - def src_label(self): - if not self._src_project_name or not self._src_version_id: - return "Source is not defined" - - asset_doc = self.src_asset_doc - if not asset_doc: - return "Source is invalid" - - asset_path_parts = list(asset_doc["data"]["parents"]) - asset_path_parts.append(asset_doc["name"]) - asset_path = "/".join(asset_path_parts) - subset_doc = self.src_subset_doc - version_doc = self.src_version_doc - return "Source: {}/{}/{}/v{:0>3}".format( - self._src_project_name, - asset_path, - subset_doc["name"], - version_doc["name"] - ) - - @property - def src_version_doc(self): - return self._src_version_doc - - @property - def src_subset_doc(self): - return self._src_subset_doc - - @property - def src_asset_doc(self): - return self._src_asset_doc - - @property - def event_system(self): - return self._event_system - - @property - def model(self): - return self._entities_model - - @property - def selection_model(self): - return self._selection_model - - @property - def user_values(self): - return self._user_values - - @property - def submission_enabled(self): - return self._submission_enabled - - def _on_project_change(self, event): - project_name = event["project_name"] - self.model.refresh_assets(project_name) - self._invalidate() - - def _invalidate(self): - submission_enabled = self._check_submit_validations() - if submission_enabled == self._submission_enabled: - return - self._submission_enabled = submission_enabled - self._event_system.emit( - "submission.enabled.changed", - {"enabled": submission_enabled}, - "controller" - ) - - def _check_submit_validations(self): - if not self._user_values.is_valid: - return False - - if not self.selection_model.project_name: - return False - - if ( - not self._user_values.new_asset_name - and not self.selection_model.asset_id - ): - return False - - return True - - def get_selected_asset_name(self): - project_name = self._selection_model.project_name - asset_id = self._selection_model.asset_id - if not project_name or not asset_id: - return None - asset_item = self._entities_model.get_asset_by_id( - project_name, asset_id - ) - if asset_item: - return asset_item.name - return None - - def submit(self, wait=True): - if not self.submission_enabled: - return - - if self._process_thread is not None: - return - - item = ProjectPushItem( - self.src_project_name, - self.src_version_id, - self.selection_model.project_name, - self.selection_model.asset_id, - self.selection_model.task_name, - self.user_values.variant, - comment=self.user_values.comment, - new_asset_name=self.user_values.new_asset_name, - dst_version=1 - ) - - status_item = ProjectPushItemStatus(event_system=self._event_system) - process_item = ProjectPushItemProcess(item, status_item) - self._process_item = process_item - self._event_system.emit("submit.started", {}, "controller") - if wait: - self._submit_callback() - self._process_item = None - return process_item - - thread = threading.Thread(target=self._submit_callback) - self._process_thread = thread - thread.start() - return process_item - - def wait_for_process_thread(self): - if self._process_thread is None: - return - self._process_thread.join() - self._process_thread = None - - def _submit_callback(self): - process_item = self._process_item - if process_item is None: - return - process_item.process() - self._event_system.emit("submit.finished", {}, "controller") - if process_item is self._process_item: - self._process_item = None diff --git a/client/ayon_core/tools/push_to_project/control_integrate.py b/client/ayon_core/tools/push_to_project/control_integrate.py deleted file mode 100644 index eb8f867650..0000000000 --- a/client/ayon_core/tools/push_to_project/control_integrate.py +++ /dev/null @@ -1,1215 +0,0 @@ -import os -import re -import copy -import socket -import itertools -import datetime -import sys -import traceback - -from bson.objectid import ObjectId - -from ayon_core.client import ( - get_project, - get_assets, - get_asset_by_id, - get_subset_by_id, - get_subset_by_name, - get_version_by_id, - get_last_version_by_subset_id, - get_version_by_name, - get_representations, -) -from ayon_core.client.operations import ( - OperationsSession, - new_asset_document, - new_subset_document, - new_version_doc, - new_representation_doc, - prepare_version_update_data, - prepare_representation_update_data, -) -from ayon_core.modules import ModulesManager -from ayon_core.lib import ( - StringTemplate, - get_openpype_username, - get_formatted_current_time, - source_hash, -) - -from ayon_core.lib.file_transaction import FileTransaction -from ayon_core.settings import get_project_settings -from ayon_core.pipeline import Anatomy -from ayon_core.pipeline.version_start import get_versioning_start -from ayon_core.pipeline.template_data import get_template_data -from ayon_core.pipeline.publish import get_publish_template_name -from ayon_core.pipeline.create import get_subset_name - -UNKNOWN = object() - - -class PushToProjectError(Exception): - pass - - -class FileItem(object): - def __init__(self, path): - self.path = path - - @property - def is_valid_file(self): - return os.path.exists(self.path) and os.path.isfile(self.path) - - -class SourceFile(FileItem): - def __init__(self, path, frame=None, udim=None): - super(SourceFile, self).__init__(path) - self.frame = frame - self.udim = udim - - def __repr__(self): - subparts = [self.__class__.__name__] - if self.frame is not None: - subparts.append("frame: {}".format(self.frame)) - if self.udim is not None: - subparts.append("UDIM: {}".format(self.udim)) - - return "<{}> '{}'".format(" - ".join(subparts), self.path) - - -class ResourceFile(FileItem): - def __init__(self, path, relative_path): - super(ResourceFile, self).__init__(path) - self.relative_path = relative_path - - def __repr__(self): - return "<{}> '{}'".format(self.__class__.__name__, self.relative_path) - - @property - def is_valid_file(self): - if not self.relative_path: - return False - return super(ResourceFile, self).is_valid_file - - -class ProjectPushItem: - def __init__( - self, - src_project_name, - src_version_id, - dst_project_name, - dst_asset_id, - dst_task_name, - variant, - comment=None, - new_asset_name=None, - dst_version=None - ): - self.src_project_name = src_project_name - self.src_version_id = src_version_id - self.dst_project_name = dst_project_name - self.dst_asset_id = dst_asset_id - self.dst_task_name = dst_task_name - self.dst_version = dst_version - self.variant = variant - self.new_asset_name = new_asset_name - self.comment = comment or "" - self._id = "|".join([ - src_project_name, - src_version_id, - dst_project_name, - str(dst_asset_id), - str(new_asset_name), - str(dst_task_name), - str(dst_version) - ]) - - @property - def id(self): - return self._id - - def __repr__(self): - return "<{} - {}>".format(self.__class__.__name__, self.id) - - -class StatusMessage: - def __init__(self, message, level): - self.message = message - self.level = level - - def __str__(self): - return "{}: {}".format(self.level.upper(), self.message) - - def __repr__(self): - return "<{} - {}> {}".format( - self.__class__.__name__, self.level.upper, self.message - ) - - -class ProjectPushItemStatus: - def __init__( - self, - failed=False, - finished=False, - fail_reason=None, - formatted_traceback=None, - messages=None, - event_system=None - ): - if messages is None: - messages = [] - self._failed = failed - self._finished = finished - self._fail_reason = fail_reason - self._traceback = formatted_traceback - self._messages = messages - self._event_system = event_system - - def emit_event(self, topic, data=None): - if self._event_system is None: - return - - self._event_system.emit(topic, data or {}, "push.status") - - def get_finished(self): - """Processing of push to project finished. - - Returns: - bool: Finished. - """ - - return self._finished - - def set_finished(self, finished=True): - """Mark status as finished. - - Args: - finished (bool): Processing finished (failed or not). - """ - - if finished != self._finished: - self._finished = finished - self.emit_event("push.finished.changed", {"finished": finished}) - - finished = property(get_finished, set_finished) - - def set_failed(self, fail_reason, exc_info=None): - """Set status as failed. - - Attribute 'fail_reason' can change automatically based on passed value. - Reason is unset if 'failed' is 'False' and is set do default reason if - is set to 'True' and reason is not set. - - Args: - failed (bool): Push to project failed. - fail_reason (str): Reason why failed. - """ - - failed = True - if not fail_reason and not exc_info: - failed = False - - full_traceback = None - if exc_info is not None: - full_traceback = "".join(traceback.format_exception(*exc_info)) - if not fail_reason: - fail_reason = "Failed without specified reason" - - if ( - self._failed == failed - and self._traceback == full_traceback - and self._fail_reason == fail_reason - ): - return - - self._failed = failed - self._fail_reason = fail_reason or None - self._traceback = full_traceback - - self.emit_event( - "push.failed.changed", - { - "failed": failed, - "reason": fail_reason, - "traceback": full_traceback - } - ) - - @property - def failed(self): - """Processing failed. - - Returns: - bool: Processing failed. - """ - - return self._failed - - @property - def fail_reason(self): - """Reason why push to process failed. - - Returns: - Union[str, None]: Reason why push failed or None. - """ - - return self._fail_reason - - @property - def traceback(self): - """Traceback of failed process. - - Traceback is available only if unhandled exception happened. - - Returns: - Union[str, None]: Formatted traceback. - """ - - return self._traceback - - # Loggin helpers - # TODO better logging - def add_message(self, message, level): - message_obj = StatusMessage(message, level) - self._messages.append(message_obj) - self.emit_event( - "push.message.added", - {"message": message, "level": level} - ) - print(message_obj) - return message_obj - - def debug(self, message): - return self.add_message(message, "debug") - - def info(self, message): - return self.add_message(message, "info") - - def warning(self, message): - return self.add_message(message, "warning") - - def error(self, message): - return self.add_message(message, "error") - - def critical(self, message): - return self.add_message(message, "critical") - - -class ProjectPushRepreItem: - """Representation item. - - Representation item based on representation document and project roots. - - Representation document may have reference to: - - source files: Files defined with publish template - - resource files: Files that should be in publish directory - but filenames are not template based. - - Args: - repre_doc (Dict[str, Ant]): Representation document. - roots (Dict[str, str]): Project roots (based on project anatomy). - """ - - def __init__(self, repre_doc, roots): - self._repre_doc = repre_doc - self._roots = roots - self._src_files = None - self._resource_files = None - self._frame = UNKNOWN - - @property - def repre_doc(self): - return self._repre_doc - - @property - def src_files(self): - if self._src_files is None: - self.get_source_files() - return self._src_files - - @property - def resource_files(self): - if self._resource_files is None: - self.get_source_files() - return self._resource_files - - @staticmethod - def _clean_path(path): - new_value = path.replace("\\", "/") - while "//" in new_value: - new_value = new_value.replace("//", "/") - return new_value - - @staticmethod - def _get_relative_path(path, src_dirpath): - dirpath, basename = os.path.split(path) - if not dirpath.lower().startswith(src_dirpath.lower()): - return None - - relative_dir = dirpath[len(src_dirpath):].lstrip("/") - if relative_dir: - relative_path = "/".join([relative_dir, basename]) - else: - relative_path = basename - return relative_path - - @property - def frame(self): - """First frame of representation files. - - This value will be in representation document context if is sequence. - - Returns: - Union[int, None]: First frame in representation files based on - source files or None if frame is not part of filename. - """ - - if self._frame is UNKNOWN: - frame = None - for src_file in self.src_files: - src_frame = src_file.frame - if ( - src_frame is not None - and (frame is None or src_frame < frame) - ): - frame = src_frame - self._frame = frame - return self._frame - - @staticmethod - def validate_source_files(src_files, resource_files): - if not src_files: - raise AssertionError(( - "Couldn't figure out source files from representation." - " Found resource files {}" - ).format(", ".join(str(i) for i in resource_files))) - - invalid_items = [ - item - for item in itertools.chain(src_files, resource_files) - if not item.is_valid_file - ] - if invalid_items: - raise AssertionError(( - "Source files that were not found on disk: {}" - ).format(", ".join(str(i) for i in invalid_items))) - - def get_source_files(self): - if self._src_files is not None: - return self._src_files, self._resource_files - - repre_context = self._repre_doc["context"] - if "frame" in repre_context or "udim" in repre_context: - src_files, resource_files = self._get_source_files_with_frames() - else: - src_files, resource_files = self._get_source_files() - - self.validate_source_files(src_files, resource_files) - - self._src_files = src_files - self._resource_files = resource_files - return self._src_files, self._resource_files - - def _get_source_files_with_frames(self): - frame_placeholder = "__frame__" - udim_placeholder = "__udim__" - src_files = [] - resource_files = [] - template = self._repre_doc["data"]["template"] - # Remove padding from 'udim' and 'frame' formatting keys - # - "{frame:0>4}" -> "{frame}" - for key in ("udim", "frame"): - sub_part = "{" + key + "[^}]*}" - replacement = "{{{}}}".format(key) - template = re.sub(sub_part, replacement, template) - - repre_context = self._repre_doc["context"] - fill_repre_context = copy.deepcopy(repre_context) - if "frame" in fill_repre_context: - fill_repre_context["frame"] = frame_placeholder - - if "udim" in fill_repre_context: - fill_repre_context["udim"] = udim_placeholder - - fill_roots = fill_repre_context["root"] - for root_name in tuple(fill_roots.keys()): - fill_roots[root_name] = "{{root[{}]}}".format(root_name) - repre_path = StringTemplate.format_template( - template, fill_repre_context) - repre_path = self._clean_path(repre_path) - src_dirpath, src_basename = os.path.split(repre_path) - src_basename = ( - re.escape(src_basename) - .replace(frame_placeholder, "(?P[0-9]+)") - .replace(udim_placeholder, "(?P[0-9]+)") - ) - src_basename_regex = re.compile("^{}$".format(src_basename)) - for file_info in self._repre_doc["files"]: - filepath_template = self._clean_path(file_info["path"]) - filepath = self._clean_path( - filepath_template.format(root=self._roots) - ) - dirpath, basename = os.path.split(filepath_template) - if ( - dirpath.lower() != src_dirpath.lower() - or not src_basename_regex.match(basename) - ): - relative_path = self._get_relative_path(filepath, src_dirpath) - resource_files.append(ResourceFile(filepath, relative_path)) - continue - - filepath = os.path.join(src_dirpath, basename) - frame = None - udim = None - for item in src_basename_regex.finditer(basename): - group_name = item.lastgroup - value = item.group(group_name) - if group_name == "frame": - frame = int(value) - elif group_name == "udim": - udim = value - - src_files.append(SourceFile(filepath, frame, udim)) - - return src_files, resource_files - - def _get_source_files(self): - src_files = [] - resource_files = [] - template = self._repre_doc["data"]["template"] - repre_context = self._repre_doc["context"] - fill_repre_context = copy.deepcopy(repre_context) - fill_roots = fill_repre_context["root"] - for root_name in tuple(fill_roots.keys()): - fill_roots[root_name] = "{{root[{}]}}".format(root_name) - repre_path = StringTemplate.format_template(template, - fill_repre_context) - repre_path = self._clean_path(repre_path) - src_dirpath = os.path.dirname(repre_path) - for file_info in self._repre_doc["files"]: - filepath_template = self._clean_path(file_info["path"]) - filepath = self._clean_path( - filepath_template.format(root=self._roots)) - - if filepath_template.lower() == repre_path.lower(): - src_files.append( - SourceFile(repre_path.format(root=self._roots)) - ) - else: - relative_path = self._get_relative_path( - filepath_template, src_dirpath - ) - resource_files.append( - ResourceFile(filepath, relative_path) - ) - return src_files, resource_files - - -class ProjectPushItemProcess: - """ - Args: - item (ProjectPushItem): Item which is being processed. - item_status (ProjectPushItemStatus): Object to store status. - """ - - # TODO where to get host?!!! - host_name = "republisher" - - def __init__(self, item, item_status=None): - self._item = item - - self._src_project_doc = None - self._src_asset_doc = None - self._src_subset_doc = None - self._src_version_doc = None - self._src_repre_items = None - self._src_anatomy = None - - self._project_doc = None - self._anatomy = None - self._asset_doc = None - self._created_asset_doc = None - self._task_info = None - self._subset_doc = None - self._version_doc = None - - self._family = None - self._subset_name = None - - self._project_settings = None - self._template_name = None - - if item_status is None: - item_status = ProjectPushItemStatus() - self._status = item_status - self._operations = OperationsSession() - self._file_transaction = FileTransaction() - - @property - def status(self): - return self._status - - @property - def src_project_doc(self): - return self._src_project_doc - - @property - def src_anatomy(self): - return self._src_anatomy - - @property - def src_asset_doc(self): - return self._src_asset_doc - - @property - def src_subset_doc(self): - return self._src_subset_doc - - @property - def src_version_doc(self): - return self._src_version_doc - - @property - def src_repre_items(self): - return self._src_repre_items - - @property - def project_doc(self): - return self._project_doc - - @property - def anatomy(self): - return self._anatomy - - @property - def project_settings(self): - return self._project_settings - - @property - def asset_doc(self): - return self._asset_doc - - @property - def task_info(self): - return self._task_info - - @property - def subset_doc(self): - return self._subset_doc - - @property - def version_doc(self): - return self._version_doc - - @property - def variant(self): - return self._item.variant - - @property - def family(self): - return self._family - - @property - def subset_name(self): - return self._subset_name - - @property - def template_name(self): - return self._template_name - - def fill_source_variables(self): - src_project_name = self._item.src_project_name - src_version_id = self._item.src_version_id - - project_doc = get_project(src_project_name) - if not project_doc: - self._status.set_failed( - f"Source project \"{src_project_name}\" was not found" - ) - raise PushToProjectError(self._status.fail_reason) - - self._status.debug(f"Project '{src_project_name}' found") - - version_doc = get_version_by_id(src_project_name, src_version_id) - if not version_doc: - self._status.set_failed(( - f"Source version with id \"{src_version_id}\"" - f" was not found in project \"{src_project_name}\"" - )) - raise PushToProjectError(self._status.fail_reason) - - subset_id = version_doc["parent"] - subset_doc = get_subset_by_id(src_project_name, subset_id) - if not subset_doc: - self._status.set_failed(( - f"Could find subset with id \"{subset_id}\"" - f" in project \"{src_project_name}\"" - )) - raise PushToProjectError(self._status.fail_reason) - - asset_id = subset_doc["parent"] - asset_doc = get_asset_by_id(src_project_name, asset_id) - if not asset_doc: - self._status.set_failed(( - f"Could find asset with id \"{asset_id}\"" - f" in project \"{src_project_name}\"" - )) - raise PushToProjectError(self._status.fail_reason) - - anatomy = Anatomy(src_project_name) - - repre_docs = get_representations( - src_project_name, - version_ids=[src_version_id] - ) - repre_items = [ - ProjectPushRepreItem(repre_doc, anatomy.roots) - for repre_doc in repre_docs - ] - self._status.debug(( - f"Found {len(repre_items)} representations on" - f" version {src_version_id} in project '{src_project_name}'" - )) - if not repre_items: - self._status.set_failed( - "Source version does not have representations" - f" (Version id: {src_version_id})" - ) - raise PushToProjectError(self._status.fail_reason) - - self._src_anatomy = anatomy - self._src_project_doc = project_doc - self._src_asset_doc = asset_doc - self._src_subset_doc = subset_doc - self._src_version_doc = version_doc - self._src_repre_items = repre_items - - def fill_destination_project(self): - # --- Destination entities --- - dst_project_name = self._item.dst_project_name - # Validate project existence - dst_project_doc = get_project(dst_project_name) - if not dst_project_doc: - self._status.set_failed( - f"Destination project '{dst_project_name}' was not found" - ) - raise PushToProjectError(self._status.fail_reason) - - self._status.debug( - f"Destination project '{dst_project_name}' found" - ) - self._project_doc = dst_project_doc - self._anatomy = Anatomy(dst_project_name) - self._project_settings = get_project_settings( - self._item.dst_project_name - ) - - def _create_asset( - self, - src_asset_doc, - project_doc, - parent_asset_doc, - asset_name - ): - parent_id = None - parents = [] - tools = [] - if parent_asset_doc: - parent_id = parent_asset_doc["_id"] - parents = list(parent_asset_doc["data"]["parents"]) - parents.append(parent_asset_doc["name"]) - _tools = parent_asset_doc["data"].get("tools_env") - if _tools: - tools = list(_tools) - - asset_name_low = asset_name.lower() - other_asset_docs = get_assets( - project_doc["name"], fields=["_id", "name", "data.visualParent"] - ) - for other_asset_doc in other_asset_docs: - other_name = other_asset_doc["name"] - other_parent_id = other_asset_doc["data"].get("visualParent") - if other_name.lower() != asset_name_low: - continue - - if other_parent_id != parent_id: - self._status.set_failed(( - f"Asset with name \"{other_name}\" already" - " exists in different hierarchy." - )) - raise PushToProjectError(self._status.fail_reason) - - self._status.debug(( - f"Found already existing asset with name \"{other_name}\"" - f" which match requested name \"{asset_name}\"" - )) - return get_asset_by_id(project_doc["name"], other_asset_doc["_id"]) - - data_keys = ( - "clipIn", - "clipOut", - "frameStart", - "frameEnd", - "handleStart", - "handleEnd", - "resolutionWidth", - "resolutionHeight", - "fps", - "pixelAspect", - ) - asset_data = { - "visualParent": parent_id, - "parents": parents, - "tasks": {}, - "tools_env": tools - } - src_asset_data = src_asset_doc["data"] - for key in data_keys: - if key in src_asset_data: - asset_data[key] = src_asset_data[key] - - asset_doc = new_asset_document( - asset_name, - project_doc["_id"], - parent_id, - parents, - data=asset_data - ) - self._operations.create_entity( - project_doc["name"], - asset_doc["type"], - asset_doc - ) - self._status.info( - f"Creating new asset with name \"{asset_name}\"" - ) - self._created_asset_doc = asset_doc - return asset_doc - - def fill_or_create_destination_asset(self): - dst_project_name = self._item.dst_project_name - dst_asset_id = self._item.dst_asset_id - dst_task_name = self._item.dst_task_name - new_asset_name = self._item.new_asset_name - if not dst_asset_id and not new_asset_name: - self._status.set_failed( - "Push item does not have defined destination asset" - ) - raise PushToProjectError(self._status.fail_reason) - - # Get asset document - parent_asset_doc = None - if dst_asset_id: - parent_asset_doc = get_asset_by_id( - self._item.dst_project_name, self._item.dst_asset_id - ) - if not parent_asset_doc: - self._status.set_failed( - f"Could find asset with id \"{dst_asset_id}\"" - f" in project \"{dst_project_name}\"" - ) - raise PushToProjectError(self._status.fail_reason) - - if not new_asset_name: - asset_doc = parent_asset_doc - else: - asset_doc = self._create_asset( - self.src_asset_doc, - self.project_doc, - parent_asset_doc, - new_asset_name - ) - self._asset_doc = asset_doc - if not dst_task_name: - self._task_info = {} - return - - asset_path_parts = list(asset_doc["data"]["parents"]) - asset_path_parts.append(asset_doc["name"]) - asset_path = "/".join(asset_path_parts) - asset_tasks = asset_doc.get("data", {}).get("tasks") or {} - task_info = asset_tasks.get(dst_task_name) - if not task_info: - self._status.set_failed( - f"Could find task with name \"{dst_task_name}\"" - f" on asset \"{asset_path}\"" - f" in project \"{dst_project_name}\"" - ) - raise PushToProjectError(self._status.fail_reason) - - # Create copy of task info to avoid changing data in asset document - task_info = copy.deepcopy(task_info) - task_info["name"] = dst_task_name - # Fill rest of task information based on task type - task_type = task_info["type"] - task_type_info = self.project_doc["config"]["tasks"].get(task_type, {}) - task_info.update(task_type_info) - self._task_info = task_info - - def determine_family(self): - subset_doc = self.src_subset_doc - family = subset_doc["data"].get("family") - families = subset_doc["data"].get("families") - if not family and families: - family = families[0] - - if not family: - self._status.set_failed( - "Couldn't figure out family from source subset" - ) - raise PushToProjectError(self._status.fail_reason) - - self._status.debug( - f"Publishing family is '{family}' (Based on source subset)" - ) - self._family = family - - def determine_publish_template_name(self): - template_name = get_publish_template_name( - self._item.dst_project_name, - self.host_name, - self.family, - self.task_info.get("name"), - self.task_info.get("type"), - project_settings=self.project_settings - ) - self._status.debug( - f"Using template '{template_name}' for integration" - ) - self._template_name = template_name - - def determine_subset_name(self): - family = self.family - asset_doc = self.asset_doc - task_info = self.task_info - subset_name = get_subset_name( - family, - self.variant, - task_info.get("name"), - asset_doc, - project_name=self._item.dst_project_name, - host_name=self.host_name, - project_settings=self.project_settings - ) - self._status.info( - f"Push will be integrating to subset with name '{subset_name}'" - ) - self._subset_name = subset_name - - def make_sure_subset_exists(self): - project_name = self._item.dst_project_name - asset_id = self.asset_doc["_id"] - subset_name = self.subset_name - family = self.family - subset_doc = get_subset_by_name(project_name, subset_name, asset_id) - if subset_doc: - self._subset_doc = subset_doc - return subset_doc - - data = { - "families": [family] - } - subset_doc = new_subset_document( - subset_name, family, asset_id, data - ) - self._operations.create_entity(project_name, "subset", subset_doc) - self._subset_doc = subset_doc - - def make_sure_version_exists(self): - """Make sure version document exits in database.""" - - project_name = self._item.dst_project_name - version = self._item.dst_version - src_version_doc = self.src_version_doc - subset_doc = self.subset_doc - subset_id = subset_doc["_id"] - src_data = src_version_doc["data"] - families = subset_doc["data"].get("families") - if not families: - families = [subset_doc["data"]["family"]] - - version_data = { - "families": list(families), - "fps": src_data.get("fps"), - "source": src_data.get("source"), - "machine": socket.gethostname(), - "comment": self._item.comment or "", - "author": get_openpype_username(), - "time": get_formatted_current_time(), - } - if version is None: - last_version_doc = get_last_version_by_subset_id( - project_name, subset_id - ) - if last_version_doc: - version = int(last_version_doc["name"]) + 1 - else: - version = get_versioning_start( - project_name, - self.host_name, - task_name=self.task_info["name"], - task_type=self.task_info["type"], - family=families[0], - subset=subset_doc["name"] - ) - - existing_version_doc = get_version_by_name( - project_name, version, subset_id - ) - # Update existing version - if existing_version_doc: - version_doc = new_version_doc( - version, subset_id, version_data, existing_version_doc["_id"] - ) - update_data = prepare_version_update_data( - existing_version_doc, version_doc - ) - if update_data: - self._operations.update_entity( - project_name, - "version", - existing_version_doc["_id"], - update_data - ) - self._version_doc = version_doc - - return - - version_doc = new_version_doc( - version, subset_id, version_data - ) - self._operations.create_entity(project_name, "version", version_doc) - - self._version_doc = version_doc - - def integrate_representations(self): - try: - self._integrate_representations() - except Exception: - self._operations.clear() - self._file_transaction.rollback() - raise - - def _integrate_representations(self): - version_doc = self.version_doc - version_id = version_doc["_id"] - existing_repres = get_representations( - self._item.dst_project_name, - version_ids=[version_id] - ) - existing_repres_by_low_name = { - repre_doc["name"].lower(): repre_doc - for repre_doc in existing_repres - } - template_name = self.template_name - anatomy = self.anatomy - formatting_data = get_template_data( - self.project_doc, - self.asset_doc, - self.task_info.get("name"), - self.host_name - ) - formatting_data.update({ - "subset": self.subset_name, - "family": self.family, - "version": version_doc["name"] - }) - - path_template = anatomy.templates[template_name]["path"].replace( - "\\", "/" - ) - file_template = StringTemplate( - anatomy.templates[template_name]["file"] - ) - self._status.info("Preparing files to transfer") - processed_repre_items = self._prepare_file_transactions( - anatomy, template_name, formatting_data, file_template - ) - self._file_transaction.process() - self._status.info("Preparing database changes") - self._prepare_database_operations( - version_id, - processed_repre_items, - path_template, - existing_repres_by_low_name - ) - self._status.info("Finalization") - self._operations.commit() - self._file_transaction.finalize() - - def _prepare_file_transactions( - self, anatomy, template_name, formatting_data, file_template - ): - processed_repre_items = [] - for repre_item in self.src_repre_items: - repre_doc = repre_item.repre_doc - repre_name = repre_doc["name"] - repre_format_data = copy.deepcopy(formatting_data) - repre_format_data["representation"] = repre_name - for src_file in repre_item.src_files: - ext = os.path.splitext(src_file.path)[-1] - repre_format_data["ext"] = ext[1:] - break - - # Re-use 'output' from source representation - repre_output_name = repre_doc["context"].get("output") - if repre_output_name is not None: - repre_format_data["output"] = repre_output_name - - template_obj = anatomy.templates_obj[template_name]["folder"] - folder_path = template_obj.format_strict(formatting_data) - repre_context = folder_path.used_values - folder_path_rootless = folder_path.rootless - repre_filepaths = [] - published_path = None - for src_file in repre_item.src_files: - file_data = copy.deepcopy(repre_format_data) - frame = src_file.frame - if frame is not None: - file_data["frame"] = frame - - udim = src_file.udim - if udim is not None: - file_data["udim"] = udim - - filename = file_template.format_strict(file_data) - dst_filepath = os.path.normpath( - os.path.join(folder_path, filename) - ) - dst_rootless_path = os.path.normpath( - os.path.join(folder_path_rootless, filename) - ) - if published_path is None or frame == repre_item.frame: - published_path = dst_filepath - repre_context.update(filename.used_values) - - repre_filepaths.append((dst_filepath, dst_rootless_path)) - self._file_transaction.add(src_file.path, dst_filepath) - - for resource_file in repre_item.resource_files: - dst_filepath = os.path.normpath( - os.path.join(folder_path, resource_file.relative_path) - ) - dst_rootless_path = os.path.normpath( - os.path.join( - folder_path_rootless, resource_file.relative_path - ) - ) - repre_filepaths.append((dst_filepath, dst_rootless_path)) - self._file_transaction.add(resource_file.path, dst_filepath) - processed_repre_items.append( - (repre_item, repre_filepaths, repre_context, published_path) - ) - return processed_repre_items - - def _prepare_database_operations( - self, - version_id, - processed_repre_items, - path_template, - existing_repres_by_low_name - ): - modules_manager = ModulesManager() - sync_server_module = modules_manager.get("sync_server") - if sync_server_module is None or not sync_server_module.enabled: - sites = [{ - "name": "studio", - "created_dt": datetime.datetime.now() - }] - else: - sites = sync_server_module.compute_resource_sync_sites( - project_name=self._item.dst_project_name - ) - - added_repre_names = set() - for item in processed_repre_items: - (repre_item, repre_filepaths, repre_context, published_path) = item - repre_name = repre_item.repre_doc["name"] - added_repre_names.add(repre_name.lower()) - new_repre_data = { - "path": published_path, - "template": path_template - } - new_repre_files = [] - for (path, rootless_path) in repre_filepaths: - new_repre_files.append({ - "_id": ObjectId(), - "path": rootless_path, - "size": os.path.getsize(path), - "hash": source_hash(path), - "sites": sites - }) - - existing_repre = existing_repres_by_low_name.get( - repre_name.lower() - ) - entity_id = None - if existing_repre: - entity_id = existing_repre["_id"] - new_repre_doc = new_representation_doc( - repre_name, - version_id, - repre_context, - data=new_repre_data, - entity_id=entity_id - ) - new_repre_doc["files"] = new_repre_files - if not existing_repre: - self._operations.create_entity( - self._item.dst_project_name, - new_repre_doc["type"], - new_repre_doc - ) - else: - update_data = prepare_representation_update_data( - existing_repre, new_repre_doc - ) - if update_data: - self._operations.update_entity( - self._item.dst_project_name, - new_repre_doc["type"], - new_repre_doc["_id"], - update_data - ) - - existing_repre_names = set(existing_repres_by_low_name.keys()) - for repre_name in (existing_repre_names - added_repre_names): - repre_doc = existing_repres_by_low_name[repre_name] - self._operations.update_entity( - self._item.dst_project_name, - repre_doc["type"], - repre_doc["_id"], - {"type": "archived_representation"} - ) - - def process(self): - try: - self._status.info("Process started") - self.fill_source_variables() - self._status.info("Source entities were found") - self.fill_destination_project() - self._status.info("Destination project was found") - self.fill_or_create_destination_asset() - self._status.info("Destination asset was determined") - self.determine_family() - self.determine_publish_template_name() - self.determine_subset_name() - self.make_sure_subset_exists() - self.make_sure_version_exists() - self._status.info("Prerequirements were prepared") - self.integrate_representations() - self._status.info("Integration finished") - - except PushToProjectError as exc: - if not self._status.failed: - self._status.set_failed(str(exc)) - - except Exception as exc: - _exc, _value, _tb = sys.exc_info() - self._status.set_failed( - "Unhandled error happened: {}".format(str(exc)), - (_exc, _value, _tb) - ) - - finally: - self._status.set_finished() diff --git a/client/ayon_core/tools/ayon_push_to_project/main.py b/client/ayon_core/tools/push_to_project/main.py similarity index 89% rename from client/ayon_core/tools/ayon_push_to_project/main.py rename to client/ayon_core/tools/push_to_project/main.py index 5a340656fb..50112b4b62 100644 --- a/client/ayon_core/tools/ayon_push_to_project/main.py +++ b/client/ayon_core/tools/push_to_project/main.py @@ -1,7 +1,7 @@ import click from ayon_core.tools.utils import get_openpype_qt_app -from ayon_core.tools.ayon_push_to_project.ui import PushToContextSelectWindow +from ayon_core.tools.push_to_project.ui import PushToContextSelectWindow def main_show(project_name, version_id): diff --git a/client/ayon_core/tools/ayon_push_to_project/models/__init__.py b/client/ayon_core/tools/push_to_project/models/__init__.py similarity index 100% rename from client/ayon_core/tools/ayon_push_to_project/models/__init__.py rename to client/ayon_core/tools/push_to_project/models/__init__.py diff --git a/client/ayon_core/tools/ayon_push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py similarity index 100% rename from client/ayon_core/tools/ayon_push_to_project/models/integrate.py rename to client/ayon_core/tools/push_to_project/models/integrate.py diff --git a/client/ayon_core/tools/ayon_push_to_project/models/selection.py b/client/ayon_core/tools/push_to_project/models/selection.py similarity index 100% rename from client/ayon_core/tools/ayon_push_to_project/models/selection.py rename to client/ayon_core/tools/push_to_project/models/selection.py diff --git a/client/ayon_core/tools/ayon_push_to_project/models/user_values.py b/client/ayon_core/tools/push_to_project/models/user_values.py similarity index 100% rename from client/ayon_core/tools/ayon_push_to_project/models/user_values.py rename to client/ayon_core/tools/push_to_project/models/user_values.py diff --git a/client/ayon_core/tools/ayon_push_to_project/ui/__init__.py b/client/ayon_core/tools/push_to_project/ui/__init__.py similarity index 100% rename from client/ayon_core/tools/ayon_push_to_project/ui/__init__.py rename to client/ayon_core/tools/push_to_project/ui/__init__.py diff --git a/client/ayon_core/tools/ayon_push_to_project/ui/window.py b/client/ayon_core/tools/push_to_project/ui/window.py similarity index 99% rename from client/ayon_core/tools/ayon_push_to_project/ui/window.py rename to client/ayon_core/tools/push_to_project/ui/window.py index d31781dba7..4d39075dc3 100644 --- a/client/ayon_core/tools/ayon_push_to_project/ui/window.py +++ b/client/ayon_core/tools/push_to_project/ui/window.py @@ -11,7 +11,7 @@ from ayon_core.tools.ayon_utils.widgets import ( FoldersWidget, TasksWidget, ) -from ayon_core.tools.ayon_push_to_project.control import ( +from ayon_core.tools.push_to_project.control import ( PushToContextController, ) diff --git a/client/ayon_core/tools/push_to_project/window.py b/client/ayon_core/tools/push_to_project/window.py deleted file mode 100644 index e3c49f8e89..0000000000 --- a/client/ayon_core/tools/push_to_project/window.py +++ /dev/null @@ -1,830 +0,0 @@ -import collections - -from qtpy import QtWidgets, QtGui, QtCore - -from ayon_core.style import load_stylesheet, get_app_icon_path -from ayon_core.tools.utils import ( - PlaceholderLineEdit, - SeparatorWidget, - get_asset_icon_by_name, - set_style_property, -) -from ayon_core.tools.utils.views import DeselectableTreeView - -from .control_context import PushToContextController - -PROJECT_NAME_ROLE = QtCore.Qt.UserRole + 1 -ASSET_NAME_ROLE = QtCore.Qt.UserRole + 2 -ASSET_ID_ROLE = QtCore.Qt.UserRole + 3 -TASK_NAME_ROLE = QtCore.Qt.UserRole + 4 -TASK_TYPE_ROLE = QtCore.Qt.UserRole + 5 - - -class ProjectsModel(QtGui.QStandardItemModel): - empty_text = "< Empty >" - refreshing_text = "< Refreshing >" - select_project_text = "< Select Project >" - - refreshed = QtCore.Signal() - - def __init__(self, controller): - super(ProjectsModel, self).__init__() - self._controller = controller - - self.event_system.add_callback( - "projects.refresh.finished", self._on_refresh_finish - ) - - placeholder_item = QtGui.QStandardItem(self.empty_text) - - root_item = self.invisibleRootItem() - root_item.appendRows([placeholder_item]) - items = {None: placeholder_item} - - self._placeholder_item = placeholder_item - self._items = items - - @property - def event_system(self): - return self._controller.event_system - - def _on_refresh_finish(self): - root_item = self.invisibleRootItem() - project_names = self._controller.model.get_projects() - - if not project_names: - placeholder_text = self.empty_text - else: - placeholder_text = self.select_project_text - self._placeholder_item.setData(placeholder_text, QtCore.Qt.DisplayRole) - - new_items = [] - if None not in self._items: - new_items.append(self._placeholder_item) - - current_project_names = set(self._items.keys()) - for project_name in current_project_names - set(project_names): - if project_name is None: - continue - item = self._items.pop(project_name) - root_item.takeRow(item.row()) - - for project_name in project_names: - if project_name in self._items: - continue - item = QtGui.QStandardItem(project_name) - item.setData(project_name, PROJECT_NAME_ROLE) - new_items.append(item) - - if new_items: - root_item.appendRows(new_items) - self.refreshed.emit() - - -class ProjectProxyModel(QtCore.QSortFilterProxyModel): - def __init__(self): - super(ProjectProxyModel, self).__init__() - self._filter_empty_projects = False - - def set_filter_empty_project(self, filter_empty_projects): - if filter_empty_projects == self._filter_empty_projects: - return - self._filter_empty_projects = filter_empty_projects - self.invalidate() - - def filterAcceptsRow(self, row, parent): - if not self._filter_empty_projects: - return True - model = self.sourceModel() - source_index = model.index(row, self.filterKeyColumn(), parent) - if model.data(source_index, PROJECT_NAME_ROLE) is None: - return False - return True - - -class AssetsModel(QtGui.QStandardItemModel): - items_changed = QtCore.Signal() - empty_text = "< Empty >" - - def __init__(self, controller): - super(AssetsModel, self).__init__() - self._controller = controller - - placeholder_item = QtGui.QStandardItem(self.empty_text) - placeholder_item.setFlags(QtCore.Qt.ItemIsEnabled) - - root_item = self.invisibleRootItem() - root_item.appendRows([placeholder_item]) - - self.event_system.add_callback( - "project.changed", self._on_project_change - ) - self.event_system.add_callback( - "assets.refresh.started", self._on_refresh_start - ) - self.event_system.add_callback( - "assets.refresh.finished", self._on_refresh_finish - ) - - self._items = {None: placeholder_item} - - self._placeholder_item = placeholder_item - self._last_project = None - - @property - def event_system(self): - return self._controller.event_system - - def _clear(self): - placeholder_in = False - root_item = self.invisibleRootItem() - for row in reversed(range(root_item.rowCount())): - item = root_item.child(row) - asset_id = item.data(ASSET_ID_ROLE) - if asset_id is None: - placeholder_in = True - continue - root_item.removeRow(item.row()) - - for key in tuple(self._items.keys()): - if key is not None: - self._items.pop(key) - - if not placeholder_in: - root_item.appendRows([self._placeholder_item]) - self._items[None] = self._placeholder_item - - def _on_project_change(self, event): - project_name = event["project_name"] - if project_name == self._last_project: - return - - self._last_project = project_name - self._clear() - self.items_changed.emit() - - def _on_refresh_start(self, event): - pass - - def _on_refresh_finish(self, event): - event_project_name = event["project_name"] - project_name = self._controller.selection_model.project_name - if event_project_name != project_name: - return - - self._last_project = event["project_name"] - if project_name is None: - if None not in self._items: - self._clear() - self.items_changed.emit() - return - - asset_items_by_id = self._controller.model.get_assets(project_name) - if not asset_items_by_id: - self._clear() - self.items_changed.emit() - return - - assets_by_parent_id = collections.defaultdict(list) - for asset_item in asset_items_by_id.values(): - assets_by_parent_id[asset_item.parent_id].append(asset_item) - - root_item = self.invisibleRootItem() - if None in self._items: - self._items.pop(None) - root_item.takeRow(self._placeholder_item.row()) - - items_to_remove = set(self._items) - set(asset_items_by_id.keys()) - hierarchy_queue = collections.deque() - hierarchy_queue.append((None, root_item)) - while hierarchy_queue: - parent_id, parent_item = hierarchy_queue.popleft() - new_items = [] - for asset_item in assets_by_parent_id[parent_id]: - item = self._items.get(asset_item.id) - if item is None: - item = QtGui.QStandardItem() - item.setFlags( - QtCore.Qt.ItemIsSelectable - | QtCore.Qt.ItemIsEnabled - ) - new_items.append(item) - self._items[asset_item.id] = item - - elif item.parent() is not parent_item: - new_items.append(item) - - icon = get_asset_icon_by_name( - asset_item.icon_name, asset_item.icon_color - ) - item.setData(asset_item.name, QtCore.Qt.DisplayRole) - item.setData(icon, QtCore.Qt.DecorationRole) - item.setData(asset_item.id, ASSET_ID_ROLE) - - hierarchy_queue.append((asset_item.id, item)) - - if new_items: - parent_item.appendRows(new_items) - - for item_id in items_to_remove: - item = self._items.pop(item_id, None) - if item is None: - continue - row = item.row() - if row < 0: - continue - parent = item.parent() - if parent is None: - parent = root_item - parent.takeRow(row) - - self.items_changed.emit() - - -class TasksModel(QtGui.QStandardItemModel): - items_changed = QtCore.Signal() - empty_text = "< Empty >" - - def __init__(self, controller): - super(TasksModel, self).__init__() - self._controller = controller - - placeholder_item = QtGui.QStandardItem(self.empty_text) - placeholder_item.setFlags(QtCore.Qt.ItemIsEnabled) - - root_item = self.invisibleRootItem() - root_item.appendRows([placeholder_item]) - - self.event_system.add_callback( - "project.changed", self._on_project_change - ) - self.event_system.add_callback( - "assets.refresh.finished", self._on_asset_refresh_finish - ) - self.event_system.add_callback( - "asset.changed", self._on_asset_change - ) - - self._items = {None: placeholder_item} - - self._placeholder_item = placeholder_item - self._last_project = None - - @property - def event_system(self): - return self._controller.event_system - - def _clear(self): - placeholder_in = False - root_item = self.invisibleRootItem() - for row in reversed(range(root_item.rowCount())): - item = root_item.child(row) - task_name = item.data(TASK_NAME_ROLE) - if task_name is None: - placeholder_in = True - continue - root_item.removeRow(item.row()) - - for key in tuple(self._items.keys()): - if key is not None: - self._items.pop(key) - - if not placeholder_in: - root_item.appendRows([self._placeholder_item]) - self._items[None] = self._placeholder_item - - def _on_project_change(self, event): - project_name = event["project_name"] - if project_name == self._last_project: - return - - self._last_project = project_name - self._clear() - self.items_changed.emit() - - def _on_asset_refresh_finish(self, event): - self._refresh(event["project_name"]) - - def _on_asset_change(self, event): - self._refresh(event["project_name"]) - - def _refresh(self, new_project_name): - project_name = self._controller.selection_model.project_name - if new_project_name != project_name: - return - - self._last_project = project_name - if project_name is None: - if None not in self._items: - self._clear() - self.items_changed.emit() - return - - asset_id = self._controller.selection_model.asset_id - task_items = self._controller.model.get_tasks( - project_name, asset_id - ) - if not task_items: - self._clear() - self.items_changed.emit() - return - - root_item = self.invisibleRootItem() - if None in self._items: - self._items.pop(None) - root_item.takeRow(self._placeholder_item.row()) - - new_items = [] - task_names = set() - for task_item in task_items: - task_name = task_item.name - item = self._items.get(task_name) - if item is None: - item = QtGui.QStandardItem() - item.setFlags( - QtCore.Qt.ItemIsSelectable - | QtCore.Qt.ItemIsEnabled - ) - new_items.append(item) - self._items[task_name] = item - - item.setData(task_name, QtCore.Qt.DisplayRole) - item.setData(task_name, TASK_NAME_ROLE) - item.setData(task_item.task_type, TASK_TYPE_ROLE) - - if new_items: - root_item.appendRows(new_items) - - items_to_remove = set(self._items) - task_names - for item_id in items_to_remove: - item = self._items.pop(item_id, None) - if item is None: - continue - parent = item.parent() - if parent is not None: - parent.removeRow(item.row()) - - self.items_changed.emit() - - -class PushToContextSelectWindow(QtWidgets.QWidget): - def __init__(self, controller=None): - super(PushToContextSelectWindow, self).__init__() - if controller is None: - controller = PushToContextController() - self._controller = controller - - self.setWindowTitle("Push to project (select context)") - self.setWindowIcon(QtGui.QIcon(get_app_icon_path())) - - main_context_widget = QtWidgets.QWidget(self) - - header_widget = QtWidgets.QWidget(main_context_widget) - - header_label = QtWidgets.QLabel(controller.src_label, header_widget) - - header_layout = QtWidgets.QHBoxLayout(header_widget) - header_layout.setContentsMargins(0, 0, 0, 0) - header_layout.addWidget(header_label) - - main_splitter = QtWidgets.QSplitter( - QtCore.Qt.Horizontal, main_context_widget - ) - - context_widget = QtWidgets.QWidget(main_splitter) - - project_combobox = QtWidgets.QComboBox(context_widget) - project_model = ProjectsModel(controller) - project_proxy = ProjectProxyModel() - project_proxy.setSourceModel(project_model) - project_proxy.setDynamicSortFilter(True) - project_delegate = QtWidgets.QStyledItemDelegate() - project_combobox.setItemDelegate(project_delegate) - project_combobox.setModel(project_proxy) - - asset_task_splitter = QtWidgets.QSplitter( - QtCore.Qt.Vertical, context_widget - ) - - asset_view = DeselectableTreeView(asset_task_splitter) - asset_view.setHeaderHidden(True) - asset_model = AssetsModel(controller) - asset_proxy = QtCore.QSortFilterProxyModel() - asset_proxy.setSourceModel(asset_model) - asset_proxy.setDynamicSortFilter(True) - asset_view.setModel(asset_proxy) - - task_view = QtWidgets.QListView(asset_task_splitter) - task_proxy = QtCore.QSortFilterProxyModel() - task_model = TasksModel(controller) - task_proxy.setSourceModel(task_model) - task_proxy.setDynamicSortFilter(True) - task_view.setModel(task_proxy) - - asset_task_splitter.addWidget(asset_view) - asset_task_splitter.addWidget(task_view) - - context_layout = QtWidgets.QVBoxLayout(context_widget) - context_layout.setContentsMargins(0, 0, 0, 0) - context_layout.addWidget(project_combobox, 0) - context_layout.addWidget(asset_task_splitter, 1) - - # --- Inputs widget --- - inputs_widget = QtWidgets.QWidget(main_splitter) - - asset_name_input = PlaceholderLineEdit(inputs_widget) - asset_name_input.setPlaceholderText("< Name of new asset >") - asset_name_input.setObjectName("ValidatedLineEdit") - - variant_input = PlaceholderLineEdit(inputs_widget) - variant_input.setPlaceholderText("< Variant >") - variant_input.setObjectName("ValidatedLineEdit") - - comment_input = PlaceholderLineEdit(inputs_widget) - comment_input.setPlaceholderText("< Publish comment >") - - inputs_layout = QtWidgets.QFormLayout(inputs_widget) - inputs_layout.setContentsMargins(0, 0, 0, 0) - inputs_layout.addRow("New asset name", asset_name_input) - inputs_layout.addRow("Variant", variant_input) - inputs_layout.addRow("Comment", comment_input) - - main_splitter.addWidget(context_widget) - main_splitter.addWidget(inputs_widget) - - # --- Buttons widget --- - btns_widget = QtWidgets.QWidget(self) - cancel_btn = QtWidgets.QPushButton("Cancel", btns_widget) - publish_btn = QtWidgets.QPushButton("Publish", btns_widget) - - btns_layout = QtWidgets.QHBoxLayout(btns_widget) - btns_layout.setContentsMargins(0, 0, 0, 0) - btns_layout.addStretch(1) - btns_layout.addWidget(cancel_btn, 0) - btns_layout.addWidget(publish_btn, 0) - - sep_1 = SeparatorWidget(parent=main_context_widget) - sep_2 = SeparatorWidget(parent=main_context_widget) - main_context_layout = QtWidgets.QVBoxLayout(main_context_widget) - main_context_layout.addWidget(header_widget, 0) - main_context_layout.addWidget(sep_1, 0) - main_context_layout.addWidget(main_splitter, 1) - main_context_layout.addWidget(sep_2, 0) - main_context_layout.addWidget(btns_widget, 0) - - # NOTE This was added in hurry - # - should be reorganized and changed styles - overlay_widget = QtWidgets.QFrame(self) - overlay_widget.setObjectName("OverlayFrame") - - overlay_label = QtWidgets.QLabel(overlay_widget) - overlay_label.setAlignment(QtCore.Qt.AlignCenter) - - overlay_btns_widget = QtWidgets.QWidget(overlay_widget) - overlay_btns_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) - - # Add try again button (requires changes in controller) - overlay_try_btn = QtWidgets.QPushButton( - "Try again", overlay_btns_widget - ) - overlay_close_btn = QtWidgets.QPushButton( - "Close", overlay_btns_widget - ) - - overlay_btns_layout = QtWidgets.QHBoxLayout(overlay_btns_widget) - overlay_btns_layout.addStretch(1) - overlay_btns_layout.addWidget(overlay_try_btn, 0) - overlay_btns_layout.addWidget(overlay_close_btn, 0) - overlay_btns_layout.addStretch(1) - - overlay_layout = QtWidgets.QVBoxLayout(overlay_widget) - overlay_layout.addWidget(overlay_label, 0) - overlay_layout.addWidget(overlay_btns_widget, 0) - overlay_layout.setAlignment(QtCore.Qt.AlignCenter) - - main_layout = QtWidgets.QStackedLayout(self) - main_layout.setContentsMargins(0, 0, 0, 0) - main_layout.addWidget(main_context_widget) - main_layout.addWidget(overlay_widget) - main_layout.setStackingMode(QtWidgets.QStackedLayout.StackAll) - main_layout.setCurrentWidget(main_context_widget) - - show_timer = QtCore.QTimer() - show_timer.setInterval(1) - - main_thread_timer = QtCore.QTimer() - main_thread_timer.setInterval(10) - - user_input_changed_timer = QtCore.QTimer() - user_input_changed_timer.setInterval(200) - user_input_changed_timer.setSingleShot(True) - - main_thread_timer.timeout.connect(self._on_main_thread_timer) - show_timer.timeout.connect(self._on_show_timer) - user_input_changed_timer.timeout.connect(self._on_user_input_timer) - asset_name_input.textChanged.connect(self._on_new_asset_change) - variant_input.textChanged.connect(self._on_variant_change) - comment_input.textChanged.connect(self._on_comment_change) - project_model.refreshed.connect(self._on_projects_refresh) - project_combobox.currentIndexChanged.connect(self._on_project_change) - asset_view.selectionModel().selectionChanged.connect( - self._on_asset_change - ) - asset_model.items_changed.connect(self._on_asset_model_change) - task_view.selectionModel().selectionChanged.connect( - self._on_task_change - ) - task_model.items_changed.connect(self._on_task_model_change) - publish_btn.clicked.connect(self._on_select_click) - cancel_btn.clicked.connect(self._on_close_click) - overlay_close_btn.clicked.connect(self._on_close_click) - overlay_try_btn.clicked.connect(self._on_try_again_click) - - controller.event_system.add_callback( - "new_asset_name.changed", self._on_controller_new_asset_change - ) - controller.event_system.add_callback( - "variant.changed", self._on_controller_variant_change - ) - controller.event_system.add_callback( - "comment.changed", self._on_controller_comment_change - ) - controller.event_system.add_callback( - "submission.enabled.changed", self._on_submission_change - ) - controller.event_system.add_callback( - "source.changed", self._on_controller_source_change - ) - controller.event_system.add_callback( - "submit.started", self._on_controller_submit_start - ) - controller.event_system.add_callback( - "submit.finished", self._on_controller_submit_end - ) - controller.event_system.add_callback( - "push.message.added", self._on_push_message - ) - - self._main_layout = main_layout - - self._main_context_widget = main_context_widget - - self._header_label = header_label - self._main_splitter = main_splitter - - self._project_combobox = project_combobox - self._project_model = project_model - self._project_proxy = project_proxy - self._project_delegate = project_delegate - - self._asset_view = asset_view - self._asset_model = asset_model - self._asset_proxy_model = asset_proxy - - self._task_view = task_view - self._task_proxy_model = task_proxy - - self._variant_input = variant_input - self._asset_name_input = asset_name_input - self._comment_input = comment_input - - self._publish_btn = publish_btn - - self._overlay_widget = overlay_widget - self._overlay_close_btn = overlay_close_btn - self._overlay_try_btn = overlay_try_btn - self._overlay_label = overlay_label - - self._user_input_changed_timer = user_input_changed_timer - # Store current value on input text change - # The value is unset when is passed to controller - # The goal is to have controll over changes happened during user change - # in UI and controller auto-changes - self._variant_input_text = None - self._new_asset_name_input_text = None - self._comment_input_text = None - self._show_timer = show_timer - self._show_counter = 2 - self._first_show = True - - self._main_thread_timer = main_thread_timer - self._main_thread_timer_can_stop = True - self._last_submit_message = None - self._process_item = None - - publish_btn.setEnabled(False) - overlay_close_btn.setVisible(False) - overlay_try_btn.setVisible(False) - - if controller.user_values.new_asset_name: - asset_name_input.setText(controller.user_values.new_asset_name) - if controller.user_values.variant: - variant_input.setText(controller.user_values.variant) - self._invalidate_variant() - self._invalidate_new_asset_name() - - @property - def controller(self): - return self._controller - - def showEvent(self, event): - super(PushToContextSelectWindow, self).showEvent(event) - if self._first_show: - self._first_show = False - self.setStyleSheet(load_stylesheet()) - self._invalidate_variant() - self._show_timer.start() - - def _on_show_timer(self): - if self._show_counter == 0: - self._show_timer.stop() - return - - self._show_counter -= 1 - if self._show_counter == 1: - width = 740 - height = 640 - inputs_width = 360 - self.resize(width, height) - self._main_splitter.setSizes([width - inputs_width, inputs_width]) - - if self._show_counter > 0: - return - - self._controller.model.refresh_projects() - - def _on_new_asset_change(self, text): - self._new_asset_name_input_text = text - self._user_input_changed_timer.start() - - def _on_variant_change(self, text): - self._variant_input_text = text - self._user_input_changed_timer.start() - - def _on_comment_change(self, text): - self._comment_input_text = text - self._user_input_changed_timer.start() - - def _on_user_input_timer(self): - asset_name = self._new_asset_name_input_text - if asset_name is not None: - self._new_asset_name_input_text = None - self._controller.user_values.set_new_asset(asset_name) - - variant = self._variant_input_text - if variant is not None: - self._variant_input_text = None - self._controller.user_values.set_variant(variant) - - comment = self._comment_input_text - if comment is not None: - self._comment_input_text = None - self._controller.user_values.set_comment(comment) - - def _on_controller_new_asset_change(self, event): - asset_name = event["changes"]["new_asset_name"]["new"] - if ( - self._new_asset_name_input_text is None - and asset_name != self._asset_name_input.text() - ): - self._asset_name_input.setText(asset_name) - - self._invalidate_new_asset_name() - - def _on_controller_variant_change(self, event): - is_valid_changes = event["changes"]["is_valid"] - variant = event["changes"]["variant"]["new"] - if ( - self._variant_input_text is None - and variant != self._variant_input.text() - ): - self._variant_input.setText(variant) - - if is_valid_changes["old"] != is_valid_changes["new"]: - self._invalidate_variant() - - def _on_controller_comment_change(self, event): - comment = event["comment"] - if ( - self._comment_input_text is None - and comment != self._comment_input.text() - ): - self._comment_input.setText(comment) - - def _on_controller_source_change(self): - self._header_label.setText(self._controller.src_label) - - def _invalidate_new_asset_name(self): - asset_name = self._controller.user_values.new_asset_name - self._task_view.setVisible(not asset_name) - - valid = None - if asset_name: - valid = self._controller.user_values.is_new_asset_name_valid - - state = "" - if valid is True: - state = "valid" - elif valid is False: - state = "invalid" - set_style_property(self._asset_name_input, "state", state) - - def _invalidate_variant(self): - valid = self._controller.user_values.is_variant_valid - state = "invalid" - if valid is True: - state = "valid" - set_style_property(self._variant_input, "state", state) - - def _on_projects_refresh(self): - self._project_proxy.sort(0, QtCore.Qt.AscendingOrder) - - def _on_project_change(self): - idx = self._project_combobox.currentIndex() - if idx < 0: - self._project_proxy.set_filter_empty_project(False) - return - - project_name = self._project_combobox.itemData(idx, PROJECT_NAME_ROLE) - self._project_proxy.set_filter_empty_project(project_name is not None) - self._controller.selection_model.select_project(project_name) - - def _on_asset_change(self): - indexes = self._asset_view.selectedIndexes() - index = next(iter(indexes), None) - asset_id = None - if index is not None: - model = self._asset_view.model() - asset_id = model.data(index, ASSET_ID_ROLE) - self._controller.selection_model.select_asset(asset_id) - - def _on_asset_model_change(self): - self._asset_proxy_model.sort(0, QtCore.Qt.AscendingOrder) - - def _on_task_model_change(self): - self._task_proxy_model.sort(0, QtCore.Qt.AscendingOrder) - - def _on_task_change(self): - indexes = self._task_view.selectedIndexes() - index = next(iter(indexes), None) - task_name = None - if index is not None: - model = self._task_view.model() - task_name = model.data(index, TASK_NAME_ROLE) - self._controller.selection_model.select_task(task_name) - - def _on_submission_change(self, event): - self._publish_btn.setEnabled(event["enabled"]) - - def _on_close_click(self): - self.close() - - def _on_select_click(self): - self._process_item = self._controller.submit(wait=False) - - def _on_try_again_click(self): - self._process_item = None - self._last_submit_message = None - - self._overlay_close_btn.setVisible(False) - self._overlay_try_btn.setVisible(False) - self._main_layout.setCurrentWidget(self._main_context_widget) - - def _on_main_thread_timer(self): - if self._last_submit_message: - self._overlay_label.setText(self._last_submit_message) - self._last_submit_message = None - - process_status = self._process_item.status - push_failed = process_status.failed - fail_traceback = process_status.traceback - if self._main_thread_timer_can_stop: - self._main_thread_timer.stop() - self._overlay_close_btn.setVisible(True) - if push_failed and not fail_traceback: - self._overlay_try_btn.setVisible(True) - - if push_failed: - message = "Push Failed:\n{}".format(process_status.fail_reason) - if fail_traceback: - message += "\n{}".format(fail_traceback) - self._overlay_label.setText(message) - set_style_property(self._overlay_close_btn, "state", "error") - - if self._main_thread_timer_can_stop: - # Join thread in controller - self._controller.wait_for_process_thread() - # Reset process item to None - self._process_item = None - - def _on_controller_submit_start(self): - self._main_thread_timer_can_stop = False - self._main_thread_timer.start() - self._main_layout.setCurrentWidget(self._overlay_widget) - self._overlay_label.setText("Submittion started") - - def _on_controller_submit_end(self): - self._main_thread_timer_can_stop = True - - def _on_push_message(self, event): - self._last_submit_message = event["message"] diff --git a/client/ayon_core/tools/sceneinventory/__init__.py b/client/ayon_core/tools/sceneinventory/__init__.py index 410b52e5fe..5412e2fea2 100644 --- a/client/ayon_core/tools/sceneinventory/__init__.py +++ b/client/ayon_core/tools/sceneinventory/__init__.py @@ -1,9 +1,6 @@ -from .window import ( - show, - SceneInventoryWindow -) +from .control import SceneInventoryController + __all__ = ( - "show", - "SceneInventoryWindow" + "SceneInventoryController", ) diff --git a/client/ayon_core/tools/ayon_sceneinventory/control.py b/client/ayon_core/tools/sceneinventory/control.py similarity index 100% rename from client/ayon_core/tools/ayon_sceneinventory/control.py rename to client/ayon_core/tools/sceneinventory/control.py diff --git a/client/ayon_core/tools/sceneinventory/lib.py b/client/ayon_core/tools/sceneinventory/lib.py deleted file mode 100644 index 0ac7622d65..0000000000 --- a/client/ayon_core/tools/sceneinventory/lib.py +++ /dev/null @@ -1,8 +0,0 @@ -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/client/ayon_core/tools/sceneinventory/model.py b/client/ayon_core/tools/sceneinventory/model.py index 09078950e5..05ecfd442d 100644 --- a/client/ayon_core/tools/sceneinventory/model.py +++ b/client/ayon_core/tools/sceneinventory/model.py @@ -1,37 +1,57 @@ +import collections import re import logging +import uuid +import copy from collections import defaultdict from qtpy import QtCore, QtGui import qtawesome -from ayon_core.host import ILoadHost from ayon_core.client import ( - get_asset_by_id, - get_subset_by_id, - get_version_by_id, + get_assets, + get_subsets, + get_versions, get_last_version_by_subset_id, - get_representation_by_id, + get_representations, ) from ayon_core.pipeline import ( get_current_project_name, schema, HeroVersionType, - registered_host, ) from ayon_core.style import get_default_entity_icon_color from ayon_core.tools.utils.models import TreeModel, Item -from ayon_core.modules import ModulesManager +from ayon_core.tools.ayon_utils.widgets import get_qt_icon -from .lib import walk_hierarchy + +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 class InventoryModel(TreeModel): """The model for the inventory""" - Columns = ["Name", "version", "count", "family", - "group", "loader", "objectName"] + Columns = [ + "Name", + "version", + "count", + "family", + "group", + "loader", + "objectName", + "active_site", + "remote_site", + ] + active_site_col = Columns.index("active_site") + remote_site_col = Columns.index("remote_site") OUTDATED_COLOR = QtGui.QColor(235, 30, 30) CHILD_OUTDATED_COLOR = QtGui.QColor(200, 160, 30) @@ -39,58 +59,22 @@ class InventoryModel(TreeModel): UniqueRole = QtCore.Qt.UserRole + 2 # unique label role - def __init__(self, family_config_cache, parent=None): + def __init__(self, controller, parent=None): super(InventoryModel, self).__init__(parent) self.log = logging.getLogger(self.__class__.__name__) - self.family_config_cache = family_config_cache + self._controller = controller self._hierarchy_view = False self._default_icon_color = get_default_entity_icon_color() - manager = ModulesManager() - sync_server = manager.modules_by_name.get("sync_server") - self.sync_enabled = ( - sync_server is not None and sync_server.enabled - ) - self._site_icons = {} - self.active_site = self.remote_site = None - self.active_provider = self.remote_provider = None + site_icons = self._controller.get_site_provider_icons() - if not self.sync_enabled: - return - - project_name = get_current_project_name() - active_site = sync_server.get_active_site(project_name) - remote_site = sync_server.get_remote_site(project_name) - - 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 - ) - - 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._site_icons = { - provider: QtGui.QIcon(icon_path) - for provider, icon_path in sync_server.get_site_icons().items() + provider: get_qt_icon(icon_def) + for provider, icon_def in site_icons.items() } - if "active_site" not in self.Columns: - self.Columns.append("active_site") - if "remote_site" not in self.Columns: - self.Columns.append("remote_site") def outdated(self, item): value = item.get("version") @@ -177,9 +161,9 @@ class InventoryModel(TreeModel): if role == QtCore.Qt.DisplayRole and item.get("isGroupNode"): column_name = self.Columns[index.column()] progress = None - if column_name == 'active_site': + if column_name == "active_site": progress = item.get("active_site_progress", 0) - elif column_name == 'remote_site': + elif column_name == "remote_site": progress = item.get("remote_site_progress", 0) if progress is not None: return "{}%".format(max(progress, 0) * 100) @@ -196,109 +180,26 @@ class InventoryModel(TreeModel): if state != self._hierarchy_view: self._hierarchy_view = state - def refresh(self, selected=None, items=None): + def refresh(self, selected=None, containers=None): """Refresh the model""" - host = registered_host() # for debugging or testing, injecting items from outside - if items is None: - if isinstance(host, ILoadHost): - items = host.get_containers() - elif hasattr(host, "ls"): - items = host.ls() - else: - items = [] + if containers is None: + containers = self._controller.get_containers() self.clear() if not selected or not self._hierarchy_view: - self.add_items(items) + self._add_containers(containers) return - if ( - not hasattr(host, "pipeline") - or 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 - )) - return + # Filter by cherry-picked items + self._add_containers(( + container + for container in containers + if container["objectName"] in selected + )) - # TODO find out what this part does. Function 'update_hierarchy' is - # available only in 'blender' at this moment. - - # 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 - - def add_items(self, items, parent=None): + def _add_containers(self, containers, parent=None): """Add the items to the model. The items should be formatted similar to `api.ls()` returns, an item @@ -313,7 +214,7 @@ class InventoryModel(TreeModel): same type. Args: - items (generator): the items to be processed as returned by `ls()` + containers (generator): Container items. parent (Item, optional): Set this item as parent for the added items when provided. Defaults to the root of the model. @@ -321,109 +222,109 @@ class InventoryModel(TreeModel): node.Item: root node which has children added based on the data """ - # NOTE: @iLLiCiTiT this need refactor project_name = get_current_project_name() self.beginResetModel() # Group by representation - grouped = defaultdict(lambda: {"items": list()}) - for item in items: - grouped[item["representation"]]["items"].append(item) + grouped = defaultdict(lambda: {"containers": list()}) + for container in containers: + repre_id = container["representation"] + grouped[repre_id]["containers"].append(container) + ( + repres_by_id, + versions_by_id, + products_by_id, + folders_by_id, + ) = self._query_entities(project_name, set(grouped.keys())) # 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 = get_representation_by_id( - project_name, repre_id - ) + group_containers = group_dict["containers"] + representation = repres_by_id.get(repre_id) if not representation: - not_found["representation"].extend(group_items) + not_found["representation"].extend(group_containers) not_found_ids.append(repre_id) continue - version = get_version_by_id( - project_name, representation["parent"] - ) + version = versions_by_id.get(representation["parent"]) if not version: - not_found["version"].extend(group_items) + not_found["version"].extend(group_containers) not_found_ids.append(repre_id) continue - elif version["type"] == "hero_version": - _version = get_version_by_id( - project_name, version["version_id"] - ) - version["name"] = HeroVersionType(_version["name"]) - version["data"] = _version["data"] - - subset = get_subset_by_id(project_name, version["parent"]) - if not subset: - not_found["subset"].extend(group_items) + product = products_by_id.get(version["parent"]) + if not product: + not_found["product"].extend(group_containers) not_found_ids.append(repre_id) continue - asset = get_asset_by_id(project_name, subset["parent"]) - if not asset: - not_found["asset"].extend(group_items) + folder = folders_by_id.get(product["parent"]) + if not folder: + not_found["folder"].extend(group_containers) not_found_ids.append(repre_id) continue - grouped[repre_id].update({ + group_dict.update({ "representation": representation, "version": version, - "subset": subset, - "asset": asset + "subset": product, + "asset": folder }) - for id in not_found_ids: - grouped.pop(id) + for _repre_id in not_found_ids: + grouped.pop(_repre_id) - for where, group_items in not_found.items(): + for where, group_containers 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["count"] = len(group_containers) group_node["isGroupNode"] = False group_node["isNotSet"] = True self.add_child(group_node, parent=parent) - for item in group_items: + for container in group_containers: item_node = Item() - item_node.update(item) - item_node["Name"] = item.get("objectName", "NO NAME") + item_node.update(container) + item_node["Name"] = container.get("objectName", "NO NAME") item_node["isNotFound"] = True self.add_child(item_node, parent=group_node) + # TODO Use product icons + family_icon = qtawesome.icon( + "fa.folder", color="#0091B2" + ) + # Prepare site sync specific data + progress_by_id = self._controller.get_representations_site_progress( + set(grouped.keys()) + ) + sites_info = self._controller.get_sites_information() + 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"] + group_containers = group_dict["containers"] + representation = group_dict["representation"] + version = group_dict["version"] + subset = group_dict["subset"] + asset = group_dict["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 + src_doc = version else: - families = subset["data"].get("families") or [] - prim_family = families[0] if families else no_family + src_doc = subset - # 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) + prim_family = src_doc["data"].get("family") + if not prim_family: + families = src_doc["data"].get("families") + if families: + prim_family = families[0] # Store the highest available version so the model can know # whether current version is currently up-to-date. @@ -433,34 +334,29 @@ class InventoryModel(TreeModel): # create the group header group_node = Item() - group_node["Name"] = "%s_%s: (%s)" % (asset["name"], - subset["name"], - representation["name"]) + group_node["Name"] = "{}_{}: ({})".format( + 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["family"] = prim_family or "" group_node["familyIcon"] = family_icon - group_node["count"] = len(group_items) + group_node["count"] = len(group_containers) group_node["isGroupNode"] = True group_node["group"] = subset["data"].get("subsetGroup") - if self.sync_enabled: - progress = self.sync_server.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] + # Site sync specific data + progress = progress_by_id[repre_id] + group_node.update(sites_info) + group_node["active_site_progress"] = progress["active_site"] + group_node["remote_site_progress"] = progress["remote_site"] self.add_child(group_node, parent=parent) - for item in group_items: + for container in group_containers: item_node = Item() - item_node.update(item) + item_node.update(container) # store the current version on the item item_node["version"] = version["name"] @@ -468,7 +364,7 @@ class InventoryModel(TreeModel): # 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"] + item_node["Name"] = container["namespace"] self.add_child(item_node, parent=group_node) @@ -476,6 +372,108 @@ class InventoryModel(TreeModel): return self._root_item + def _query_entities(self, project_name, repre_ids): + """Query entities for representations from containers. + + Returns: + tuple[dict, dict, dict, dict]: Representation, version, product + and folder documents by id. + """ + + repres_by_id = {} + versions_by_id = {} + products_by_id = {} + folders_by_id = {} + output = ( + repres_by_id, + versions_by_id, + products_by_id, + folders_by_id, + ) + + filtered_repre_ids = set() + for repre_id in repre_ids: + # Filter out invalid representation ids + # NOTE: This is added because scenes from OpenPype did contain + # ObjectId from mongo. + try: + uuid.UUID(repre_id) + filtered_repre_ids.add(repre_id) + except ValueError: + continue + if not filtered_repre_ids: + return output + + repre_docs = get_representations(project_name, repre_ids) + repres_by_id.update({ + repre_doc["_id"]: repre_doc + for repre_doc in repre_docs + }) + version_ids = { + repre_doc["parent"] for repre_doc in repres_by_id.values() + } + if not version_ids: + return output + + version_docs = get_versions(project_name, version_ids, hero=True) + versions_by_id.update({ + version_doc["_id"]: version_doc + for version_doc in version_docs + }) + hero_versions_by_subversion_id = collections.defaultdict(list) + for version_doc in versions_by_id.values(): + if version_doc["type"] != "hero_version": + continue + subversion = version_doc["version_id"] + hero_versions_by_subversion_id[subversion].append(version_doc) + + if hero_versions_by_subversion_id: + subversion_ids = set( + hero_versions_by_subversion_id.keys() + ) + subversion_docs = get_versions(project_name, subversion_ids) + for subversion_doc in subversion_docs: + subversion_id = subversion_doc["_id"] + subversion_ids.discard(subversion_id) + h_version_docs = hero_versions_by_subversion_id[subversion_id] + for version_doc in h_version_docs: + version_doc["name"] = HeroVersionType( + subversion_doc["name"] + ) + version_doc["data"] = copy.deepcopy( + subversion_doc["data"] + ) + + for subversion_id in subversion_ids: + h_version_docs = hero_versions_by_subversion_id[subversion_id] + for version_doc in h_version_docs: + versions_by_id.pop(version_doc["_id"]) + + product_ids = { + version_doc["parent"] + for version_doc in versions_by_id.values() + } + if not product_ids: + return output + product_docs = get_subsets(project_name, product_ids) + products_by_id.update({ + product_doc["_id"]: product_doc + for product_doc in product_docs + }) + folder_ids = { + product_doc["parent"] + for product_doc in products_by_id.values() + } + if not folder_ids: + return output + + folder_docs = get_assets(project_name, folder_ids) + folders_by_id.update({ + folder_doc["_id"]: folder_doc + for folder_doc in folder_docs + }) + return output + class FilterProxyModel(QtCore.QSortFilterProxyModel): """Filter model to where key column's value is in the filtered tags""" diff --git a/client/ayon_core/tools/ayon_sceneinventory/models/__init__.py b/client/ayon_core/tools/sceneinventory/models/__init__.py similarity index 100% rename from client/ayon_core/tools/ayon_sceneinventory/models/__init__.py rename to client/ayon_core/tools/sceneinventory/models/__init__.py diff --git a/client/ayon_core/tools/ayon_sceneinventory/models/site_sync.py b/client/ayon_core/tools/sceneinventory/models/site_sync.py similarity index 100% rename from client/ayon_core/tools/ayon_sceneinventory/models/site_sync.py rename to client/ayon_core/tools/sceneinventory/models/site_sync.py diff --git a/client/ayon_core/tools/sceneinventory/switch_dialog.py b/client/ayon_core/tools/sceneinventory/switch_dialog.py deleted file mode 100644 index 34d7c9f3a3..0000000000 --- a/client/ayon_core/tools/sceneinventory/switch_dialog.py +++ /dev/null @@ -1,1355 +0,0 @@ -import collections -import logging -from qtpy import QtWidgets, QtCore -import qtawesome - -from ayon_core.client import ( - get_asset_by_name, - get_assets, - get_subset_by_name, - get_subsets, - get_versions, - get_hero_versions, - get_last_versions, - get_representations, -) -from ayon_core.pipeline import legacy_io -from ayon_core.pipeline.load import ( - discover_loader_plugins, - switch_container, - get_repres_contexts, - loaders_from_repre_context, - LoaderSwitchNotImplementedError, - IncompatibleLoaderError, - LoaderNotFoundError -) - -from .widgets import ( - ButtonWithMenu, - SearchComboBox -) - -log = logging.getLogger("SwitchAssetDialog") - - -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 = ButtonWithMenu(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) - 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 - ) - 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.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() - - self.content_loaders = set() - self.content_assets = {} - self.content_subsets = {} - self.content_versions = {} - self.content_repres = {} - - self.hero_version_ids = set() - - self.missing_assets = [] - self.missing_versions = [] - self.missing_subsets = [] - self.missing_repres = [] - self.missing_docs = False - - self.archived_assets = [] - self.archived_subsets = [] - self.archived_repres = [] - - 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) - - def active_project(self): - return legacy_io.active_project() - - def _prepare_content_data(self): - repre_ids = set() - content_loaders = set() - for item in self._items: - repre_ids.add(str(item["representation"])) - content_loaders.add(item["loader"]) - - project_name = self.active_project() - repres = list(get_representations( - project_name, - representation_ids=repre_ids, - archived=True - )) - repres_by_id = {str(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 = set() - 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.add(repre["parent"]) - else: - repre = repres_by_id[repre_id] - content_repres[repre_id] = repres_by_id[repre_id] - version_ids.add(repre["parent"]) - - versions = get_versions( - project_name, - version_ids=version_ids, - hero=True - ) - 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 = get_subsets( - project_name, subset_ids=subset_ids, archived=True - ) - 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 = get_assets(project_name, asset_ids=asset_ids, archived=True) - 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_loaders = content_loaders - 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) - - self._build_loaders_menu() - - 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 _build_loaders_menu(self): - repre_ids = self._get_current_output_repre_ids() - loaders = self._get_loaders(repre_ids) - # Get and destroy the action group - self._accept_btn.clear_actions() - - if not loaders: - return - - # Build new action group - group = QtWidgets.QActionGroup(self._accept_btn) - - for loader in loaders: - # Label - label = getattr(loader, "label", None) - if label is None: - label = loader.__name__ - - action = group.addAction(label) - # action = QtWidgets.QAction(label) - action.setData(loader) - - # Support font-awesome icons using the `.icon` and `.color` - # attributes on plug-ins. - icon = getattr(loader, "icon", None) - if icon is not None: - try: - key = "fa.{0}".format(icon) - color = getattr(loader, "color", "white") - action.setIcon(qtawesome.icon(key, color=color)) - - except Exception as exc: - print("Unable to set icon for loader {}: {}".format( - loader, str(exc) - )) - - self._accept_btn.add_action(action) - - group.triggered.connect(self._on_action_clicked) - - def _on_action_clicked(self, action): - loader_plugin = action.data() - self._trigger_switch(loader_plugin) - - def _get_loaders(self, repre_ids): - repre_contexts = None - if repre_ids: - repre_contexts = get_repres_contexts(repre_ids) - - if not repre_contexts: - return list() - - available_loaders = [] - for loader_plugin in discover_loader_plugins(): - # Skip loaders without switch method - if not hasattr(loader_plugin, "switch"): - continue - - # Skip utility loaders - if ( - hasattr(loader_plugin, "is_utility") - and loader_plugin.is_utility - ): - continue - available_loaders.append(loader_plugin) - - loaders = None - for repre_context in repre_contexts.values(): - _loaders = set(loaders_from_repre_context( - available_loaders, repre_context - )) - if loaders is None: - loaders = _loaders - else: - loaders = _loaders.intersection(loaders) - - if not loaders: - break - - if loaders is None: - loaders = [] - else: - loaders = list(loaders) - - 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;" - - asset_sheet = None - subset_sheet = None - repre_sheet = None - accept_state = "" - 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_state = "1" - - 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._set_style_property(self._accept_btn, "state", accept_state) - - def _set_style_property(self, widget, name, value): - cur_value = widget.property(name) - if cur_value == value: - return - widget.setProperty(name, value) - widget.style().polish(widget) - - def _get_current_output_repre_ids(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() - selected_repre = self._representations_box.currentText() - - # Nothing is selected - # [ ] [ ] [ ] - if not selected_asset and not selected_subset and not selected_repre: - return list(self.content_repres.keys()) - - # Prepare asset document if asset is selected - asset_doc = None - if selected_asset: - asset_doc = get_asset_by_name( - self.active_project(), - selected_asset, - fields=["_id"] - ) - if not asset_doc: - return [] - - # Everything is selected - # [x] [x] [x] - if selected_asset and selected_subset and selected_repre: - return self._get_current_output_repre_ids_xxx( - asset_doc, selected_subset, selected_repre - ) - - # [x] [x] [ ] - # If asset and subset is selected - if selected_asset and selected_subset: - return self._get_current_output_repre_ids_xxo( - asset_doc, selected_subset - ) - - # [x] [ ] [x] - # If asset and repre is selected - if selected_asset and selected_repre: - return self._get_current_output_repre_ids_xox( - asset_doc, selected_repre - ) - - # [x] [ ] [ ] - # If asset and subset is selected - if selected_asset: - return self._get_current_output_repre_ids_xoo(asset_doc) - - # [ ] [x] [x] - if selected_subset and selected_repre: - return self._get_current_output_repre_ids_oxx( - selected_subset, selected_repre - ) - - # [ ] [x] [ ] - if selected_subset: - return self._get_current_output_repre_ids_oxo( - selected_subset - ) - - # [ ] [ ] [x] - return self._get_current_output_repre_ids_oox(selected_repre) - - def _get_current_output_repre_ids_xxx( - self, asset_doc, selected_subset, selected_repre - ): - project_name = self.active_project() - subset_doc = get_subset_by_name( - project_name, - selected_subset, - asset_doc["_id"], - fields=["_id"] - ) - - 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) - if not version_doc: - return [] - - repre_docs = get_representations( - project_name, - version_ids=[version_doc["_id"]], - representation_names=[selected_repre], - fields=["_id"] - ) - return [repre_doc["_id"] for repre_doc in repre_docs] - - def _get_current_output_repre_ids_xxo(self, asset_doc, selected_subset): - project_name = self.active_project() - subset_doc = get_subset_by_name( - project_name, - selected_subset, - asset_doc["_id"], - fields=["_id"] - ) - if not subset_doc: - return [] - - repre_names = set() - for repre_doc in self.content_repres.values(): - repre_names.add(repre_doc["name"]) - - # TODO where to take version ids? - version_ids = [] - repre_docs = get_representations( - project_name, - representation_names=repre_names, - version_ids=version_ids, - fields=["_id"] - ) - return [repre_doc["_id"] for repre_doc in repre_docs] - - def _get_current_output_repre_ids_xox(self, asset_doc, selected_repre): - subset_names = set() - for subset_doc in self.content_subsets.values(): - subset_names.add(subset_doc["name"]) - - project_name = self.active_project() - subset_docs = get_subsets( - project_name, - asset_ids=[asset_doc["_id"]], - subset_names=subset_names, - fields=["_id", "name"] - ) - subset_name_by_id = { - subset_doc["_id"]: subset_doc["name"] - for subset_doc in subset_docs - } - subset_ids = list(subset_name_by_id.keys()) - last_versions_by_subset_id = self.find_last_versions(subset_ids) - last_version_id_by_subset_name = {} - for subset_id, last_version in last_versions_by_subset_id.items(): - subset_name = subset_name_by_id[subset_id] - last_version_id_by_subset_name[subset_name] = ( - last_version["_id"] - ) - - repre_docs = get_representations( - project_name, - version_ids=last_version_id_by_subset_name.values(), - representation_names=[selected_repre], - fields=["_id"] - ) - return [repre_doc["_id"] for repre_doc in repre_docs] - - def _get_current_output_repre_ids_xoo(self, asset_doc): - project_name = self.active_project() - repres_by_subset_name = collections.defaultdict(set) - for repre_doc in self.content_repres.values(): - repre_name = repre_doc["name"] - version_doc = self.content_versions[repre_doc["parent"]] - subset_doc = self.content_subsets[version_doc["parent"]] - subset_name = subset_doc["name"] - repres_by_subset_name[subset_name].add(repre_name) - - subset_docs = list(get_subsets( - project_name, - asset_ids=[asset_doc["_id"]], - subset_names=repres_by_subset_name.keys(), - fields=["_id", "name"] - )) - subset_name_by_id = { - subset_doc["_id"]: subset_doc["name"] - for subset_doc in subset_docs - } - subset_ids = list(subset_name_by_id.keys()) - last_versions_by_subset_id = self.find_last_versions(subset_ids) - last_version_id_by_subset_name = {} - for subset_id, last_version in last_versions_by_subset_id.items(): - subset_name = subset_name_by_id[subset_id] - last_version_id_by_subset_name[subset_name] = ( - last_version["_id"] - ) - - repre_names_by_version_id = {} - for subset_name, repre_names in repres_by_subset_name.items(): - version_id = last_version_id_by_subset_name.get(subset_name) - # This should not happen but why to crash? - if version_id is not None: - repre_names_by_version_id[version_id] = list(repre_names) - - repre_docs = get_representations( - project_name, - names_by_version_ids=repre_names_by_version_id, - fields=["_id"] - ) - return [repre_doc["_id"] for repre_doc in repre_docs] - - def _get_current_output_repre_ids_oxx( - self, selected_subset, selected_repre - ): - project_name = self.active_project() - subset_docs = get_subsets( - project_name, - asset_ids=self.content_assets.keys(), - subset_names=[selected_subset], - fields=["_id"] - ) - subset_ids = [subset_doc["_id"] for subset_doc in subset_docs] - last_versions_by_subset_id = self.find_last_versions(subset_ids) - last_version_ids = [ - last_version["_id"] - for last_version in last_versions_by_subset_id.values() - ] - repre_docs = get_representations( - project_name, - version_ids=last_version_ids, - representation_names=[selected_repre], - fields=["_id"] - ) - return [repre_doc["_id"] for repre_doc in repre_docs] - - def _get_current_output_repre_ids_oxo(self, selected_subset): - project_name = self.active_project() - subset_docs = get_subsets( - project_name, - asset_ids=self.content_assets.keys(), - subset_names=[selected_subset], - fields=["_id", "parent"] - ) - subset_docs_by_id = { - subset_doc["_id"]: subset_doc - for subset_doc in subset_docs - } - if not subset_docs: - return list() - - 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_names_by_asset_id = collections.defaultdict(set) - 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_doc = self.content_assets[subset_doc["parent"]] - repre_name = repre_doc["name"] - asset_id = asset_doc["_id"] - repre_names_by_asset_id[asset_id].add(repre_name) - - repre_names_by_version_id = {} - for last_version_id, subset_id in subset_id_by_version_id.items(): - subset_doc = subset_docs_by_id[subset_id] - asset_id = subset_doc["parent"] - repre_names = repre_names_by_asset_id.get(asset_id) - if not repre_names: - continue - repre_names_by_version_id[last_version_id] = repre_names - - repre_docs = get_representations( - project_name, - names_by_version_ids=repre_names_by_version_id, - fields=["_id"] - ) - return [repre_doc["_id"] for repre_doc in repre_docs] - - def _get_current_output_repre_ids_oox(self, selected_repre): - project_name = self.active_project() - repre_docs = get_representations( - project_name, - representation_names=[selected_repre], - version_ids=self.content_versions.keys(), - fields=["_id"] - ) - return [repre_doc["_id"] for repre_doc in repre_docs] - - def _get_asset_box_values(self): - project_name = self.active_project() - asset_docs = get_assets(project_name, fields=["_id", "name"]) - asset_names_by_id = { - asset_doc["_id"]: asset_doc["name"] - for asset_doc in asset_docs - } - subsets = get_subsets( - project_name, - asset_ids=asset_names_by_id.keys(), - fields=["parent"] - ) - 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): - project_name = self.active_project() - selected_asset = self._assets_box.get_valid_value() - if selected_asset: - asset_doc = get_asset_by_name( - project_name, selected_asset, fields=["_id"] - ) - asset_ids = [asset_doc["_id"]] - else: - asset_ids = list(self.content_assets.keys()) - - subsets = get_subsets( - project_name, - asset_ids=asset_ids, - fields=["parent", "name"] - ) - - 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 - project_name = self.active_project() - 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 = get_representations( - project_name, - version_ids=self.content_versions.keys(), - fields=["parent", "name"] - ) - - 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 = get_asset_by_name( - project_name, selected_asset, fields=["_id"] - ) - subset_doc = get_subset_by_name( - project_name, - selected_subset, - asset_doc["_id"], - fields=["_id"] - ) - - 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 = get_representations( - project_name, - version_ids=[version_doc["_id"]], - fields=["name"] - ) - return [ - repre_doc["name"] - for repre_doc in repre_docs - ] - - # [x] [ ] [?] - # If asset only is selected - if selected_asset: - asset_doc = get_asset_by_name( - project_name, selected_asset, fields=["_id"] - ) - 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 = get_subsets( - project_name, - asset_ids=[asset_doc["_id"]], - subset_names=subset_names, - fields=["_id"] - ) - 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(get_representations( - project_name, - version_ids=subset_id_by_version_id.keys(), - fields=["name", "parent"] - )) - 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(get_subsets( - project_name, - asset_ids=self.content_assets.keys(), - subset_names=[selected_subset], - fields=["_id", "parent"] - )) - 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( - get_representations( - project_name, - version_ids=subset_id_by_version_id.keys(), - fields=["name", "parent"] - ) - ) - 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] [ ] [?] - project_name = self.active_project() - asset_doc = get_asset_by_name( - project_name, selected_asset, fields=["_id"] - ) - subset_docs = get_subsets( - project_name, asset_ids=[asset_doc["_id"]], fields=["name"] - ) - - 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): - project_name = self.active_project() - return get_last_versions( - project_name, - subset_ids=subset_ids, - fields=["_id", "parent", "type"] - ) - - 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] [ ] - project_name = self.active_project() - if selected_asset is not None and selected_subset is not None: - asset_doc = get_asset_by_name( - project_name, selected_asset, fields=["_id"] - ) - subset_doc = get_subset_by_name( - project_name, - selected_subset, - asset_doc["_id"], - fields=["_id"] - ) - subset_id = subset_doc["_id"] - last_versions_by_subset_id = self.find_last_versions([subset_id]) - last_version = last_versions_by_subset_id.get(subset_id) - if not last_version: - validation_state.repre_ok = False - return - - repre_docs = get_representations( - project_name, - version_ids=[last_version["_id"]], - fields=["name"] - ) - - 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 = get_asset_by_name( - project_name, selected_asset, fields=["_id"] - ) - subset_docs = list(get_subsets( - project_name, - asset_ids=[asset_doc["_id"]], - fields=["_id", "name"] - )) - - 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 = get_representations( - project_name, - version_ids=subset_id_by_version_id.keys(), - fields=["name", "parent"] - ) - 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 = get_subsets( - project_name, - asset_ids=self.content_assets.keys(), - subset_names=[selected_subset], - fields=["_id", "name", "parent"] - ) - 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 = get_representations( - project_name, - version_ids=subset_id_by_version_id.keys(), - fields=["name", "parent"] - ) - 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 = legacy_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): - self._trigger_switch() - - def _trigger_switch(self, loader=None): - # 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() - - project_name = self.active_project() - if selected_asset: - asset_doc = get_asset_by_name(project_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() - } - - subset_names = None - if selected_subset: - subset_names = [selected_subset] - - subset_docs = list(get_subsets( - project_name, - subset_names=subset_names, - asset_ids=asset_docs_by_id.keys() - )) - 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 = get_versions(project_name, subset_ids=subset_ids) - version_docs = list(reversed( - sorted(_version_docs, key=lambda item: item["name"]) - )) - - hero_version_docs = list(get_hero_versions( - project_name, subset_ids=subset_ids - )) - - version_ids = list() - - version_docs_by_parent_id_and_name = collections.defaultdict(dict) - for version_doc in version_docs: - parent_id = version_doc["parent"] - version_ids.append(version_doc["_id"]) - name = version_doc["name"] - version_docs_by_parent_id_and_name[parent_id][name] = 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 = get_representations(project_name, version_ids=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 = 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_docs_by_name = version_docs_by_parent_id_and_name[ - subset_id - ] - - # If asset or subset are selected for switching, we use latest - # version else we try to keep the current container version. - version_name = None - if ( - selected_asset in (None, container_asset_name) - and selected_subset in (None, container_subset_name) - ): - version_name = container_version.get("name") - - version_doc = None - if version_name is not None: - version_doc = version_docs_by_name.get(version_name) - - if version_doc is None: - version_name = max(version_docs_by_name) - version_doc = version_docs_by_name[version_name] - - version_id = version_doc["_id"] - repres_docs_by_name = repre_docs_by_parent_id_by_name[ - version_id - ] - - if selected_representation: - repres_name = selected_representation - else: - repres_name = container_repre_name - - repre_doc = repres_docs_by_name[repres_name] - - error = None - try: - switch_container(container, repre_doc, loader) - except ( - LoaderSwitchNotImplementedError, - IncompatibleLoaderError, - LoaderNotFoundError, - ) as exc: - error = str(exc) - except Exception: - error = ( - "Switch asset failed. " - "Search console log for more details." - ) - if error is not None: - log.warning(( - "Couldn't switch asset." - "See traceback for more information." - ), exc_info=True) - dialog = QtWidgets.QMessageBox(self) - dialog.setWindowTitle("Switch asset failed") - dialog.setText(error) - dialog.exec_() - - self.switched.emit() - - self.close() diff --git a/client/ayon_core/tools/ayon_sceneinventory/switch_dialog/__init__.py b/client/ayon_core/tools/sceneinventory/switch_dialog/__init__.py similarity index 100% rename from client/ayon_core/tools/ayon_sceneinventory/switch_dialog/__init__.py rename to client/ayon_core/tools/sceneinventory/switch_dialog/__init__.py diff --git a/client/ayon_core/tools/ayon_sceneinventory/switch_dialog/dialog.py b/client/ayon_core/tools/sceneinventory/switch_dialog/dialog.py similarity index 100% rename from client/ayon_core/tools/ayon_sceneinventory/switch_dialog/dialog.py rename to client/ayon_core/tools/sceneinventory/switch_dialog/dialog.py diff --git a/client/ayon_core/tools/ayon_sceneinventory/switch_dialog/folders_input.py b/client/ayon_core/tools/sceneinventory/switch_dialog/folders_input.py similarity index 100% rename from client/ayon_core/tools/ayon_sceneinventory/switch_dialog/folders_input.py rename to client/ayon_core/tools/sceneinventory/switch_dialog/folders_input.py diff --git a/client/ayon_core/tools/ayon_sceneinventory/switch_dialog/widgets.py b/client/ayon_core/tools/sceneinventory/switch_dialog/widgets.py similarity index 100% rename from client/ayon_core/tools/ayon_sceneinventory/switch_dialog/widgets.py rename to client/ayon_core/tools/sceneinventory/switch_dialog/widgets.py diff --git a/client/ayon_core/tools/sceneinventory/view.py b/client/ayon_core/tools/sceneinventory/view.py index 14afb3a379..214be68ae0 100644 --- a/client/ayon_core/tools/sceneinventory/view.py +++ b/client/ayon_core/tools/sceneinventory/view.py @@ -1,3 +1,4 @@ +import uuid import collections import logging import itertools @@ -15,13 +16,11 @@ from ayon_core.client import ( ) from ayon_core import style from ayon_core.pipeline import ( - legacy_io, HeroVersionType, update_container, remove_container, discover_inventory_actions, ) -from ayon_core.modules import ModulesManager from ayon_core.tools.utils.lib import ( iter_model_rows, format_version @@ -40,7 +39,7 @@ class SceneInventoryView(QtWidgets.QTreeView): data_changed = QtCore.Signal() hierarchy_view_changed = QtCore.Signal(bool) - def __init__(self, parent=None): + def __init__(self, controller, parent): super(SceneInventoryView, self).__init__(parent=parent) # view settings @@ -49,16 +48,13 @@ class SceneInventoryView(QtWidgets.QTreeView): self.setSortingEnabled(True) self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.customContextMenuRequested.connect(self._show_right_mouse_menu) + self._hierarchy_view = False self._selected = None - manager = ModulesManager() - sync_server = manager.modules_by_name.get("sync_server") - sync_enabled = sync_server is not None and sync_server.enabled - - self.sync_server = sync_server - self.sync_enabled = sync_enabled + self._controller = controller def _set_hierarchy_view(self, enabled): if enabled == self._hierarchy_view: @@ -83,22 +79,24 @@ class SceneInventoryView(QtWidgets.QTreeView): self.setStyleSheet("QTreeView {}") def _build_item_menu_for_selection(self, items, menu): - # Exclude items that are "NOT FOUND" since setting versions, updating # and removal won't work for those items. items = [item for item in items if not item.get("isNotFound")] - if not items: return # An item might not have a representation, for example when an item # is listed as "NOT FOUND" - repre_ids = { - item["representation"] - for item in items - } + repre_ids = set() + for item in items: + repre_id = item["representation"] + try: + uuid.UUID(repre_id) + repre_ids.add(repre_id) + except ValueError: + pass - project_name = legacy_io.active_project() + project_name = self._controller.get_current_project_name() repre_docs = get_representations( project_name, representation_ids=repre_ids, fields=["parent"] ) @@ -265,14 +263,14 @@ class SceneInventoryView(QtWidgets.QTreeView): 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", + # switch folder + switch_folder_icon = qtawesome.icon("fa.sitemap", color=DEFAULT_COLOR) + switch_folder_action = QtWidgets.QAction( + switch_folder_icon, + "Switch Folder", menu ) - switch_asset_action.triggered.connect( + switch_folder_action.triggered.connect( lambda: self._show_switch_dialog(items)) # remove @@ -292,7 +290,7 @@ class SceneInventoryView(QtWidgets.QTreeView): menu.addAction(change_to_hero) menu.addAction(set_version_action) - menu.addAction(switch_asset_action) + menu.addAction(switch_folder_action) menu.addSeparator() @@ -301,16 +299,17 @@ class SceneInventoryView(QtWidgets.QTreeView): self._handle_sync_server(menu, repre_ids) def _handle_sync_server(self, menu, repre_ids): - """ - Adds actions for download/upload when SyncServer is enabled + """Adds actions for download/upload when SyncServer is enabled - Args: - menu (OptionMenu) - repre_ids (list) of object_ids - Returns: - (OptionMenu) + Args: + menu (OptionMenu) + repre_ids (list) of object_ids + + Returns: + (OptionMenu) """ - if not self.sync_enabled: + + if not self._controller.is_sync_server_enabled(): return menu.addSeparator() @@ -322,7 +321,7 @@ class SceneInventoryView(QtWidgets.QTreeView): menu ) download_active_action.triggered.connect( - lambda: self._add_sites(repre_ids, 'active_site')) + lambda: self._add_sites(repre_ids, "active_site")) upload_icon = qtawesome.icon("fa.upload", color=DEFAULT_COLOR) upload_remote_action = QtWidgets.QAction( @@ -331,55 +330,23 @@ class SceneInventoryView(QtWidgets.QTreeView): menu ) upload_remote_action.triggered.connect( - lambda: self._add_sites(repre_ids, 'remote_site')) + lambda: self._add_sites(repre_ids, "remote_site")) menu.addAction(download_active_action) menu.addAction(upload_remote_action) - def _add_sites(self, repre_ids, side): + def _add_sites(self, repre_ids, site_type): + """(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) + site_type (Literal[active_site, remote_site]): Site type. """ - (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_name = legacy_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 = get_representations( - project_name, representation_ids=repre_ids - ) - repre_docs_by_id = { - repre_doc["_id"]: repre_doc - for repre_doc in repre_docs - } - for repre_id in repre_ids: - repre_doc = repre_docs_by_id.get(repre_id) - if not repre_doc: - continue - - progress = self.sync_server.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 - else: - check_progress = progress[active_site] - site = remote_site - - if check_progress == 1: - self.sync_server.add_site( - project_name, repre_id, site, force=True - ) + self._controller.resync_representations(repre_ids, site_type) self.data_changed.emit() @@ -421,6 +388,7 @@ class SceneInventoryView(QtWidgets.QTreeView): menu.addMenu(submenu) # go back to flat view + back_to_flat_action = None if self._hierarchy_view: back_to_flat_icon = qtawesome.icon("fa.list", color=DEFAULT_COLOR) back_to_flat_action = QtWidgets.QAction( @@ -443,7 +411,7 @@ class SceneInventoryView(QtWidgets.QTreeView): if items: menu.addAction(enter_hierarchy_action) - if self._hierarchy_view: + if back_to_flat_action is not None: menu.addAction(back_to_flat_action) return menu @@ -638,7 +606,7 @@ class SceneInventoryView(QtWidgets.QTreeView): active = items[-1] - project_name = legacy_io.active_project() + project_name = self._controller.get_current_project_name() # Get available versions for active representation repre_doc = get_representation_by_id( project_name, @@ -725,7 +693,7 @@ class SceneInventoryView(QtWidgets.QTreeView): def _show_switch_dialog(self, items): """Display Switch dialog""" - dialog = SwitchAssetDialog(self, items) + dialog = SwitchAssetDialog(self._controller, self, items) dialog.switched.connect(self.data_changed.emit) dialog.show() @@ -771,7 +739,7 @@ class SceneInventoryView(QtWidgets.QTreeView): dialog.setWindowTitle("Update failed") switch_btn = dialog.addButton( - "Switch Asset", + "Switch Folder", QtWidgets.QMessageBox.ActionRole ) switch_btn.clicked.connect(lambda: self._show_switch_dialog(items)) @@ -781,7 +749,7 @@ class SceneInventoryView(QtWidgets.QTreeView): 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." + " OR \n use 'Switch Folder' button to change folder." ).format(version_str) dialog.setText(msg) dialog.exec_() diff --git a/client/ayon_core/tools/sceneinventory/widgets.py b/client/ayon_core/tools/sceneinventory/widgets.py deleted file mode 100644 index 76de044d52..0000000000 --- a/client/ayon_core/tools/sceneinventory/widgets.py +++ /dev/null @@ -1,93 +0,0 @@ -from qtpy import QtWidgets, QtCore -from ayon_core import style - - -class ButtonWithMenu(QtWidgets.QToolButton): - def __init__(self, parent=None): - super(ButtonWithMenu, self).__init__(parent) - - self.setObjectName("ButtonWithMenu") - - self.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup) - menu = QtWidgets.QMenu(self) - - self.setMenu(menu) - - self._menu = menu - self._actions = [] - - def menu(self): - return self._menu - - def clear_actions(self): - if self._menu is not None: - self._menu.clear() - self._actions = [] - - def add_action(self, action): - self._actions.append(action) - self._menu.addAction(action) - - def _on_action_trigger(self): - action = self.sender() - if action not in self._actions: - return - action.trigger() - - -class SearchComboBox(QtWidgets.QComboBox): - """Searchable ComboBox with empty placeholder value as first value""" - - def __init__(self, parent): - super(SearchComboBox, self).__init__(parent) - - self.setEditable(True) - self.setInsertPolicy(QtWidgets.QComboBox.NoInsert) - - combobox_delegate = QtWidgets.QStyledItemDelegate(self) - self.setItemDelegate(combobox_delegate) - - completer = self.completer() - completer.setCompletionMode( - QtWidgets.QCompleter.PopupCompletion - ) - completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive) - - completer_view = completer.popup() - completer_view.setObjectName("CompleterView") - completer_delegate = QtWidgets.QStyledItemDelegate(completer_view) - completer_view.setItemDelegate(completer_delegate) - completer_view.setStyleSheet(style.load_stylesheet()) - - self._combobox_delegate = combobox_delegate - - self._completer_delegate = completer_delegate - self._completer = completer - - 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/client/ayon_core/tools/sceneinventory/window.py b/client/ayon_core/tools/sceneinventory/window.py index a31834fbfb..14a02c534c 100644 --- a/client/ayon_core/tools/sceneinventory/window.py +++ b/client/ayon_core/tools/sceneinventory/window.py @@ -1,19 +1,13 @@ -import os -import sys - -from qtpy import QtWidgets, QtCore +from qtpy import QtWidgets, QtCore, QtGui import qtawesome -from ayon_core import style -from ayon_core.client import get_projects -from ayon_core.pipeline import legacy_io +from ayon_core import style, resources from ayon_core.tools.utils.delegates import VersionDelegate from ayon_core.tools.utils.lib import ( - qt_app_context, preserve_expanded_rows, preserve_selection, - FamilyConfigCache ) +from ayon_core.tools.sceneinventory import SceneInventoryController from .model import ( InventoryModel, @@ -22,28 +16,36 @@ from .model import ( from .view import SceneInventoryView -module = sys.modules[__name__] -module.window = None +class ControllerVersionDelegate(VersionDelegate): + """Version delegate that uses controller to get project. + + Original VersionDelegate is using 'AvalonMongoDB' object instead. Don't + worry about the variable name, object is stored to '_dbcon' attribute. + """ + + def get_project_name(self): + self._dbcon.get_current_project_name() class SceneInventoryWindow(QtWidgets.QDialog): """Scene Inventory window""" - def __init__(self, parent=None): + def __init__(self, controller=None, parent=None): super(SceneInventoryWindow, self).__init__(parent) - if not parent: - self.setWindowFlags( - self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint - ) + if controller is None: + controller = SceneInventoryController() - project_name = os.getenv("AVALON_PROJECT") or "" - self.setWindowTitle("Scene Inventory 1.0 - {}".format(project_name)) + project_name = controller.get_current_project_name() + icon = QtGui.QIcon(resources.get_openpype_icon_filepath()) + self.setWindowIcon(icon) + self.setWindowTitle("Scene Inventory - {}".format(project_name)) self.setObjectName("SceneInventory") self.resize(1100, 480) # region control + filter_label = QtWidgets.QLabel("Search", self) text_filter = QtWidgets.QLineEdit(self) @@ -70,18 +72,19 @@ class SceneInventoryWindow(QtWidgets.QDialog): control_layout.addWidget(update_all_button) control_layout.addWidget(refresh_button) - # endregion control - family_config_cache = FamilyConfigCache(legacy_io) - - model = InventoryModel(family_config_cache) + model = InventoryModel(controller) proxy = FilterProxyModel() proxy.setSourceModel(model) proxy.setDynamicSortFilter(True) proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) - view = SceneInventoryView(self) + view = SceneInventoryView(controller, self) view.setModel(proxy) + sync_enabled = controller.is_sync_server_enabled() + view.setColumnHidden(model.active_site_col, not sync_enabled) + view.setColumnHidden(model.remote_site_col, not sync_enabled) + # set some nice default widths for the view view.setColumnWidth(0, 250) # name view.setColumnWidth(1, 55) # version @@ -91,7 +94,7 @@ class SceneInventoryWindow(QtWidgets.QDialog): view.setColumnWidth(5, 150) # loader # apply delegates - version_delegate = VersionDelegate(legacy_io, self) + version_delegate = ControllerVersionDelegate(controller, self) column = model.Columns.index("version") view.setItemDelegateForColumn(column, version_delegate) @@ -99,7 +102,12 @@ class SceneInventoryWindow(QtWidgets.QDialog): layout.addLayout(control_layout) layout.addWidget(view) + show_timer = QtCore.QTimer() + show_timer.setInterval(0) + show_timer.setSingleShot(False) + # signals + show_timer.timeout.connect(self._on_show_timer) text_filter.textChanged.connect(self._on_text_filter_change) outdated_only_checkbox.stateChanged.connect( self._on_outdated_state_change @@ -111,17 +119,18 @@ class SceneInventoryWindow(QtWidgets.QDialog): refresh_button.clicked.connect(self._on_refresh_request) update_all_button.clicked.connect(self._on_update_all) + self._show_timer = show_timer + self._show_counter = 0 + self._controller = controller self._update_all_button = update_all_button self._outdated_only_checkbox = outdated_only_checkbox self._view = view self._model = model self._proxy = proxy self._version_delegate = version_delegate - self._family_config_cache = family_config_cache self._first_show = True - - family_config_cache.refresh() + self._first_refresh = True def showEvent(self, event): super(SceneInventoryWindow, self).showEvent(event) @@ -129,6 +138,9 @@ class SceneInventoryWindow(QtWidgets.QDialog): self._first_show = False self.setStyleSheet(style.load_stylesheet()) + self._show_counter = 0 + self._show_timer.start() + def keyPressEvent(self, event): """Custom keyPressEvent. @@ -144,7 +156,9 @@ class SceneInventoryWindow(QtWidgets.QDialog): self.refresh() - def refresh(self, items=None): + def refresh(self, containers=None): + self._first_refresh = False + self._controller.reset() with preserve_expanded_rows( tree_view=self._view, role=self._model.UniqueRole @@ -154,12 +168,19 @@ class SceneInventoryWindow(QtWidgets.QDialog): role=self._model.UniqueRole, current_index=False ): - kwargs = {"items": items} + kwargs = {"containers": containers} # TODO do not touch view's inner attribute if self._view._hierarchy_view: kwargs["selected"] = self._view._selected self._model.refresh(**kwargs) + def _on_show_timer(self): + if self._show_counter < 3: + self._show_counter += 1 + return + self._show_timer.stop() + self.refresh() + def _on_hierarchy_view_change(self, enabled): self._proxy.set_hierarchy_view(enabled) self._model.set_hierarchy_view(enabled) @@ -177,47 +198,3 @@ class SceneInventoryWindow(QtWidgets.QDialog): def _on_update_all(self): self._view.update_all() - - -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: - legacy_io.install() - - if not os.environ.get("AVALON_PROJECT"): - any_project = next( - project for project in get_projects() - ) - - project_name = any_project["name"] - else: - project_name = os.environ.get("AVALON_PROJECT") - legacy_io.Session["AVALON_PROJECT"] = project_name - - with qt_app_context(): - window = SceneInventoryWindow(parent) - window.show() - window.refresh(items=items) - - module.window = window - - # Pull window to the front. - module.window.raise_() - module.window.activateWindow() diff --git a/client/ayon_core/tools/tray/pype_info_widget.py b/client/ayon_core/tools/tray/pype_info_widget.py index 7f70d6980a..ffb8599e68 100644 --- a/client/ayon_core/tools/tray/pype_info_widget.py +++ b/client/ayon_core/tools/tray/pype_info_widget.py @@ -8,8 +8,6 @@ from qtpy import QtCore, QtGui, QtWidgets from ayon_core import style import ayon_core.version from ayon_core import resources -from ayon_core import AYON_SERVER_ENABLED -from ayon_core.settings.lib import get_local_settings from ayon_core.lib import get_openpype_execute_args from ayon_core.lib.pype_info import ( get_all_current_info, @@ -220,9 +218,7 @@ class PypeInfoWidget(QtWidgets.QWidget): icon = QtGui.QIcon(resources.get_openpype_icon_filepath()) self.setWindowIcon(icon) - self.setWindowTitle( - "{} info".format("AYON" if AYON_SERVER_ENABLED else "OpenPype") - ) + self.setWindowTitle("AYON info") scroll_area = QtWidgets.QScrollArea(self) info_widget = PypeInfoSubWidget(scroll_area) @@ -333,9 +329,6 @@ class PypeInfoSubWidget(QtWidgets.QWidget): main_layout.addWidget(self._create_openpype_info_widget(), 0) main_layout.addWidget(self._create_separator(), 0) main_layout.addWidget(self._create_workstation_widget(), 0) - if not AYON_SERVER_ENABLED: - main_layout.addWidget(self._create_separator(), 0) - main_layout.addWidget(self._create_local_settings_widget(), 0) main_layout.addWidget(self._create_separator(), 0) main_layout.addWidget(self._create_environ_widget(), 1) @@ -405,19 +398,6 @@ class PypeInfoSubWidget(QtWidgets.QWidget): return wokstation_info_widget - def _create_local_settings_widget(self): - local_settings = get_local_settings() - - local_settings_widget = CollapsibleWidget("Local settings", self) - - settings_input = QtWidgets.QPlainTextEdit(local_settings_widget) - settings_input.setReadOnly(True) - settings_input.setPlainText(json.dumps(local_settings, indent=4)) - - local_settings_widget.set_content_widget(settings_input) - - return local_settings_widget - def _create_environ_widget(self): env_widget = CollapsibleWidget("Environments", self) @@ -432,60 +412,33 @@ class PypeInfoSubWidget(QtWidgets.QWidget): def _create_openpype_info_widget(self): """Create widget with information about OpenPype application.""" - if AYON_SERVER_ENABLED: - executable_args = get_openpype_execute_args() - username = "N/A" - user_info = ayon_api.get_user() - if user_info: - username = user_info.get("name") or username - full_name = user_info.get("attrib", {}).get("fullName") - if full_name: - username = "{} ({})".format(full_name, username) - info_values = { - "executable": executable_args[-1], - "server_url": os.environ["AYON_SERVER_URL"], - "bundle_name": os.environ["AYON_BUNDLE_NAME"], - "username": username - } - key_label_mapping = { - "executable": "AYON Executable:", - "server_url": "AYON Server:", - "bundle_name": "AYON Bundle:", - "username": "AYON Username:" - } - # Prepare keys order - keys_order = [ - "server_url", - "bundle_name", - "username", - "executable", - ] - - else: - # Get pype info data - info_values = get_openpype_info() - # Modify version key/values - version_value = "{} ({})".format( - info_values.pop("version", self.not_applicable), - info_values.pop("version_type", self.not_applicable) - ) - info_values["version_value"] = version_value - # Prepare label mapping - key_label_mapping = { - "version_value": "Running version:", - "build_verison": "Build version:", - "executable": "OpenPype executable:", - "pype_root": "OpenPype location:", - "mongo_url": "OpenPype Mongo URL:" - } - # Prepare keys order - keys_order = [ - "version_value", - "build_verison", - "executable", - "pype_root", - "mongo_url" - ] + executable_args = get_openpype_execute_args() + username = "N/A" + user_info = ayon_api.get_user() + if user_info: + username = user_info.get("name") or username + full_name = user_info.get("attrib", {}).get("fullName") + if full_name: + username = "{} ({})".format(full_name, username) + info_values = { + "executable": executable_args[-1], + "server_url": os.environ["AYON_SERVER_URL"], + "bundle_name": os.environ["AYON_BUNDLE_NAME"], + "username": username + } + key_label_mapping = { + "executable": "AYON Executable:", + "server_url": "AYON Server:", + "bundle_name": "AYON Bundle:", + "username": "AYON Username:" + } + # Prepare keys order + keys_order = [ + "server_url", + "bundle_name", + "username", + "executable", + ] for key in info_values.keys(): if key not in keys_order: @@ -519,16 +472,16 @@ class PypeInfoSubWidget(QtWidgets.QWidget): info_layout.addWidget( value_label, row, 1, 1, 1 ) - if AYON_SERVER_ENABLED: - row = info_layout.rowCount() - info_layout.addWidget( - QtWidgets.QLabel("OpenPype Addon:"), row, 0, 1, 1 - ) - value_label = QtWidgets.QLabel(ayon_core.version.__version__) - value_label.setTextInteractionFlags( - QtCore.Qt.TextSelectableByMouse - ) - info_layout.addWidget( - value_label, row, 1, 1, 1 - ) + + row = info_layout.rowCount() + info_layout.addWidget( + QtWidgets.QLabel("Core Addon:"), row, 0, 1, 1 + ) + value_label = QtWidgets.QLabel(ayon_core.version.__version__) + value_label.setTextInteractionFlags( + QtCore.Qt.TextSelectableByMouse + ) + info_layout.addWidget( + value_label, row, 1, 1, 1 + ) return info_widget diff --git a/client/ayon_core/tools/tray/pype_tray.py b/client/ayon_core/tools/tray/pype_tray.py index 462dcaf69a..3d13264398 100644 --- a/client/ayon_core/tools/tray/pype_tray.py +++ b/client/ayon_core/tools/tray/pype_tray.py @@ -7,8 +7,6 @@ import platform from qtpy import QtCore, QtGui, QtWidgets -import ayon_core.version -from ayon_core import AYON_SERVER_ENABLED from ayon_core import resources, style from ayon_core.lib import ( Logger, @@ -591,23 +589,11 @@ class TrayManager: self.tray_widget.showMessage(*args, **kwargs) def _add_version_item(self): - if AYON_SERVER_ENABLED: - login_action = QtWidgets.QAction("Login", self.tray_widget) - login_action.triggered.connect(self._on_ayon_login) - self.tray_widget.menu.addAction(login_action) + login_action = QtWidgets.QAction("Login", self.tray_widget) + login_action.triggered.connect(self._on_ayon_login) + self.tray_widget.menu.addAction(login_action) - subversion = os.environ.get("OPENPYPE_SUBVERSION") - client_name = os.environ.get("OPENPYPE_CLIENT") - - if AYON_SERVER_ENABLED: - version_string = os.getenv("AYON_VERSION", "AYON Info") - else: - version_string = ayon_core.version.__version__ - if subversion: - version_string += " ({})".format(subversion) - - if client_name: - version_string += ", {}".format(client_name) + version_string = os.getenv("AYON_VERSION", "AYON Info") version_action = QtWidgets.QAction(version_string, self.tray_widget) version_action.triggered.connect(self._on_version_action) diff --git a/client/ayon_core/tools/utils/assets_widget.py b/client/ayon_core/tools/utils/assets_widget.py index 1296f29a09..2dc9a6496f 100644 --- a/client/ayon_core/tools/utils/assets_widget.py +++ b/client/ayon_core/tools/utils/assets_widget.py @@ -5,7 +5,6 @@ import qtpy from qtpy import QtWidgets, QtCore, QtGui import qtawesome -from ayon_core import AYON_SERVER_ENABLED from ayon_core.client import ( get_project, get_assets, @@ -609,8 +608,7 @@ class AssetsWidget(QtWidgets.QWidget): refresh_btn.setToolTip("Refresh items") filter_input = PlaceholderLineEdit(header_widget) - filter_input.setPlaceholderText("Filter {}..".format( - "folders" if AYON_SERVER_ENABLED else "assets")) + filter_input.setPlaceholderText("Filter folders..") # Header header_layout = QtWidgets.QHBoxLayout(header_widget) diff --git a/client/ayon_core/tools/utils/host_tools.py b/client/ayon_core/tools/utils/host_tools.py index c57a39ae70..8841a377cf 100644 --- a/client/ayon_core/tools/utils/host_tools.py +++ b/client/ayon_core/tools/utils/host_tools.py @@ -7,7 +7,6 @@ import os import pyblish.api -from ayon_core import AYON_SERVER_ENABLED from ayon_core.host import IWorkfileHost, ILoadHost from ayon_core.lib import Logger from ayon_core.pipeline import ( @@ -39,7 +38,6 @@ class HostToolsHelper: self._publisher_tool = None self._subset_manager_tool = None self._scene_inventory_tool = None - self._library_loader_tool = None self._experimental_tools_dialog = None @property @@ -48,29 +46,13 @@ class HostToolsHelper: self._log = Logger.get_logger(self.__class__.__name__) return self._log - def _init_ayon_workfiles_tool(self, parent): - from ayon_core.tools.ayon_workfiles.widgets import WorkfilesToolWindow - - workfiles_window = WorkfilesToolWindow(parent=parent) - self._workfiles_tool = workfiles_window - - def _init_openpype_workfiles_tool(self, parent): - from ayon_core.tools.workfiles.app import Window - - # Host validation - host = registered_host() - IWorkfileHost.validate_workfile_methods(host) - - workfiles_window = Window(parent=parent) - self._workfiles_tool = workfiles_window - def get_workfiles_tool(self, parent): """Create, cache and return workfiles tool window.""" if self._workfiles_tool is None: - if AYON_SERVER_ENABLED: - self._init_ayon_workfiles_tool(parent) - else: - self._init_openpype_workfiles_tool(parent) + from ayon_core.tools.workfiles.widgets import WorkfilesToolWindow + + workfiles_window = WorkfilesToolWindow(parent=parent) + self._workfiles_tool = workfiles_window return self._workfiles_tool @@ -86,22 +68,18 @@ class HostToolsHelper: def get_loader_tool(self, parent): """Create, cache and return loader tool window.""" if self._loader_tool is None: + from ayon_core.tools.loader.ui import LoaderWindow + from ayon_core.tools.loader import LoaderController + host = registered_host() ILoadHost.validate_load_methods(host) - if AYON_SERVER_ENABLED: - from ayon_core.tools.ayon_loader.ui import LoaderWindow - from ayon_core.tools.ayon_loader import LoaderController - controller = LoaderController(host=host) - loader_window = LoaderWindow( - controller=controller, - parent=parent or self._parent - ) + controller = LoaderController(host=host) + loader_window = LoaderWindow( + controller=controller, + parent=parent or self._parent + ) - else: - from ayon_core.tools.loader import LoaderWindow - - loader_window = LoaderWindow(parent=parent or self._parent) self._loader_tool = loader_window return self._loader_tool @@ -119,11 +97,7 @@ class HostToolsHelper: if use_context is None: use_context = False - if not AYON_SERVER_ENABLED and use_context: - context = {"asset": get_current_asset_name()} - loader_tool.set_context(context, refresh=True) - else: - loader_tool.refresh() + loader_tool.refresh() def get_creator_tool(self, parent): """Create, cache and return creator tool window.""" @@ -174,20 +148,12 @@ class HostToolsHelper: host = registered_host() ILoadHost.validate_load_methods(host) - if AYON_SERVER_ENABLED: - from ayon_core.tools.ayon_sceneinventory.window import ( - SceneInventoryWindow) + from ayon_core.tools.sceneinventory.window import ( + SceneInventoryWindow) - scene_inventory_window = SceneInventoryWindow( - parent=parent or self._parent - ) - - else: - from ayon_core.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 @@ -206,31 +172,11 @@ class HostToolsHelper: def get_library_loader_tool(self, parent): """Create, cache and return library loader tool window.""" - if AYON_SERVER_ENABLED: - return self.get_loader_tool(parent) - - if self._library_loader_tool is None: - from ayon_core.tools.libraryloader import LibraryLoaderWindow - - library_window = LibraryLoaderWindow( - parent=parent or self._parent - ) - self._library_loader_tool = library_window - - return self._library_loader_tool + return self.get_loader_tool(parent) def show_library_loader(self, parent=None): """Loader tool for loading representations from library project.""" - if AYON_SERVER_ENABLED: - return self.show_loader(parent) - - with qt_app_context(): - library_loader_tool = self.get_library_loader_tool(parent) - library_loader_tool.show() - library_loader_tool.raise_() - library_loader_tool.activateWindow() - library_loader_tool.showNormal() - library_loader_tool.refresh() + return self.show_loader(parent) def show_publish(self, parent=None): """Try showing the most desirable publish GUI diff --git a/client/ayon_core/tools/workfiles/README.md b/client/ayon_core/tools/workfiles/README.md deleted file mode 100644 index 92ad4a8577..0000000000 --- a/client/ayon_core/tools/workfiles/README.md +++ /dev/null @@ -1,143 +0,0 @@ -# Workfiles App - -The Workfiles app facilitates easy saving, creation and launching of work files. - -The current supported hosts are: - -- Maya -- Houdini -- Fusion - -The app is available inside hosts via. the ```Avalon > Work Files``` menu. - -## Enabling Workfiles on launch - -By default the Workfiles app will not launch on startup, so it has to be explicitly enabled in a config. - -```python -workfiles.show() -``` - -## Naming Files - -Workfiles app enables user to easily save and create new work files. - -The user is presented with a two parameters; ```version``` and ```comment```. The name of the work file is determined from a template. - -### ```Next Available Version``` - -Will search for the next version number that is not in use. - -## Templates - -The default template for work files is ```{task[name]}_v{version:0>4}<_{comment}>```. Launching Maya on an animation task and creating a version 1 will result in ```animation_v0001.ma```. Adding "blocking" to the optional comment input will result in ```animation_v0001_blocking.ma```. - -This template can be customized per project with the ```workfile``` template. - -There are other variables to customize the template with: - -```python -{ - "project": project, # The project data from the database. - "asset": asset, # The asset data from the database. - "task": { - "label": label, # Label of task chosen. - "name": name # Sanitize version of the label. - }, - "user": user, # Name of the user on the machine. - "version": version, # Chosen version of the user. - "comment": comment, # Chosen comment of the user. -} -``` - -### Optional template groups - -The default template contains an optional template group ```<_{comment}>```. If any template group (```{comment}```) within angle bracket ```<>``` does not exist, the whole optional group is discarded. - - -## Implementing a new host integration for Work Files - -For the Work Files tool to work with a new host integration the host must -implement the following functions: - -- `file_extensions()`: The files the host should allow to open and show in the Work Files view. -- `open_file(filepath)`: Open a file. -- `save_file(filepath)`: Save the current file. This should return None if it failed to save, and return the path if it succeeded -- `has_unsaved_changes()`: Return whether the current scene has unsaved changes. -- `current_file()`: The path to the current file. None if not saved. -- `work_root()`: The path to where the work files for this app should be saved. - -Here's an example code layout: - -```python -def file_extensions(): - """Return the filename extension formats that should be shown. - - Note: - The first entry in the list will be used as the default file - format to save to when the current scene is not saved yet. - - Returns: - list: A list of the file extensions supported by Work Files. - - """ - return list() - - -def has_unsaved_changes(): - """Return whether current file has unsaved modifications.""" - - -def save_file(filepath): - """Save to filepath. - - This should return None if it failed to save, and return the path if it - succeeded. - """ - pass - - -def open_file(filepath): - """Open file""" - pass - - -def current_file(): - """Return path to currently open file or None if not saved. - - Returns: - str or None: The full path to current file or None when not saved. - - """ - pass - - -def work_root(): - """Return the default root for the Host to browse in for Work Files - - Returns: - str: The path to look in. - - """ - pass -``` - -#### Work Files Scenes root (AVALON_SCENEDIR) - -Whenever the host application has no built-in implementation that defines -where scene files should be saved to then the Work Files API for that host -should fall back to the `AVALON_SCENEDIR` variable in `api.Session`. - -When `AVALON_SCENEDIR` is set the directory is the relative folder inside the -`AVALON_WORKDIR`. Otherwise, when it is not set or empty it should fall back -to the Work Directory's root, `AVALON_WORKDIR` - -```python -AVALON_WORKDIR="/path/to/work" -AVALON_SCENEDIR="scenes" -# Result: /path/to/work/scenes - -AVALON_WORKDIR="/path/to/work" -AVALON_SCENEDIR=None -# Result: /path/to/work -``` \ No newline at end of file diff --git a/client/ayon_core/tools/workfiles/__init__.py b/client/ayon_core/tools/workfiles/__init__.py index 205fd44838..e69de29bb2 100644 --- a/client/ayon_core/tools/workfiles/__init__.py +++ b/client/ayon_core/tools/workfiles/__init__.py @@ -1,10 +0,0 @@ -from .window import Window -from .app import ( - show, -) - -__all__ = [ - "Window", - - "show", -] diff --git a/client/ayon_core/tools/ayon_workfiles/abstract.py b/client/ayon_core/tools/workfiles/abstract.py similarity index 100% rename from client/ayon_core/tools/ayon_workfiles/abstract.py rename to client/ayon_core/tools/workfiles/abstract.py diff --git a/client/ayon_core/tools/workfiles/app.py b/client/ayon_core/tools/workfiles/app.py deleted file mode 100644 index 9943b4be0c..0000000000 --- a/client/ayon_core/tools/workfiles/app.py +++ /dev/null @@ -1,54 +0,0 @@ -import sys -import logging - -from ayon_core.host import IWorkfileHost -from ayon_core.pipeline import ( - registered_host, - legacy_io, -) -from ayon_core.tools.utils import qt_app_context -from .window import Window - -log = logging.getLogger(__name__) - -module = sys.modules[__name__] -module.window = None - - -def show(root=None, debug=False, parent=None, use_context=True, save=True): - """Show Work Files GUI""" - # todo: remove `root` argument to show() - - try: - module.window.close() - del(module.window) - except (AttributeError, RuntimeError): - pass - - host = registered_host() - IWorkfileHost.validate_workfile_methods(host) - - if debug: - legacy_io.Session["AVALON_ASSET"] = "Mock" - legacy_io.Session["AVALON_TASK"] = "Testing" - - with qt_app_context(): - window = Window(parent=parent) - window.refresh() - - if use_context: - context = { - "asset": legacy_io.Session["AVALON_ASSET"], - "task": legacy_io.Session["AVALON_TASK"] - } - window.set_context(context) - - window.set_save_enabled(save) - - window.show() - - module.window = window - - # Pull window to the front. - module.window.raise_() - module.window.activateWindow() diff --git a/client/ayon_core/tools/ayon_workfiles/control.py b/client/ayon_core/tools/workfiles/control.py similarity index 100% rename from client/ayon_core/tools/ayon_workfiles/control.py rename to client/ayon_core/tools/workfiles/control.py diff --git a/client/ayon_core/tools/workfiles/files_widget.py b/client/ayon_core/tools/workfiles/files_widget.py deleted file mode 100644 index 432eab089e..0000000000 --- a/client/ayon_core/tools/workfiles/files_widget.py +++ /dev/null @@ -1,828 +0,0 @@ -import os -import logging -import shutil -import copy - -import qtpy -from qtpy import QtWidgets, QtCore - -from ayon_core.host import IWorkfileHost -from ayon_core.client import get_asset_by_id -from ayon_core.pipeline.workfile.lock_workfile import ( - is_workfile_locked, - is_workfile_lock_enabled, - is_workfile_locked_for_current_process -) -from ayon_core.tools.utils import PlaceholderLineEdit -from ayon_core.tools.utils.delegates import PrettyTimeDelegate -from ayon_core.lib import emit_event -from ayon_core.tools.workfiles.lock_dialog import WorkfileLockDialog -from ayon_core.pipeline import ( - registered_host, - legacy_io, - Anatomy, - get_current_project_name, -) -from ayon_core.pipeline.context_tools import ( - compute_session_changes, - change_current_context -) -from ayon_core.pipeline.workfile import ( - get_workfile_template_key, - create_workdir_extra_folders, -) - -from .model import ( - WorkAreaFilesModel, - PublishFilesModel, - - FILEPATH_ROLE, - DATE_MODIFIED_ROLE, -) -from .save_as_dialog import SaveAsDialog - -log = logging.getLogger(__name__) - - -class FilesView(QtWidgets.QTreeView): - doubleClickedLeft = QtCore.Signal() - doubleClickedRight = QtCore.Signal() - - def mouseDoubleClickEvent(self, event): - if event.button() == QtCore.Qt.LeftButton: - self.doubleClickedLeft.emit() - - elif event.button() == QtCore.Qt.RightButton: - self.doubleClickedRight.emit() - - return super(FilesView, self).mouseDoubleClickEvent(event) - - -class SelectContextOverlay(QtWidgets.QFrame): - def __init__(self, parent): - super(SelectContextOverlay, self).__init__(parent) - - self.setObjectName("WorkfilesPublishedContextSelect") - label_widget = QtWidgets.QLabel( - "Please choose context on the left
<", - self - ) - label_widget.setAlignment(QtCore.Qt.AlignCenter) - - layout = QtWidgets.QHBoxLayout(self) - layout.addWidget(label_widget, 1, QtCore.Qt.AlignCenter) - - label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) - - parent.installEventFilter(self) - - def eventFilter(self, obj, event): - if event.type() == QtCore.QEvent.Resize: - self.resize(obj.size()) - - return super(SelectContextOverlay, self).eventFilter(obj, event) - - -class FilesWidget(QtWidgets.QWidget): - """A widget displaying files that allows to save and open files.""" - file_selected = QtCore.Signal(str) - file_opened = QtCore.Signal() - workfile_created = QtCore.Signal(str) - published_visible_changed = QtCore.Signal(bool) - - def __init__(self, parent): - super(FilesWidget, self).__init__(parent) - - # Setup - self._asset_id = None - self._asset_doc = None - self._task_name = None - self._task_type = None - - # Pype's anatomy object for current project - project_name = get_current_project_name() - self.anatomy = Anatomy(project_name) - self.project_name = project_name - # Template key used to get work template from anatomy templates - self.template_key = "work" - - # This is not root but workfile directory - self._workfiles_root = None - self._workdir_path = None - self.host = registered_host() - self.host_name = os.environ["AVALON_APP"] - - # Whether to automatically select the latest modified - # file on a refresh of the files model. - self.auto_select_latest_modified = True - - # Avoid crash in Blender and store the message box - # (setting parent doesn't work as it hides the message box) - self._messagebox = None - - # Filtering input - filter_widget = QtWidgets.QWidget(self) - - published_checkbox = QtWidgets.QCheckBox("Published", filter_widget) - - filter_input = PlaceholderLineEdit(filter_widget) - filter_input.setPlaceholderText("Filter files..") - - filter_layout = QtWidgets.QHBoxLayout(filter_widget) - filter_layout.setContentsMargins(0, 0, 0, 0) - filter_layout.addWidget(filter_input, 1) - filter_layout.addWidget(published_checkbox, 0) - - # Create the Files models - extensions = set(self._get_host_extensions()) - - views_widget = QtWidgets.QWidget(self) - # --- Workarea view --- - workarea_files_model = WorkAreaFilesModel(extensions) - - # Create proxy model for files to be able sort and filter - workarea_proxy_model = QtCore.QSortFilterProxyModel() - workarea_proxy_model.setSourceModel(workarea_files_model) - workarea_proxy_model.setDynamicSortFilter(True) - workarea_proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive) - - # Set up the file list tree view - workarea_files_view = FilesView(views_widget) - workarea_files_view.setModel(workarea_proxy_model) - workarea_files_view.setSortingEnabled(True) - workarea_files_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - - # Date modified delegate - workarea_time_delegate = PrettyTimeDelegate() - workarea_files_view.setItemDelegateForColumn(1, workarea_time_delegate) - # smaller indentation - workarea_files_view.setIndentation(3) - - # Default to a wider first filename column it is what we mostly care - # about and the date modified is relatively small anyway. - workarea_files_view.setColumnWidth(0, 330) - - # --- Publish files view --- - publish_files_model = PublishFilesModel( - extensions, legacy_io, self.anatomy - ) - - publish_proxy_model = QtCore.QSortFilterProxyModel() - publish_proxy_model.setSourceModel(publish_files_model) - publish_proxy_model.setDynamicSortFilter(True) - publish_proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive) - - publish_files_view = FilesView(views_widget) - publish_files_view.setModel(publish_proxy_model) - - publish_files_view.setSortingEnabled(True) - publish_files_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - - # Date modified delegate - publish_time_delegate = PrettyTimeDelegate() - publish_files_view.setItemDelegateForColumn(1, publish_time_delegate) - # smaller indentation - publish_files_view.setIndentation(3) - - # Default to a wider first filename column it is what we mostly care - # about and the date modified is relatively small anyway. - publish_files_view.setColumnWidth(0, 330) - - publish_context_overlay = SelectContextOverlay(views_widget) - publish_context_overlay.setVisible(False) - - views_layout = QtWidgets.QHBoxLayout(views_widget) - views_layout.setContentsMargins(0, 0, 0, 0) - views_layout.addWidget(workarea_files_view, 1) - views_layout.addWidget(publish_files_view, 1) - - # Home Page - # Build buttons widget for files widget - btns_widget = QtWidgets.QWidget(self) - - workarea_btns_widget = QtWidgets.QWidget(btns_widget) - btn_save = QtWidgets.QPushButton("Save As", workarea_btns_widget) - btn_browse = QtWidgets.QPushButton("Browse", workarea_btns_widget) - btn_open = QtWidgets.QPushButton("Open", workarea_btns_widget) - - workarea_btns_layout = QtWidgets.QHBoxLayout(workarea_btns_widget) - workarea_btns_layout.setContentsMargins(0, 0, 0, 0) - workarea_btns_layout.addWidget(btn_open, 1) - workarea_btns_layout.addWidget(btn_browse, 1) - workarea_btns_layout.addWidget(btn_save, 1) - - publish_btns_widget = QtWidgets.QWidget(btns_widget) - btn_save_as_published = QtWidgets.QPushButton( - "Copy && Open", publish_btns_widget - ) - btn_change_context = QtWidgets.QPushButton( - "Choose different context", publish_btns_widget - ) - btn_select_context_published = QtWidgets.QPushButton( - "Copy && Open", publish_btns_widget - ) - btn_cancel_published = QtWidgets.QPushButton( - "Cancel", publish_btns_widget - ) - - publish_btns_layout = QtWidgets.QHBoxLayout(publish_btns_widget) - publish_btns_layout.setContentsMargins(0, 0, 0, 0) - publish_btns_layout.addWidget(btn_save_as_published, 1) - publish_btns_layout.addWidget(btn_change_context, 1) - publish_btns_layout.addWidget(btn_select_context_published, 1) - publish_btns_layout.addWidget(btn_cancel_published, 1) - - btns_layout = QtWidgets.QHBoxLayout(btns_widget) - btns_layout.setContentsMargins(0, 0, 0, 0) - btns_layout.addWidget(workarea_btns_widget, 1) - btns_layout.addWidget(publish_btns_widget, 1) - - # Build files widgets for home page - main_layout = QtWidgets.QVBoxLayout(self) - main_layout.setContentsMargins(0, 0, 0, 0) - main_layout.addWidget(filter_widget, 0) - main_layout.addWidget(views_widget, 1) - main_layout.addWidget(btns_widget, 0) - - # Register signal callbacks - published_checkbox.stateChanged.connect(self._on_published_change) - filter_input.textChanged.connect(self._on_filter_text_change) - - workarea_files_view.doubleClickedLeft.connect( - self._on_workarea_open_pressed - ) - workarea_files_view.customContextMenuRequested.connect( - self._on_workarea_context_menu - ) - workarea_files_view.selectionModel().selectionChanged.connect( - self.on_file_select - ) - - btn_open.pressed.connect(self._on_workarea_open_pressed) - btn_browse.pressed.connect(self.on_browse_pressed) - btn_save.pressed.connect(self._on_save_as_pressed) - btn_save_as_published.pressed.connect( - self._on_published_save_as_pressed - ) - btn_change_context.pressed.connect( - self._on_publish_change_context_pressed - ) - btn_select_context_published.pressed.connect( - self._on_publish_select_context_pressed - ) - btn_cancel_published.pressed.connect( - self._on_publish_cancel_pressed - ) - - # Store attributes - self._published_checkbox = published_checkbox - self._filter_input = filter_input - - self._workarea_time_delegate = workarea_time_delegate - self._workarea_files_view = workarea_files_view - self._workarea_files_model = workarea_files_model - self._workarea_proxy_model = workarea_proxy_model - - self._publish_time_delegate = publish_time_delegate - self._publish_files_view = publish_files_view - self._publish_files_model = publish_files_model - self._publish_proxy_model = publish_proxy_model - - self._publish_context_overlay = publish_context_overlay - - self._workarea_btns_widget = workarea_btns_widget - self._publish_btns_widget = publish_btns_widget - self._btn_open = btn_open - self._btn_browse = btn_browse - self._btn_save = btn_save - - self._btn_save_as_published = btn_save_as_published - self._btn_change_context = btn_change_context - self._btn_select_context_published = btn_select_context_published - self._btn_cancel_published = btn_cancel_published - - # Create a proxy widget for files widget - self.setFocusProxy(btn_open) - - # Hide publish files widgets - publish_files_view.setVisible(False) - publish_btns_widget.setVisible(False) - btn_select_context_published.setVisible(False) - btn_cancel_published.setVisible(False) - - self._publish_context_select_mode = False - - @property - def published_enabled(self): - return self._published_checkbox.isChecked() - - def _on_published_change(self): - published_enabled = self.published_enabled - - self._workarea_files_view.setVisible(not published_enabled) - self._workarea_btns_widget.setVisible(not published_enabled) - - self._publish_files_view.setVisible(published_enabled) - self._publish_btns_widget.setVisible(published_enabled) - - self._update_filtering() - self._update_asset_task() - - self.published_visible_changed.emit(published_enabled) - - self._select_last_modified_file() - - def _on_filter_text_change(self): - self._update_filtering() - - def _update_filtering(self): - text = self._filter_input.text() - if self.published_enabled: - self._publish_proxy_model.setFilterFixedString(text) - else: - self._workarea_proxy_model.setFilterFixedString(text) - - def set_save_enabled(self, enabled): - self._btn_save.setEnabled(enabled) - if not enabled and self._published_checkbox.isChecked(): - self._published_checkbox.setChecked(False) - self._published_checkbox.setVisible(enabled) - - def set_asset_task(self, asset_id, task_name, task_type): - if asset_id != self._asset_id: - self._asset_doc = None - self._asset_id = asset_id - self._task_name = task_name - self._task_type = task_type - self._update_asset_task() - - def _update_asset_task(self): - if self.published_enabled and not self._publish_context_select_mode: - self._publish_files_model.set_context( - self._asset_id, self._task_name - ) - has_valid_items = self._publish_files_model.has_valid_items() - self._btn_save_as_published.setEnabled(has_valid_items) - self._btn_change_context.setEnabled(has_valid_items) - - else: - # Define a custom session so we can query the work root - # for a "Work area" that is not our current Session. - # This way we can browse it even before we enter it. - if self._asset_id and self._task_name and self._task_type: - session = self._get_session() - self._workdir_path = session["AVALON_WORKDIR"] - self._workfiles_root = self.host.work_root(session) - self._workarea_files_model.set_root(self._workfiles_root) - - else: - self._workarea_files_model.set_root(None) - - # Disable/Enable buttons based on available files in model - has_valid_items = self._workarea_files_model.has_valid_items() - self._btn_browse.setEnabled(True) - self._btn_open.setEnabled(has_valid_items) - - if self._publish_context_select_mode: - self._btn_select_context_published.setEnabled( - bool(self._asset_id) and bool(self._task_name) - ) - return - - # Manually trigger file selection - if not has_valid_items: - self.on_file_select() - - def _get_asset_doc(self): - if self._asset_id is None: - return None - - if self._asset_doc is None: - self._asset_doc = get_asset_by_id( - self.project_name, self._asset_id - ) - - return self._asset_doc - - def _get_session(self): - """Return a modified session for the current asset and task""" - - session = legacy_io.Session.copy() - self.template_key = get_workfile_template_key( - self._task_type, - self.host_name, - project_name=self.project_name - ) - changes = compute_session_changes( - session, - self._get_asset_doc(), - self._task_name, - template_key=self.template_key - ) - session.update(changes) - - return session - - def _enter_session(self): - """Enter the asset and task session currently selected""" - - session = legacy_io.Session.copy() - changes = compute_session_changes( - session, - self._get_asset_doc(), - self._task_name, - template_key=self.template_key - ) - if not changes: - # Return early if we're already in the right Session context - # to avoid any unwanted Task Changed callbacks to be triggered. - return - - change_current_context( - self._get_asset_doc(), - self._task_name, - template_key=self.template_key - ) - - def _get_event_context_data(self): - asset_id = None - asset_name = None - asset_doc = self._get_asset_doc() - if asset_doc: - asset_id = asset_doc["_id"] - asset_name = asset_doc["name"] - return { - "project_name": self.project_name, - "asset_id": asset_id, - "asset_name": asset_name, - "task_name": self._task_name, - "host_name": self.host_name - } - - def _is_workfile_locked(self, filepath): - if not is_workfile_lock_enabled(self.host_name, self.project_name): - return False - if not is_workfile_locked(filepath): - return False - return not is_workfile_locked_for_current_process(filepath) - - def open_file(self, filepath): - host = self.host - if self._is_workfile_locked(filepath): - # add lockfile dialog - WorkfileLockDialog(filepath) - - if isinstance(host, IWorkfileHost): - has_unsaved_changes = host.workfile_has_unsaved_changes() - else: - has_unsaved_changes = host.has_unsaved_changes() - - if has_unsaved_changes: - result = self.save_changes_prompt() - if result is None: - # Cancel operation - return False - - # Save first if has changes - if result: - if isinstance(host, IWorkfileHost): - current_file = host.get_current_workfile() - else: - current_file = host.current_file() - if not current_file: - # If the user requested to save the current scene - # we can't actually automatically do so if the current - # file has not been saved with a name yet. So we'll have - # to opt out. - log.error("Can't save scene with no filename. Please " - "first save your work file using 'Save As'.") - return - - # Save current scene, continue to open file - if isinstance(host, IWorkfileHost): - host.save_workfile(current_file) - else: - host.save_file(current_file) - - event_data_before = self._get_event_context_data() - event_data_before["filepath"] = filepath - event_data_after = copy.deepcopy(event_data_before) - emit_event( - "workfile.open.before", - event_data_before, - source="workfiles.tool" - ) - self._enter_session() - if isinstance(host, IWorkfileHost): - host.open_workfile(filepath) - else: - host.open_file(filepath) - emit_event( - "workfile.open.after", - event_data_after, - source="workfiles.tool" - ) - self.file_opened.emit() - - def save_changes_prompt(self): - self._messagebox = messagebox = QtWidgets.QMessageBox(parent=self) - messagebox.setWindowFlags( - messagebox.windowFlags() | QtCore.Qt.FramelessWindowHint - ) - messagebox.setIcon(QtWidgets.QMessageBox.Warning) - messagebox.setWindowTitle("Unsaved Changes!") - messagebox.setText( - "There are unsaved changes to the current file." - "\nDo you want to save the changes?" - ) - messagebox.setStandardButtons( - QtWidgets.QMessageBox.Yes - | QtWidgets.QMessageBox.No - | QtWidgets.QMessageBox.Cancel - ) - - result = messagebox.exec_() - if result == QtWidgets.QMessageBox.Yes: - return True - if result == QtWidgets.QMessageBox.No: - return False - return None - - def get_filename(self): - """Show save dialog to define filename for save or duplicate - - Returns: - str: The filename to create. - - """ - session = self._get_session() - - if self.published_enabled: - filepath = self._get_selected_filepath() - extensions = [os.path.splitext(filepath)[1]] - else: - extensions = self._get_host_extensions() - - window = SaveAsDialog( - parent=self, - root=self._workfiles_root, - anatomy=self.anatomy, - template_key=self.template_key, - extensions=extensions, - session=session - ) - window.exec_() - - return window.get_result() - - def on_duplicate_pressed(self): - work_file = self.get_filename() - if not work_file: - return - - src = self._get_selected_filepath() - dst = os.path.join(self._workfiles_root, work_file) - shutil.copyfile(src, dst) - - self.workfile_created.emit(dst) - - self.refresh() - - def _get_selected_filepath(self): - """Return current filepath selected in view""" - if self.published_enabled: - source_view = self._publish_files_view - else: - source_view = self._workarea_files_view - selection = source_view.selectionModel() - index = selection.currentIndex() - if not index.isValid(): - return - - return index.data(FILEPATH_ROLE) - - def _on_workarea_open_pressed(self): - path = self._get_selected_filepath() - if not path: - print("No file selected to open..") - return - - self.open_file(path) - - def _get_host_extensions(self): - if isinstance(self.host, IWorkfileHost): - return self.host.get_workfile_extensions() - return self.host.file_extensions() - - def on_browse_pressed(self): - ext_filter = "Work File (*{0})".format( - " *".join(self._get_host_extensions()) - ) - dir_key = "directory" - if qtpy.API in ("pyside", "pyside2", "pyside6"): - dir_key = "dir" - - workfile_root = self._workfiles_root - # Find existing directory of workfile root - # - Qt will use 'cwd' instead, if path does not exist, which may lead - # to igniter directory - while workfile_root: - if os.path.exists(workfile_root): - break - workfile_root = os.path.dirname(workfile_root) - - kwargs = { - "caption": "Work Files", - "filter": ext_filter, - dir_key: workfile_root - } - - work_file = QtWidgets.QFileDialog.getOpenFileName(**kwargs)[0] - if work_file: - self.open_file(work_file) - - def _on_save_as_pressed(self): - self._save_as_with_dialog() - - def _save_as_with_dialog(self): - work_filename = self.get_filename() - if not work_filename: - return None - - src_path = self._get_selected_filepath() - - # Trigger before save event - event_data_before = self._get_event_context_data() - event_data_before.update({ - "filename": work_filename, - "workdir_path": self._workdir_path - }) - emit_event( - "workfile.save.before", - event_data_before, - source="workfiles.tool" - ) - - # Make sure workfiles root is updated - # - this triggers 'workio.work_root(...)' which may change value of - # '_workfiles_root' - self.set_asset_task( - self._asset_id, self._task_name, self._task_type - ) - - # Create workfiles root folder - if not os.path.exists(self._workfiles_root): - log.debug("Initializing Work Directory: %s", self._workfiles_root) - os.makedirs(self._workfiles_root) - - # Prepare full path to workfile and save it - filepath = os.path.join( - os.path.normpath(self._workfiles_root), work_filename - ) - - # Update session if context has changed - self._enter_session() - - if not self.published_enabled: - if isinstance(self.host, IWorkfileHost): - self.host.save_workfile(filepath) - else: - self.host.save_file(filepath) - else: - shutil.copyfile(src_path, filepath) - if isinstance(self.host, IWorkfileHost): - self.host.open_workfile(filepath) - else: - self.host.open_file(filepath) - - # Create extra folders - create_workdir_extra_folders( - self._workdir_path, - self.host_name, - self._task_type, - self._task_name, - self.project_name - ) - event_data_after = self._get_event_context_data() - event_data_after.update({ - "filename": work_filename, - "workdir_path": self._workdir_path - }) - # Trigger after save events - emit_event( - "workfile.save.after", - event_data_after, - source="workfiles.tool" - ) - - self.workfile_created.emit(filepath) - # Refresh files model - if self.published_enabled: - self._published_checkbox.setChecked(False) - else: - self.refresh() - return filepath - - def _on_published_save_as_pressed(self): - self._save_as_with_dialog() - - def _set_publish_context_select_mode(self, enabled): - self._publish_context_select_mode = enabled - - # Show buttons related to context selection - self._publish_context_overlay.setVisible(enabled) - self._btn_cancel_published.setVisible(enabled) - self._btn_select_context_published.setVisible(enabled) - # Change enabled state based on select context - self._btn_select_context_published.setEnabled( - bool(self._asset_id) and bool(self._task_name) - ) - - self._btn_save_as_published.setVisible(not enabled) - self._btn_change_context.setVisible(not enabled) - - # Change views and disable workarea view if enabled - self._workarea_files_view.setEnabled(not enabled) - if self.published_enabled: - self._workarea_files_view.setVisible(enabled) - self._publish_files_view.setVisible(not enabled) - else: - self._workarea_files_view.setVisible(True) - self._publish_files_view.setVisible(False) - - # Disable filter widgets - self._published_checkbox.setEnabled(not enabled) - self._filter_input.setEnabled(not enabled) - - def _on_publish_change_context_pressed(self): - self._set_publish_context_select_mode(True) - - def _on_publish_select_context_pressed(self): - result = self._save_as_with_dialog() - if result is not None: - self._set_publish_context_select_mode(False) - self._update_asset_task() - - def _on_publish_cancel_pressed(self): - self._set_publish_context_select_mode(False) - self._update_asset_task() - - def on_file_select(self): - self.file_selected.emit(self._get_selected_filepath()) - - def refresh(self): - """Refresh listed files for current selection in the interface""" - if self.published_enabled: - self._publish_files_model.refresh() - else: - self._workarea_files_model.refresh() - - if self.auto_select_latest_modified: - self._select_last_modified_file() - - def _on_workarea_context_menu(self, point): - index = self._workarea_files_view.indexAt(point) - if not index.isValid(): - return - - if not index.flags() & QtCore.Qt.ItemIsEnabled: - return - - menu = QtWidgets.QMenu(self) - - # Duplicate - action = QtWidgets.QAction("Duplicate", menu) - tip = "Duplicate selected file." - action.setToolTip(tip) - action.setStatusTip(tip) - action.triggered.connect(self.on_duplicate_pressed) - menu.addAction(action) - - # Show the context action menu - global_point = self._workarea_files_view.mapToGlobal(point) - action = menu.exec_(global_point) - if not action: - return - - def _select_last_modified_file(self): - """Utility function to select the file with latest date modified""" - if self.published_enabled: - source_view = self._publish_files_view - else: - source_view = self._workarea_files_view - model = source_view.model() - - highest_index = None - highest = 0 - for row in range(model.rowCount()): - index = model.index(row, 0, parent=QtCore.QModelIndex()) - if not index.isValid(): - continue - - modified = index.data(DATE_MODIFIED_ROLE) - if modified is not None and modified > highest: - highest_index = index - highest = modified - - if highest_index: - source_view.setCurrentIndex(highest_index) diff --git a/client/ayon_core/tools/workfiles/lock_dialog.py b/client/ayon_core/tools/workfiles/lock_dialog.py deleted file mode 100644 index 45144b7d2a..0000000000 --- a/client/ayon_core/tools/workfiles/lock_dialog.py +++ /dev/null @@ -1,47 +0,0 @@ -from qtpy import QtWidgets, QtCore, QtGui -from ayon_core.style import load_stylesheet, get_app_icon_path - -from ayon_core.pipeline.workfile.lock_workfile import get_workfile_lock_data - - -class WorkfileLockDialog(QtWidgets.QDialog): - def __init__(self, workfile_path, parent=None): - super(WorkfileLockDialog, self).__init__(parent) - self.setWindowTitle("Warning") - icon = QtGui.QIcon(get_app_icon_path()) - self.setWindowIcon(icon) - - data = get_workfile_lock_data(workfile_path) - - message = "{} on {} machine is working on the same workfile.".format( - data["username"], - data["hostname"] - ) - - msg_label = QtWidgets.QLabel(message, self) - - btns_widget = QtWidgets.QWidget(self) - - cancel_btn = QtWidgets.QPushButton("Cancel", btns_widget) - ignore_btn = QtWidgets.QPushButton("Ignore lock", btns_widget) - - btns_layout = QtWidgets.QHBoxLayout(btns_widget) - btns_layout.setContentsMargins(0, 0, 0, 0) - btns_layout.setSpacing(10) - btns_layout.addStretch(1) - btns_layout.addWidget(cancel_btn, 0) - btns_layout.addWidget(ignore_btn, 0) - - main_layout = QtWidgets.QVBoxLayout(self) - main_layout.setContentsMargins(15, 15, 15, 15) - main_layout.addWidget(msg_label, 1, QtCore.Qt.AlignCenter), - main_layout.addSpacing(10) - main_layout.addWidget(btns_widget, 0) - - cancel_btn.clicked.connect(self.reject) - ignore_btn.clicked.connect(self.accept) - - def showEvent(self, event): - super(WorkfileLockDialog, self).showEvent(event) - - self.setStyleSheet(load_stylesheet()) diff --git a/client/ayon_core/tools/workfiles/model.py b/client/ayon_core/tools/workfiles/model.py deleted file mode 100644 index e2e720904a..0000000000 --- a/client/ayon_core/tools/workfiles/model.py +++ /dev/null @@ -1,458 +0,0 @@ -import os -import logging - -from qtpy import QtCore, QtGui -import qtawesome - -from ayon_core.client import ( - get_subsets, - get_versions, - get_representations, -) -from ayon_core.style import ( - get_default_entity_icon_color, - get_disabled_entity_icon_color, -) -from ayon_core.pipeline import get_representation_path - -log = logging.getLogger(__name__) - - -FILEPATH_ROLE = QtCore.Qt.UserRole + 2 -DATE_MODIFIED_ROLE = QtCore.Qt.UserRole + 3 -ITEM_ID_ROLE = QtCore.Qt.UserRole + 4 - - -class WorkAreaFilesModel(QtGui.QStandardItemModel): - """Model is looking into one folder for files with extension.""" - - def __init__(self, extensions, *args, **kwargs): - super(WorkAreaFilesModel, self).__init__(*args, **kwargs) - - self.setColumnCount(2) - - self._root = None - self._file_extensions = extensions - self._invalid_path_item = None - self._empty_root_item = None - self._file_icon = qtawesome.icon( - "fa.file-o", - color=get_default_entity_icon_color() - ) - self._invalid_item_visible = False - self._items_by_filename = {} - - def _get_invalid_path_item(self): - if self._invalid_path_item is None: - message = "Work Area does not exist. Use Save As to create it." - item = QtGui.QStandardItem(message) - icon = qtawesome.icon( - "fa.times", - color=get_disabled_entity_icon_color() - ) - item.setData(icon, QtCore.Qt.DecorationRole) - item.setFlags(QtCore.Qt.NoItemFlags) - item.setColumnCount(self.columnCount()) - self._invalid_path_item = item - return self._invalid_path_item - - def _get_empty_root_item(self): - if self._empty_root_item is None: - message = "Work Area is empty." - item = QtGui.QStandardItem(message) - icon = qtawesome.icon( - "fa.times", - color=get_disabled_entity_icon_color() - ) - item.setData(icon, QtCore.Qt.DecorationRole) - item.setFlags(QtCore.Qt.NoItemFlags) - item.setColumnCount(self.columnCount()) - self._empty_root_item = item - return self._empty_root_item - - def set_root(self, root): - """Change directory where to look for file.""" - self._root = root - if root and not os.path.exists(root): - log.debug("Work Area does not exist: {}".format(root)) - self.refresh() - - def _clear(self): - root_item = self.invisibleRootItem() - rows = root_item.rowCount() - if rows > 0: - if self._invalid_item_visible: - for row in range(rows): - root_item.takeRow(row) - else: - root_item.removeRows(0, rows) - self._items_by_filename = {} - - def refresh(self): - """Refresh and update model items.""" - root_item = self.invisibleRootItem() - # If path is not set or does not exist then add invalid path item - if not self._root or not os.path.exists(self._root): - self._clear() - # Add Work Area does not exist placeholder - item = self._get_invalid_path_item() - root_item.appendRow(item) - self._invalid_item_visible = True - return - - # Clear items if previous refresh set '_invalid_item_visible' to True - # - Invalid items are not stored to '_items_by_filename' so they would - # not be removed - if self._invalid_item_visible: - self._clear() - - # Check for new items that should be added and items that should be - # removed - new_items = [] - items_to_remove = set(self._items_by_filename.keys()) - for filename in os.listdir(self._root): - filepath = os.path.join(self._root, filename) - if os.path.isdir(filepath): - continue - - ext = os.path.splitext(filename)[1] - if ext not in self._file_extensions: - continue - - modified = os.path.getmtime(filepath) - - # Use existing item or create new one - if filename in items_to_remove: - items_to_remove.remove(filename) - item = self._items_by_filename[filename] - else: - item = QtGui.QStandardItem(filename) - item.setColumnCount(self.columnCount()) - item.setFlags( - QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable - ) - item.setData(self._file_icon, QtCore.Qt.DecorationRole) - new_items.append(item) - self._items_by_filename[filename] = item - # Update data that may be different - item.setData(filepath, FILEPATH_ROLE) - item.setData(modified, DATE_MODIFIED_ROLE) - - # Add new items if there are any - if new_items: - root_item.appendRows(new_items) - - # Remove items that are no longer available - for filename in items_to_remove: - item = self._items_by_filename.pop(filename) - root_item.removeRow(item.row()) - - # Add empty root item if there are not filenames that could be shown - if root_item.rowCount() > 0: - self._invalid_item_visible = False - else: - self._invalid_item_visible = True - item = self._get_empty_root_item() - root_item.appendRow(item) - - def has_valid_items(self): - """Directory has files that are listed in items.""" - return not self._invalid_item_visible - - def flags(self, index): - # Use flags of first column for all columns - if index.column() != 0: - index = self.index(index.row(), 0, index.parent()) - return super(WorkAreaFilesModel, self).flags(index) - - def data(self, index, role=None): - if role is None: - role = QtCore.Qt.DisplayRole - - # Handle roles for first column - if index.column() == 1: - if role == QtCore.Qt.DecorationRole: - return None - - if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole): - role = DATE_MODIFIED_ROLE - index = self.index(index.row(), 0, index.parent()) - - return super(WorkAreaFilesModel, self).data(index, role) - - def headerData(self, section, orientation, role): - # Show nice labels in the header - if ( - role == QtCore.Qt.DisplayRole - and orientation == QtCore.Qt.Horizontal - ): - if section == 0: - return "Name" - elif section == 1: - return "Date modified" - - return super(WorkAreaFilesModel, self).headerData( - section, orientation, role - ) - - -class PublishFilesModel(QtGui.QStandardItemModel): - """Model filling files with published files calculated from representation. - - This model looks for workfile family representations based on selected - asset and task. - - Asset must set to be able look for representations that could be used. - Task is used to filter representations by task. - Model has few filter criteria for filling. - - First criteria is that version document must have "workfile" in - "data.families". - - Second cirteria is that representation must have extension same as - defined extensions - - If task is set then representation must have 'task["name"]' with same - name. - """ - - def __init__(self, extensions, dbcon, anatomy, *args, **kwargs): - super(PublishFilesModel, self).__init__(*args, **kwargs) - - self.setColumnCount(2) - - self._dbcon = dbcon - self._anatomy = anatomy - - self._file_extensions = extensions - - self._invalid_context_item = None - self._empty_root_item = None - self._file_icon = qtawesome.icon( - "fa.file-o", - color=get_default_entity_icon_color() - ) - self._invalid_icon = qtawesome.icon( - "fa.times", - color=get_disabled_entity_icon_color() - ) - self._invalid_item_visible = False - - self._items_by_id = {} - - self._asset_id = None - self._task_name = None - - @property - def project_name(self): - return self._dbcon.Session["AVALON_PROJECT"] - - def _set_item_invalid(self, item): - item.setFlags(QtCore.Qt.NoItemFlags) - item.setData(self._invalid_icon, QtCore.Qt.DecorationRole) - - def _set_item_valid(self, item): - item.setFlags( - QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable - ) - item.setData(self._file_icon, QtCore.Qt.DecorationRole) - - def _get_invalid_context_item(self): - if self._invalid_context_item is None: - item = QtGui.QStandardItem("Selected context is not valid.") - item.setColumnCount(self.columnCount()) - self._set_item_invalid(item) - self._invalid_context_item = item - return self._invalid_context_item - - def _get_empty_root_item(self): - if self._empty_root_item is None: - item = QtGui.QStandardItem("Didn't find any published workfiles.") - item.setColumnCount(self.columnCount()) - self._set_item_invalid(item) - self._empty_root_item = item - return self._empty_root_item - - def set_context(self, asset_id, task_name): - """Change context to asset and task. - - Args: - asset_id (ObjectId): Id of selected asset. - task_name (str): Name of selected task. - """ - self._asset_id = asset_id - self._task_name = task_name - self.refresh() - - def _clear(self): - root_item = self.invisibleRootItem() - rows = root_item.rowCount() - if rows > 0: - if self._invalid_item_visible: - for row in range(rows): - root_item.takeRow(row) - else: - root_item.removeRows(0, rows) - self._items_by_id = {} - - def _get_workfie_representations(self): - output = [] - # Get subset docs of asset - subset_docs = get_subsets( - self.project_name, - asset_ids=[self._asset_id], - fields=["_id", "name"] - ) - - subset_ids = [subset_doc["_id"] for subset_doc in subset_docs] - if not subset_ids: - return output - - # Get version docs of subsets with their families - version_docs = get_versions( - self.project_name, - subset_ids=subset_ids, - fields=["_id", "parent", "data.families"] - ) - - # Filter versions if they contain 'workfile' family - filtered_versions = [] - for version_doc in version_docs: - data = version_doc.get("data") or {} - families = data.get("families") or [] - if "workfile" in families: - filtered_versions.append(version_doc) - - version_ids = [version_doc["_id"] for version_doc in filtered_versions] - if not version_ids: - return output - - # Query representations of filtered versions and add filter for - # extension - extensions = [ext.replace(".", "") for ext in self._file_extensions] - repre_docs = get_representations( - self.project_name, - version_ids=version_ids, - context_filters={"ext": extensions} - ) - - # Filter queried representations by task name if task is set - filtered_repre_docs = [] - for repre_doc in repre_docs: - if self._task_name is None: - filtered_repre_docs.append(repre_doc) - continue - - task_info = repre_doc["context"].get("task") - if not task_info: - print("Not task info") - continue - - if isinstance(task_info, dict): - task_name = task_info.get("name") - else: - task_name = task_info - - if task_name == self._task_name: - filtered_repre_docs.append(repre_doc) - - # Collect paths of representations - for repre_doc in filtered_repre_docs: - path = get_representation_path( - repre_doc, root=self._anatomy.roots - ) - output.append((path, repre_doc["_id"])) - return output - - def refresh(self): - root_item = self.invisibleRootItem() - if not self._asset_id: - self._clear() - # Add Work Area does not exist placeholder - item = self._get_invalid_context_item() - root_item.appendRow(item) - self._invalid_item_visible = True - return - - if self._invalid_item_visible: - self._clear() - - new_items = [] - items_to_remove = set(self._items_by_id.keys()) - for item in self._get_workfie_representations(): - filepath, repre_id = item - # TODO handle empty filepaths - if not filepath: - continue - filename = os.path.basename(filepath) - - if repre_id in items_to_remove: - items_to_remove.remove(repre_id) - item = self._items_by_id[repre_id] - else: - item = QtGui.QStandardItem(filename) - item.setColumnCount(self.columnCount()) - new_items.append(item) - self._items_by_id[repre_id] = item - - if os.path.exists(filepath): - modified = os.path.getmtime(filepath) - tooltip = None - self._set_item_valid(item) - else: - modified = None - tooltip = "File is not available from this machine" - self._set_item_invalid(item) - - item.setData(tooltip, QtCore.Qt.ToolTipRole) - item.setData(filepath, FILEPATH_ROLE) - item.setData(modified, DATE_MODIFIED_ROLE) - item.setData(repre_id, ITEM_ID_ROLE) - - if new_items: - root_item.appendRows(new_items) - - for filename in items_to_remove: - item = self._items_by_id.pop(filename) - root_item.removeRow(item.row()) - - if root_item.rowCount() > 0: - self._invalid_item_visible = False - else: - self._invalid_item_visible = True - item = self._get_empty_root_item() - root_item.appendRow(item) - - def has_valid_items(self): - return not self._invalid_item_visible - - def flags(self, index): - if index.column() != 0: - index = self.index(index.row(), 0, index.parent()) - return super(PublishFilesModel, self).flags(index) - - def data(self, index, role=None): - if role is None: - role = QtCore.Qt.DisplayRole - - if index.column() == 1: - if role == QtCore.Qt.DecorationRole: - return None - - if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole): - role = DATE_MODIFIED_ROLE - index = self.index(index.row(), 0, index.parent()) - - return super(PublishFilesModel, self).data(index, role) - - def headerData(self, section, orientation, role): - # Show nice labels in the header - if ( - role == QtCore.Qt.DisplayRole - and orientation == QtCore.Qt.Horizontal - ): - if section == 0: - return "Name" - elif section == 1: - return "Date modified" - - return super(PublishFilesModel, self).headerData( - section, orientation, role - ) diff --git a/client/ayon_core/tools/ayon_workfiles/models/__init__.py b/client/ayon_core/tools/workfiles/models/__init__.py similarity index 100% rename from client/ayon_core/tools/ayon_workfiles/models/__init__.py rename to client/ayon_core/tools/workfiles/models/__init__.py diff --git a/client/ayon_core/tools/ayon_workfiles/models/selection.py b/client/ayon_core/tools/workfiles/models/selection.py similarity index 100% rename from client/ayon_core/tools/ayon_workfiles/models/selection.py rename to client/ayon_core/tools/workfiles/models/selection.py diff --git a/client/ayon_core/tools/ayon_workfiles/models/workfiles.py b/client/ayon_core/tools/workfiles/models/workfiles.py similarity index 99% rename from client/ayon_core/tools/ayon_workfiles/models/workfiles.py rename to client/ayon_core/tools/workfiles/models/workfiles.py index 395c5dbd6f..55653e34d4 100644 --- a/client/ayon_core/tools/ayon_workfiles/models/workfiles.py +++ b/client/ayon_core/tools/workfiles/models/workfiles.py @@ -19,7 +19,7 @@ from ayon_core.pipeline.workfile import ( get_last_workfile_with_version, ) from ayon_core.pipeline.version_start import get_versioning_start -from ayon_core.tools.ayon_workfiles.abstract import ( +from ayon_core.tools.workfiles.abstract import ( WorkareaFilepathResult, FileItem, WorkfileInfo, diff --git a/client/ayon_core/tools/workfiles/save_as_dialog.py b/client/ayon_core/tools/workfiles/save_as_dialog.py deleted file mode 100644 index d63bc9cb80..0000000000 --- a/client/ayon_core/tools/workfiles/save_as_dialog.py +++ /dev/null @@ -1,484 +0,0 @@ -import os -import re -import copy -import logging - -from qtpy import QtWidgets, QtCore - -from ayon_core.pipeline import ( - registered_host, - legacy_io, -) -from ayon_core.pipeline.workfile import get_last_workfile_with_version -from ayon_core.pipeline.template_data import get_template_data_with_names -from ayon_core.tools.utils import PlaceholderLineEdit -from ayon_core.pipeline import version_start, get_current_host_name - -log = logging.getLogger(__name__) - - -def build_workfile_data(session): - """Get the data required for workfile formatting from avalon `session`""" - - # Set work file data for template formatting - project_name = session["AVALON_PROJECT"] - asset_name = session["AVALON_ASSET"] - task_name = session["AVALON_TASK"] - host_name = session["AVALON_APP"] - - data = get_template_data_with_names( - project_name, asset_name, task_name, host_name - ) - data.update({ - "version": 1, - "comment": "", - "ext": None - }) - - return data - - -class CommentMatcher(object): - """Use anatomy and work file data to parse comments from filenames""" - def __init__(self, anatomy, template_key, data): - - self.fname_regex = None - - template = anatomy.templates[template_key]["file"] - if "{comment}" not in template: - # Don't look for comment if template doesn't allow it - return - - # Create a regex group for extensions - extensions = registered_host().file_extensions() - any_extension = "(?:{})".format( - "|".join(re.escape(ext.lstrip(".")) for ext in extensions) - ) - - # Use placeholders that will never be in the filename - temp_data = copy.deepcopy(data) - temp_data["comment"] = "<>" - temp_data["version"] = "<>" - temp_data["ext"] = "<>" - - template_obj = anatomy.templates_obj[template_key]["file"] - fname_pattern = template_obj.format_strict(temp_data) - fname_pattern = re.escape(fname_pattern) - - # Replace comment and version with something we can match with regex - replacements = { - "<>": "(.+)", - "<>": "[0-9]+", - "<>": any_extension, - } - for src, dest in replacements.items(): - fname_pattern = fname_pattern.replace(re.escape(src), dest) - - # Match from beginning to end of string to be safe - fname_pattern = "^{}$".format(fname_pattern) - - self.fname_regex = re.compile(fname_pattern) - - def parse_comment(self, filepath): - """Parse the {comment} part from a filename""" - if not self.fname_regex: - return - - fname = os.path.basename(filepath) - match = self.fname_regex.match(fname) - if match: - return match.group(1) - - -class SubversionLineEdit(QtWidgets.QWidget): - """QLineEdit with QPushButton for drop down selection of list of strings""" - - text_changed = QtCore.Signal(str) - - def __init__(self, *args, **kwargs): - super(SubversionLineEdit, self).__init__(*args, **kwargs) - - input_field = PlaceholderLineEdit(self) - menu_btn = QtWidgets.QPushButton(self) - menu_btn.setFixedWidth(18) - - menu = QtWidgets.QMenu(self) - menu_btn.setMenu(menu) - - layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(3) - - layout.addWidget(input_field, 1) - layout.addWidget(menu_btn, 0) - - input_field.textChanged.connect(self.text_changed) - - self.setFocusProxy(input_field) - - self._input_field = input_field - self._menu_btn = menu_btn - self._menu = menu - - def set_placeholder(self, placeholder): - self._input_field.setPlaceholderText(placeholder) - - def set_text(self, text): - self._input_field.setText(text) - - def set_values(self, values): - self._update(values) - - def _on_button_clicked(self): - self._menu.exec_() - - def _on_action_clicked(self, action): - self._input_field.setText(action.text()) - - def _update(self, values): - """Create optional predefined subset names - - Args: - default_names(list): all predefined names - - Returns: - None - """ - - menu = self._menu - button = self._menu_btn - - state = any(values) - button.setEnabled(state) - if state is False: - return - - # Include an empty string - values = [""] + sorted(values) - - # Get and destroy the action group - group = button.findChild(QtWidgets.QActionGroup) - if group: - group.deleteLater() - - # Build new action group - group = QtWidgets.QActionGroup(button) - for name in values: - action = group.addAction(name) - menu.addAction(action) - - group.triggered.connect(self._on_action_clicked) - - -class SaveAsDialog(QtWidgets.QDialog): - """Name Window to define a unique filename inside a root folder - - The filename will be based on the "workfile" template defined in the - project["config"]["template"]. - - """ - - def __init__( - self, parent, root, anatomy, template_key, extensions, session=None - ): - super(SaveAsDialog, self).__init__(parent=parent) - self.setWindowFlags(self.windowFlags() | QtCore.Qt.FramelessWindowHint) - - self.result = None - self.host = registered_host() - self.root = root - self.work_file = None - self._extensions = extensions - - if not session: - # Fallback to active session - session = legacy_io.Session - - self.data = build_workfile_data(session) - - # Store project anatomy - self.anatomy = anatomy - self.template = anatomy.templates[template_key]["file"] - self.template_key = template_key - - # Btns widget - btns_widget = QtWidgets.QWidget(self) - - btn_ok = QtWidgets.QPushButton("Ok", btns_widget) - btn_cancel = QtWidgets.QPushButton("Cancel", btns_widget) - - btns_layout = QtWidgets.QHBoxLayout(btns_widget) - btns_layout.addWidget(btn_ok) - btns_layout.addWidget(btn_cancel) - - # Inputs widget - inputs_widget = QtWidgets.QWidget(self) - - # Version widget - version_widget = QtWidgets.QWidget(inputs_widget) - - # Version number input - version_input = QtWidgets.QSpinBox(version_widget) - version_input.setMinimum( - version_start.get_versioning_start( - self.data["project"]["name"], - get_current_host_name(), - task_name=self.data["task"]["name"], - task_type=self.data["task"]["type"], - family="workfile" - ) - ) - version_input.setMaximum(9999) - - # Last version checkbox - last_version_check = QtWidgets.QCheckBox( - "Next Available Version", version_widget - ) - last_version_check.setChecked(True) - - version_layout = QtWidgets.QHBoxLayout(version_widget) - version_layout.setContentsMargins(0, 0, 0, 0) - version_layout.addWidget(version_input) - version_layout.addWidget(last_version_check) - - # Preview widget - preview_label = QtWidgets.QLabel("Preview filename", inputs_widget) - - # Subversion input - subversion = SubversionLineEdit(inputs_widget) - subversion.set_placeholder("Will be part of filename.") - - # Extensions combobox - ext_combo = QtWidgets.QComboBox(inputs_widget) - # Add styled delegate to use stylesheets - ext_delegate = QtWidgets.QStyledItemDelegate() - ext_combo.setItemDelegate(ext_delegate) - ext_combo.addItems(self._extensions) - - # Build inputs - inputs_layout = QtWidgets.QFormLayout(inputs_widget) - # Add version only if template contains version key - # - since the version can be padded with "{version:0>4}" we only search - # for "{version". - if "{version" in self.template: - inputs_layout.addRow("Version:", version_widget) - else: - version_widget.setVisible(False) - - # Add subversion only if template contains `{comment}` - if "{comment}" in self.template: - inputs_layout.addRow("Subversion:", subversion) - - # Detect whether a {comment} is in the current filename - if so, - # preserve it by default and set it in the comment/subversion field - current_filepath = self.host.current_file() - if current_filepath: - # We match the current filename against the current session - # instead of the session where the user is saving to. - current_data = build_workfile_data(legacy_io.Session) - matcher = CommentMatcher(anatomy, template_key, current_data) - comment = matcher.parse_comment(current_filepath) - if comment: - log.info("Detected subversion comment: {}".format(comment)) - self.data["comment"] = comment - subversion.set_text(comment) - - existing_comments = self.get_existing_comments() - subversion.set_values(existing_comments) - - else: - subversion.setVisible(False) - inputs_layout.addRow("Extension:", ext_combo) - inputs_layout.addRow("Preview:", preview_label) - - # Build layout - main_layout = QtWidgets.QVBoxLayout(self) - main_layout.addWidget(inputs_widget) - main_layout.addWidget(btns_widget) - - # Signal callback registration - version_input.valueChanged.connect(self.on_version_spinbox_changed) - last_version_check.stateChanged.connect( - self.on_version_checkbox_changed - ) - - subversion.text_changed.connect(self.on_comment_changed) - ext_combo.currentIndexChanged.connect(self.on_extension_changed) - - btn_ok.pressed.connect(self.on_ok_pressed) - btn_cancel.pressed.connect(self.on_cancel_pressed) - - # Allow "Enter" key to accept the save. - btn_ok.setDefault(True) - - # Force default focus to comment, some hosts didn't automatically - # apply focus to this line edit (e.g. Houdini) - subversion.setFocus() - - # Store widgets - self.btn_ok = btn_ok - - self.version_widget = version_widget - - self.version_input = version_input - self.last_version_check = last_version_check - - self.preview_label = preview_label - self.subversion = subversion - self.ext_combo = ext_combo - self._ext_delegate = ext_delegate - - self.refresh() - - def get_existing_comments(self): - matcher = CommentMatcher(self.anatomy, self.template_key, self.data) - host_extensions = set(self._extensions) - comments = set() - if os.path.isdir(self.root): - for fname in os.listdir(self.root): - if not os.path.isfile(os.path.join(self.root, fname)): - continue - - ext = os.path.splitext(fname)[-1] - if ext not in host_extensions: - continue - - comment = matcher.parse_comment(fname) - if comment: - comments.add(comment) - - return list(comments) - - def on_version_spinbox_changed(self, value): - self.data["version"] = value - self.refresh() - - def on_version_checkbox_changed(self, _value): - self.refresh() - - def on_comment_changed(self, text): - self.data["comment"] = text - self.refresh() - - def on_extension_changed(self): - ext = self.ext_combo.currentText() - if ext == self.data["ext"]: - return - self.data["ext"] = ext - self.refresh() - - def on_ok_pressed(self): - self.result = self.work_file - self.close() - - def on_cancel_pressed(self): - self.close() - - def get_result(self): - return self.result - - def get_work_file(self): - data = copy.deepcopy(self.data) - if not data["comment"]: - data.pop("comment", None) - - data["ext"] = data["ext"].lstrip(".") - - template_obj = self.anatomy.templates_obj[self.template_key]["file"] - return template_obj.format_strict(data) - - def refresh(self): - extensions = list(self._extensions) - extension = self.data["ext"] - if extension is None: - # Define saving file extension - current_file = self.host.current_file() - if current_file: - # Match the extension of current file - _, extension = os.path.splitext(current_file) - else: - extension = extensions[0] - - if extension != self.data["ext"]: - self.data["ext"] = extension - index = self.ext_combo.findText( - extension, QtCore.Qt.MatchFixedString - ) - if index >= 0: - self.ext_combo.setCurrentIndex(index) - - if not self.last_version_check.isChecked(): - self.version_input.setEnabled(True) - self.data["version"] = self.version_input.value() - - work_file = self.get_work_file() - - else: - self.version_input.setEnabled(False) - - data = copy.deepcopy(self.data) - template = str(self.template) - - if not data["comment"]: - data.pop("comment", None) - - data["ext"] = data["ext"].lstrip(".") - - version = get_last_workfile_with_version( - self.root, template, data, extensions - )[1] - - if version is None: - version = version_start.get_versioning_start( - data["project"]["name"], - get_current_host_name(), - task_name=self.data["task"]["name"], - task_type=self.data["task"]["type"], - family="workfile" - ) - else: - version += 1 - - found_valid_version = False - # Check if next version is valid version and give a chance to try - # next 100 versions - for idx in range(100): - # Store version to data - self.data["version"] = version - - work_file = self.get_work_file() - # Safety check - path = os.path.join(self.root, work_file) - if not os.path.exists(path): - found_valid_version = True - break - - # Try next version - version += 1 - # Log warning - if idx == 0: - log.warning(( - "BUG: Function `get_last_workfile_with_version` " - "didn't return last version." - )) - # Raise exception if even 100 version fallback didn't help - if not found_valid_version: - raise AssertionError( - "This is a bug. Couldn't find valid version!" - ) - - self.work_file = work_file - - path_exists = os.path.exists(os.path.join(self.root, work_file)) - - self.btn_ok.setEnabled(not path_exists) - - if path_exists: - self.preview_label.setText( - "Cannot create \"{0}\" because file exists!" - "".format(work_file) - ) - else: - self.preview_label.setText( - "{0}".format(work_file) - ) diff --git a/client/ayon_core/tools/ayon_workfiles/widgets/__init__.py b/client/ayon_core/tools/workfiles/widgets/__init__.py similarity index 100% rename from client/ayon_core/tools/ayon_workfiles/widgets/__init__.py rename to client/ayon_core/tools/workfiles/widgets/__init__.py diff --git a/client/ayon_core/tools/ayon_workfiles/widgets/constants.py b/client/ayon_core/tools/workfiles/widgets/constants.py similarity index 100% rename from client/ayon_core/tools/ayon_workfiles/widgets/constants.py rename to client/ayon_core/tools/workfiles/widgets/constants.py diff --git a/client/ayon_core/tools/ayon_workfiles/widgets/files_widget.py b/client/ayon_core/tools/workfiles/widgets/files_widget.py similarity index 100% rename from client/ayon_core/tools/ayon_workfiles/widgets/files_widget.py rename to client/ayon_core/tools/workfiles/widgets/files_widget.py diff --git a/client/ayon_core/tools/ayon_workfiles/widgets/files_widget_published.py b/client/ayon_core/tools/workfiles/widgets/files_widget_published.py similarity index 100% rename from client/ayon_core/tools/ayon_workfiles/widgets/files_widget_published.py rename to client/ayon_core/tools/workfiles/widgets/files_widget_published.py diff --git a/client/ayon_core/tools/ayon_workfiles/widgets/files_widget_workarea.py b/client/ayon_core/tools/workfiles/widgets/files_widget_workarea.py similarity index 100% rename from client/ayon_core/tools/ayon_workfiles/widgets/files_widget_workarea.py rename to client/ayon_core/tools/workfiles/widgets/files_widget_workarea.py diff --git a/client/ayon_core/tools/ayon_workfiles/widgets/save_as_dialog.py b/client/ayon_core/tools/workfiles/widgets/save_as_dialog.py similarity index 100% rename from client/ayon_core/tools/ayon_workfiles/widgets/save_as_dialog.py rename to client/ayon_core/tools/workfiles/widgets/save_as_dialog.py diff --git a/client/ayon_core/tools/ayon_workfiles/widgets/side_panel.py b/client/ayon_core/tools/workfiles/widgets/side_panel.py similarity index 100% rename from client/ayon_core/tools/ayon_workfiles/widgets/side_panel.py rename to client/ayon_core/tools/workfiles/widgets/side_panel.py diff --git a/client/ayon_core/tools/ayon_workfiles/widgets/utils.py b/client/ayon_core/tools/workfiles/widgets/utils.py similarity index 100% rename from client/ayon_core/tools/ayon_workfiles/widgets/utils.py rename to client/ayon_core/tools/workfiles/widgets/utils.py diff --git a/client/ayon_core/tools/ayon_workfiles/widgets/window.py b/client/ayon_core/tools/workfiles/widgets/window.py similarity index 99% rename from client/ayon_core/tools/ayon_workfiles/widgets/window.py rename to client/ayon_core/tools/workfiles/widgets/window.py index 62ff542ff4..9fad14cde3 100644 --- a/client/ayon_core/tools/ayon_workfiles/widgets/window.py +++ b/client/ayon_core/tools/workfiles/widgets/window.py @@ -7,7 +7,7 @@ from ayon_core.tools.utils import ( ) from ayon_core.tools.ayon_utils.widgets import FoldersWidget, TasksWidget -from ayon_core.tools.ayon_workfiles.control import BaseWorkfileController +from ayon_core.tools.workfiles.control import BaseWorkfileController from ayon_core.tools.utils import GoToCurrentButton, RefreshButton from .side_panel import SidePanelWidget diff --git a/client/ayon_core/tools/workfiles/window.py b/client/ayon_core/tools/workfiles/window.py deleted file mode 100644 index 7ce35bbf43..0000000000 --- a/client/ayon_core/tools/workfiles/window.py +++ /dev/null @@ -1,471 +0,0 @@ -import os -import datetime -import copy -import platform -from qtpy import QtCore, QtWidgets, QtGui - -from ayon_core.client import ( - get_asset_by_name, - get_workfile_info, -) -from ayon_core.client.operations import ( - OperationsSession, - new_workfile_info_doc, - prepare_workfile_info_update_data, -) -from ayon_core import style -from ayon_core import resources -from ayon_core.pipeline import ( - Anatomy, - get_current_project_name, - get_current_asset_name, - get_current_task_name, -) -from ayon_core.pipeline import legacy_io -from ayon_core.tools.utils.assets_widget import SingleSelectAssetsWidget -from ayon_core.tools.utils.tasks_widget import TasksWidget - -from .files_widget import FilesWidget - - -def file_size_to_string(file_size): - size = 0 - size_ending_mapping = { - "KB": 1024 ** 1, - "MB": 1024 ** 2, - "GB": 1024 ** 3 - } - ending = "B" - for _ending, _size in size_ending_mapping.items(): - if file_size < _size: - break - size = file_size / _size - ending = _ending - return "{:.2f} {}".format(size, ending) - - -class SidePanelWidget(QtWidgets.QWidget): - save_clicked = QtCore.Signal() - published_workfile_message = ( - "INFO: Opened published workfiles will be stored in" - " temp directory on your machine. Current temp size: {}." - ) - - def __init__(self, parent=None): - super(SidePanelWidget, self).__init__(parent) - - details_label = QtWidgets.QLabel("Details", self) - details_input = QtWidgets.QPlainTextEdit(self) - details_input.setReadOnly(True) - - artist_note_widget = QtWidgets.QWidget(self) - note_label = QtWidgets.QLabel("Artist note", artist_note_widget) - note_input = QtWidgets.QPlainTextEdit(artist_note_widget) - btn_note_save = QtWidgets.QPushButton("Save note", artist_note_widget) - - artist_note_layout = QtWidgets.QVBoxLayout(artist_note_widget) - artist_note_layout.setContentsMargins(0, 0, 0, 0) - artist_note_layout.addWidget(note_label, 0) - artist_note_layout.addWidget(note_input, 1) - artist_note_layout.addWidget( - btn_note_save, 0, alignment=QtCore.Qt.AlignRight - ) - - main_layout = QtWidgets.QVBoxLayout(self) - main_layout.setContentsMargins(0, 0, 0, 0) - main_layout.addWidget(details_label, 0) - main_layout.addWidget(details_input, 1) - main_layout.addWidget(artist_note_widget, 1) - - note_input.textChanged.connect(self._on_note_change) - btn_note_save.clicked.connect(self._on_save_click) - - self._details_input = details_input - self._artist_note_widget = artist_note_widget - self._note_input = note_input - self._btn_note_save = btn_note_save - - self._orig_note = "" - self._workfile_doc = None - - def set_published_visible(self, published_visible): - self._artist_note_widget.setVisible(not published_visible) - - def _on_note_change(self): - text = self._note_input.toPlainText() - self._btn_note_save.setEnabled(self._orig_note != text) - - def _on_save_click(self): - self._orig_note = self._note_input.toPlainText() - self._on_note_change() - self.save_clicked.emit() - - def get_user_name(self, file): - """Get user name from file path""" - # Only run on Unix because pwd module is not available on Windows. - # NOTE: we tried adding "win32security" module but it was not working - # on all hosts so we decided to just support Linux until migration - # to Ayon - if platform.system().lower() == "windows": - return None - import pwd - - filestat = os.stat(file) - return pwd.getpwuid(filestat.st_uid).pw_name - - def set_context(self, asset_id, task_name, filepath, workfile_doc): - # Check if asset, task and file are selected - # NOTE workfile document is not requirement - enabled = bool(asset_id) and bool(task_name) and bool(filepath) - - self._details_input.setEnabled(enabled) - self._note_input.setEnabled(enabled) - self._btn_note_save.setEnabled(enabled) - - # Make sure workfile doc is overridden - self._workfile_doc = workfile_doc - # Disable inputs and remove texts if any required arguments are missing - if not enabled: - self._orig_note = "" - self._details_input.setPlainText("") - self._note_input.setPlainText("") - return - - orig_note = "" - if workfile_doc: - orig_note = workfile_doc["data"].get("note") or orig_note - - self._orig_note = orig_note - self._note_input.setPlainText(orig_note) - # Set as empty string - self._details_input.setPlainText("") - - filestat = os.stat(filepath) - size_value = file_size_to_string(filestat.st_size) - - # Append html string - datetime_format = "%b %d %Y %H:%M:%S" - creation_time = datetime.datetime.fromtimestamp(filestat.st_ctime) - modification_time = datetime.datetime.fromtimestamp(filestat.st_mtime) - lines = ( - "Size:", - size_value, - "Created:", - creation_time.strftime(datetime_format), - "Modified:", - modification_time.strftime(datetime_format), - ) - username = self.get_user_name(filepath) - if username: - lines += ( - "User:", - username, - ) - self._details_input.appendHtml("
".join(lines)) - - def get_workfile_data(self): - data = { - "note": self._note_input.toPlainText() - } - return self._workfile_doc, data - - -class Window(QtWidgets.QWidget): - """Work Files Window""" - title = "Work Files" - - def __init__(self, parent=None): - super(Window, self).__init__(parent=parent) - self.setWindowTitle(self.title) - icon = QtGui.QIcon(resources.get_openpype_icon_filepath()) - self.setWindowIcon(icon) - self.setWindowFlags(self.windowFlags() | QtCore.Qt.Window) - - # Create pages widget and set it as central widget - pages_widget = QtWidgets.QStackedWidget(self) - - home_page_widget = QtWidgets.QWidget(pages_widget) - home_body_widget = QtWidgets.QWidget(home_page_widget) - - assets_widget = SingleSelectAssetsWidget( - legacy_io, parent=home_body_widget - ) - assets_widget.set_current_asset_btn_visibility(True) - - tasks_widget = TasksWidget(legacy_io, home_body_widget) - files_widget = FilesWidget(home_body_widget) - side_panel = SidePanelWidget(home_body_widget) - - pages_widget.addWidget(home_page_widget) - - # Build home - home_page_layout = QtWidgets.QVBoxLayout(home_page_widget) - home_page_layout.addWidget(home_body_widget) - - # Build home - body - body_layout = QtWidgets.QVBoxLayout(home_body_widget) - split_widget = QtWidgets.QSplitter(home_body_widget) - split_widget.addWidget(assets_widget) - split_widget.addWidget(tasks_widget) - split_widget.addWidget(files_widget) - split_widget.addWidget(side_panel) - split_widget.setSizes([255, 160, 455, 175]) - - body_layout.addWidget(split_widget) - - # Add top margin for tasks to align it visually with files as - # the files widget has a filter field which tasks does not. - tasks_widget.setContentsMargins(0, 32, 0, 0) - - main_layout = QtWidgets.QHBoxLayout(self) - main_layout.addWidget(pages_widget, 1) - - # Set context after asset widget is refreshed - # - to do so it is necessary to wait until refresh is done - set_context_timer = QtCore.QTimer() - set_context_timer.setInterval(100) - - # Connect signals - set_context_timer.timeout.connect(self._on_context_set_timeout) - assets_widget.selection_changed.connect(self._on_asset_changed) - tasks_widget.task_changed.connect(self._on_task_changed) - files_widget.file_selected.connect(self.on_file_select) - files_widget.workfile_created.connect(self.on_workfile_create) - files_widget.file_opened.connect(self._on_file_opened) - files_widget.published_visible_changed.connect( - self._on_published_change - ) - side_panel.save_clicked.connect(self.on_side_panel_save) - - self._set_context_timer = set_context_timer - self.home_page_widget = home_page_widget - self.pages_widget = pages_widget - self.home_body_widget = home_body_widget - self.split_widget = split_widget - - self.assets_widget = assets_widget - self.tasks_widget = tasks_widget - self.files_widget = files_widget - self.side_panel = side_panel - - # Force focus on the open button by default, required for Houdini. - files_widget.setFocus() - - self.resize(1200, 600) - - self._first_show = True - self._context_to_set = None - - def ensure_visible( - self, use_context=None, save=None, on_top=None - ): - if save is None: - save = True - - self.set_save_enabled(save) - - if self.isVisible(): - use_context = False - elif use_context is None: - use_context = True - - if on_top is None and self._first_show: - on_top = self.parent() is None - - window_flags = self.windowFlags() - new_window_flags = window_flags - if on_top is True: - new_window_flags = window_flags | QtCore.Qt.WindowStaysOnTopHint - elif on_top is False: - new_window_flags = window_flags & ~QtCore.Qt.WindowStaysOnTopHint - - if new_window_flags != window_flags: - # Note this is not propagated after initialization of widget in - # some Qt builds - self.setWindowFlags(new_window_flags) - self.show() - - elif not self.isVisible(): - self.show() - - if use_context is None or use_context is True: - context = { - "asset": get_current_asset_name(), - "task": get_current_task_name() - } - self.set_context(context) - - # Pull window to the front. - self.raise_() - self.activateWindow() - - @property - def project_name(self): - return get_current_project_name() - - def showEvent(self, event): - super(Window, self).showEvent(event) - if self._first_show: - self._first_show = False - self.refresh() - self.setStyleSheet(style.load_stylesheet()) - - 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 accidentally perform Maya commands - whilst trying to name an instance. - - """ - - def set_save_enabled(self, enabled): - self.files_widget.set_save_enabled(enabled) - - def on_file_select(self, filepath): - asset_id = self.assets_widget.get_selected_asset_id() - task_name = self.tasks_widget.get_selected_task_name() - - workfile_doc = None - if asset_id and task_name and filepath: - filename = os.path.split(filepath)[1] - project_name = self.project_name - workfile_doc = get_workfile_info( - project_name, asset_id, task_name, filename - ) - self.side_panel.set_context( - asset_id, task_name, filepath, workfile_doc - ) - - def on_workfile_create(self, filepath): - self._create_workfile_doc(filepath) - - def _on_file_opened(self): - self.close() - - def _on_published_change(self, visible): - self.side_panel.set_published_visible(visible) - - def on_side_panel_save(self): - workfile_doc, data = self.side_panel.get_workfile_data() - if not workfile_doc: - filepath = self.files_widget._get_selected_filepath() - workfile_doc = self._create_workfile_doc(filepath) - - new_workfile_doc = copy.deepcopy(workfile_doc) - new_workfile_doc["data"] = data - update_data = prepare_workfile_info_update_data( - workfile_doc, new_workfile_doc - ) - if not update_data: - return - - project_name = self.project_name - - session = OperationsSession() - session.update_entity( - project_name, "workfile", workfile_doc["_id"], update_data - ) - session.commit() - - def _get_current_workfile_doc(self, filepath=None): - if filepath is None: - filepath = self.files_widget._get_selected_filepath() - task_name = self.tasks_widget.get_selected_task_name() - asset_id = self.assets_widget.get_selected_asset_id() - if not task_name or not asset_id or not filepath: - return - - filename = os.path.split(filepath)[1] - project_name = self.project_name - return get_workfile_info( - project_name, asset_id, task_name, filename - ) - - def _create_workfile_doc(self, filepath): - workfile_doc = self._get_current_workfile_doc(filepath) - if workfile_doc: - return workfile_doc - - workdir, filename = os.path.split(filepath) - - project_name = self.project_name - asset_id = self.assets_widget.get_selected_asset_id() - task_name = self.tasks_widget.get_selected_task_name() - - anatomy = Anatomy(project_name) - success, rootless_dir = anatomy.find_root_template_from_path(workdir) - filepath = "/".join([ - os.path.normpath(rootless_dir).replace("\\", "/"), - filename - ]) - - workfile_doc = new_workfile_info_doc( - filename, asset_id, task_name, [filepath] - ) - - session = OperationsSession() - session.create_entity(project_name, "workfile", workfile_doc) - session.commit() - return workfile_doc - - def refresh(self): - # Refresh asset widget - self.assets_widget.refresh() - - self._on_task_changed() - - def set_context(self, context): - self._context_to_set = context - self._set_context_timer.start() - - def _on_context_set_timeout(self): - if self._context_to_set is None: - self._set_context_timer.stop() - return - - if self.assets_widget.refreshing: - return - - self._set_context_timer.stop() - self._context_to_set, context = None, self._context_to_set - if "asset" in context: - asset_doc = get_asset_by_name( - self.project_name, context["asset"], fields=["_id"] - ) - - asset_id = None - if asset_doc: - asset_id = asset_doc["_id"] - # Select the asset - self.assets_widget.select_asset(asset_id) - self.tasks_widget.set_asset_id(asset_id) - - if "task" in context: - self.tasks_widget.select_task_name(context["task"]) - self._on_task_changed() - - def _on_asset_changed(self): - asset_id = self.assets_widget.get_selected_asset_id() - if asset_id: - self.tasks_widget.setEnabled(True) - else: - # Force disable the other widgets if no - # active selection - self.tasks_widget.setEnabled(False) - self.files_widget.setEnabled(False) - - self.tasks_widget.set_asset_id(asset_id) - - def _on_task_changed(self): - asset_id = self.assets_widget.get_selected_asset_id() - task_name = self.tasks_widget.get_selected_task_name() - task_type = self.tasks_widget.get_selected_task_type() - - asset_is_valid = asset_id is not None - self.tasks_widget.setEnabled(asset_is_valid) - - self.files_widget.setEnabled(bool(task_name) and asset_is_valid) - self.files_widget.set_asset_task(asset_id, task_name, task_type) - self.files_widget.refresh()