diff --git a/client/ayon_core/tools/loader/ui/product_types_widget.py b/client/ayon_core/tools/loader/ui/product_types_widget.py index 9b1bf6326f..ff2a70a7fa 100644 --- a/client/ayon_core/tools/loader/ui/product_types_widget.py +++ b/client/ayon_core/tools/loader/ui/product_types_widget.py @@ -1,57 +1,66 @@ from qtpy import QtWidgets, QtGui, QtCore -from ayon_core.tools.utils import get_qt_icon +from ._multicombobox import ( + CustomPaintMultiselectComboBox, + BaseQtModel, +) + +STATUS_ITEM_TYPE = 0 +SELECT_ALL_TYPE = 1 +DESELECT_ALL_TYPE = 2 +SWAP_STATE_TYPE = 3 PRODUCT_TYPE_ROLE = QtCore.Qt.UserRole + 1 +ITEM_TYPE_ROLE = QtCore.Qt.UserRole + 2 +ITEM_SUBTYPE_ROLE = QtCore.Qt.UserRole + 3 -class ProductTypesQtModel(QtGui.QStandardItemModel): - refreshed = QtCore.Signal() - filter_changed = QtCore.Signal() - +class ProductTypesQtModel(BaseQtModel): def __init__(self, controller): - super(ProductTypesQtModel, self).__init__() - self._controller = controller - self._reset_filters_on_refresh = True self._refreshing = False self._bulk_change = False - self._last_project = None self._items_by_name = {} - controller.register_event_callback( - "controller.reset.finished", - self._on_controller_reset_finish, + super().__init__( + item_type_role=ITEM_TYPE_ROLE, + item_subtype_role=ITEM_SUBTYPE_ROLE, + empty_values_label="No product types...", + controller=controller, ) def is_refreshing(self): return self._refreshing - def get_filter_info(self): - """Product types filtering info. - - Returns: - dict[str, bool]: Filtering value by product type name. False value - means to hide product type. - """ - - return { - name: item.checkState() == QtCore.Qt.Checked - for name, item in self._items_by_name.items() - } - def refresh(self, project_name): self._refreshing = True + super().refresh(project_name) + + self._reset_filters_on_refresh = False + self._refreshing = False + + def reset_product_types_filter_on_refresh(self): + self._reset_filters_on_refresh = True + + def _get_standard_items(self) -> list[QtGui.QStandardItem]: + return list(self._items_by_name.values()) + + def _clear_standard_items(self): + self._items_by_name.clear() + + def _prepare_new_value_items(self, project_name: str, _: bool) -> tuple[ + list[QtGui.QStandardItem], list[QtGui.QStandardItem] + ]: product_type_items = self._controller.get_product_type_items( project_name) self._last_project = project_name - items_to_remove = set(self._items_by_name.keys()) - new_items = [] + names_to_remove = set(self._items_by_name.keys()) + items = [] items_filter_required = {} for product_type_item in product_type_items: name = product_type_item.name - items_to_remove.discard(name) + names_to_remove.discard(name) item = self._items_by_name.get(name) # Apply filter to new items or if filters reset is requested filter_required = self._reset_filters_on_refresh @@ -61,15 +70,13 @@ class ProductTypesQtModel(QtGui.QStandardItemModel): item.setData(name, PRODUCT_TYPE_ROLE) item.setEditable(False) item.setCheckable(True) - new_items.append(item) self._items_by_name[name] = item + items.append(item) + if filter_required: items_filter_required[name] = item - icon = get_qt_icon(product_type_item.icon) - item.setData(icon, QtCore.Qt.DecorationRole) - if items_filter_required: product_types_filter = self._controller.get_product_types_filter() for product_type, item in items_filter_required.items(): @@ -77,180 +84,77 @@ class ProductTypesQtModel(QtGui.QStandardItemModel): int(product_type in product_types_filter.product_types) + int(product_types_filter.is_allow_list) ) - state = ( + item.setCheckState( QtCore.Qt.Checked if matching % 2 == 0 else QtCore.Qt.Unchecked ) - item.setCheckState(state) - root_item = self.invisibleRootItem() - if new_items: - root_item.appendRows(new_items) + items_to_remove = [] + for name in names_to_remove: + items_to_remove.append( + self._items_by_name.pop(name) + ) - for name in items_to_remove: - item = self._items_by_name.pop(name) - root_item.removeRow(item.row()) + # Uncheck all if all are checked (same result) + if all( + item.checkState() == QtCore.Qt.Checked + for item in items + ): + for item in items: + item.setCheckState(QtCore.Qt.Unchecked) - self._reset_filters_on_refresh = False - self._refreshing = False - self.refreshed.emit() - - def reset_product_types_filter_on_refresh(self): - self._reset_filters_on_refresh = True - - def setData(self, index, value, role=None): - checkstate_changed = False - if role is None: - role = QtCore.Qt.EditRole - elif role == QtCore.Qt.CheckStateRole: - checkstate_changed = True - output = super(ProductTypesQtModel, self).setData(index, value, role) - if checkstate_changed and not self._bulk_change: - self.filter_changed.emit() - return output - - def change_state_for_all(self, checked): - if self._items_by_name: - self.change_states(checked, self._items_by_name.keys()) - - def change_states(self, checked, product_types): - product_types = set(product_types) - if not product_types: - return - - if checked is None: - state = None - elif checked: - state = QtCore.Qt.Checked - else: - state = QtCore.Qt.Unchecked - - self._bulk_change = True - - changed = False - for product_type in product_types: - item = self._items_by_name.get(product_type) - if item is None: - continue - new_state = state - item_checkstate = item.checkState() - if new_state is None: - if item_checkstate == QtCore.Qt.Checked: - new_state = QtCore.Qt.Unchecked - else: - new_state = QtCore.Qt.Checked - elif item_checkstate == new_state: - continue - changed = True - item.setCheckState(new_state) - - self._bulk_change = False - - if changed: - self.filter_changed.emit() - - def _on_controller_reset_finish(self): - self.refresh(self._last_project) + return items, items_to_remove -class ProductTypesView(QtWidgets.QListView): - filter_changed = QtCore.Signal() - +class ProductTypesCombobox(CustomPaintMultiselectComboBox): def __init__(self, controller, parent): - super(ProductTypesView, self).__init__(parent) - - self.setSelectionMode( - QtWidgets.QAbstractItemView.ExtendedSelection + self._controller = controller + model = ProductTypesQtModel(controller) + super().__init__( + PRODUCT_TYPE_ROLE, + PRODUCT_TYPE_ROLE, + QtCore.Qt.ForegroundRole, + QtCore.Qt.DecorationRole, + item_type_role=ITEM_TYPE_ROLE, + model=model, + parent=parent ) - self.setAlternatingRowColors(True) - self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - - product_types_model = ProductTypesQtModel(controller) - product_types_proxy_model = QtCore.QSortFilterProxyModel() - product_types_proxy_model.setSourceModel(product_types_model) - - self.setModel(product_types_proxy_model) - - product_types_model.refreshed.connect(self._on_refresh_finished) - product_types_model.filter_changed.connect(self._on_filter_change) - self.customContextMenuRequested.connect(self._on_context_menu) + self.set_placeholder_text("Product types filter...") + self._model = model + self._last_project_name = None + self._fully_disabled_filter = False controller.register_event_callback( "selection.project.changed", self._on_project_change ) - - self._controller = controller - self._refresh_product_types_filter = False - - self._product_types_model = product_types_model - self._product_types_proxy_model = product_types_proxy_model - - def get_filter_info(self): - return self._product_types_model.get_filter_info() + controller.register_event_callback( + "projects.refresh.finished", + self._on_projects_refresh + ) + self.setToolTip("Product types filter") + self.value_changed.connect( + self._on_product_type_filter_change + ) def reset_product_types_filter_on_refresh(self): - self._product_types_model.reset_product_types_filter_on_refresh() + self._model.reset_product_types_filter_on_refresh() + + def _on_product_type_filter_change(self): + lines = ["Product types filter"] + for item in self.get_value_info(): + status_name, enabled = item + lines.append(f"{'✔' if enabled else '☐'} {status_name}") + + self.setToolTip("\n".join(lines)) def _on_project_change(self, event): project_name = event["project_name"] - self._product_types_model.refresh(project_name) + self._last_project_name = project_name + self._model.refresh(project_name) - def _on_refresh_finished(self): - # Apply product types filter on first show - self.filter_changed.emit() - - def _on_filter_change(self): - if not self._product_types_model.is_refreshing(): - self.filter_changed.emit() - - def _change_selection_state(self, checkstate): - selection_model = self.selectionModel() - product_types = { - index.data(PRODUCT_TYPE_ROLE) - for index in selection_model.selectedIndexes() - } - product_types.discard(None) - self._product_types_model.change_states(checkstate, product_types) - - def _on_enable_all(self): - self._product_types_model.change_state_for_all(True) - - def _on_disable_all(self): - self._product_types_model.change_state_for_all(False) - - def _on_context_menu(self, pos): - menu = QtWidgets.QMenu(self) - - # Add enable all action - action_check_all = QtWidgets.QAction(menu) - action_check_all.setText("Enable All") - action_check_all.triggered.connect(self._on_enable_all) - # Add disable all action - action_uncheck_all = QtWidgets.QAction(menu) - action_uncheck_all.setText("Disable All") - action_uncheck_all.triggered.connect(self._on_disable_all) - - menu.addAction(action_check_all) - menu.addAction(action_uncheck_all) - - # Get mouse position - global_pos = self.viewport().mapToGlobal(pos) - menu.exec_(global_pos) - - def event(self, event): - if event.type() == QtCore.QEvent.KeyPress: - if event.key() == QtCore.Qt.Key_Space: - self._change_selection_state(None) - return True - - if event.key() == QtCore.Qt.Key_Backspace: - self._change_selection_state(False) - return True - - if event.key() == QtCore.Qt.Key_Return: - self._change_selection_state(True) - return True - - return super(ProductTypesView, self).event(event) + def _on_projects_refresh(self): + if self._last_project_name: + self._model.refresh(self._last_project_name) + self._on_product_type_filter_change() diff --git a/client/ayon_core/tools/loader/ui/products_widget.py b/client/ayon_core/tools/loader/ui/products_widget.py index 748a1b5fb8..41a49b8ae1 100644 --- a/client/ayon_core/tools/loader/ui/products_widget.py +++ b/client/ayon_core/tools/loader/ui/products_widget.py @@ -1,4 +1,6 @@ +from __future__ import annotations import collections +from typing import Optional from qtpy import QtWidgets, QtCore @@ -36,7 +38,7 @@ class ProductsProxyModel(RecursiveSortFilterProxyModel): def __init__(self, parent=None): super().__init__(parent) - self._product_type_filters = {} + self._product_type_filters = None self._statuses_filter = None self._ascending_sort = True @@ -46,6 +48,8 @@ class ProductsProxyModel(RecursiveSortFilterProxyModel): return set(self._statuses_filter) def set_product_type_filters(self, product_type_filters): + if self._product_type_filters == product_type_filters: + return self._product_type_filters = product_type_filters self.invalidateFilter() @@ -59,28 +63,32 @@ class ProductsProxyModel(RecursiveSortFilterProxyModel): source_model = self.sourceModel() index = source_model.index(source_row, 0, source_parent) - product_types_s = source_model.data(index, PRODUCT_TYPE_ROLE) - product_types = [] - if product_types_s: - product_types = product_types_s.split("|") - - for product_type in product_types: - if not self._product_type_filters.get(product_type, True): - return False - - if not self._accept_row_by_statuses(index): + if not self._accept_row_by_role_value( + index, self._product_type_filters, PRODUCT_TYPE_ROLE + ): return False + + if not self._accept_row_by_role_value( + index, self._statuses_filter, STATUS_NAME_FILTER_ROLE + ): + return False + return super().filterAcceptsRow(source_row, source_parent) - def _accept_row_by_statuses(self, index): - if self._statuses_filter is None: + def _accept_row_by_role_value( + self, + index: QtCore.QModelIndex, + filter_value: Optional[set[str]], + role: int + ): + if filter_value is None: return True - if not self._statuses_filter: + if not filter_value: return False - status_s = index.data(STATUS_NAME_FILTER_ROLE) + status_s = index.data(role) for status in status_s.split("|"): - if status in self._statuses_filter: + if status in filter_value: return True return False @@ -120,7 +128,7 @@ class ProductsWidget(QtWidgets.QWidget): 90, # Product type 130, # Folder label 60, # Version - 100, # Status + 100, # Status 125, # Time 75, # Author 75, # Frames diff --git a/client/ayon_core/tools/loader/ui/window.py b/client/ayon_core/tools/loader/ui/window.py index 31c9908b23..31d88d4ed7 100644 --- a/client/ayon_core/tools/loader/ui/window.py +++ b/client/ayon_core/tools/loader/ui/window.py @@ -15,7 +15,7 @@ from ayon_core.tools.loader.control import LoaderController from .folders_widget import LoaderFoldersWidget from .products_widget import ProductsWidget -from .product_types_widget import ProductTypesView +from .product_types_widget import ProductTypesCombobox from .product_group_dialog import ProductGroupDialog from .info_widget import InfoWidget from .repres_widget import RepresentationsWidget @@ -164,8 +164,6 @@ class LoaderWindow(QtWidgets.QWidget): folders_widget = LoaderFoldersWidget(controller, context_widget) - product_types_widget = ProductTypesView(controller, context_splitter) - context_layout = QtWidgets.QVBoxLayout(context_widget) context_layout.setContentsMargins(0, 0, 0, 0) context_layout.addWidget(context_top_widget, 0) @@ -173,7 +171,6 @@ class LoaderWindow(QtWidgets.QWidget): context_layout.addWidget(folders_widget, 1) context_splitter.addWidget(context_widget) - context_splitter.addWidget(product_types_widget) context_splitter.setStretchFactor(0, 65) context_splitter.setStretchFactor(1, 35) @@ -185,6 +182,10 @@ class LoaderWindow(QtWidgets.QWidget): products_filter_input = PlaceholderLineEdit(products_inputs_widget) products_filter_input.setPlaceholderText("Product name filter...") + product_types_filter_combo = ProductTypesCombobox( + controller, products_inputs_widget + ) + product_status_filter_combo = StatusesCombobox(controller, self) product_group_checkbox = QtWidgets.QCheckBox( @@ -196,6 +197,7 @@ class LoaderWindow(QtWidgets.QWidget): products_inputs_layout = QtWidgets.QHBoxLayout(products_inputs_widget) products_inputs_layout.setContentsMargins(0, 0, 0, 0) products_inputs_layout.addWidget(products_filter_input, 1) + products_inputs_layout.addWidget(product_types_filter_combo, 1) products_inputs_layout.addWidget(product_status_filter_combo, 1) products_inputs_layout.addWidget(product_group_checkbox, 0) @@ -244,12 +246,12 @@ class LoaderWindow(QtWidgets.QWidget): folders_filter_input.textChanged.connect( self._on_folder_filter_change ) - product_types_widget.filter_changed.connect( - self._on_product_type_filter_change - ) products_filter_input.textChanged.connect( self._on_product_filter_change ) + product_types_filter_combo.value_changed.connect( + self._on_product_type_filter_change + ) product_status_filter_combo.value_changed.connect( self._on_status_filter_change ) @@ -304,9 +306,8 @@ class LoaderWindow(QtWidgets.QWidget): self._folders_filter_input = folders_filter_input self._folders_widget = folders_widget - self._product_types_widget = product_types_widget - self._products_filter_input = products_filter_input + self._product_types_filter_combo = product_types_filter_combo self._product_status_filter_combo = product_status_filter_combo self._product_group_checkbox = product_group_checkbox self._products_widget = products_widget @@ -335,7 +336,7 @@ class LoaderWindow(QtWidgets.QWidget): self._controller.reset() def showEvent(self, event): - super(LoaderWindow, self).showEvent(event) + super().showEvent(event) if self._first_show: self._on_first_show() @@ -343,9 +344,13 @@ class LoaderWindow(QtWidgets.QWidget): self._show_timer.start() def closeEvent(self, event): - super(LoaderWindow, self).closeEvent(event) + super().closeEvent(event) - self._product_types_widget.reset_product_types_filter_on_refresh() + ( + self + ._product_types_filter_combo + .reset_product_types_filter_on_refresh() + ) self._reset_on_show = True @@ -363,7 +368,7 @@ class LoaderWindow(QtWidgets.QWidget): event.setAccepted(True) return - super(LoaderWindow, self).keyPressEvent(event) + super().keyPressEvent(event) def _on_first_show(self): self._first_show = False @@ -428,9 +433,8 @@ class LoaderWindow(QtWidgets.QWidget): self._products_widget.set_statuses_filter(status_names) def _on_product_type_filter_change(self): - self._products_widget.set_product_type_filter( - self._product_types_widget.get_filter_info() - ) + product_types = self._product_types_filter_combo.get_value() + self._products_widget.set_product_type_filter(product_types) def _on_merged_products_selection_change(self): items = self._products_widget.get_selected_merged_products()