Merge branch 'develop' into enhancement/OP-7078_ValidateMeshHasUVs

This commit is contained in:
Kayla Man 2024-02-29 20:37:53 +08:00
commit 07e10beec9
18 changed files with 1110 additions and 1257 deletions

View file

@ -1138,17 +1138,17 @@ ValidationArtistMessage QLabel {
font-size: 13pt;
}
#AssetNameInputWidget {
#FolderPathInputWidget {
background: {color:bg-inputs};
border: 1px solid {color:border};
border-radius: 0.2em;
}
#AssetNameInputWidget QWidget {
#FolderPathInputWidget QWidget {
background: transparent;
}
#AssetNameInputButton {
#FolderPathInputButton {
border-bottom-left-radius: 0px;
border-top-left-radius: 0px;
padding: 0px;
@ -1159,23 +1159,23 @@ ValidationArtistMessage QLabel {
border-bottom: none;
}
#AssetNameInput {
#FolderPathInput {
border-bottom-right-radius: 0px;
border-top-right-radius: 0px;
border: none;
}
#AssetNameInputWidget:hover {
#FolderPathInputWidget:hover {
border-color: {color:border-hover};
}
#AssetNameInputWidget:focus{
#FolderPathInputWidget:focus{
border-color: {color:border-focus};
}
#AssetNameInputWidget:disabled {
#FolderPathInputWidget:disabled {
background: {color:bg-inputs-disabled};
}
#TasksCombobox[state="invalid"], #AssetNameInputWidget[state="invalid"], #AssetNameInputButton[state="invalid"] {
#TasksCombobox[state="invalid"], #FolderPathInputWidget[state="invalid"], #FolderPathInputButton[state="invalid"] {
border-color: {color:publisher:error};
}

View file

@ -191,12 +191,12 @@ def _get_folder_item_from_hierarchy_item(item):
name = item["name"]
path_parts = list(item["parents"])
path_parts.append(name)
path = "/" + "/".join(path_parts)
return FolderItem(
item["id"],
item["parentId"],
name,
"/".join(path_parts),
path,
item["folderType"],
item["label"],
None,
@ -307,8 +307,44 @@ class HierarchyModel(object):
})
return output
def get_folder_items_by_paths(self, project_name, folder_paths):
"""Get folder items by ids.
This function will query folders if they are not in cache. But the
queried items are not added to cache back.
Args:
project_name (str): Name of project where to look for folders.
folder_paths (Iterable[str]): Folder paths.
Returns:
dict[str, Union[FolderItem, None]]: Folder items by id.
"""
folder_paths = set(folder_paths)
output = {folder_path: None for folder_path in folder_paths}
if not folder_paths:
return output
if self._folders_items[project_name].is_valid:
cache_data = self._folders_items[project_name].get_data()
for folder_item in cache_data.values():
if folder_item.path in folder_paths:
output[folder_item.path] = folder_item
return output
folders = ayon_api.get_folders(
project_name,
folder_paths=folder_paths,
fields=["id", "name", "label", "parentId", "path", "folderType"]
)
# Make sure all folder ids are in output
for folder in folders:
item = _get_folder_item_from_entity(folder)
output[item.path] = item
return output
def get_folder_item(self, project_name, folder_id):
"""Get folder items by id.
"""Get folder item by id.
This function will query folder if they is not in cache. But the
queried items are not added to cache back.
@ -325,6 +361,25 @@ class HierarchyModel(object):
)
return items.get(folder_id)
def get_folder_item_by_path(self, project_name, folder_path):
"""Get folder item by path.
This function will query folder if they is not in cache. But the
queried items are not added to cache back.
Args:
project_name (str): Name of project where to look for folders.
folder_path (str): Folder path.
Returns:
Union[FolderItem, None]: Folder item.
"""
items = self.get_folder_items_by_paths(
project_name, [folder_path]
)
return items.get(folder_path)
def get_task_items(self, project_name, folder_id, sender):
if not project_name or not folder_id:
return []

View file

@ -3,6 +3,10 @@ from .projects_widget import (
ProjectsCombobox,
ProjectsQtModel,
ProjectSortFilterProxy,
PROJECT_NAME_ROLE,
PROJECT_IS_CURRENT_ROLE,
PROJECT_IS_ACTIVE_ROLE,
PROJECT_IS_LIBRARY_ROLE,
)
from .folders_widget import (
@ -28,6 +32,10 @@ __all__ = (
"ProjectsCombobox",
"ProjectsQtModel",
"ProjectSortFilterProxy",
"PROJECT_NAME_ROLE",
"PROJECT_IS_CURRENT_ROLE",
"PROJECT_IS_ACTIVE_ROLE",
"PROJECT_IS_LIBRARY_ROLE",
"FoldersWidget",
"FoldersQtModel",

View file

@ -91,6 +91,21 @@ class FoldersQtModel(QtGui.QStandardItemModel):
return QtCore.QModelIndex()
return self.indexFromItem(item)
def get_item_id_by_path(self, folder_path):
"""Get folder id by path.
Args:
folder_path (str): Folder path.
Returns:
Union[str, None]: Folder id or None if folder is not available.
"""
for folder_id, item in self._items_by_id.values():
if item.data(FOLDER_PATH_ROLE) == folder_path:
return folder_id
return None
def get_project_name(self):
"""Project name which model currently use.
@ -431,8 +446,10 @@ class FoldersWidget(QtWidgets.QWidget):
Args:
folder_id (Union[str, None]): Folder id or None to deselect.
"""
Returns:
bool: Requested folder was selected.
"""
if folder_id is None:
self._folders_view.clearSelection()
return True
@ -453,6 +470,25 @@ class FoldersWidget(QtWidgets.QWidget):
)
return True
def set_selected_folder_path(self, folder_path):
"""Set selected folder by path.
Args:
folder_path (str): Folder path.
Returns:
bool: Requested folder was selected.
"""
if folder_path is None:
self._folders_view.clearSelection()
return True
folder_id = self._folders_model.get_item_id_by_path(folder_path)
if folder_id is None:
return False
return self.set_selected_folder(folder_id)
def set_deselectable(self, enabled):
"""Set deselectable mode.

View file

@ -47,6 +47,22 @@ class ProjectsQtModel(QtGui.QStandardItemModel):
def has_content(self):
return len(self._project_items) > 0
def get_index_by_project_name(self, project_name):
"""Get index of project by name.
Args:
project_name (str): Project name.
Returns:
QtCore.QModelIndex: Index of project item. Index is not valid
if project is not found.
"""
item = self._project_items.get(project_name)
if item is None:
return QtCore.QModelIndex()
return self.indexFromItem(item)
def set_select_item_visible(self, visible):
if self._select_item_visible is visible:
return

View file

@ -214,6 +214,7 @@ class TasksQtModel(QtGui.QStandardItemModel):
item.setData(task_item.label, QtCore.Qt.DisplayRole)
item.setData(name, ITEM_NAME_ROLE)
item.setData(task_item.id, ITEM_ID_ROLE)
item.setData(task_item.task_type, TASK_TYPE_ROLE)
item.setData(task_item.parent_id, PARENT_ID_ROLE)
item.setData(icon, QtCore.Qt.DecorationRole)
@ -358,6 +359,78 @@ class TasksWidget(QtWidgets.QWidget):
self._tasks_model.refresh()
def get_selected_task_info(self):
"""Get selected task info.
Example output::
{
"task_id": "5e7e3e3e3e3e3e3e3e3e3e3e",
"task_name": "modeling",
"task_type": "Modeling"
}
Returns:
dict[str, Union[str, None]]: Task info.
"""
_, task_id, task_name, task_type = self._get_selected_item_ids()
return {
"task_id": task_id,
"task_name": task_name,
"task_type": task_type,
}
def get_selected_task_name(self):
"""Get selected task name.
Returns:
Union[str, None]: Task name.
"""
_, _, task_name, _ = self._get_selected_item_ids()
return task_name
def get_selected_task_type(self):
"""Get selected task type.
Returns:
Union[str, None]: Task type.
"""
_, _, _, task_type = self._get_selected_item_ids()
return task_type
def set_selected_task(self, task_name):
"""Set selected task by name.
Args:
task_name (str): Task name.
Returns:
bool: Task was selected.
"""
if task_name is None:
self._tasks_view.clearSelection()
return True
if task_name == self.get_selected_task_name():
return True
index = self._tasks_model.get_index_by_name(task_name)
if not index.isValid():
return False
proxy_index = self._tasks_proxy_model.mapFromSource(index)
if not proxy_index.isValid():
return False
selection_model = self._folders_view.selectionModel()
selection_model.setCurrentIndex(
proxy_index, QtCore.QItemSelectionModel.SelectCurrent
)
return True
def _on_tasks_refresh_finished(self, event):
"""Tasks were refreshed in controller.
@ -395,10 +468,11 @@ class TasksWidget(QtWidgets.QWidget):
for index in selection_model.selectedIndexes():
task_id = index.data(ITEM_ID_ROLE)
task_name = index.data(ITEM_NAME_ROLE)
task_type = index.data(TASK_TYPE_ROLE)
parent_id = index.data(PARENT_ID_ROLE)
if task_name is not None:
return parent_id, task_id, task_name
return self._selected_folder_id, None, None
return parent_id, task_id, task_name, task_type
return self._selected_folder_id, None, None, None
def _on_selection_change(self):
# Don't trigger task change during refresh
@ -407,7 +481,7 @@ class TasksWidget(QtWidgets.QWidget):
if self._tasks_model.is_refreshing:
return
parent_id, task_id, task_name = self._get_selected_item_ids()
parent_id, task_id, task_name, _ = self._get_selected_item_ids()
self._controller.set_selected_task(task_id, task_name)
self.selection_changed.emit()

View file

@ -12,14 +12,13 @@ from abc import ABCMeta, abstractmethod
import six
import arrow
import pyblish.api
import ayon_api
from ayon_core.client import (
get_assets,
get_asset_by_id,
get_asset_by_name,
get_subsets,
get_asset_name_identifier,
)
from ayon_core.lib.events import EventSystem
from ayon_core.lib.events import QueuedEventSystem
from ayon_core.lib.attribute_definitions import (
UIDef,
serialize_attr_defs,
@ -43,6 +42,7 @@ from ayon_core.pipeline.create.context import (
ConvertorsOperationFailed,
)
from ayon_core.pipeline.publish import get_publish_instance_label
from ayon_core.tools.ayon_utils.models import HierarchyModel
# Define constant for plugin orders offset
PLUGIN_ORDER_OFFSET = 0.5
@ -69,103 +69,19 @@ class MainThreadItem:
class AssetDocsCache:
"""Cache asset documents for creation part."""
projection = {
"_id": True,
"name": True,
"data.visualParent": True,
"data.tasks": True,
"data.parents": True,
}
def __init__(self, controller):
self._controller = controller
self._asset_docs = None
self._asset_docs_hierarchy = None
self._task_names_by_asset_name = {}
self._asset_docs_by_name = {}
self._full_asset_docs_by_name = {}
self._asset_docs_by_path = {}
def reset(self):
self._asset_docs = None
self._asset_docs_hierarchy = None
self._task_names_by_asset_name = {}
self._asset_docs_by_name = {}
self._full_asset_docs_by_name = {}
self._asset_docs_by_path = {}
def _query(self):
if self._asset_docs is not None:
return
project_name = self._controller.project_name
asset_docs = list(get_assets(
project_name, fields=self.projection.keys()
))
asset_docs_by_name = {}
task_names_by_asset_name = {}
for asset_doc in asset_docs:
if "data" not in asset_doc:
asset_doc["data"] = {"tasks": {}, "visualParent": None}
elif "tasks" not in asset_doc["data"]:
asset_doc["data"]["tasks"] = {}
asset_name = get_asset_name_identifier(asset_doc)
asset_tasks = asset_doc["data"]["tasks"]
task_names_by_asset_name[asset_name] = list(asset_tasks.keys())
asset_docs_by_name[asset_name] = asset_doc
self._asset_docs = asset_docs
self._asset_docs_by_name = asset_docs_by_name
self._task_names_by_asset_name = task_names_by_asset_name
def get_asset_docs(self):
self._query()
return copy.deepcopy(self._asset_docs)
def get_asset_hierarchy(self):
"""Prepare asset documents into hierarchy.
Convert ObjectId to string. Asset id is not used during whole
process of publisher but asset name is used rather.
Returns:
Dict[Union[str, None]: Any]: Mapping of parent id to it's children.
Top level assets have parent id 'None'.
"""
if self._asset_docs_hierarchy is None:
_queue = collections.deque(self.get_asset_docs())
output = collections.defaultdict(list)
while _queue:
asset_doc = _queue.popleft()
asset_doc["_id"] = str(asset_doc["_id"])
parent_id = asset_doc["data"]["visualParent"]
if parent_id is not None:
parent_id = str(parent_id)
asset_doc["data"]["visualParent"] = parent_id
output[parent_id].append(asset_doc)
self._asset_docs_hierarchy = output
return copy.deepcopy(self._asset_docs_hierarchy)
def get_task_names_by_asset_name(self):
self._query()
return copy.deepcopy(self._task_names_by_asset_name)
def get_asset_by_name(self, asset_name):
self._query()
asset_doc = self._asset_docs_by_name.get(asset_name)
if asset_doc is None:
return None
return copy.deepcopy(asset_doc)
def get_full_asset_by_name(self, asset_name):
self._query()
if asset_name not in self._full_asset_docs_by_name:
asset_doc = self._asset_docs_by_name.get(asset_name)
def get_asset_doc_by_folder_path(self, folder_path):
if folder_path not in self._asset_docs_by_path:
project_name = self._controller.project_name
full_asset_doc = get_asset_by_id(project_name, asset_doc["_id"])
self._full_asset_docs_by_name[asset_name] = full_asset_doc
return copy.deepcopy(self._full_asset_docs_by_name[asset_name])
asset_doc = get_asset_by_name(project_name, folder_path)
self._asset_docs_by_path[folder_path] = asset_doc
return copy.deepcopy(self._asset_docs_by_path[folder_path])
class PublishReportMaker:
@ -1036,13 +952,13 @@ class AbstractPublisherController(object):
@property
@abstractmethod
def current_asset_name(self):
"""Current context asset name.
def current_folder_path(self):
"""Current context folder path.
Returns:
Union[str, None]: Name of asset.
"""
Union[str, None]: Folder path.
"""
pass
@property
@ -1106,19 +1022,7 @@ class AbstractPublisherController(object):
pass
@abstractmethod
def get_asset_docs(self):
pass
@abstractmethod
def get_asset_hierarchy(self):
pass
@abstractmethod
def get_task_names_by_asset_names(self, asset_names):
pass
@abstractmethod
def get_existing_product_names(self, asset_name):
def get_existing_product_names(self, folder_path):
pass
@abstractmethod
@ -1158,7 +1062,7 @@ class AbstractPublisherController(object):
creator_identifier,
variant,
task_name,
asset_name,
folder_path,
instance_id=None
):
"""Get product name based on passed data.
@ -1168,7 +1072,7 @@ class AbstractPublisherController(object):
responsible for product name creation.
variant (str): Variant value from user's input.
task_name (str): Name of task for which is instance created.
asset_name (str): Name of asset for which is instance created.
folder_path (str): Folder path for which is instance created.
instance_id (Union[str, None]): Existing instance id when product
name is updated.
"""
@ -1187,7 +1091,7 @@ class AbstractPublisherController(object):
creator_identifier (str): Identifier of Creator plugin.
product_name (str): Calculated product name.
instance_data (Dict[str, Any]): Base instance data with variant,
asset name and task name.
folder path and task name.
options (Dict[str, Any]): Data from pre-create attributes.
"""
@ -1500,13 +1404,22 @@ class BasePublisherController(AbstractPublisherController):
"""
if self._event_system is None:
self._event_system = EventSystem()
self._event_system = QueuedEventSystem()
return self._event_system
def _emit_event(self, topic, data=None):
# Events system
def emit_event(self, topic, data=None, source=None):
"""Use implemented event system to trigger event."""
if data is None:
data = {}
self.event_system.emit(topic, data, "controller")
self.event_system.emit(topic, data, source)
def register_event_callback(self, topic, callback):
self.event_system.add_callback(topic, callback)
def _emit_event(self, topic, data=None):
self.emit_event(topic, data, "controller")
def _get_host_is_valid(self):
return self._host_is_valid
@ -1739,6 +1652,7 @@ class PublisherController(BasePublisherController):
self._resetting_instances = False
# Cacher of avalon documents
self._hierarchy_model = HierarchyModel(self)
self._asset_docs_cache = AssetDocsCache(self)
@property
@ -1752,11 +1666,11 @@ class PublisherController(BasePublisherController):
return self._create_context.get_current_project_name()
@property
def current_asset_name(self):
"""Current context asset name defined by host.
def current_folder_path(self):
"""Current context folder path defined by host.
Returns:
Union[str, None]: Asset name or None if asset is not set.
Union[str, None]: Folder path or None if folder is not set.
"""
return self._create_context.get_current_asset_name()
@ -1795,11 +1709,69 @@ class PublisherController(BasePublisherController):
"""Publish plugins."""
return self._create_context.publish_plugins
# --- Publish specific callbacks ---
def get_asset_docs(self):
"""Get asset documents from cache for whole project."""
return self._asset_docs_cache.get_asset_docs()
# Hierarchy model
def get_folder_items(self, project_name, sender=None):
return self._hierarchy_model.get_folder_items(project_name, sender)
def get_task_items(self, project_name, folder_id, sender=None):
return self._hierarchy_model.get_task_items(
project_name, folder_id, sender
)
def get_folder_entity(self, project_name, folder_id):
return self._hierarchy_model.get_folder_entity(
project_name, folder_id
)
def get_task_entity(self, project_name, task_id):
return self._hierarchy_model.get_task_entity(project_name, task_id)
# Publisher custom method
def get_folder_id_from_path(self, folder_path):
if not folder_path:
return None
folder_item = self._hierarchy_model.get_folder_item_by_path(
self.project_name, folder_path
)
if folder_item:
return folder_item.entity_id
return None
def get_task_names_by_folder_paths(self, folder_paths):
if not folder_paths:
return {}
folder_items = self._hierarchy_model.get_folder_items_by_paths(
self.project_name, folder_paths
)
output = {
folder_path: set()
for folder_path in folder_paths
}
project_name = self.project_name
for folder_item in folder_items.values():
task_items = self._hierarchy_model.get_task_items(
project_name, folder_item.entity_id, None
)
output[folder_item.path] = {
task_item.name
for task_item in task_items
}
return output
def are_folder_paths_valid(self, folder_paths):
if not folder_paths:
return True
folder_paths = set(folder_paths)
folder_items = self._hierarchy_model.get_folder_items_by_paths(
self.project_name, folder_paths
)
for folder_item in folder_items.values():
if folder_item is None:
return False
return True
# --- Publish specific callbacks ---
def get_context_title(self):
"""Get context title for artist shown at the top of main window."""
@ -1814,32 +1786,20 @@ class PublisherController(BasePublisherController):
return context_title
def get_asset_hierarchy(self):
"""Prepare asset documents into hierarchy."""
return self._asset_docs_cache.get_asset_hierarchy()
def get_task_names_by_asset_names(self, asset_names):
"""Prepare task names by asset name."""
task_names_by_asset_name = (
self._asset_docs_cache.get_task_names_by_asset_name()
)
result = {}
for asset_name in asset_names:
result[asset_name] = set(
task_names_by_asset_name.get(asset_name) or []
)
return result
def get_existing_product_names(self, asset_name):
def get_existing_product_names(self, folder_path):
if not folder_path:
return None
project_name = self.project_name
asset_doc = self._asset_docs_cache.get_asset_by_name(asset_name)
if not asset_doc:
folder_item = self._hierarchy_model.get_folder_item_by_path(
project_name, folder_path
)
if not folder_item:
return None
asset_id = asset_doc["_id"]
subset_docs = get_subsets(
project_name, asset_ids=[asset_id], fields=["name"]
project_name,
asset_ids=[folder_item.entity_id],
fields=["name"]
)
return {
subset_doc["name"]
@ -1859,6 +1819,7 @@ class PublisherController(BasePublisherController):
# Reset avalon context
self._create_context.reset_current_context()
self._hierarchy_model.reset()
self._asset_docs_cache.reset()
self._reset_plugins()
@ -2075,7 +2036,7 @@ class PublisherController(BasePublisherController):
creator_identifier,
variant,
task_name,
asset_name,
folder_path,
instance_id=None
):
"""Get product name based on passed data.
@ -2085,14 +2046,16 @@ class PublisherController(BasePublisherController):
responsible for product name creation.
variant (str): Variant value from user's input.
task_name (str): Name of task for which is instance created.
asset_name (str): Name of asset for which is instance created.
folder_path (str): Folder path for which is instance created.
instance_id (Union[str, None]): Existing instance id when product
name is updated.
"""
creator = self._creators[creator_identifier]
project_name = self.project_name
asset_doc = self._asset_docs_cache.get_full_asset_by_name(asset_name)
asset_doc = self._asset_docs_cache.get_asset_doc_by_folder_path(
folder_path
)
instance = None
if instance_id:
instance = self.instances[instance_id]

View file

@ -212,13 +212,13 @@ class QtRemotePublishController(BasePublisherController):
pass
@abstractproperty
def current_asset_name(self):
"""Current context asset name from client.
def current_folder_path(self):
"""Current context folder path from host.
Returns:
Union[str, None]: Name of asset.
"""
Union[str, None]: Folder path.
"""
pass
@abstractproperty
@ -251,16 +251,7 @@ class QtRemotePublishController(BasePublisherController):
pass
def get_asset_docs(self):
pass
def get_asset_hierarchy(self):
pass
def get_task_names_by_asset_names(self, asset_names):
pass
def get_existing_product_names(self, asset_name):
def get_existing_product_names(self, folder_path):
pass
@property
@ -305,7 +296,7 @@ class QtRemotePublishController(BasePublisherController):
creator_identifier,
variant,
task_name,
asset_name,
folder_path,
instance_id=None
):
"""Get product name based on passed data.
@ -315,7 +306,7 @@ class QtRemotePublishController(BasePublisherController):
responsible for product name creation.
variant (str): Variant value from user's input.
task_name (str): Name of task for which is instance created.
asset_name (str): Name of asset for which is instance created.
folder_path (str): Folder path for which is instance created.
instance_id (Union[str, None]): Existing instance id when product
name is updated.
"""
@ -334,7 +325,7 @@ class QtRemotePublishController(BasePublisherController):
creator_identifier (str): Identifier of Creator plugin.
product_name (str): Calculated product name.
instance_data (Dict[str, Any]): Base instance data with variant,
asset name and task name.
folder path and task name.
options (Dict[str, Any]): Data from pre-create attributes.
"""

View file

@ -1,363 +0,0 @@
import collections
from qtpy import QtWidgets, QtCore, QtGui
from ayon_core.tools.utils import (
PlaceholderLineEdit,
RecursiveSortFilterProxyModel,
)
from ayon_core.tools.utils.assets_widget import (
SingleSelectAssetsWidget,
ASSET_ID_ROLE,
ASSET_NAME_ROLE,
ASSET_PATH_ROLE,
get_asset_icon,
)
class CreateWidgetAssetsWidget(SingleSelectAssetsWidget):
current_context_required = QtCore.Signal()
header_height_changed = QtCore.Signal(int)
def __init__(self, controller, parent):
self._controller = controller
super(CreateWidgetAssetsWidget, self).__init__(parent)
self.set_refresh_btn_visibility(False)
self.set_current_asset_btn_visibility(False)
self._last_selection = None
self._enabled = None
self._last_filter_height = None
def get_project_name(self):
return self._controller.project_name
def get_selected_asset_name(self):
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.
Label on top of creaters should have same height so Creators view has
same offset.
"""
height = self.header_widget.height()
if height != self._last_filter_height:
self._last_filter_height = height
self.header_height_changed.emit(height)
def resizeEvent(self, event):
super(CreateWidgetAssetsWidget, self).resizeEvent(event)
self._check_header_height()
def showEvent(self, event):
super(CreateWidgetAssetsWidget, self).showEvent(event)
self._check_header_height()
def _on_current_asset_click(self):
self.current_context_required.emit()
def set_enabled(self, enabled):
if self._enabled == enabled:
return
self._enabled = enabled
if not enabled:
self._last_selection = self.get_selected_asset_id()
self._clear_selection()
elif self._last_selection is not None:
self.select_asset(self._last_selection)
def _select_indexes(self, *args, **kwargs):
super(CreateWidgetAssetsWidget, self)._select_indexes(*args, **kwargs)
if self._enabled:
return
self._last_selection = self.get_selected_asset_id()
self._clear_selection()
def update_current_asset(self):
# Hide set current asset if there is no one
asset_name = self._get_current_asset_name()
self.set_current_asset_btn_visibility(bool(asset_name))
def _get_current_asset_name(self):
return self._controller.current_asset_name
def _create_source_model(self):
return AssetsHierarchyModel(self._controller)
def _refresh_model(self):
self._model.reset()
self._on_model_refresh(self._model.rowCount() > 0)
class AssetsHierarchyModel(QtGui.QStandardItemModel):
"""Assets hierarchy model.
For selecting asset for which an instance should be created.
Uses controller to load asset hierarchy. All asset documents are stored by
their parents.
"""
def __init__(self, controller):
super(AssetsHierarchyModel, self).__init__()
self._controller = controller
self._items_by_name = {}
self._items_by_path = {}
self._items_by_asset_id = {}
def reset(self):
self.clear()
self._items_by_name = {}
self._items_by_path = {}
self._items_by_asset_id = {}
assets_by_parent_id = self._controller.get_asset_hierarchy()
items_by_name = {}
items_by_path = {}
items_by_asset_id = {}
_queue = collections.deque()
_queue.append((self.invisibleRootItem(), None, None))
while _queue:
parent_item, parent_id, parent_path = _queue.popleft()
children = assets_by_parent_id.get(parent_id)
if not children:
continue
children_by_name = {
child["name"]: child
for child in children
}
items = []
for name in sorted(children_by_name.keys()):
child = children_by_name[name]
child_id = child["_id"]
if parent_path:
child_path = "{}/{}".format(parent_path, name)
else:
child_path = "/{}".format(name)
has_children = bool(assets_by_parent_id.get(child_id))
icon = get_asset_icon(child, has_children)
item = QtGui.QStandardItem(name)
item.setFlags(
QtCore.Qt.ItemIsEnabled
| QtCore.Qt.ItemIsSelectable
)
item.setData(icon, QtCore.Qt.DecorationRole)
item.setData(child_id, ASSET_ID_ROLE)
item.setData(name, ASSET_NAME_ROLE)
item.setData(child_path, ASSET_PATH_ROLE)
items_by_name[name] = item
items_by_path[child_path] = item
items_by_asset_id[child_id] = item
items.append(item)
_queue.append((item, child_id, child_path))
parent_item.appendRows(items)
self._items_by_name = items_by_name
self._items_by_path = items_by_path
self._items_by_asset_id = items_by_asset_id
def get_index_by_asset_id(self, asset_id):
item = self._items_by_asset_id.get(asset_id)
if item is not None:
return item.index()
return QtCore.QModelIndex()
def get_index_by_asset_name(self, asset_name):
item = self._items_by_path.get(asset_name)
if item is None:
item = self._items_by_name.get(asset_name)
if item is None:
return QtCore.QModelIndex()
return item.index()
def name_is_valid(self, item_name):
return item_name in self._items_by_path
class AssetDialogView(QtWidgets.QTreeView):
double_clicked = QtCore.Signal(QtCore.QModelIndex)
def mouseDoubleClickEvent(self, event):
index = self.indexAt(event.pos())
if index.isValid():
self.double_clicked.emit(index)
event.accept()
class AssetsDialog(QtWidgets.QDialog):
"""Dialog to select asset for a context of instance."""
def __init__(self, controller, parent):
super(AssetsDialog, self).__init__(parent)
self.setWindowTitle("Select asset")
model = AssetsHierarchyModel(controller)
proxy_model = RecursiveSortFilterProxyModel()
proxy_model.setSourceModel(model)
proxy_model.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
filter_input = PlaceholderLineEdit(self)
filter_input.setPlaceholderText("Filter folders..")
asset_view = AssetDialogView(self)
asset_view.setModel(proxy_model)
asset_view.setHeaderHidden(True)
asset_view.setFrameShape(QtWidgets.QFrame.NoFrame)
asset_view.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
asset_view.setAlternatingRowColors(True)
asset_view.setSelectionBehavior(QtWidgets.QTreeView.SelectRows)
asset_view.setAllColumnsShowFocus(True)
ok_btn = QtWidgets.QPushButton("OK", self)
cancel_btn = QtWidgets.QPushButton("Cancel", self)
btns_layout = QtWidgets.QHBoxLayout()
btns_layout.addStretch(1)
btns_layout.addWidget(ok_btn)
btns_layout.addWidget(cancel_btn)
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(filter_input, 0)
layout.addWidget(asset_view, 1)
layout.addLayout(btns_layout, 0)
controller.event_system.add_callback(
"controller.reset.finished", self._on_controller_reset
)
asset_view.double_clicked.connect(self._on_ok_clicked)
filter_input.textChanged.connect(self._on_filter_change)
ok_btn.clicked.connect(self._on_ok_clicked)
cancel_btn.clicked.connect(self._on_cancel_clicked)
self._filter_input = filter_input
self._ok_btn = ok_btn
self._cancel_btn = cancel_btn
self._model = model
self._proxy_model = proxy_model
self._asset_view = asset_view
self._selected_asset = None
# Soft refresh is enabled
# - reset will happen at all cost if soft reset is enabled
# - adds ability to call reset on multiple places without repeating
self._soft_reset_enabled = True
self._first_show = True
self._default_height = 500
def _on_first_show(self):
center = self.rect().center()
size = self.size()
size.setHeight(self._default_height)
self.resize(size)
new_pos = self.mapToGlobal(center)
new_pos.setX(new_pos.x() - int(self.width() / 2))
new_pos.setY(new_pos.y() - int(self.height() / 2))
self.move(new_pos)
def _on_controller_reset(self):
# Change reset enabled so model is reset on show event
self._soft_reset_enabled = True
def showEvent(self, event):
"""Refresh asset model on show."""
super(AssetsDialog, self).showEvent(event)
if self._first_show:
self._first_show = False
self._on_first_show()
# Refresh on show
self.reset(False)
def reset(self, force=True):
"""Reset asset model."""
if not force and not self._soft_reset_enabled:
return
if self._soft_reset_enabled:
self._soft_reset_enabled = False
self._model.reset()
def name_is_valid(self, name):
"""Is asset name valid.
Args:
name(str): Asset name that should be checked.
"""
# Make sure we're reset
self.reset(False)
# Valid the name by model
return self._model.name_is_valid(name)
def _on_filter_change(self, text):
"""Trigger change of filter of assets."""
self._proxy_model.setFilterFixedString(text)
def _on_cancel_clicked(self):
self.done(0)
def _on_ok_clicked(self):
index = self._asset_view.currentIndex()
asset_name = None
if index.isValid():
asset_name = index.data(ASSET_PATH_ROLE)
self._selected_asset = asset_name
self.done(1)
def set_selected_assets(self, asset_names):
"""Change preselected asset before showing the dialog.
This also resets model and clean filter.
"""
self.reset(False)
self._asset_view.collapseAll()
self._filter_input.setText("")
indexes = []
for asset_name in asset_names:
index = self._model.get_index_by_asset_name(asset_name)
if index.isValid():
indexes.append(index)
if not indexes:
return
index_deque = collections.deque()
for index in indexes:
index_deque.append(index)
all_indexes = []
while index_deque:
index = index_deque.popleft()
all_indexes.append(index)
parent_index = index.parent()
if parent_index.isValid():
index_deque.append(parent_index)
for index in all_indexes:
proxy_index = self._proxy_model.mapFromSource(index)
self._asset_view.expand(proxy_index)
def get_selected_asset(self):
"""Get selected asset name."""
return self._selected_asset

View file

@ -0,0 +1,296 @@
from qtpy import QtWidgets, QtCore, QtGui
from ayon_core.lib.events import QueuedEventSystem
from ayon_core.tools.utils import PlaceholderLineEdit, GoToCurrentButton
from ayon_core.tools.ayon_utils.models import HierarchyExpectedSelection
from ayon_core.tools.ayon_utils.widgets import FoldersWidget, TasksWidget
class CreateSelectionModel(object):
"""Model handling selection changes.
Triggering events:
- "selection.project.changed"
- "selection.folder.changed"
- "selection.task.changed"
"""
event_source = "publisher.create.selection.model"
def __init__(self, controller):
self._controller = controller
self._project_name = None
self._folder_id = None
self._task_name = None
self._task_id = None
def get_selected_project_name(self):
return self._project_name
def set_selected_project(self, project_name):
if project_name == self._project_name:
return
self._project_name = project_name
self._controller.emit_event(
"selection.project.changed",
{"project_name": project_name},
self.event_source
)
def get_selected_folder_id(self):
return self._folder_id
def set_selected_folder(self, folder_id):
if folder_id == self._folder_id:
return
self._folder_id = folder_id
self._controller.emit_event(
"selection.folder.changed",
{
"project_name": self._project_name,
"folder_id": folder_id,
},
self.event_source
)
def get_selected_task_name(self):
return self._task_name
def get_selected_task_id(self):
return self._task_id
def set_selected_task(self, task_id, task_name):
if task_id == self._task_id:
return
self._task_name = task_name
self._task_id = task_id
self._controller.emit_event(
"selection.task.changed",
{
"project_name": self._project_name,
"folder_id": self._folder_id,
"task_name": task_name,
"task_id": task_id,
},
self.event_source
)
class CreateHierarchyController:
"""Controller for hierarchy widgets.
Helper for handling hierarchy widgets in create tab. It handles selection
of folder and task to properly propagate it to other widgets.
At the same time handles expected selection so can pre-select folder and
task based on current context.
Args:
controller (PublisherController): Publisher controller.
"""
def __init__(self, controller):
self._event_system = QueuedEventSystem()
self._controller = controller
self._selection_model = CreateSelectionModel(self)
self._expected_selection = HierarchyExpectedSelection(
self, handle_project=False
)
# Events system
@property
def event_system(self):
return self._event_system
def emit_event(self, topic, data=None, source=None):
"""Use implemented event system to trigger event."""
if data is None:
data = {}
self.event_system.emit(topic, data, source)
def register_event_callback(self, topic, callback):
self.event_system.add_callback(topic, callback)
def get_project_name(self):
return self._controller.project_name
def get_folder_items(self, project_name, sender=None):
return self._controller.get_folder_items(project_name, sender)
def get_task_items(self, project_name, folder_id, sender=None):
return self._controller.get_task_items(
project_name, folder_id, sender
)
# Selection model
def set_selected_project(self, project_name):
self._selection_model.set_selected_project(project_name)
def set_selected_folder(self, folder_id):
self._selection_model.set_selected_folder(folder_id)
def set_selected_task(self, task_id, task_name):
self._selection_model.set_selected_task(task_id, task_name)
# Expected selection
def get_expected_selection_data(self):
return self._expected_selection.get_expected_selection_data()
def set_expected_selection(self, project_name, folder_id, task_name):
self._expected_selection.set_expected_selection(
project_name, folder_id, task_name
)
def expected_folder_selected(self, folder_id):
self._expected_selection.expected_folder_selected(folder_id)
def expected_task_selected(self, folder_id, task_name):
self._expected_selection.expected_task_selected(folder_id, task_name)
class CreateContextWidget(QtWidgets.QWidget):
folder_changed = QtCore.Signal()
task_changed = QtCore.Signal()
def __init__(self, controller, parent):
super(CreateContextWidget, self).__init__(parent)
self._controller = controller
self._enabled = True
self._last_project_name = None
self._last_folder_id = None
self._last_selected_task_name = None
headers_widget = QtWidgets.QWidget(self)
folder_filter_input = PlaceholderLineEdit(headers_widget)
folder_filter_input.setPlaceholderText("Filter folders..")
current_context_btn = GoToCurrentButton(headers_widget)
current_context_btn.setToolTip("Go to current context")
current_context_btn.setVisible(False)
headers_layout = QtWidgets.QHBoxLayout(headers_widget)
headers_layout.setContentsMargins(0, 0, 0, 0)
headers_layout.addWidget(folder_filter_input, 1)
headers_layout.addWidget(current_context_btn, 0)
hierarchy_controller = CreateHierarchyController(controller)
folders_widget = FoldersWidget(
hierarchy_controller, self, handle_expected_selection=True
)
folders_widget.set_deselectable(True)
tasks_widget = TasksWidget(
hierarchy_controller, self, handle_expected_selection=True
)
main_layout = QtWidgets.QVBoxLayout(self)
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.setSpacing(0)
main_layout.addWidget(headers_widget, 0)
main_layout.addWidget(folders_widget, 2)
main_layout.addWidget(tasks_widget, 1)
folders_widget.selection_changed.connect(self._on_folder_change)
tasks_widget.selection_changed.connect(self._on_task_change)
current_context_btn.clicked.connect(self._on_current_context_click)
folder_filter_input.textChanged.connect(self._on_folder_filter_change)
self._folder_filter_input = folder_filter_input
self._current_context_btn = current_context_btn
self._folders_widget = folders_widget
self._tasks_widget = tasks_widget
self._hierarchy_controller = hierarchy_controller
def get_selected_folder_id(self):
return self._folders_widget.get_selected_folder_id()
def get_selected_folder_path(self):
return self._folders_widget.get_selected_folder_path()
def get_selected_task_name(self):
return self._tasks_widget.get_selected_task_name()
def get_selected_task_type(self):
return self._tasks_widget.get_selected_task_type()
def update_current_context_btn(self):
# Hide set current folder if there is no one
folder_path = self._controller.current_folder_path
self._current_context_btn.setVisible(bool(folder_path))
def set_selected_context(self, folder_id, task_name):
self._hierarchy_controller.set_expected_selection(
self._controller.project_name,
folder_id,
task_name
)
def is_enabled(self):
return self._enabled
def set_enabled(self, enabled):
if enabled is self._enabled:
return
self.setEnabled(enabled)
self._enabled = enabled
if not enabled:
self._last_folder_id = self.get_selected_folder_id()
self._folders_widget.set_selected_folder(None)
last_selected_task_name = self.get_selected_task_name()
if last_selected_task_name:
self._last_selected_task_name = last_selected_task_name
self._clear_selection()
elif self._last_selected_task_name is not None:
self._hierarchy_controller.set_expected_selection(
self._last_project_name,
self._last_folder_id,
self._last_selected_task_name
)
def refresh(self):
self._last_project_name = self._controller.project_name
folder_id = self._last_folder_id
task_name = self._last_selected_task_name
if folder_id is None:
folder_path = self._controller.current_folder_path
folder_id = self._controller.get_folder_id_from_path(folder_path)
task_name = self._controller.current_task_name
self._hierarchy_controller.set_selected_project(
self._last_project_name
)
self._folders_widget.set_project_name(self._last_project_name)
self._hierarchy_controller.set_expected_selection(
self._last_project_name, folder_id, task_name
)
def _clear_selection(self):
self._folders_widget.set_selected_folder(None)
def _on_folder_change(self):
self.folder_changed.emit()
def _on_task_change(self):
self.task_changed.emit()
def _on_current_context_click(self):
folder_path = self._controller.current_folder_path
task_name = self._controller.current_task_name
folder_id = self._controller.get_folder_id_from_path(folder_path)
self._hierarchy_controller.set_expected_selection(
self._last_project_name, folder_id, task_name
)
def _on_folder_filter_change(self, text):
self._folders_widget.set_name_filter(text)

View file

@ -14,8 +14,7 @@ from .widgets import (
IconValuePixmapLabel,
CreateBtn,
)
from .assets_widget import CreateWidgetAssetsWidget
from .tasks_widget import CreateWidgetTasksWidget
from .create_context_widgets import CreateContextWidget
from .precreate_widget import PreCreateWidget
from ..constants import (
VARIANT_TOOLTIP,
@ -109,7 +108,7 @@ class CreateWidget(QtWidgets.QWidget):
self._controller = controller
self._asset_name = None
self._folder_path = None
self._product_names = None
self._selected_creator = None
@ -121,16 +120,7 @@ class CreateWidget(QtWidgets.QWidget):
main_splitter_widget = QtWidgets.QSplitter(self)
context_widget = QtWidgets.QWidget(main_splitter_widget)
assets_widget = CreateWidgetAssetsWidget(controller, context_widget)
tasks_widget = CreateWidgetTasksWidget(controller, context_widget)
context_layout = QtWidgets.QVBoxLayout(context_widget)
context_layout.setContentsMargins(0, 0, 0, 0)
context_layout.setSpacing(0)
context_layout.addWidget(assets_widget, 2)
context_layout.addWidget(tasks_widget, 1)
context_widget = CreateContextWidget(controller, main_splitter_widget)
# --- Creators view ---
creators_widget = QtWidgets.QWidget(main_splitter_widget)
@ -279,11 +269,8 @@ class CreateWidget(QtWidgets.QWidget):
)
variant_hints_btn.clicked.connect(self._on_variant_btn_click)
variant_hints_menu.triggered.connect(self._on_variant_action)
assets_widget.selection_changed.connect(self._on_asset_change)
assets_widget.current_context_required.connect(
self._on_current_session_context_request
)
tasks_widget.task_changed.connect(self._on_task_change)
context_widget.folder_changed.connect(self._on_folder_change)
context_widget.task_changed.connect(self._on_task_change)
thumbnail_widget.thumbnail_created.connect(self._on_thumbnail_create)
thumbnail_widget.thumbnail_cleared.connect(self._on_thumbnail_clear)
@ -299,8 +286,6 @@ class CreateWidget(QtWidgets.QWidget):
self._creators_splitter = creators_splitter
self._context_widget = context_widget
self._assets_widget = assets_widget
self._tasks_widget = tasks_widget
self.product_name_input = product_name_input
@ -324,47 +309,51 @@ class CreateWidget(QtWidgets.QWidget):
self._first_show = True
self._last_thumbnail_path = None
self._last_current_context_asset = None
self._last_current_context_folder_path = None
self._last_current_context_task = None
self._use_current_context = True
@property
def current_asset_name(self):
return self._controller.current_asset_name
def current_folder_path(self):
return self._controller.current_folder_path
@property
def current_task_name(self):
return self._controller.current_task_name
def _context_change_is_enabled(self):
return self._context_widget.isEnabled()
return self._context_widget.is_enabled()
def _get_asset_name(self):
asset_name = None
def _get_folder_path(self):
folder_path = None
if self._context_change_is_enabled():
asset_name = self._assets_widget.get_selected_asset_name()
folder_path = self._context_widget.get_selected_folder_path()
if asset_name is None:
asset_name = self.current_asset_name
return asset_name or None
if folder_path is None:
folder_path = self.current_folder_path
return folder_path or None
def _get_folder_id(self):
folder_id = None
if self._context_widget.is_enabled():
folder_id = self._context_widget.get_selected_folder_id()
return folder_id
def _get_task_name(self):
task_name = None
if self._context_change_is_enabled():
# Don't use selection of task if asset is not set
asset_name = self._assets_widget.get_selected_asset_name()
if asset_name:
task_name = self._tasks_widget.get_selected_task_name()
# Don't use selection of task if folder is not set
folder_path = self._context_widget.get_selected_folder_path()
if folder_path:
task_name = self._context_widget.get_selected_task_name()
if not task_name:
task_name = self.current_task_name
return task_name
def _set_context_enabled(self, enabled):
self._assets_widget.set_enabled(enabled)
self._tasks_widget.set_enabled(enabled)
check_prereq = self._context_widget.isEnabled() != enabled
self._context_widget.setEnabled(enabled)
check_prereq = self._context_widget.is_enabled() != enabled
self._context_widget.set_enabled(enabled)
if check_prereq:
self._invalidate_prereq()
@ -375,12 +364,12 @@ class CreateWidget(QtWidgets.QWidget):
self._use_current_context = True
def refresh(self):
current_asset_name = self._controller.current_asset_name
current_folder_path = self._controller.current_folder_path
current_task_name = self._controller.current_task_name
# Get context before refresh to keep selection of asset and
# Get context before refresh to keep selection of folder and
# task widgets
asset_name = self._get_asset_name()
folder_path = self._get_folder_path()
task_name = self._get_task_name()
# Replace by current context if last loaded context was
@ -388,37 +377,36 @@ class CreateWidget(QtWidgets.QWidget):
if (
self._use_current_context
or (
self._last_current_context_asset
and asset_name == self._last_current_context_asset
self._last_current_context_folder_path
and folder_path == self._last_current_context_folder_path
and task_name == self._last_current_context_task
)
):
asset_name = current_asset_name
folder_path = current_folder_path
task_name = current_task_name
# Store values for future refresh
self._last_current_context_asset = current_asset_name
self._last_current_context_folder_path = current_folder_path
self._last_current_context_task = current_task_name
self._use_current_context = False
self._prereq_available = False
# Disable context widget so refresh of asset will use context asset
# name
# Disable context widget so refresh of folder will use context folder
# path
self._set_context_enabled(False)
self._assets_widget.refresh()
# Refresh data before update of creators
self._refresh_asset()
self._context_widget.refresh()
self._refresh_product_name()
# Then refresh creators which may trigger callbacks using refreshed
# data
self._refresh_creators()
self._assets_widget.update_current_asset()
self._assets_widget.select_asset_by_name(asset_name)
self._tasks_widget.set_asset_name(asset_name)
self._tasks_widget.select_task_name(task_name)
folder_id = self._controller.get_folder_id_from_path(folder_path)
self._context_widget.update_current_context_btn()
self._context_widget.set_selected_context(folder_id, task_name)
self._invalidate_prereq_deffered()
@ -439,9 +427,9 @@ class CreateWidget(QtWidgets.QWidget):
if (
self._context_change_is_enabled()
and self._get_asset_name() is None
and self._get_folder_path() is None
):
# QUESTION how to handle invalid asset?
# QUESTION how to handle invalid folder?
prereq_available = False
creator_btn_tooltips.append("Context is not selected")
@ -460,24 +448,26 @@ class CreateWidget(QtWidgets.QWidget):
self._on_variant_change()
def _refresh_asset(self):
asset_name = self._get_asset_name()
def _refresh_product_name(self):
folder_path = self._get_folder_path()
# Skip if asset did not change
if self._asset_name and self._asset_name == asset_name:
# Skip if folder did not change
if self._folder_path and self._folder_path == folder_path:
return
# Make sure `_asset_name` and `_product_names` variables are reset
self._asset_name = asset_name
# Make sure `_folder_path` and `_product_names` variables are reset
self._folder_path = folder_path
self._product_names = None
if asset_name is None:
if folder_path is None:
return
product_names = self._controller.get_existing_product_names(asset_name)
product_names = self._controller.get_existing_product_names(
folder_path
)
self._product_names = product_names
if product_names is None:
self.product_name_input.setText("< Asset is not set >")
self.product_name_input.setText("< Folder is not set >")
def _refresh_creators(self):
# Refresh creators and add their product types to list
@ -545,11 +535,8 @@ class CreateWidget(QtWidgets.QWidget):
# Trigger refresh only if is visible
self.refresh()
def _on_asset_change(self):
self._refresh_asset()
asset_name = self._assets_widget.get_selected_asset_name()
self._tasks_widget.set_asset_name(asset_name)
def _on_folder_change(self):
self._refresh_product_name()
if self._context_change_is_enabled():
self._invalidate_prereq_deffered()
@ -564,12 +551,6 @@ class CreateWidget(QtWidgets.QWidget):
def _on_thumbnail_clear(self):
self._last_thumbnail_path = None
def _on_current_session_context_request(self):
self._assets_widget.select_current_asset()
task_name = self.current_task_name
if task_name:
self._tasks_widget.select_task_name(task_name)
def _on_creator_item_change(self, new_index, _old_index):
identifier = None
if new_index.isValid():
@ -616,7 +597,7 @@ class CreateWidget(QtWidgets.QWidget):
!= self._context_change_is_enabled()
):
self._set_context_enabled(creator_item.create_allow_context_change)
self._refresh_asset()
self._refresh_product_name()
self._thumbnail_widget.setVisible(
creator_item.create_allow_thumbnail
@ -685,13 +666,13 @@ class CreateWidget(QtWidgets.QWidget):
self.product_name_input.setText("< Valid variant >")
return
asset_name = self._get_asset_name()
folder_path = self._get_folder_path()
task_name = self._get_task_name()
creator_idenfier = self._selected_creator.identifier
# Calculate product name with Creator plugin
try:
product_name = self._controller.get_product_name(
creator_idenfier, variant_value, task_name, asset_name
creator_idenfier, variant_value, task_name, folder_path
)
except TaskNotSetError:
self._create_btn.setEnabled(False)
@ -705,7 +686,7 @@ class CreateWidget(QtWidgets.QWidget):
self._validate_product_name(product_name, variant_value)
def _validate_product_name(self, product_name, variant_value):
# Get all products of the current asset
# Get all products of the current folder
if self._product_names:
existing_product_names = set(self._product_names)
else:
@ -798,11 +779,11 @@ class CreateWidget(QtWidgets.QWidget):
variant = self.variant_input.text()
# Care about product name only if context change is enabled
product_name = None
asset_name = None
folder_path = None
task_name = None
if self._context_change_is_enabled():
product_name = self.product_name_input.text()
asset_name = self._get_asset_name()
folder_path = self._get_folder_path()
task_name = self._get_task_name()
pre_create_data = self._pre_create_widget.current_value()
@ -814,7 +795,7 @@ class CreateWidget(QtWidgets.QWidget):
# Where to define these data?
# - what data show be stored?
instance_data = {
"folderPath": asset_name,
"folderPath": folder_path,
"task": task_name,
"variant": variant,
"productType": product_type

View file

@ -0,0 +1,151 @@
from qtpy import QtWidgets, QtCore, QtGui
from ayon_core.lib.events import QueuedEventSystem
from ayon_core.tools.ayon_utils.widgets import FoldersWidget
from ayon_core.tools.utils import PlaceholderLineEdit
class FoldersDialogController:
def __init__(self, controller):
self._event_system = QueuedEventSystem()
self._controller = controller
@property
def event_system(self):
return self._event_system
def emit_event(self, topic, data=None, source=None):
"""Use implemented event system to trigger event."""
if data is None:
data = {}
self.event_system.emit(topic, data, source)
def register_event_callback(self, topic, callback):
self.event_system.add_callback(topic, callback)
def get_folder_items(self, project_name, sender=None):
return self._controller.get_folder_items(project_name, sender)
def set_selected_folder(self, folder_id):
pass
class FoldersDialog(QtWidgets.QDialog):
"""Dialog to select folder for a context of instance."""
def __init__(self, controller, parent):
super(FoldersDialog, self).__init__(parent)
self.setWindowTitle("Select folder")
filter_input = PlaceholderLineEdit(self)
filter_input.setPlaceholderText("Filter folders..")
folders_controller = FoldersDialogController(controller)
folders_widget = FoldersWidget(folders_controller, self)
folders_widget.set_deselectable(True)
ok_btn = QtWidgets.QPushButton("OK", self)
cancel_btn = QtWidgets.QPushButton("Cancel", self)
btns_layout = QtWidgets.QHBoxLayout()
btns_layout.addStretch(1)
btns_layout.addWidget(ok_btn)
btns_layout.addWidget(cancel_btn)
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(filter_input, 0)
layout.addWidget(folders_widget, 1)
layout.addLayout(btns_layout, 0)
controller.event_system.add_callback(
"controller.reset.finished", self._on_controller_reset
)
folders_widget.double_clicked.connect(self._on_ok_clicked)
filter_input.textChanged.connect(self._on_filter_change)
ok_btn.clicked.connect(self._on_ok_clicked)
cancel_btn.clicked.connect(self._on_cancel_clicked)
self._controller = controller
self._filter_input = filter_input
self._ok_btn = ok_btn
self._cancel_btn = cancel_btn
self._folders_widget = folders_widget
self._selected_folder_path = None
# Soft refresh is enabled
# - reset will happen at all cost if soft reset is enabled
# - adds ability to call reset on multiple places without repeating
self._soft_reset_enabled = True
self._first_show = True
self._default_height = 500
def _on_first_show(self):
center = self.rect().center()
size = self.size()
size.setHeight(self._default_height)
self.resize(size)
new_pos = self.mapToGlobal(center)
new_pos.setX(new_pos.x() - int(self.width() / 2))
new_pos.setY(new_pos.y() - int(self.height() / 2))
self.move(new_pos)
def _on_controller_reset(self):
# Change reset enabled so model is reset on show event
self._soft_reset_enabled = True
def showEvent(self, event):
"""Refresh folders widget on show."""
super(FoldersDialog, self).showEvent(event)
if self._first_show:
self._first_show = False
self._on_first_show()
# Refresh on show
self.reset(False)
def reset(self, force=True):
"""Reset widget."""
if not force and not self._soft_reset_enabled:
return
if self._soft_reset_enabled:
self._soft_reset_enabled = False
self._folders_widget.set_project_name(self._controller.project_name)
def _on_filter_change(self, text):
"""Trigger change of filter of folders."""
self._folders_widget.set_name_filter(text)
def _on_cancel_clicked(self):
self.done(0)
def _on_ok_clicked(self):
self._selected_folder_path = (
self._folders_widget.get_selected_folder_path()
)
self.done(1)
def set_selected_folders(self, folder_paths):
"""Change preselected folder before showing the dialog.
This also resets model and clean filter.
"""
self.reset(False)
self._filter_input.setText("")
folder_id = None
for folder_path in folder_paths:
folder_id = self._controller.get_folder_id_from_path(folder_path)
if folder_id:
break
if folder_id:
self._folders_widget.set_selected_folder(folder_id)
def get_selected_folder_path(self):
"""Get selected folder path."""
return self._selected_folder_path

View file

@ -0,0 +1,137 @@
from qtpy import QtWidgets, QtCore, QtGui
from ayon_core.tools.utils.lib import get_default_task_icon
TASK_NAME_ROLE = QtCore.Qt.UserRole + 1
TASK_TYPE_ROLE = QtCore.Qt.UserRole + 2
TASK_ORDER_ROLE = QtCore.Qt.UserRole + 3
class TasksModel(QtGui.QStandardItemModel):
"""Tasks model.
Task model must have set context of folder paths.
Items in model are based on 0-infinite folders. Always contain
an interserction of context folder tasks. When no folders are in context
them model is empty if 2 or more are in context folders that don't have
tasks with same names then model is empty too.
Args:
controller (PublisherController): Controller which handles creation and
publishing.
"""
def __init__(self, controller, allow_empty_task=False):
super(TasksModel, self).__init__()
self._allow_empty_task = allow_empty_task
self._controller = controller
self._items_by_name = {}
self._folder_paths = []
self._task_names_by_folder_path = {}
def set_folder_paths(self, folder_paths):
"""Set folders context."""
self._folder_paths = folder_paths
self.reset()
@staticmethod
def get_intersection_of_tasks(task_names_by_folder_path):
"""Calculate intersection of task names from passed data.
Example:
```
# Passed `task_names_by_folder_path`
{
"/folder_1": ["compositing", "animation"],
"/folder_2": ["compositing", "editorial"]
}
```
Result:
```
# Set
{"compositing"}
```
Args:
task_names_by_folder_path (dict): Task names in iterable by parent.
"""
tasks = None
for task_names in task_names_by_folder_path.values():
if tasks is None:
tasks = set(task_names)
else:
tasks &= set(task_names)
if not tasks:
break
return tasks or set()
def is_task_name_valid(self, folder_path, task_name):
"""Is task name available for folder.
Todos:
Move this method to PublisherController.
Args:
folder_path (str): Fodler path where should look for task.
task_name (str): Name of task which should be available in folder
tasks.
"""
if folder_path not in self._task_names_by_folder_path:
return False
if self._allow_empty_task and not task_name:
return True
task_names = self._task_names_by_folder_path[folder_path]
if task_name in task_names:
return True
return False
def reset(self):
"""Update model by current context."""
if not self._folder_paths:
self._items_by_name = {}
self._task_names_by_folder_path = {}
root_item = self.invisibleRootItem()
root_item.removeRows(0, self.rowCount())
return
task_names_by_folder_path = (
self._controller.get_task_names_by_folder_paths(
self._folder_paths
)
)
self._task_names_by_folder_path = task_names_by_folder_path
new_task_names = self.get_intersection_of_tasks(
task_names_by_folder_path
)
if self._allow_empty_task:
new_task_names.add("")
old_task_names = set(self._items_by_name.keys())
if new_task_names == old_task_names:
return
root_item = self.invisibleRootItem()
for task_name in old_task_names:
if task_name not in new_task_names:
item = self._items_by_name.pop(task_name)
root_item.removeRow(item.row())
new_items = []
for task_name in new_task_names:
if task_name in self._items_by_name:
continue
item = QtGui.QStandardItem(task_name)
item.setData(task_name, TASK_NAME_ROLE)
if task_name:
item.setData(get_default_task_icon(), QtCore.Qt.DecorationRole)
self._items_by_name[task_name] = item
new_items.append(item)
if new_items:
root_item.appendRows(new_items)

View file

@ -1,326 +0,0 @@
from qtpy import QtWidgets, QtCore, QtGui
from ayon_core.tools.utils.views import DeselectableTreeView
from ayon_core.tools.utils.lib import get_default_task_icon
TASK_NAME_ROLE = QtCore.Qt.UserRole + 1
TASK_TYPE_ROLE = QtCore.Qt.UserRole + 2
TASK_ORDER_ROLE = QtCore.Qt.UserRole + 3
class TasksModel(QtGui.QStandardItemModel):
"""Tasks model.
Task model must have set context of asset documents.
Items in model are based on 0-infinite asset documents. Always contain
an interserction of context asset tasks. When no assets are in context
them model is empty if 2 or more are in context assets that don't have
tasks with same names then model is empty too.
Args:
controller (PublisherController): Controller which handles creation and
publishing.
"""
def __init__(self, controller, allow_empty_task=False):
super(TasksModel, self).__init__()
self._allow_empty_task = allow_empty_task
self._controller = controller
self._items_by_name = {}
self._asset_names = []
self._task_names_by_asset_name = {}
def set_asset_names(self, asset_names):
"""Set assets context."""
self._asset_names = asset_names
self.reset()
@staticmethod
def get_intersection_of_tasks(task_names_by_asset_name):
"""Calculate intersection of task names from passed data.
Example:
```
# Passed `task_names_by_asset_name`
{
"asset_1": ["compositing", "animation"],
"asset_2": ["compositing", "editorial"]
}
```
Result:
```
# Set
{"compositing"}
```
Args:
task_names_by_asset_name (dict): Task names in iterable by parent.
"""
tasks = None
for task_names in task_names_by_asset_name.values():
if tasks is None:
tasks = set(task_names)
else:
tasks &= set(task_names)
if not tasks:
break
return tasks or set()
def is_task_name_valid(self, asset_name, task_name):
"""Is task name available for asset.
Args:
asset_name (str): Name of asset where should look for task.
task_name (str): Name of task which should be available in asset's
tasks.
"""
if asset_name not in self._task_names_by_asset_name:
return False
if self._allow_empty_task and not task_name:
return True
task_names = self._task_names_by_asset_name[asset_name]
if task_name in task_names:
return True
return False
def reset(self):
"""Update model by current context."""
if not self._asset_names:
self._items_by_name = {}
self._task_names_by_asset_name = {}
self.clear()
return
task_names_by_asset_name = (
self._controller.get_task_names_by_asset_names(self._asset_names)
)
self._task_names_by_asset_name = task_names_by_asset_name
new_task_names = self.get_intersection_of_tasks(
task_names_by_asset_name
)
if self._allow_empty_task:
new_task_names.add("")
old_task_names = set(self._items_by_name.keys())
if new_task_names == old_task_names:
return
root_item = self.invisibleRootItem()
for task_name in old_task_names:
if task_name not in new_task_names:
item = self._items_by_name.pop(task_name)
root_item.removeRow(item.row())
new_items = []
for task_name in new_task_names:
if task_name in self._items_by_name:
continue
item = QtGui.QStandardItem(task_name)
item.setData(task_name, TASK_NAME_ROLE)
if task_name:
item.setData(get_default_task_icon(), QtCore.Qt.DecorationRole)
self._items_by_name[task_name] = item
new_items.append(item)
if new_items:
root_item.appendRows(new_items)
def headerData(self, section, orientation, role=None):
if role is None:
role = QtCore.Qt.EditRole
# Show nice labels in the header
if section == 0:
if (
role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole)
and orientation == QtCore.Qt.Horizontal
):
return "Tasks"
return super(TasksModel, self).headerData(section, orientation, role)
class TasksProxyModel(QtCore.QSortFilterProxyModel):
def lessThan(self, x_index, y_index):
x_order = x_index.data(TASK_ORDER_ROLE)
y_order = y_index.data(TASK_ORDER_ROLE)
if x_order is not None and y_order is not None:
if x_order < y_order:
return True
if x_order > y_order:
return False
elif x_order is None and y_order is not None:
return True
elif y_order is None and x_order is not None:
return False
x_name = x_index.data(QtCore.Qt.DisplayRole)
y_name = y_index.data(QtCore.Qt.DisplayRole)
if x_name == y_name:
return True
if x_name == tuple(sorted((x_name, y_name)))[0]:
return True
return False
class CreateWidgetTasksWidget(QtWidgets.QWidget):
"""Widget showing active Tasks
Deprecated:
This widget will be removed soon. Please do not use it in new code.
"""
task_changed = QtCore.Signal()
def __init__(self, controller, parent):
self._controller = controller
self._enabled = None
super(CreateWidgetTasksWidget, self).__init__(parent)
tasks_view = DeselectableTreeView(self)
tasks_view.setIndentation(0)
tasks_view.setSortingEnabled(True)
tasks_view.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
header_view = tasks_view.header()
header_view.setSortIndicator(0, QtCore.Qt.AscendingOrder)
tasks_model = TasksModel(self._controller)
tasks_proxy = TasksProxyModel()
tasks_proxy.setSourceModel(tasks_model)
tasks_view.setModel(tasks_proxy)
layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(tasks_view)
selection_model = tasks_view.selectionModel()
selection_model.selectionChanged.connect(self._on_task_change)
self._tasks_model = tasks_model
self._tasks_proxy = tasks_proxy
self._tasks_view = tasks_view
self._last_selected_task_name = None
def refresh(self):
self._tasks_model.refresh()
def set_asset_id(self, asset_id):
# Try and preserve the last selected task and reselect it
# after switching assets. If there's no currently selected
# asset keep whatever the "last selected" was prior to it.
current = self.get_selected_task_name()
if current:
self._last_selected_task_name = current
self._tasks_model.set_asset_id(asset_id)
if self._last_selected_task_name:
self.select_task_name(self._last_selected_task_name)
# Force a task changed emit.
self.task_changed.emit()
def _clear_selection(self):
selection_model = self._tasks_view.selectionModel()
selection_model.clearSelection()
def select_task_name(self, task_name):
"""Select a task by name.
If the task does not exist in the current model then selection is only
cleared.
Args:
task_name (str): Name of the task to select.
"""
task_view_model = self._tasks_view.model()
if not task_view_model:
return
# Clear selection
selection_model = self._tasks_view.selectionModel()
selection_model.clearSelection()
# Select the task
mode = (
QtCore.QItemSelectionModel.Select
| QtCore.QItemSelectionModel.Rows
)
for row in range(task_view_model.rowCount()):
index = task_view_model.index(row, 0)
name = index.data(TASK_NAME_ROLE)
if name == task_name:
selection_model.select(index, mode)
# Set the currently active index
self._tasks_view.setCurrentIndex(index)
break
last_selected_task_name = self.get_selected_task_name()
if last_selected_task_name:
self._last_selected_task_name = last_selected_task_name
if not self._enabled:
current = self.get_selected_task_name()
if current:
self._last_selected_task_name = current
self._clear_selection()
def get_selected_task_name(self):
"""Return name of task at current index (selected)
Returns:
str: Name of the current task.
"""
index = self._tasks_view.currentIndex()
selection_model = self._tasks_view.selectionModel()
if index.isValid() and selection_model.isSelected(index):
return index.data(TASK_NAME_ROLE)
return None
def get_selected_task_type(self):
index = self._tasks_view.currentIndex()
selection_model = self._tasks_view.selectionModel()
if index.isValid() and selection_model.isSelected(index):
return index.data(TASK_TYPE_ROLE)
return None
def set_asset_name(self, asset_name):
current = self.get_selected_task_name()
if current:
self._last_selected_task_name = current
self._tasks_model.set_asset_names([asset_name])
if self._last_selected_task_name and self._enabled:
self.select_task_name(self._last_selected_task_name)
# Force a task changed emit.
self.task_changed.emit()
def set_enabled(self, enabled):
self._enabled = enabled
if not enabled:
last_selected_task_name = self.get_selected_task_name()
if last_selected_task_name:
self._last_selected_task_name = last_selected_task_name
self._clear_selection()
elif self._last_selected_task_name is not None:
self.select_task_name(self._last_selected_task_name)
def _on_task_change(self):
self.task_changed.emit()

View file

@ -26,8 +26,8 @@ from ayon_core.pipeline.create import (
TaskNotSetError,
)
from .thumbnail_widget import ThumbnailWidget
from .assets_widget import AssetsDialog
from .tasks_widget import TasksModel
from .folders_dialog import FoldersDialog
from .tasks_model import TasksModel
from .icons import (
get_pixmap,
get_icon_path
@ -422,29 +422,29 @@ class ClickableLineEdit(QtWidgets.QLineEdit):
event.accept()
class AssetsField(BaseClickableFrame):
"""Field where asset name of selected instance/s is showed.
class FoldersFields(BaseClickableFrame):
"""Field where folder path of selected instance/s is showed.
Click on the field will trigger `AssetsDialog`.
Click on the field will trigger `FoldersDialog`.
"""
value_changed = QtCore.Signal()
def __init__(self, controller, parent):
super(AssetsField, self).__init__(parent)
self.setObjectName("AssetNameInputWidget")
super(FoldersFields, self).__init__(parent)
self.setObjectName("FolderPathInputWidget")
# Don't use 'self' for parent!
# - this widget has specific styles
dialog = AssetsDialog(controller, parent)
dialog = FoldersDialog(controller, parent)
name_input = ClickableLineEdit(self)
name_input.setObjectName("AssetNameInput")
name_input.setObjectName("FolderPathInput")
icon_name = "fa.window-maximize"
icon = qtawesome.icon(icon_name, color="white")
icon_btn = QtWidgets.QPushButton(self)
icon_btn.setIcon(icon)
icon_btn.setObjectName("AssetNameInputButton")
icon_btn.setObjectName("FolderPathInputButton")
layout = QtWidgets.QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
@ -465,6 +465,7 @@ class AssetsField(BaseClickableFrame):
icon_btn.clicked.connect(self._mouse_release_callback)
dialog.finished.connect(self._on_dialog_finish)
self._controller = controller
self._dialog = dialog
self._name_input = name_input
self._icon_btn = icon_btn
@ -480,28 +481,28 @@ class AssetsField(BaseClickableFrame):
if not result:
return
asset_name = self._dialog.get_selected_asset()
if asset_name is None:
folder_path = self._dialog.get_selected_folder_path()
if folder_path is None:
return
self._selected_items = [asset_name]
self._selected_items = [folder_path]
self._has_value_changed = (
self._origin_value != self._selected_items
)
self.set_text(asset_name)
self.set_text(folder_path)
self._set_is_valid(True)
self.value_changed.emit()
def _mouse_release_callback(self):
self._dialog.set_selected_assets(self._selected_items)
self._dialog.set_selected_folders(self._selected_items)
self._dialog.open()
def set_multiselection_text(self, text):
"""Change text for multiselection of different assets.
"""Change text for multiselection of different folders.
When there are selected multiple instances at once and they don't have
same asset in context.
same folder in context.
"""
self._multiselection_text = text
@ -520,63 +521,58 @@ class AssetsField(BaseClickableFrame):
set_style_property(self._icon_btn, "state", state)
def is_valid(self):
"""Is asset valid."""
"""Is folder valid."""
return self._is_valid
def has_value_changed(self):
"""Value of asset has changed."""
"""Value of folder has changed."""
return self._has_value_changed
def get_selected_items(self):
"""Selected asset names."""
"""Selected folder paths."""
return list(self._selected_items)
def set_text(self, text):
"""Set text in text field.
Does not change selected items (assets).
Does not change selected items (folders).
"""
self._name_input.setText(text)
self._name_input.end(False)
def set_selected_items(self, asset_names=None):
"""Set asset names for selection of instances.
def set_selected_items(self, folder_paths=None):
"""Set folder paths for selection of instances.
Passed asset names are validated and if there are 2 or more different
asset names then multiselection text is shown.
Passed folder paths are validated and if there are 2 or more different
folder paths then multiselection text is shown.
Args:
asset_names (list, tuple, set, NoneType): List of asset names.
folder_paths (list, tuple, set, NoneType): List of folder paths.
"""
if asset_names is None:
asset_names = []
if folder_paths is None:
folder_paths = []
self._has_value_changed = False
self._origin_value = list(asset_names)
self._selected_items = list(asset_names)
is_valid = True
if not asset_names:
self._origin_value = list(folder_paths)
self._selected_items = list(folder_paths)
is_valid = self._controller.are_folder_paths_valid(folder_paths)
if not folder_paths:
self.set_text("")
elif len(asset_names) == 1:
asset_name = tuple(asset_names)[0]
is_valid = self._dialog.name_is_valid(asset_name)
self.set_text(asset_name)
elif len(folder_paths) == 1:
folder_path = tuple(folder_paths)[0]
self.set_text(folder_path)
else:
for asset_name in asset_names:
is_valid = self._dialog.name_is_valid(asset_name)
if not is_valid:
break
multiselection_text = self._multiselection_text
if multiselection_text is None:
multiselection_text = "|".join(asset_names)
multiselection_text = "|".join(folder_paths)
self.set_text(multiselection_text)
self._set_is_valid(is_valid)
def reset_to_origin(self):
"""Change to asset names set with last `set_selected_items` call."""
"""Change to folder paths set with last `set_selected_items` call."""
self.set_selected_items(self._origin_value)
def confirm_value(self):
@ -610,9 +606,9 @@ class TasksCombobox(QtWidgets.QComboBox):
"""Combobox to show tasks for selected instances.
Combobox gives ability to select only from intersection of task names for
asset names in selected instances.
folder paths in selected instances.
If asset names in selected instances does not have same tasks then combobox
If folder paths in selected instances does not have same tasks then combobox
will be empty.
"""
value_changed = QtCore.Signal()
@ -746,23 +742,23 @@ class TasksCombobox(QtWidgets.QComboBox):
"""
return list(self._selected_items)
def set_asset_names(self, asset_names):
"""Set asset names for which should show tasks."""
def set_folder_paths(self, folder_paths):
"""Set folder paths for which should show tasks."""
self._ignore_index_change = True
self._model.set_asset_names(asset_names)
self._model.set_folder_paths(folder_paths)
self._proxy_model.set_filter_empty(False)
self._proxy_model.sort(0)
self._ignore_index_change = False
# It is a bug if not exactly one asset got here
if len(asset_names) != 1:
# It is a bug if not exactly one folder got here
if len(folder_paths) != 1:
self.set_selected_item("")
self._set_is_valid(False)
return
asset_name = tuple(asset_names)[0]
folder_path = tuple(folder_paths)[0]
is_valid = False
if self._selected_items:
@ -770,7 +766,7 @@ class TasksCombobox(QtWidgets.QComboBox):
valid_task_names = []
for task_name in self._selected_items:
_is_valid = self._model.is_task_name_valid(asset_name, task_name)
_is_valid = self._model.is_task_name_valid(folder_path, task_name)
if _is_valid:
valid_task_names.append(task_name)
else:
@ -791,42 +787,42 @@ class TasksCombobox(QtWidgets.QComboBox):
self._set_is_valid(is_valid)
def confirm_value(self, asset_names):
def confirm_value(self, folder_paths):
new_task_name = self._selected_items[0]
self._origin_value = [
(asset_name, new_task_name)
for asset_name in asset_names
(folder_path, new_task_name)
for folder_path in folder_paths
]
self._origin_selection = copy.deepcopy(self._selected_items)
self._has_value_changed = False
def set_selected_items(self, asset_task_combinations=None):
def set_selected_items(self, folder_task_combinations=None):
"""Set items for selected instances.
Args:
asset_task_combinations (list): List of tuples. Each item in
the list contain asset name and task name.
folder_task_combinations (list): List of tuples. Each item in
the list contain folder path and task name.
"""
self._proxy_model.set_filter_empty(False)
self._proxy_model.sort(0)
if asset_task_combinations is None:
asset_task_combinations = []
if folder_task_combinations is None:
folder_task_combinations = []
task_names = set()
task_names_by_asset_name = collections.defaultdict(set)
for asset_name, task_name in asset_task_combinations:
task_names_by_folder_path = collections.defaultdict(set)
for folder_path, task_name in folder_task_combinations:
task_names.add(task_name)
task_names_by_asset_name[asset_name].add(task_name)
asset_names = set(task_names_by_asset_name.keys())
task_names_by_folder_path[folder_path].add(task_name)
folder_paths = set(task_names_by_folder_path.keys())
self._ignore_index_change = True
self._model.set_asset_names(asset_names)
self._model.set_folder_paths(folder_paths)
self._has_value_changed = False
self._origin_value = copy.deepcopy(asset_task_combinations)
self._origin_value = copy.deepcopy(folder_task_combinations)
self._origin_selection = list(task_names)
self._selected_items = list(task_names)
@ -840,9 +836,9 @@ class TasksCombobox(QtWidgets.QComboBox):
task_name = tuple(task_names)[0]
idx = self.findText(task_name)
is_valid = not idx < 0
if not is_valid and len(asset_names) > 1:
is_valid = self._validate_task_names_by_asset_names(
task_names_by_asset_name
if not is_valid and len(folder_paths) > 1:
is_valid = self._validate_task_names_by_folder_paths(
task_names_by_folder_path
)
self.set_selected_item(task_name)
@ -853,9 +849,9 @@ class TasksCombobox(QtWidgets.QComboBox):
if not is_valid:
break
if not is_valid and len(asset_names) > 1:
is_valid = self._validate_task_names_by_asset_names(
task_names_by_asset_name
if not is_valid and len(folder_paths) > 1:
is_valid = self._validate_task_names_by_folder_paths(
task_names_by_folder_path
)
multiselection_text = self._multiselection_text
if multiselection_text is None:
@ -868,10 +864,10 @@ class TasksCombobox(QtWidgets.QComboBox):
self.value_changed.emit()
def _validate_task_names_by_asset_names(self, task_names_by_asset_name):
for asset_name, task_names in task_names_by_asset_name.items():
def _validate_task_names_by_folder_paths(self, task_names_by_folder_path):
for folder_path, task_names in task_names_by_folder_path.items():
for task_name in task_names:
if not self._model.is_task_name_valid(asset_name, task_name):
if not self._model.is_task_name_valid(folder_path, task_name):
return False
return True
@ -1106,17 +1102,17 @@ class GlobalAttrsWidget(QtWidgets.QWidget):
self._current_instances = []
variant_input = VariantInputWidget(self)
asset_value_widget = AssetsField(controller, self)
folder_value_widget = FoldersFields(controller, self)
task_value_widget = TasksCombobox(controller, self)
product_type_value_widget = MultipleItemWidget(self)
product_value_widget = MultipleItemWidget(self)
variant_input.set_multiselection_text(self.multiselection_text)
asset_value_widget.set_multiselection_text(self.multiselection_text)
folder_value_widget.set_multiselection_text(self.multiselection_text)
task_value_widget.set_multiselection_text(self.multiselection_text)
variant_input.set_value()
asset_value_widget.set_selected_items()
folder_value_widget.set_selected_items()
task_value_widget.set_selected_items()
product_type_value_widget.set_value()
product_value_widget.set_value()
@ -1137,20 +1133,20 @@ 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", asset_value_widget)
main_layout.addRow("Folder", folder_value_widget)
main_layout.addRow("Task", task_value_widget)
main_layout.addRow("Product type", product_type_value_widget)
main_layout.addRow("Product name", product_value_widget)
main_layout.addRow(btns_layout)
variant_input.value_changed.connect(self._on_variant_change)
asset_value_widget.value_changed.connect(self._on_asset_change)
folder_value_widget.value_changed.connect(self._on_folder_change)
task_value_widget.value_changed.connect(self._on_task_change)
submit_btn.clicked.connect(self._on_submit)
cancel_btn.clicked.connect(self._on_cancel)
self.variant_input = variant_input
self.asset_value_widget = asset_value_widget
self.folder_value_widget = folder_value_widget
self.task_value_widget = task_value_widget
self.product_type_value_widget = product_type_value_widget
self.product_value_widget = product_value_widget
@ -1161,40 +1157,40 @@ class GlobalAttrsWidget(QtWidgets.QWidget):
"""Commit changes for selected instances."""
variant_value = None
asset_name = None
folder_path = None
task_name = None
if self.variant_input.has_value_changed():
variant_value = self.variant_input.get_value()[0]
if self.asset_value_widget.has_value_changed():
asset_name = self.asset_value_widget.get_selected_items()[0]
if self.folder_value_widget.has_value_changed():
folder_path = self.folder_value_widget.get_selected_items()[0]
if self.task_value_widget.has_value_changed():
task_name = self.task_value_widget.get_selected_items()[0]
product_names = set()
invalid_tasks = False
asset_names = []
folder_paths = []
for instance in self._current_instances:
new_variant_value = instance.get("variant")
new_asset_name = instance.get("folderPath")
new_folder_path = instance.get("folderPath")
new_task_name = instance.get("task")
if variant_value is not None:
new_variant_value = variant_value
if asset_name is not None:
new_asset_name = asset_name
if folder_path is not None:
new_folder_path = folder_path
if task_name is not None:
new_task_name = task_name
asset_names.append(new_asset_name)
folder_paths.append(new_folder_path)
try:
new_product_name = self._controller.get_product_name(
instance.creator_identifier,
new_variant_value,
new_task_name,
new_asset_name,
new_folder_path,
instance.id,
)
@ -1208,8 +1204,8 @@ class GlobalAttrsWidget(QtWidgets.QWidget):
if variant_value is not None:
instance["variant"] = variant_value
if asset_name is not None:
instance["folderPath"] = asset_name
if folder_path is not None:
instance["folderPath"] = folder_path
instance.set_asset_invalid(False)
if task_name is not None:
@ -1229,11 +1225,11 @@ class GlobalAttrsWidget(QtWidgets.QWidget):
if variant_value is not None:
self.variant_input.confirm_value()
if asset_name is not None:
self.asset_value_widget.confirm_value()
if folder_path is not None:
self.folder_value_widget.confirm_value()
if task_name is not None:
self.task_value_widget.confirm_value(asset_names)
self.task_value_widget.confirm_value(folder_paths)
self.instance_context_changed.emit()
@ -1241,19 +1237,19 @@ class GlobalAttrsWidget(QtWidgets.QWidget):
"""Cancel changes and set back to their irigin value."""
self.variant_input.reset_to_origin()
self.asset_value_widget.reset_to_origin()
self.folder_value_widget.reset_to_origin()
self.task_value_widget.reset_to_origin()
self._set_btns_enabled(False)
def _on_value_change(self):
any_invalid = (
not self.variant_input.is_valid()
or not self.asset_value_widget.is_valid()
or not self.folder_value_widget.is_valid()
or not self.task_value_widget.is_valid()
)
any_changed = (
self.variant_input.has_value_changed()
or self.asset_value_widget.has_value_changed()
or self.folder_value_widget.has_value_changed()
or self.task_value_widget.has_value_changed()
)
self._set_btns_visible(any_changed or any_invalid)
@ -1263,9 +1259,9 @@ class GlobalAttrsWidget(QtWidgets.QWidget):
def _on_variant_change(self):
self._on_value_change()
def _on_asset_change(self):
asset_names = self.asset_value_widget.get_selected_items()
self.task_value_widget.set_asset_names(asset_names)
def _on_folder_change(self):
folder_paths = self.folder_value_widget.get_selected_items()
self.task_value_widget.set_folder_paths(folder_paths)
self._on_value_change()
def _on_task_change(self):
@ -1290,7 +1286,7 @@ class GlobalAttrsWidget(QtWidgets.QWidget):
self._current_instances = instances
asset_names = set()
folder_paths = set()
variants = set()
product_types = set()
product_names = set()
@ -1299,7 +1295,7 @@ class GlobalAttrsWidget(QtWidgets.QWidget):
if len(instances) == 0:
editable = False
asset_task_combinations = []
folder_task_combinations = []
for instance in instances:
# NOTE I'm not sure how this can even happen?
if instance.creator_identifier is None:
@ -1307,23 +1303,23 @@ class GlobalAttrsWidget(QtWidgets.QWidget):
variants.add(instance.get("variant") or self.unknown_value)
product_types.add(instance.get("productType") or self.unknown_value)
asset_name = instance.get("folderPath") or self.unknown_value
folder_path = 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))
folder_paths.add(folder_path)
folder_task_combinations.append((folder_path, task_name))
product_names.add(instance.get("productName") or self.unknown_value)
self.variant_input.set_value(variants)
# Set context of asset widget
self.asset_value_widget.set_selected_items(asset_names)
# Set context of folder widget
self.folder_value_widget.set_selected_items(folder_paths)
# Set context of task widget
self.task_value_widget.set_selected_items(asset_task_combinations)
self.task_value_widget.set_selected_items(folder_task_combinations)
self.product_type_value_widget.set_value(product_types)
self.product_value_widget.set_value(product_names)
self.variant_input.setEnabled(editable)
self.asset_value_widget.setEnabled(editable)
self.folder_value_widget.setEnabled(editable)
self.task_value_widget.setEnabled(editable)

View file

@ -196,19 +196,19 @@ class FoldersField(BaseClickableFrame):
def __init__(self, controller, parent):
super(FoldersField, self).__init__(parent)
self.setObjectName("AssetNameInputWidget")
self.setObjectName("FolderPathInputWidget")
# Don't use 'self' for parent!
# - this widget has specific styles
dialog = FoldersDialog(controller, parent)
name_input = ClickableLineEdit(self)
name_input.setObjectName("AssetNameInput")
name_input.setObjectName("FolderPathInput")
icon = qtawesome.icon("fa.window-maximize", color="white")
icon_btn = QtWidgets.QPushButton(self)
icon_btn.setIcon(icon)
icon_btn.setObjectName("AssetNameInputButton")
icon_btn.setObjectName("FolderPathInputButton")
layout = QtWidgets.QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)

View file

@ -10,51 +10,47 @@ import platform
from qtpy import QtWidgets, QtCore
import qtawesome
import appdirs
from ayon_core.lib import JSONSettingRegistry, is_running_from_build
from ayon_core.lib import AYONSettingsRegistry, is_running_from_build
from ayon_core.pipeline import install_host
from ayon_core.hosts.traypublisher.api import TrayPublisherHost
from ayon_core.tools.publisher.control_qt import QtPublisherController
from ayon_core.tools.publisher.window import PublisherWindow
from ayon_core.tools.utils import PlaceholderLineEdit, get_ayon_qt_app
from ayon_core.tools.utils.constants import PROJECT_NAME_ROLE
from ayon_core.tools.utils.models import (
ProjectModel,
ProjectSortFilterProxy
from ayon_core.tools.ayon_utils.models import ProjectsModel
from ayon_core.tools.ayon_utils.widgets import (
ProjectsQtModel,
ProjectSortFilterProxy,
PROJECT_NAME_ROLE,
)
class TrayPublisherRegistry(AYONSettingsRegistry):
def __init__(self):
super(TrayPublisherRegistry, self).__init__("traypublisher")
class TrayPublisherController(QtPublisherController):
def __init__(self, *args, **kwargs):
super(TrayPublisherController, self).__init__(*args, **kwargs)
self._projects_model = ProjectsModel(self)
@property
def host(self):
return self._host
def reset_project_data_cache(self):
def reset_hierarchy_cache(self):
self._hierarchy_model.reset()
self._asset_docs_cache.reset()
class TrayPublisherRegistry(JSONSettingRegistry):
"""Class handling AYON general settings registry.
Attributes:
vendor (str): Name used for path construction.
product (str): Additional name used for path construction.
"""
def __init__(self):
self.vendor = "pypeclub"
self.product = "openpype"
name = "tray_publisher"
path = appdirs.user_data_dir(self.product, self.vendor)
super(TrayPublisherRegistry, self).__init__(name, path)
def get_project_items(self, sender=None):
return self._projects_model.get_project_items(sender)
class StandaloneOverlayWidget(QtWidgets.QFrame):
project_selected = QtCore.Signal(str)
def __init__(self, publisher_window):
def __init__(self, controller, publisher_window):
super(StandaloneOverlayWidget, self).__init__(publisher_window)
self.setObjectName("OverlayFrame")
@ -66,7 +62,7 @@ class StandaloneOverlayWidget(QtWidgets.QFrame):
header_label = QtWidgets.QLabel("Choose project", content_widget)
header_label.setObjectName("ChooseProjectLabel")
# Create project models and view
projects_model = ProjectModel()
projects_model = ProjectsQtModel(controller)
projects_proxy = ProjectSortFilterProxy()
projects_proxy.setSourceModel(projects_model)
projects_proxy.setFilterKeyColumn(0)
@ -137,12 +133,11 @@ class StandaloneOverlayWidget(QtWidgets.QFrame):
project_name = None
if project_name:
index = None
src_index = self._projects_model.find_project(project_name)
if src_index is not None:
index = self._projects_proxy.mapFromSource(src_index)
if index is not None:
src_index = self._projects_model.get_index_by_project_name(
project_name
)
index = self._projects_proxy.mapFromSource(src_index)
if index.isValid():
selection_model = self._projects_view.selectionModel()
selection_model.select(
index,
@ -201,7 +196,7 @@ class TrayPublishWindow(PublisherWindow):
self.setWindowFlags(flags)
overlay_widget = StandaloneOverlayWidget(self)
overlay_widget = StandaloneOverlayWidget(controller, self)
btns_widget = self._header_extra_widget
@ -248,7 +243,7 @@ class TrayPublishWindow(PublisherWindow):
def _on_project_select(self, project_name):
# TODO register project specific plugin paths
self._controller.save_changes(False)
self._controller.reset_project_data_cache()
self._controller.reset_hierarchy_cache()
self.reset()
if not self._controller.instances:

View file

@ -243,160 +243,3 @@ class RecursiveSortFilterProxyModel(QtCore.QSortFilterProxyModel):
return super(RecursiveSortFilterProxyModel, self).filterAcceptsRow(
row, parent_index
)
# TODO remove 'ProjectModel' and 'ProjectSortFilterProxy' classes
# - replace their usage with current 'ayon_utils' models
class ProjectModel(QtGui.QStandardItemModel):
def __init__(
self, only_active=True, add_default_project=False, *args, **kwargs
):
super(ProjectModel, self).__init__(*args, **kwargs)
self._only_active = only_active
self._add_default_project = add_default_project
self._default_item = None
self._items_by_name = {}
self._refreshed = False
def set_default_project_available(self, available=True):
if available is None:
available = not self._add_default_project
if self._add_default_project == available:
return
self._add_default_project = available
if not available and self._default_item is not None:
root_item = self.invisibleRootItem()
root_item.removeRow(self._default_item.row())
self._default_item = None
def set_only_active(self, only_active=True):
if only_active is None:
only_active = not self._only_active
if self._only_active == only_active:
return
self._only_active = only_active
if self._refreshed:
self.refresh()
def project_name_is_available(self, project_name):
"""Check availability of project name in current items."""
return project_name in self._items_by_name
def refresh(self):
# Change '_refreshed' state
self._refreshed = True
new_items = []
# Add default item to model if should
if self._add_default_project and self._default_item is None:
item = QtGui.QStandardItem(DEFAULT_PROJECT_LABEL)
item.setData(None, PROJECT_NAME_ROLE)
item.setData(True, PROJECT_IS_ACTIVE_ROLE)
new_items.append(item)
self._default_item = item
project_names = set()
project_docs = get_projects(
inactive=not self._only_active,
fields=["name", "data.active"]
)
for project_doc in project_docs:
project_name = project_doc["name"]
project_names.add(project_name)
if project_name in self._items_by_name:
item = self._items_by_name[project_name]
else:
item = QtGui.QStandardItem(project_name)
self._items_by_name[project_name] = item
new_items.append(item)
is_active = project_doc.get("data", {}).get("active", True)
item.setData(project_name, PROJECT_NAME_ROLE)
item.setData(is_active, PROJECT_IS_ACTIVE_ROLE)
if not is_active:
font = item.font()
font.setItalic(True)
item.setFont(font)
root_item = self.invisibleRootItem()
for project_name in tuple(self._items_by_name.keys()):
if project_name not in project_names:
item = self._items_by_name.pop(project_name)
root_item.removeRow(item.row())
if new_items:
root_item.appendRows(new_items)
def find_project(self, project_name):
"""
Get index of 'project_name' value.
Args:
project_name (str):
Returns:
(QModelIndex)
"""
val = self._items_by_name.get(project_name)
if val:
return self.indexFromItem(val)
class ProjectSortFilterProxy(QtCore.QSortFilterProxyModel):
def __init__(self, *args, **kwargs):
super(ProjectSortFilterProxy, self).__init__(*args, **kwargs)
self._filter_enabled = True
# Disable case sensitivity
self.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)
def lessThan(self, left_index, right_index):
if left_index.data(PROJECT_NAME_ROLE) is None:
return True
if right_index.data(PROJECT_NAME_ROLE) is None:
return False
left_is_active = left_index.data(PROJECT_IS_ACTIVE_ROLE)
right_is_active = right_index.data(PROJECT_IS_ACTIVE_ROLE)
if right_is_active == left_is_active:
return super(ProjectSortFilterProxy, self).lessThan(
left_index, right_index
)
if left_is_active:
return True
return False
def filterAcceptsRow(self, source_row, source_parent):
index = self.sourceModel().index(source_row, 0, source_parent)
string_pattern = self.filterRegularExpression().pattern()
if self._filter_enabled:
result = self._custom_index_filter(index)
if result is not None:
project_name = index.data(PROJECT_NAME_ROLE)
if project_name is None:
return result
return string_pattern.lower() in project_name.lower()
return super(ProjectSortFilterProxy, self).filterAcceptsRow(
source_row, source_parent
)
def _custom_index_filter(self, index):
is_active = bool(index.data(PROJECT_IS_ACTIVE_ROLE))
return is_active
def is_filter_enabled(self):
return self._filter_enabled
def set_filter_enabled(self, value):
self._filter_enabled = value
self.invalidateFilter()