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