mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
remove openpype tools and rename ayon tools
This commit is contained in:
parent
53c9acc004
commit
95bcc7015a
123 changed files with 450 additions and 16135 deletions
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
from .control import LoaderController
|
||||
|
||||
|
||||
__all__ = (
|
||||
"LoaderController",
|
||||
)
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
from .control import PushToContextController
|
||||
|
||||
|
||||
__all__ = (
|
||||
"PushToContextController",
|
||||
)
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
from .control import SceneInventoryController
|
||||
|
||||
|
||||
__all__ = (
|
||||
"SceneInventoryController",
|
||||
)
|
||||
|
|
@ -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", "<none>")
|
||||
|
||||
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
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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__ = (
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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):
|
|||
"<span style='font-weight:bold;'>{}:</span> {{}}<br>"
|
||||
"<span style='font-weight:bold;'>{}:</span> {{}}<br>"
|
||||
).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 = "<span style='font-weight:bold'>{}</span>"
|
||||
|
||||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
"<br><b>{} tray -> Settings -> Experimental tools</b>"
|
||||
).format(app_label),
|
||||
"<br><b>AYON tray -> Settings -> Experimental tools</b>"
|
||||
),
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
from .window import LauncherWindow
|
||||
from . import actions
|
||||
|
||||
__all__ = [
|
||||
"LauncherWindow",
|
||||
"actions"
|
||||
]
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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])
|
||||
|
|
@ -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)
|
||||
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
|
@ -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)
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
from .app import (
|
||||
LibraryLoaderWindow,
|
||||
show,
|
||||
cli
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"LibraryLoaderWindow",
|
||||
"show",
|
||||
"cli",
|
||||
]
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
from . import cli
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
sys.exit(cli(sys.argv[1:]))
|
||||
|
|
@ -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)
|
||||
|
|
@ -1,11 +1,6 @@
|
|||
from .app import (
|
||||
LoaderWindow,
|
||||
show,
|
||||
cli,
|
||||
)
|
||||
from .control import LoaderController
|
||||
|
||||
|
||||
__all__ = (
|
||||
"LoaderWindow",
|
||||
"show",
|
||||
"cli",
|
||||
"LoaderController",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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:]))
|
||||
|
|
@ -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()
|
||||
|
|
@ -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)
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 5 KiB |
|
|
@ -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)
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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()
|
||||
|
|
@ -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,
|
||||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
from .control import PushToContextController
|
||||
|
||||
|
||||
__all__ = (
|
||||
"PushToContextController",
|
||||
)
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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):
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
@ -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"]
|
||||
|
|
@ -1,9 +1,6 @@
|
|||
from .window import (
|
||||
show,
|
||||
SceneInventoryWindow
|
||||
)
|
||||
from .control import SceneInventoryController
|
||||
|
||||
|
||||
__all__ = (
|
||||
"show",
|
||||
"SceneInventoryWindow"
|
||||
"SceneInventoryController",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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"""
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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_()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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 "<Project not set>"
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue