mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 12:54:40 +01:00
Merge pull request #132 from ynput/enhancement/publisher-using-hierarchy-model
Publisher: Use hierarchy model
This commit is contained in:
commit
29cc3d97e5
15 changed files with 1061 additions and 1069 deletions
|
|
@ -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};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 []
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
"""
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
151
client/ayon_core/tools/publisher/widgets/folders_dialog.py
Normal file
151
client/ayon_core/tools/publisher/widgets/folders_dialog.py
Normal 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
|
||||
137
client/ayon_core/tools/publisher/widgets/tasks_model.py
Normal file
137
client/ayon_core/tools/publisher/widgets/tasks_model.py
Normal 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)
|
||||
|
|
@ -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()
|
||||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -30,7 +30,8 @@ class TrayPublisherController(QtPublisherController):
|
|||
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()
|
||||
|
||||
|
||||
|
|
@ -248,7 +249,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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue