From e5ab9a78cb89f38f42d02588afbfdd3075094e1f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 28 Feb 2024 14:28:31 +0100 Subject: [PATCH 01/19] hierarchy model can return FolderItem by path --- .../tools/ayon_utils/models/hierarchy.py | 57 ++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/ayon_utils/models/hierarchy.py b/client/ayon_core/tools/ayon_utils/models/hierarchy.py index 07773dfb78..efd6a57f9a 100644 --- a/client/ayon_core/tools/ayon_utils/models/hierarchy.py +++ b/client/ayon_core/tools/ayon_utils/models/hierarchy.py @@ -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 [] From 7c81a526231249fecc78f53bf273fd2169a210ee Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 28 Feb 2024 14:29:04 +0100 Subject: [PATCH 02/19] task type is stored to Qt model item --- client/ayon_core/tools/ayon_utils/widgets/tasks_widget.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/tools/ayon_utils/widgets/tasks_widget.py b/client/ayon_core/tools/ayon_utils/widgets/tasks_widget.py index 3d6cc47fe3..43759cda68 100644 --- a/client/ayon_core/tools/ayon_utils/widgets/tasks_widget.py +++ b/client/ayon_core/tools/ayon_utils/widgets/tasks_widget.py @@ -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) From 65865c2e8fb9c0dd2c36ee1752ffec56198d5884 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 28 Feb 2024 14:29:31 +0100 Subject: [PATCH 03/19] added more public functions to folder/task widgets --- .../ayon_utils/widgets/folders_widget.py | 38 ++++++++++- .../tools/ayon_utils/widgets/tasks_widget.py | 65 ++++++++++++++++++- 2 files changed, 99 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/tools/ayon_utils/widgets/folders_widget.py b/client/ayon_core/tools/ayon_utils/widgets/folders_widget.py index cf81d1c8ff..e42a5b635c 100644 --- a/client/ayon_core/tools/ayon_utils/widgets/folders_widget.py +++ b/client/ayon_core/tools/ayon_utils/widgets/folders_widget.py @@ -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. diff --git a/client/ayon_core/tools/ayon_utils/widgets/tasks_widget.py b/client/ayon_core/tools/ayon_utils/widgets/tasks_widget.py index 43759cda68..437eef2180 100644 --- a/client/ayon_core/tools/ayon_utils/widgets/tasks_widget.py +++ b/client/ayon_core/tools/ayon_utils/widgets/tasks_widget.py @@ -359,6 +359,64 @@ class TasksWidget(QtWidgets.QWidget): self._tasks_model.refresh() + def get_selected_task_info(self): + _, 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. @@ -396,10 +454,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 @@ -408,7 +467,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() From 175185d177e4a9ba96f411a12db5074127c37eae Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 28 Feb 2024 14:30:59 +0100 Subject: [PATCH 04/19] initial commit --- client/ayon_core/tools/publisher/control.py | 172 +++---------- .../ayon_core/tools/publisher/control_qt.py | 6 - .../{assets_widget.py => assets_dialog.py} | 95 +------ .../widgets/create_context_widgets.py | 239 ++++++++++++++++++ .../tools/publisher/widgets/create_widget.py | 92 +++---- .../tools/publisher/widgets/tasks_widget.py | 181 ------------- .../tools/publisher/widgets/widgets.py | 2 +- .../ayon_core/tools/traypublisher/window.py | 6 +- 8 files changed, 324 insertions(+), 469 deletions(-) rename client/ayon_core/tools/publisher/widgets/{assets_widget.py => assets_dialog.py} (75%) create mode 100644 client/ayon_core/tools/publisher/widgets/create_context_widgets.py diff --git a/client/ayon_core/tools/publisher/control.py b/client/ayon_core/tools/publisher/control.py index 487748afd9..a7c23112b0 100644 --- a/client/ayon_core/tools/publisher/control.py +++ b/client/ayon_core/tools/publisher/control.py @@ -14,12 +14,10 @@ import arrow import pyblish.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 +41,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,101 +68,17 @@ 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 = {} 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 = {} - 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) project_name = self._controller.project_name - full_asset_doc = get_asset_by_id(project_name, asset_doc["_id"]) + full_asset_doc = get_asset_by_name(project_name, asset_name) self._full_asset_docs_by_name[asset_name] = full_asset_doc return copy.deepcopy(self._full_asset_docs_by_name[asset_name]) @@ -1104,18 +1019,6 @@ 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_subset_names(self, asset_name): pass @@ -1499,13 +1402,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 @@ -1738,6 +1650,7 @@ class PublisherController(BasePublisherController): self._resetting_instances = False # Cacher of avalon documents + self._hierarchy_model = HierarchyModel(self) self._asset_docs_cache = AssetDocsCache(self) @property @@ -1794,11 +1707,24 @@ 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) + + # --- Publish specific callbacks --- def get_context_title(self): """Get context title for artist shown at the top of main window.""" @@ -1813,32 +1739,18 @@ 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_subset_names(self, asset_name): 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, asset_name + ) + 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"] @@ -1858,8 +1770,6 @@ class PublisherController(BasePublisherController): # Reset avalon context self._create_context.reset_current_context() - self._asset_docs_cache.reset() - self._reset_plugins() # Publish part must be reset after plugins self._reset_publish() diff --git a/client/ayon_core/tools/publisher/control_qt.py b/client/ayon_core/tools/publisher/control_qt.py index 3d56c08131..53fed7866a 100644 --- a/client/ayon_core/tools/publisher/control_qt.py +++ b/client/ayon_core/tools/publisher/control_qt.py @@ -251,15 +251,9 @@ 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_subset_names(self, asset_name): pass diff --git a/client/ayon_core/tools/publisher/widgets/assets_widget.py b/client/ayon_core/tools/publisher/widgets/assets_dialog.py similarity index 75% rename from client/ayon_core/tools/publisher/widgets/assets_widget.py rename to client/ayon_core/tools/publisher/widgets/assets_dialog.py index 1c5016de99..9b54767624 100644 --- a/client/ayon_core/tools/publisher/widgets/assets_widget.py +++ b/client/ayon_core/tools/publisher/widgets/assets_dialog.py @@ -2,99 +2,13 @@ import collections from qtpy import QtWidgets, QtCore, QtGui +from ayon_core.tools.utils.assets_widget import ( + get_asset_icon, +) 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. @@ -119,7 +33,8 @@ class AssetsHierarchyModel(QtGui.QStandardItemModel): self._items_by_name = {} self._items_by_path = {} self._items_by_asset_id = {} - assets_by_parent_id = self._controller.get_asset_hierarchy() + # assets_by_parent_id = self._controller.get_asset_hierarchy() + assets_by_parent_id = {} items_by_name = {} items_by_path = {} diff --git a/client/ayon_core/tools/publisher/widgets/create_context_widgets.py b/client/ayon_core/tools/publisher/widgets/create_context_widgets.py new file mode 100644 index 0000000000..317d922251 --- /dev/null +++ b/client/ayon_core/tools/publisher/widgets/create_context_widgets.py @@ -0,0 +1,239 @@ +from qtpy import QtWidgets, QtCore, QtGui + +from ayon_core.lib.events import QueuedEventSystem +from ayon_core.tools.utils import PlaceholderLineEdit + +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): + print(folder_id, 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: + def __init__(self, controller): + self._event_system = QueuedEventSystem() + self._controller = controller + self._selection_model = CreateSelectionModel(controller) + + # 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 = {} + print("emit_event", topic, data, source) + 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) + + +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_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 = QtWidgets.QPushButton( + "Go to current context", 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) + tasks_widget = TasksWidget(hierarchy_controller, self) + + 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) + + 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 asset if there is no one + folder_path = self._controller.current_asset_name + self._current_context_btn.setVisible(bool(folder_path)) + + def set_selected_context(self, folder_path, task_name): + self._folders_widget.set_selected_folder_path(folder_path) + self._tasks_widget.set_selected_task(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.set_selected_folder(self._last_folder_id) + self.select_task_name(self._last_selected_task_name) + + def refresh(self): + self._hierarchy_controller.set_selected_project( + self._controller.project_name + ) + self._folders_widget.set_project_name(self._controller.project_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): + # TODO implement + folder_path = self._controller.current_asset_name + task_name = self._controller.current_task_name diff --git a/client/ayon_core/tools/publisher/widgets/create_widget.py b/client/ayon_core/tools/publisher/widgets/create_widget.py index 8eae205882..daaa167460 100644 --- a/client/ayon_core/tools/publisher/widgets/create_widget.py +++ b/client/ayon_core/tools/publisher/widgets/create_widget.py @@ -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, @@ -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.subset_name_input = subset_name_input @@ -324,7 +309,7 @@ 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 @@ -340,31 +325,35 @@ class CreateWidget(QtWidgets.QWidget): return self._context_widget.isEnabled() def _get_asset_name(self): - asset_name = None + 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_asset_name + return folder_path or None + + def _get_folder_id(self): + folder_id = None + if self._context_widget.isEnabled(): + 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() + 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) + 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_asset_name current_task_name = self._controller.current_task_name # Get context before refresh to keep selection of asset and # task widgets - asset_name = self._get_asset_name() + folder_path = self._get_asset_name() task_name = self._get_task_name() # Replace by current context if last loaded context was @@ -388,16 +377,16 @@ 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 @@ -407,18 +396,16 @@ class CreateWidget(QtWidgets.QWidget): # name 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) + self._context_widget.update_current_context_btn() + self._context_widget.set_selected_context(folder_path, task_name) self._invalidate_prereq_deffered() @@ -460,7 +447,7 @@ class CreateWidget(QtWidgets.QWidget): self._on_variant_change() - def _refresh_asset(self): + def _refresh_product_name(self): asset_name = self._get_asset_name() # Skip if asset did not change @@ -545,11 +532,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 +548,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 +594,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 diff --git a/client/ayon_core/tools/publisher/widgets/tasks_widget.py b/client/ayon_core/tools/publisher/widgets/tasks_widget.py index 9a1b22b9a5..37c32ccb97 100644 --- a/client/ayon_core/tools/publisher/widgets/tasks_widget.py +++ b/client/ayon_core/tools/publisher/widgets/tasks_widget.py @@ -1,6 +1,5 @@ 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 @@ -144,183 +143,3 @@ class TasksModel(QtGui.QStandardItemModel): 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() diff --git a/client/ayon_core/tools/publisher/widgets/widgets.py b/client/ayon_core/tools/publisher/widgets/widgets.py index bd5ab250bd..94825432ee 100644 --- a/client/ayon_core/tools/publisher/widgets/widgets.py +++ b/client/ayon_core/tools/publisher/widgets/widgets.py @@ -26,7 +26,7 @@ from ayon_core.pipeline.create import ( TaskNotSetError, ) from .thumbnail_widget import ThumbnailWidget -from .assets_widget import AssetsDialog +from .assets_dialog import AssetsDialog from .tasks_widget import TasksModel from .icons import ( get_pixmap, diff --git a/client/ayon_core/tools/traypublisher/window.py b/client/ayon_core/tools/traypublisher/window.py index 79386d7ea0..07ec826db7 100644 --- a/client/ayon_core/tools/traypublisher/window.py +++ b/client/ayon_core/tools/traypublisher/window.py @@ -30,8 +30,8 @@ class TrayPublisherController(QtPublisherController): def host(self): return self._host - def reset_project_data_cache(self): - self._asset_docs_cache.reset() + def reset_hierarchy_cache(self): + self._hierarchy_model.reset() class TrayPublisherRegistry(JSONSettingRegistry): @@ -248,7 +248,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: From 1027a41e8a7b5d57a2e96af9708d0e783378f8ba Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 28 Feb 2024 15:14:46 +0100 Subject: [PATCH 05/19] fix path calculated from hierarchy item --- client/ayon_core/tools/ayon_utils/models/hierarchy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/ayon_utils/models/hierarchy.py b/client/ayon_core/tools/ayon_utils/models/hierarchy.py index efd6a57f9a..10495cf10b 100644 --- a/client/ayon_core/tools/ayon_utils/models/hierarchy.py +++ b/client/ayon_core/tools/ayon_utils/models/hierarchy.py @@ -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, From c75c9d8d70fc1668ae2afc18cb2d0035bfc8ef62 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 28 Feb 2024 15:15:18 +0100 Subject: [PATCH 06/19] create context selection is using hierarchy model --- client/ayon_core/tools/publisher/control.py | 9 +++ .../widgets/create_context_widgets.py | 71 +++++++++++++++---- .../tools/publisher/widgets/create_widget.py | 9 +-- 3 files changed, 72 insertions(+), 17 deletions(-) diff --git a/client/ayon_core/tools/publisher/control.py b/client/ayon_core/tools/publisher/control.py index c0dcad9fcc..b4101b1f86 100644 --- a/client/ayon_core/tools/publisher/control.py +++ b/client/ayon_core/tools/publisher/control.py @@ -1725,6 +1725,15 @@ class PublisherController(BasePublisherController): 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): + 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 + # --- Publish specific callbacks --- def get_context_title(self): """Get context title for artist shown at the top of main window.""" diff --git a/client/ayon_core/tools/publisher/widgets/create_context_widgets.py b/client/ayon_core/tools/publisher/widgets/create_context_widgets.py index 317d922251..72db38d629 100644 --- a/client/ayon_core/tools/publisher/widgets/create_context_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/create_context_widgets.py @@ -3,6 +3,7 @@ from qtpy import QtWidgets, QtCore, QtGui from ayon_core.lib.events import QueuedEventSystem from ayon_core.tools.utils import PlaceholderLineEdit +from ayon_core.tools.ayon_utils.models import HierarchyExpectedSelection from ayon_core.tools.ayon_utils.widgets import FoldersWidget, TasksWidget @@ -43,7 +44,6 @@ class CreateSelectionModel(object): return self._folder_id def set_selected_folder(self, folder_id): - print(folder_id, self._folder_id) if folder_id == self._folder_id: return @@ -85,7 +85,10 @@ class CreateHierarchyController: def __init__(self, controller): self._event_system = QueuedEventSystem() self._controller = controller - self._selection_model = CreateSelectionModel(controller) + self._selection_model = CreateSelectionModel(self) + self._expected_selection = HierarchyExpectedSelection( + self, handle_project=False + ) # Events system @property @@ -97,7 +100,6 @@ class CreateHierarchyController: if data is None: data = {} - print("emit_event", topic, data, source) self.event_system.emit(topic, data, source) def register_event_callback(self, topic, callback): @@ -124,6 +126,21 @@ class CreateHierarchyController: 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() @@ -134,6 +151,7 @@ class CreateContextWidget(QtWidgets.QWidget): self._controller = controller self._enabled = True + self._last_project_name = None self._last_folder_id = None self._last_selected_task_name = None @@ -155,8 +173,12 @@ class CreateContextWidget(QtWidgets.QWidget): hierarchy_controller = CreateHierarchyController(controller) - folders_widget = FoldersWidget(hierarchy_controller, self) - tasks_widget = TasksWidget(hierarchy_controller, self) + folders_widget = FoldersWidget( + hierarchy_controller, self, handle_expected_selection=True + ) + tasks_widget = TasksWidget( + hierarchy_controller, self, handle_expected_selection=True + ) main_layout = QtWidgets.QVBoxLayout(self) main_layout.setContentsMargins(0, 0, 0, 0) @@ -168,6 +190,7 @@ class CreateContextWidget(QtWidgets.QWidget): 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 @@ -192,9 +215,12 @@ class CreateContextWidget(QtWidgets.QWidget): folder_path = self._controller.current_asset_name self._current_context_btn.setVisible(bool(folder_path)) - def set_selected_context(self, folder_path, task_name): - self._folders_widget.set_selected_folder_path(folder_path) - self._tasks_widget.set_selected_task(task_name) + 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 @@ -215,14 +241,27 @@ class CreateContextWidget(QtWidgets.QWidget): self._clear_selection() elif self._last_selected_task_name is not None: - self.set_selected_folder(self._last_folder_id) - self.select_task_name(self._last_selected_task_name) + 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_asset_name + 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._controller.project_name + 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 ) - self._folders_widget.set_project_name(self._controller.project_name) def _clear_selection(self): self._folders_widget.set_selected_folder(None) @@ -234,6 +273,12 @@ class CreateContextWidget(QtWidgets.QWidget): self.task_changed.emit() def _on_current_context_click(self): - # TODO implement folder_path = self._controller.current_asset_name 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) diff --git a/client/ayon_core/tools/publisher/widgets/create_widget.py b/client/ayon_core/tools/publisher/widgets/create_widget.py index 13abd562ba..4287e9ce07 100644 --- a/client/ayon_core/tools/publisher/widgets/create_widget.py +++ b/client/ayon_core/tools/publisher/widgets/create_widget.py @@ -322,7 +322,7 @@ class CreateWidget(QtWidgets.QWidget): 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): folder_path = None @@ -335,7 +335,7 @@ class CreateWidget(QtWidgets.QWidget): def _get_folder_id(self): folder_id = None - if self._context_widget.isEnabled(): + if self._context_widget.is_enabled(): folder_id = self._context_widget.get_selected_folder_id() return folder_id @@ -352,7 +352,7 @@ class CreateWidget(QtWidgets.QWidget): return task_name def _set_context_enabled(self, enabled): - check_prereq = self._context_widget.isEnabled() != enabled + check_prereq = self._context_widget.is_enabled() != enabled self._context_widget.set_enabled(enabled) if check_prereq: self._invalidate_prereq() @@ -404,8 +404,9 @@ class CreateWidget(QtWidgets.QWidget): # data self._refresh_creators() + 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_path, task_name) + self._context_widget.set_selected_context(folder_id, task_name) self._invalidate_prereq_deffered() From 4044feb98856df439508de494b230234fa3239ee Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 28 Feb 2024 15:54:04 +0100 Subject: [PATCH 07/19] validation of folder paths happens in controller --- client/ayon_core/tools/publisher/control.py | 12 ++++++ .../tools/publisher/widgets/widgets.py | 40 +++++++++---------- 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/client/ayon_core/tools/publisher/control.py b/client/ayon_core/tools/publisher/control.py index b4101b1f86..3beef4030a 100644 --- a/client/ayon_core/tools/publisher/control.py +++ b/client/ayon_core/tools/publisher/control.py @@ -1734,6 +1734,18 @@ class PublisherController(BasePublisherController): return folder_item.entity_id return None + 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.""" diff --git a/client/ayon_core/tools/publisher/widgets/widgets.py b/client/ayon_core/tools/publisher/widgets/widgets.py index 7b7d842d7d..6a3f097fe9 100644 --- a/client/ayon_core/tools/publisher/widgets/widgets.py +++ b/client/ayon_core/tools/publisher/widgets/widgets.py @@ -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 @@ -539,38 +540,33 @@ class AssetsField(BaseClickableFrame): self._name_input.setText(text) self._name_input.end(False) - def set_selected_items(self, asset_names=None): + def set_selected_items(self, folder_paths=None): """Set asset names for selection of instances. Passed asset names are validated and if there are 2 or more different asset names 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) @@ -746,11 +742,11 @@ class TasksCombobox(QtWidgets.QComboBox): """ return list(self._selected_items) - def set_asset_names(self, asset_names): + def set_folder_paths(self, asset_names): """Set asset names for which should show tasks.""" self._ignore_index_change = True - self._model.set_asset_names(asset_names) + self._model.set_folder_paths(asset_names) self._proxy_model.set_filter_empty(False) self._proxy_model.sort(0) @@ -822,7 +818,7 @@ class TasksCombobox(QtWidgets.QComboBox): self._ignore_index_change = True - self._model.set_asset_names(asset_names) + self._model.set_folder_paths(asset_names) self._has_value_changed = False @@ -1265,7 +1261,7 @@ class GlobalAttrsWidget(QtWidgets.QWidget): def _on_asset_change(self): asset_names = self.asset_value_widget.get_selected_items() - self.task_value_widget.set_asset_names(asset_names) + self.task_value_widget.set_folder_paths(asset_names) self._on_value_change() def _on_task_change(self): From de2f48ff54daa1b27b534dc8a607f6d1761397bd Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 28 Feb 2024 16:05:03 +0100 Subject: [PATCH 08/19] fixed change of context on existing instances --- client/ayon_core/tools/publisher/control.py | 71 ++++-- .../ayon_core/tools/publisher/control_qt.py | 10 +- .../tools/publisher/widgets/assets_dialog.py | 224 ++++-------------- .../widgets/create_context_widgets.py | 6 +- .../tools/publisher/widgets/create_widget.py | 42 ++-- .../tools/publisher/widgets/tasks_widget.py | 50 ++-- .../tools/publisher/widgets/widgets.py | 4 +- 7 files changed, 159 insertions(+), 248 deletions(-) diff --git a/client/ayon_core/tools/publisher/control.py b/client/ayon_core/tools/publisher/control.py index 3beef4030a..f59ffa9588 100644 --- a/client/ayon_core/tools/publisher/control.py +++ b/client/ayon_core/tools/publisher/control.py @@ -12,6 +12,7 @@ from abc import ABCMeta, abstractmethod import six import arrow import pyblish.api +import ayon_api from ayon_core.client import ( get_asset_by_name, @@ -70,17 +71,17 @@ class AssetDocsCache: def __init__(self, controller): self._controller = controller - self._full_asset_docs_by_name = {} + self._asset_docs_by_path = {} def reset(self): - self._full_asset_docs_by_name = {} + self._asset_docs_by_path = {} - def get_full_asset_by_name(self, asset_name): - if asset_name not in self._full_asset_docs_by_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_name(project_name, asset_name) - 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: @@ -951,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 @@ -1021,7 +1022,7 @@ class AbstractPublisherController(object): pass @abstractmethod - def get_existing_product_names(self, asset_name): + def get_existing_product_names(self, folder_path): pass @abstractmethod @@ -1665,7 +1666,7 @@ class PublisherController(BasePublisherController): return self._create_context.get_current_project_name() @property - def current_asset_name(self): + def current_folder_path(self): """Current context asset name defined by host. Returns: @@ -1727,6 +1728,8 @@ class PublisherController(BasePublisherController): # 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 ) @@ -1734,6 +1737,34 @@ class PublisherController(BasePublisherController): return folder_item.entity_id return None + def get_task_names_by_folder_paths(self, folder_paths): + # TODO implement model and cache values + if not folder_paths: + return {} + folder_items = self._hierarchy_model.get_folder_items_by_paths( + self.project_name, folder_paths + ) + folder_paths_by_id = { + folder_item.entity_id: folder_item.path + for folder_item in folder_items.values() + if folder_item + } + tasks = ayon_api.get_tasks( + self.project_name, + folder_ids=set(folder_paths_by_id), + fields=["name", "folderId"] + ) + output = { + folder_path: set() + for folder_path in folder_paths + } + for task in tasks: + folder_path = folder_paths_by_id.get(task["folderId"]) + if folder_path: + output[folder_path].add(task["name"]) + + return output + def are_folder_paths_valid(self, folder_paths): if not folder_paths: return True @@ -1761,10 +1792,12 @@ class PublisherController(BasePublisherController): return context_title - 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 folder_item = self._hierarchy_model.get_folder_item_by_path( - project_name, asset_name + project_name, folder_path ) if not folder_item: return None @@ -2006,7 +2039,7 @@ class PublisherController(BasePublisherController): creator_identifier, variant, task_name, - asset_name, + folder_path, instance_id=None ): """Get product name based on passed data. @@ -2016,14 +2049,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] diff --git a/client/ayon_core/tools/publisher/control_qt.py b/client/ayon_core/tools/publisher/control_qt.py index aae550681d..46b1228dc9 100644 --- a/client/ayon_core/tools/publisher/control_qt.py +++ b/client/ayon_core/tools/publisher/control_qt.py @@ -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 @@ -254,7 +254,7 @@ class QtRemotePublishController(BasePublisherController): def get_asset_hierarchy(self): pass - def get_existing_product_names(self, asset_name): + def get_existing_product_names(self, folder_path): pass @property diff --git a/client/ayon_core/tools/publisher/widgets/assets_dialog.py b/client/ayon_core/tools/publisher/widgets/assets_dialog.py index 9b54767624..3609095231 100644 --- a/client/ayon_core/tools/publisher/widgets/assets_dialog.py +++ b/client/ayon_core/tools/publisher/widgets/assets_dialog.py @@ -1,142 +1,48 @@ -import collections - from qtpy import QtWidgets, QtCore, QtGui -from ayon_core.tools.utils.assets_widget import ( - get_asset_icon, -) -from ayon_core.tools.utils import ( - PlaceholderLineEdit, - RecursiveSortFilterProxyModel, -) +from ayon_core.lib.events import QueuedEventSystem +from ayon_core.tools.ayon_utils.widgets import FoldersWidget +from ayon_core.tools.utils import PlaceholderLineEdit -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. - """ +class FoldersDialogController: def __init__(self, controller): - super(AssetsHierarchyModel, self).__init__() + self._event_system = QueuedEventSystem() self._controller = controller - self._items_by_name = {} - self._items_by_path = {} - self._items_by_asset_id = {} + @property + def event_system(self): + return self._event_system - def reset(self): - self.clear() + def emit_event(self, topic, data=None, source=None): + """Use implemented event system to trigger event.""" - self._items_by_name = {} - self._items_by_path = {} - self._items_by_asset_id = {} - # assets_by_parent_id = self._controller.get_asset_hierarchy() - assets_by_parent_id = {} + if data is None: + data = {} + self.event_system.emit(topic, data, source) - 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 + def register_event_callback(self, topic, callback): + self.event_system.add_callback(topic, callback) - 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) + def get_folder_items(self, project_name, sender=None): + return self._controller.get_folder_items(project_name, sender) - 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() + def set_selected_folder(self, folder_id): + pass class AssetsDialog(QtWidgets.QDialog): - """Dialog to select asset for a context of instance.""" + """Dialog to select folder 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) + self.setWindowTitle("Select folder") 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) + folders_controller = FoldersDialogController(controller) + folders_widget = FoldersWidget(folders_controller, self) ok_btn = QtWidgets.QPushButton("OK", self) cancel_btn = QtWidgets.QPushButton("Cancel", self) @@ -148,28 +54,26 @@ class AssetsDialog(QtWidgets.QDialog): layout = QtWidgets.QVBoxLayout(self) layout.addWidget(filter_input, 0) - layout.addWidget(asset_view, 1) + layout.addWidget(folders_widget, 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) + 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._model = model - self._proxy_model = proxy_model + self._folders_widget = folders_widget - self._asset_view = asset_view - - self._selected_asset = None + 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 @@ -194,7 +98,7 @@ class AssetsDialog(QtWidgets.QDialog): self._soft_reset_enabled = True def showEvent(self, event): - """Refresh asset model on show.""" + """Refresh folders widget on show.""" super(AssetsDialog, self).showEvent(event) if self._first_show: self._first_show = False @@ -203,76 +107,44 @@ class AssetsDialog(QtWidgets.QDialog): self.reset(False) def reset(self, force=True): - """Reset asset model.""" + """Reset widget.""" 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) + self._folders_widget.set_project_name(self._controller.project_name) def _on_filter_change(self, text): - """Trigger change of filter of assets.""" - self._proxy_model.setFilterFixedString(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): - index = self._asset_view.currentIndex() - asset_name = None - if index.isValid(): - asset_name = index.data(ASSET_PATH_ROLE) - self._selected_asset = asset_name + self._selected_folder_path = ( + self._folders_widget.get_selected_folder_path() + ) self.done(1) - def set_selected_assets(self, asset_names): - """Change preselected asset before showing the dialog. + 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._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) + 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) - 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 + def get_selected_folder_path(self): + """Get selected folder path.""" + return self._selected_folder_path diff --git a/client/ayon_core/tools/publisher/widgets/create_context_widgets.py b/client/ayon_core/tools/publisher/widgets/create_context_widgets.py index 72db38d629..7107f786c3 100644 --- a/client/ayon_core/tools/publisher/widgets/create_context_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/create_context_widgets.py @@ -212,7 +212,7 @@ class CreateContextWidget(QtWidgets.QWidget): def update_current_context_btn(self): # Hide set current asset if there is no one - folder_path = self._controller.current_asset_name + folder_path = self._controller.current_folder_path self._current_context_btn.setVisible(bool(folder_path)) def set_selected_context(self, folder_id, task_name): @@ -252,7 +252,7 @@ class CreateContextWidget(QtWidgets.QWidget): folder_id = self._last_folder_id task_name = self._last_selected_task_name if folder_id is None: - folder_path = self._controller.current_asset_name + 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( @@ -273,7 +273,7 @@ class CreateContextWidget(QtWidgets.QWidget): self.task_changed.emit() def _on_current_context_click(self): - folder_path = self._controller.current_asset_name + 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( diff --git a/client/ayon_core/tools/publisher/widgets/create_widget.py b/client/ayon_core/tools/publisher/widgets/create_widget.py index 4287e9ce07..728f1937d8 100644 --- a/client/ayon_core/tools/publisher/widgets/create_widget.py +++ b/client/ayon_core/tools/publisher/widgets/create_widget.py @@ -108,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 @@ -314,8 +314,8 @@ class CreateWidget(QtWidgets.QWidget): 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): @@ -324,13 +324,13 @@ class CreateWidget(QtWidgets.QWidget): def _context_change_is_enabled(self): return self._context_widget.is_enabled() - def _get_asset_name(self): + def _get_folder_path(self): folder_path = None if self._context_change_is_enabled(): folder_path = self._context_widget.get_selected_folder_path() if folder_path is None: - folder_path = self.current_asset_name + folder_path = self.current_folder_path return folder_path or None def _get_folder_id(self): @@ -364,12 +364,12 @@ class CreateWidget(QtWidgets.QWidget): self._use_current_context = True def refresh(self): - current_folder_path = 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 # task widgets - folder_path = 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 @@ -427,7 +427,7 @@ 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? prereq_available = False @@ -449,23 +449,25 @@ class CreateWidget(QtWidgets.QWidget): self._on_variant_change() def _refresh_product_name(self): - asset_name = self._get_asset_name() + folder_path = self._get_folder_path() # Skip if asset did not change - if self._asset_name and self._asset_name == asset_name: + 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 @@ -664,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) @@ -777,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() @@ -793,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 diff --git a/client/ayon_core/tools/publisher/widgets/tasks_widget.py b/client/ayon_core/tools/publisher/widgets/tasks_widget.py index 37c32ccb97..179475a3ad 100644 --- a/client/ayon_core/tools/publisher/widgets/tasks_widget.py +++ b/client/ayon_core/tools/publisher/widgets/tasks_widget.py @@ -10,11 +10,11 @@ TASK_ORDER_ROLE = QtCore.Qt.UserRole + 3 class TasksModel(QtGui.QStandardItemModel): """Tasks model. - Task model must have set context of asset documents. + Task model must have set context of folder paths. - 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 + 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: @@ -27,21 +27,21 @@ class TasksModel(QtGui.QStandardItemModel): self._allow_empty_task = allow_empty_task self._controller = controller self._items_by_name = {} - self._asset_names = [] - self._task_names_by_asset_name = {} + self._folder_paths = [] + self._task_names_by_folder_path = {} - def set_asset_names(self, asset_names): + def set_folder_paths(self, folder_paths): """Set assets context.""" - self._asset_names = asset_names + self._folder_paths = folder_paths self.reset() @staticmethod - def get_intersection_of_tasks(task_names_by_asset_name): + def get_intersection_of_tasks(task_names_by_folder_path): """Calculate intersection of task names from passed data. Example: ``` - # Passed `task_names_by_asset_name` + # Passed `task_names_by_folder_path` { "asset_1": ["compositing", "animation"], "asset_2": ["compositing", "editorial"] @@ -54,10 +54,10 @@ class TasksModel(QtGui.QStandardItemModel): ``` Args: - task_names_by_asset_name (dict): Task names in iterable by parent. + task_names_by_folder_path (dict): Task names in iterable by parent. """ tasks = None - for task_names in task_names_by_asset_name.values(): + for task_names in task_names_by_folder_path.values(): if tasks is None: tasks = set(task_names) else: @@ -67,41 +67,43 @@ class TasksModel(QtGui.QStandardItemModel): break return tasks or set() - def is_task_name_valid(self, asset_name, task_name): - """Is task name available for asset. + def is_task_name_valid(self, folder_path, task_name): + """Is task name available for folder. 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 + folder_path (str): Name of asset where should look for task. + task_name (str): Name of task which should be available in folder tasks. """ - if asset_name not in self._task_names_by_asset_name: + 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_asset_name[asset_name] + 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._asset_names: + if not self._folder_paths: self._items_by_name = {} - self._task_names_by_asset_name = {} + self._task_names_by_folder_path = {} self.clear() return - task_names_by_asset_name = ( - self._controller.get_task_names_by_asset_names(self._asset_names) + task_names_by_folder_path = ( + self._controller.get_task_names_by_folder_paths( + self._folder_paths + ) ) - self._task_names_by_asset_name = task_names_by_asset_name + self._task_names_by_folder_path = task_names_by_folder_path new_task_names = self.get_intersection_of_tasks( - task_names_by_asset_name + task_names_by_folder_path ) if self._allow_empty_task: new_task_names.add("") diff --git a/client/ayon_core/tools/publisher/widgets/widgets.py b/client/ayon_core/tools/publisher/widgets/widgets.py index 6a3f097fe9..fb387dba85 100644 --- a/client/ayon_core/tools/publisher/widgets/widgets.py +++ b/client/ayon_core/tools/publisher/widgets/widgets.py @@ -481,7 +481,7 @@ class AssetsField(BaseClickableFrame): if not result: return - asset_name = self._dialog.get_selected_asset() + asset_name = self._dialog.get_selected_folder_path() if asset_name is None: return @@ -495,7 +495,7 @@ class AssetsField(BaseClickableFrame): 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): From 259983768cac374ae721df59694d00ae2a413eb5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 28 Feb 2024 16:11:38 +0100 Subject: [PATCH 09/19] use folder naming in widgets --- .../{assets_dialog.py => folders_dialog.py} | 6 +- .../tools/publisher/widgets/widgets.py | 174 +++++++++--------- 2 files changed, 90 insertions(+), 90 deletions(-) rename client/ayon_core/tools/publisher/widgets/{assets_dialog.py => folders_dialog.py} (97%) diff --git a/client/ayon_core/tools/publisher/widgets/assets_dialog.py b/client/ayon_core/tools/publisher/widgets/folders_dialog.py similarity index 97% rename from client/ayon_core/tools/publisher/widgets/assets_dialog.py rename to client/ayon_core/tools/publisher/widgets/folders_dialog.py index 3609095231..bccbb600a8 100644 --- a/client/ayon_core/tools/publisher/widgets/assets_dialog.py +++ b/client/ayon_core/tools/publisher/widgets/folders_dialog.py @@ -31,11 +31,11 @@ class FoldersDialogController: pass -class AssetsDialog(QtWidgets.QDialog): +class FoldersDialog(QtWidgets.QDialog): """Dialog to select folder for a context of instance.""" def __init__(self, controller, parent): - super(AssetsDialog, self).__init__(parent) + super(FoldersDialog, self).__init__(parent) self.setWindowTitle("Select folder") filter_input = PlaceholderLineEdit(self) @@ -99,7 +99,7 @@ class AssetsDialog(QtWidgets.QDialog): def showEvent(self, event): """Refresh folders widget on show.""" - super(AssetsDialog, self).showEvent(event) + super(FoldersDialog, self).showEvent(event) if self._first_show: self._first_show = False self._on_first_show() diff --git a/client/ayon_core/tools/publisher/widgets/widgets.py b/client/ayon_core/tools/publisher/widgets/widgets.py index fb387dba85..35b4ff46d0 100644 --- a/client/ayon_core/tools/publisher/widgets/widgets.py +++ b/client/ayon_core/tools/publisher/widgets/widgets.py @@ -26,7 +26,7 @@ from ayon_core.pipeline.create import ( TaskNotSetError, ) from .thumbnail_widget import ThumbnailWidget -from .assets_dialog import AssetsDialog +from .folders_dialog import FoldersDialog from .tasks_widget import TasksModel from .icons import ( get_pixmap, @@ -422,20 +422,20 @@ 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) + super(FoldersFields, self).__init__(parent) self.setObjectName("AssetNameInputWidget") # 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") @@ -481,15 +481,15 @@ class AssetsField(BaseClickableFrame): if not result: return - asset_name = self._dialog.get_selected_folder_path() - 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() @@ -499,10 +499,10 @@ class AssetsField(BaseClickableFrame): 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 @@ -521,30 +521,30 @@ 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, folder_paths=None): - """Set asset names for selection of instances. + """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: folder_paths (list, tuple, set, NoneType): List of folder paths. @@ -572,7 +572,7 @@ class AssetsField(BaseClickableFrame): 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): @@ -606,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() @@ -742,23 +742,23 @@ class TasksCombobox(QtWidgets.QComboBox): """ return list(self._selected_items) - def set_folder_paths(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_folder_paths(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: @@ -766,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: @@ -787,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_folder_paths(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) @@ -836,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) @@ -849,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: @@ -864,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 @@ -1102,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() @@ -1133,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 @@ -1157,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, ) @@ -1204,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: @@ -1225,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() @@ -1237,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) @@ -1259,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_folder_paths(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): @@ -1286,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() @@ -1295,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: @@ -1303,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) From 9d4b98be45c094767784e40bc3cb419d4003591e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 28 Feb 2024 16:16:38 +0100 Subject: [PATCH 10/19] moved tasks model around --- .../{tasks_widget.py => tasks_model.py} | 28 ++++++------------- .../tools/publisher/widgets/widgets.py | 2 +- 2 files changed, 10 insertions(+), 20 deletions(-) rename client/ayon_core/tools/publisher/widgets/{tasks_widget.py => tasks_model.py} (84%) diff --git a/client/ayon_core/tools/publisher/widgets/tasks_widget.py b/client/ayon_core/tools/publisher/widgets/tasks_model.py similarity index 84% rename from client/ayon_core/tools/publisher/widgets/tasks_widget.py rename to client/ayon_core/tools/publisher/widgets/tasks_model.py index 179475a3ad..8f00dc37a2 100644 --- a/client/ayon_core/tools/publisher/widgets/tasks_widget.py +++ b/client/ayon_core/tools/publisher/widgets/tasks_model.py @@ -31,7 +31,7 @@ class TasksModel(QtGui.QStandardItemModel): self._task_names_by_folder_path = {} def set_folder_paths(self, folder_paths): - """Set assets context.""" + """Set folders context.""" self._folder_paths = folder_paths self.reset() @@ -43,8 +43,8 @@ class TasksModel(QtGui.QStandardItemModel): ``` # Passed `task_names_by_folder_path` { - "asset_1": ["compositing", "animation"], - "asset_2": ["compositing", "editorial"] + "/folder_1": ["compositing", "animation"], + "/folder_2": ["compositing", "editorial"] } ``` Result: @@ -70,8 +70,11 @@ class TasksModel(QtGui.QStandardItemModel): 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): Name of asset where should look for task. + folder_path (str): Fodler path where should look for task. task_name (str): Name of task which should be available in folder tasks. """ @@ -91,7 +94,8 @@ class TasksModel(QtGui.QStandardItemModel): if not self._folder_paths: self._items_by_name = {} self._task_names_by_folder_path = {} - self.clear() + root_item = self.invisibleRootItem() + root_item.removeRows(0, self.rowCount()) return task_names_by_folder_path = ( @@ -131,17 +135,3 @@ class TasksModel(QtGui.QStandardItemModel): 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) - diff --git a/client/ayon_core/tools/publisher/widgets/widgets.py b/client/ayon_core/tools/publisher/widgets/widgets.py index 35b4ff46d0..efc47555d5 100644 --- a/client/ayon_core/tools/publisher/widgets/widgets.py +++ b/client/ayon_core/tools/publisher/widgets/widgets.py @@ -27,7 +27,7 @@ from ayon_core.pipeline.create import ( ) from .thumbnail_widget import ThumbnailWidget from .folders_dialog import FoldersDialog -from .tasks_widget import TasksModel +from .tasks_model import TasksModel from .icons import ( get_pixmap, get_icon_path From 9b0dcce5a82d9155b5b735b17431edce63d9f88a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 28 Feb 2024 16:21:51 +0100 Subject: [PATCH 11/19] rename 'AssetNameInputWidget' to 'FolderPathInputWidget' --- client/ayon_core/style/style.css | 12 ++++++------ client/ayon_core/tools/publisher/widgets/widgets.py | 2 +- .../sceneinventory/switch_dialog/folders_input.py | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/client/ayon_core/style/style.css b/client/ayon_core/style/style.css index ce467663fc..e6d427a136 100644 --- a/client/ayon_core/style/style.css +++ b/client/ayon_core/style/style.css @@ -1138,13 +1138,13 @@ 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; } @@ -1165,17 +1165,17 @@ ValidationArtistMessage QLabel { 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"], #AssetNameInputButton[state="invalid"] { border-color: {color:publisher:error}; } diff --git a/client/ayon_core/tools/publisher/widgets/widgets.py b/client/ayon_core/tools/publisher/widgets/widgets.py index efc47555d5..dda204fbfd 100644 --- a/client/ayon_core/tools/publisher/widgets/widgets.py +++ b/client/ayon_core/tools/publisher/widgets/widgets.py @@ -431,7 +431,7 @@ class FoldersFields(BaseClickableFrame): def __init__(self, controller, parent): super(FoldersFields, self).__init__(parent) - self.setObjectName("AssetNameInputWidget") + self.setObjectName("FolderPathInputWidget") # Don't use 'self' for parent! # - this widget has specific styles diff --git a/client/ayon_core/tools/sceneinventory/switch_dialog/folders_input.py b/client/ayon_core/tools/sceneinventory/switch_dialog/folders_input.py index 2358a82a7f..8f6a12e8e3 100644 --- a/client/ayon_core/tools/sceneinventory/switch_dialog/folders_input.py +++ b/client/ayon_core/tools/sceneinventory/switch_dialog/folders_input.py @@ -196,7 +196,7 @@ 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 From 284d395b01f8de84591e86dbcd750eedce1a566e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 28 Feb 2024 16:22:47 +0100 Subject: [PATCH 12/19] rename 'AssetNameInput' and 'AssetNameInputButton' --- client/ayon_core/style/style.css | 6 +++--- client/ayon_core/tools/publisher/widgets/widgets.py | 4 ++-- .../tools/sceneinventory/switch_dialog/folders_input.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/style/style.css b/client/ayon_core/style/style.css index e6d427a136..fcc76b0bff 100644 --- a/client/ayon_core/style/style.css +++ b/client/ayon_core/style/style.css @@ -1148,7 +1148,7 @@ ValidationArtistMessage QLabel { background: transparent; } -#AssetNameInputButton { +#FolderPathInputButton { border-bottom-left-radius: 0px; border-top-left-radius: 0px; padding: 0px; @@ -1159,7 +1159,7 @@ ValidationArtistMessage QLabel { border-bottom: none; } -#AssetNameInput { +#FolderPathInput { border-bottom-right-radius: 0px; border-top-right-radius: 0px; border: none; @@ -1175,7 +1175,7 @@ ValidationArtistMessage QLabel { background: {color:bg-inputs-disabled}; } -#TasksCombobox[state="invalid"], #FolderPathInputWidget[state="invalid"], #AssetNameInputButton[state="invalid"] { +#TasksCombobox[state="invalid"], #FolderPathInputWidget[state="invalid"], #FolderPathInputButton[state="invalid"] { border-color: {color:publisher:error}; } diff --git a/client/ayon_core/tools/publisher/widgets/widgets.py b/client/ayon_core/tools/publisher/widgets/widgets.py index dda204fbfd..4005cf2c84 100644 --- a/client/ayon_core/tools/publisher/widgets/widgets.py +++ b/client/ayon_core/tools/publisher/widgets/widgets.py @@ -438,13 +438,13 @@ class FoldersFields(BaseClickableFrame): 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) diff --git a/client/ayon_core/tools/sceneinventory/switch_dialog/folders_input.py b/client/ayon_core/tools/sceneinventory/switch_dialog/folders_input.py index 8f6a12e8e3..e46c28474f 100644 --- a/client/ayon_core/tools/sceneinventory/switch_dialog/folders_input.py +++ b/client/ayon_core/tools/sceneinventory/switch_dialog/folders_input.py @@ -203,12 +203,12 @@ class FoldersField(BaseClickableFrame): 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) From 93ad1ad9a3b5cb19c43c0a13209202d95df7f26e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 28 Feb 2024 16:23:22 +0100 Subject: [PATCH 13/19] more folder naming --- client/ayon_core/tools/publisher/control.py | 10 +++++----- client/ayon_core/tools/publisher/control_qt.py | 9 +++------ .../publisher/widgets/create_context_widgets.py | 2 +- .../tools/publisher/widgets/create_widget.py | 14 +++++++------- 4 files changed, 16 insertions(+), 19 deletions(-) diff --git a/client/ayon_core/tools/publisher/control.py b/client/ayon_core/tools/publisher/control.py index f59ffa9588..6408056c04 100644 --- a/client/ayon_core/tools/publisher/control.py +++ b/client/ayon_core/tools/publisher/control.py @@ -1062,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. @@ -1072,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. """ @@ -1091,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. """ @@ -1667,10 +1667,10 @@ class PublisherController(BasePublisherController): @property def current_folder_path(self): - """Current context asset name defined by host. + """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() diff --git a/client/ayon_core/tools/publisher/control_qt.py b/client/ayon_core/tools/publisher/control_qt.py index 46b1228dc9..ee08899cac 100644 --- a/client/ayon_core/tools/publisher/control_qt.py +++ b/client/ayon_core/tools/publisher/control_qt.py @@ -251,9 +251,6 @@ class QtRemotePublishController(BasePublisherController): pass - def get_asset_hierarchy(self): - pass - def get_existing_product_names(self, folder_path): pass @@ -299,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. @@ -309,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. """ @@ -328,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. """ diff --git a/client/ayon_core/tools/publisher/widgets/create_context_widgets.py b/client/ayon_core/tools/publisher/widgets/create_context_widgets.py index 7107f786c3..dc80f47631 100644 --- a/client/ayon_core/tools/publisher/widgets/create_context_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/create_context_widgets.py @@ -211,7 +211,7 @@ class CreateContextWidget(QtWidgets.QWidget): return self._tasks_widget.get_selected_task_type() def update_current_context_btn(self): - # Hide set current asset if there is no one + # Hide set current folder if there is no one folder_path = self._controller.current_folder_path self._current_context_btn.setVisible(bool(folder_path)) diff --git a/client/ayon_core/tools/publisher/widgets/create_widget.py b/client/ayon_core/tools/publisher/widgets/create_widget.py index 728f1937d8..2e4ca34138 100644 --- a/client/ayon_core/tools/publisher/widgets/create_widget.py +++ b/client/ayon_core/tools/publisher/widgets/create_widget.py @@ -342,7 +342,7 @@ class CreateWidget(QtWidgets.QWidget): 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 + # 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() @@ -367,7 +367,7 @@ class CreateWidget(QtWidgets.QWidget): 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 folder_path = self._get_folder_path() task_name = self._get_task_name() @@ -392,8 +392,8 @@ class CreateWidget(QtWidgets.QWidget): 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) # Refresh data before update of creators @@ -429,7 +429,7 @@ class CreateWidget(QtWidgets.QWidget): self._context_change_is_enabled() 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") @@ -451,7 +451,7 @@ class CreateWidget(QtWidgets.QWidget): def _refresh_product_name(self): folder_path = self._get_folder_path() - # Skip if asset did not change + # Skip if folder did not change if self._folder_path and self._folder_path == folder_path: return @@ -686,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: From 4b901af4fc6e0022163250808132feaba32216e9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 28 Feb 2024 16:28:50 +0100 Subject: [PATCH 14/19] folders widget are deselectable --- .../ayon_core/tools/publisher/widgets/create_context_widgets.py | 2 ++ client/ayon_core/tools/publisher/widgets/folders_dialog.py | 1 + 2 files changed, 3 insertions(+) diff --git a/client/ayon_core/tools/publisher/widgets/create_context_widgets.py b/client/ayon_core/tools/publisher/widgets/create_context_widgets.py index dc80f47631..e17d3d1603 100644 --- a/client/ayon_core/tools/publisher/widgets/create_context_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/create_context_widgets.py @@ -176,6 +176,8 @@ class CreateContextWidget(QtWidgets.QWidget): 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 ) diff --git a/client/ayon_core/tools/publisher/widgets/folders_dialog.py b/client/ayon_core/tools/publisher/widgets/folders_dialog.py index bccbb600a8..8f93264b2e 100644 --- a/client/ayon_core/tools/publisher/widgets/folders_dialog.py +++ b/client/ayon_core/tools/publisher/widgets/folders_dialog.py @@ -43,6 +43,7 @@ class FoldersDialog(QtWidgets.QDialog): 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) From ac33a8dd41c1bf636c31b39144ece52fe6c24c68 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 28 Feb 2024 16:34:12 +0100 Subject: [PATCH 15/19] use hierarchy model to get task names --- client/ayon_core/tools/publisher/control.py | 24 ++++++++------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/client/ayon_core/tools/publisher/control.py b/client/ayon_core/tools/publisher/control.py index 6408056c04..6407a25c37 100644 --- a/client/ayon_core/tools/publisher/control.py +++ b/client/ayon_core/tools/publisher/control.py @@ -1738,30 +1738,24 @@ class PublisherController(BasePublisherController): return None def get_task_names_by_folder_paths(self, folder_paths): - # TODO implement model and cache values if not folder_paths: return {} folder_items = self._hierarchy_model.get_folder_items_by_paths( self.project_name, folder_paths ) - folder_paths_by_id = { - folder_item.entity_id: folder_item.path - for folder_item in folder_items.values() - if folder_item - } - tasks = ayon_api.get_tasks( - self.project_name, - folder_ids=set(folder_paths_by_id), - fields=["name", "folderId"] - ) output = { folder_path: set() for folder_path in folder_paths } - for task in tasks: - folder_path = folder_paths_by_id.get(task["folderId"]) - if folder_path: - output[folder_path].add(task["name"]) + 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 From 46846e055147948a6854606ac2cda0acaaf7dedd Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 28 Feb 2024 16:37:03 +0100 Subject: [PATCH 16/19] reset asset docs cache too --- client/ayon_core/tools/traypublisher/window.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/tools/traypublisher/window.py b/client/ayon_core/tools/traypublisher/window.py index 07ec826db7..78517b4185 100644 --- a/client/ayon_core/tools/traypublisher/window.py +++ b/client/ayon_core/tools/traypublisher/window.py @@ -32,6 +32,7 @@ class TrayPublisherController(QtPublisherController): def reset_hierarchy_cache(self): self._hierarchy_model.reset() + self._asset_docs_cache.reset() class TrayPublisherRegistry(JSONSettingRegistry): From bcda4aa985fd92cea6235e700af95bdefa5934d4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 28 Feb 2024 16:38:47 +0100 Subject: [PATCH 17/19] reset hierarchy model and asset docs cache --- client/ayon_core/tools/publisher/control.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/ayon_core/tools/publisher/control.py b/client/ayon_core/tools/publisher/control.py index 6407a25c37..712142f662 100644 --- a/client/ayon_core/tools/publisher/control.py +++ b/client/ayon_core/tools/publisher/control.py @@ -1819,6 +1819,9 @@ class PublisherController(BasePublisherController): # Reset avalon context self._create_context.reset_current_context() + self._hierarchy_model.reset() + self._asset_docs_cache.reset() + self._reset_plugins() # Publish part must be reset after plugins self._reset_publish() From dbab27483382264ef106729cc098b17c106ef6a4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 28 Feb 2024 23:11:00 +0100 Subject: [PATCH 18/19] use 'GoToCurrentButton' --- .../tools/publisher/widgets/create_context_widgets.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/tools/publisher/widgets/create_context_widgets.py b/client/ayon_core/tools/publisher/widgets/create_context_widgets.py index e17d3d1603..cd3d1103e3 100644 --- a/client/ayon_core/tools/publisher/widgets/create_context_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/create_context_widgets.py @@ -1,7 +1,7 @@ from qtpy import QtWidgets, QtCore, QtGui from ayon_core.lib.events import QueuedEventSystem -from ayon_core.tools.utils import PlaceholderLineEdit +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 @@ -160,9 +160,7 @@ class CreateContextWidget(QtWidgets.QWidget): folder_filter_input = PlaceholderLineEdit(headers_widget) folder_filter_input.setPlaceholderText("Filter folders..") - current_context_btn = QtWidgets.QPushButton( - "Go to current context", headers_widget - ) + current_context_btn = GoToCurrentButton(headers_widget) current_context_btn.setToolTip("Go to current context") current_context_btn.setVisible(False) From 123d2b851c206db8fa8a18394bfd7a371fbf2864 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 29 Feb 2024 11:53:12 +0100 Subject: [PATCH 19/19] added some docstrings --- .../tools/ayon_utils/widgets/tasks_widget.py | 14 ++++++++++++++ .../publisher/widgets/create_context_widgets.py | 12 ++++++++++++ 2 files changed, 26 insertions(+) diff --git a/client/ayon_core/tools/ayon_utils/widgets/tasks_widget.py b/client/ayon_core/tools/ayon_utils/widgets/tasks_widget.py index 437eef2180..b273d83fa6 100644 --- a/client/ayon_core/tools/ayon_utils/widgets/tasks_widget.py +++ b/client/ayon_core/tools/ayon_utils/widgets/tasks_widget.py @@ -360,6 +360,20 @@ 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, diff --git a/client/ayon_core/tools/publisher/widgets/create_context_widgets.py b/client/ayon_core/tools/publisher/widgets/create_context_widgets.py index cd3d1103e3..d65a2ace8d 100644 --- a/client/ayon_core/tools/publisher/widgets/create_context_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/create_context_widgets.py @@ -82,6 +82,18 @@ class CreateSelectionModel(object): 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