remove openpype tools and rename ayon tools

This commit is contained in:
Jakub Trllo 2024-02-05 16:44:25 +01:00
parent 53c9acc004
commit 95bcc7015a
123 changed files with 450 additions and 16135 deletions

View file

@ -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()

View file

@ -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):

View file

@ -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"]

View file

@ -1,6 +0,0 @@
from .control import LoaderController
__all__ = (
"LoaderController",
)

View file

@ -1,6 +0,0 @@
from .control import PushToContextController
__all__ = (
"PushToContextController",
)

View file

@ -1,6 +0,0 @@
from .control import SceneInventoryController
__all__ = (
"SceneInventoryController",
)

View file

@ -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

View file

@ -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()

View file

@ -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()

View file

@ -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__ = (

View file

@ -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)

View file

@ -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]

View file

@ -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)

View file

@ -1,7 +0,0 @@
from .window import LauncherWindow
from . import actions
__all__ = [
"LauncherWindow",
"actions"
]

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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])

View file

@ -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)

View file

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Before After
Before After

View file

@ -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

View file

@ -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()

View file

@ -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)

View file

@ -1,11 +0,0 @@
from .app import (
LibraryLoaderWindow,
show,
cli
)
__all__ = [
"LibraryLoaderWindow",
"show",
"cli",
]

View file

@ -1,5 +0,0 @@
from . import cli
if __name__ == '__main__':
import sys
sys.exit(cli(sys.argv[1:]))

View file

@ -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)

View file

@ -1,11 +1,6 @@
from .app import (
LoaderWindow,
show,
cli,
)
from .control import LoaderController
__all__ = (
"LoaderWindow",
"show",
"cli",
"LoaderController",
)

View file

@ -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:]))

View file

@ -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()

View file

@ -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

View file

@ -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

View file

@ -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()

View file

@ -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,

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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
)

View file

@ -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))

View file

@ -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)

View file

@ -0,0 +1,6 @@
from .control import PushToContextController
__all__ = (
"PushToContextController",
)

View file

@ -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()

View file

@ -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

View file

@ -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):

View file

@ -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,
)

View file

@ -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"]

View file

@ -1,9 +1,6 @@
from .window import (
show,
SceneInventoryWindow
)
from .control import SceneInventoryController
__all__ = (
"show",
"SceneInventoryWindow"
"SceneInventoryController",
)

View file

@ -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

View file

@ -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

View file

@ -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_()

View file

@ -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)

View file

@ -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()

View file

@ -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

View file

@ -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)

View file

@ -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