diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index eff53116a2..9fb6ee645d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to AYON Tray options: + - 1.4.1 - 1.4.0 - 1.3.2 - 1.3.1 diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 05531afd05..929cc59d2a 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -79,6 +79,7 @@ _NOT_SET = object() INSTANCE_ADDED_TOPIC = "instances.added" INSTANCE_REMOVED_TOPIC = "instances.removed" VALUE_CHANGED_TOPIC = "values.changed" +INSTANCE_REQUIREMENT_CHANGED_TOPIC = "instance.requirement.changed" PRE_CREATE_ATTR_DEFS_CHANGED_TOPIC = "pre.create.attr.defs.changed" CREATE_ATTR_DEFS_CHANGED_TOPIC = "create.attr.defs.changed" PUBLISH_ATTR_DEFS_CHANGED_TOPIC = "publish.attr.defs.changed" @@ -257,6 +258,10 @@ class CreateContext: "create_attrs_change": BulkInfo(), # Publish attribute definitions changed "publish_attrs_change": BulkInfo(), + # Instance requirement changed + # - right now used only for 'mandatory' but can be extended + # in future + "requirement_change": BulkInfo(), } self._bulk_order = [] @@ -867,7 +872,7 @@ class CreateContext: Event is triggered when instances are already available in context and have set create/publish attribute definitions. - Data structure of event:: + Data structure of event: ```python { @@ -894,7 +899,7 @@ class CreateContext: Event is triggered when instances are already removed from context. - Data structure of event:: + Data structure of event: ```python { @@ -922,7 +927,7 @@ class CreateContext: Event is triggered when any value changes on any instance or context data. - Data structure of event:: + Data structure of event: ```python { @@ -960,7 +965,7 @@ class CreateContext: Create plugin can trigger refresh of pre-create attributes. Usage of this event is mainly for publisher UI. - Data structure of event:: + Data structure of event: ```python { @@ -989,7 +994,7 @@ class CreateContext: Create plugin changed attribute definitions of instance. - Data structure of event:: + Data structure of event: ```python { @@ -1018,7 +1023,7 @@ class CreateContext: Publish plugin changed attribute definitions of instance of context. - Data structure of event:: + Data structure of event: ```python { @@ -1049,6 +1054,35 @@ class CreateContext: PUBLISH_ATTR_DEFS_CHANGED_TOPIC, callback ) + def add_instance_requirement_change_callback( + self, callback: Callable + ) -> "EventCallback": + """Register callback to listen to instance requirement changes. + + Instance changed requirement of active state. + + Data structure of event: + + ```python + { + "instances": [CreatedInstance, ...], + "create_context": CreateContext + } + ``` + + Args: + callback (Callable): Callback function that will be called when + instance requirement changed. + + Returns: + EventCallback: Created callback object which can be used to + stop listening. + + """ + return self._event_hub.add_callback( + INSTANCE_REQUIREMENT_CHANGED_TOPIC, callback + ) + def context_data_to_store(self) -> dict[str, Any]: """Data that should be stored by host function. @@ -1323,6 +1357,13 @@ class CreateContext: ) as bulk_info: yield bulk_info + @contextmanager + def bulk_instance_requirement_change(self, sender: Optional[str] = None): + with self._bulk_context( + "requirement_change", sender + ) as bulk_info: + yield bulk_info + @contextmanager def bulk_publish_attr_defs_change(self, sender: Optional[str] = None): with self._bulk_context("publish_attrs_change", sender) as bulk_info: @@ -1390,6 +1431,19 @@ class CreateContext: with self.bulk_value_changes() as bulk_item: bulk_item.append((instance_id, new_values)) + def instance_requirement_changed(self, instance_id: str) -> None: + """Instance requirement changed. + + Triggered by `CreatedInstance`. + + Args: + instance_id (Optional[str]): Instance id. + + """ + if self._is_instance_events_ready(instance_id): + with self.bulk_instance_requirement_change() as bulk_item: + bulk_item.append(instance_id) + # --- context change callbacks --- def publish_attribute_value_changed( self, plugin_name: str, value: dict[str, Any] @@ -2249,6 +2303,8 @@ class CreateContext: self._bulk_create_attrs_change_finished(data, sender) elif key == "publish_attrs_change": self._bulk_publish_attrs_change_finished(data, sender) + elif key == "requirement_change": + self._bulk_instance_requirement_change_finished(data, sender) def _bulk_add_instances_finished( self, @@ -2443,3 +2499,22 @@ class CreateContext: {"instance_changes": instance_changes}, sender, ) + + def _bulk_instance_requirement_change_finished( + self, + instance_ids: list[str], + sender: Optional[str], + ) -> None: + if not instance_ids: + return + + instances = [ + self.get_instance_by_id(instance_id) + for instance_id in set(instance_ids) + ] + + self._emit_event( + INSTANCE_REQUIREMENT_CHANGED_TOPIC, + {"instances": instances}, + sender, + ) diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index d7ba6b9c24..a4c68d2502 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -507,6 +507,7 @@ class CreatedInstance: if transient_data is None: transient_data = {} self._transient_data = transient_data + self._is_mandatory = False # Create a copy of passed data to avoid changing them on the fly data = copy.deepcopy(data or {}) @@ -605,6 +606,12 @@ class CreatedInstance: if key in self._data and self._data[key] == value: return + if self.is_mandatory and key == "active" and value is not True: + raise ImmutableKeyError( + key, + "Instance is mandatory and can't be disabled." + ) + self._data[key] = value self._create_context.instance_values_changed( self.id, {key: value} @@ -718,6 +725,33 @@ class CreatedInstance: return self._transient_data + @property + def is_mandatory(self) -> bool: + """Check if instance is mandatory. + + Returns: + bool: True if instance is mandatory, False otherwise. + + """ + return self._is_mandatory + + def set_mandatory(self, value: bool) -> None: + """Set instance as mandatory or not. + + Mandatory instance can't be disabled in UI. + + Args: + value (bool): True if instance should be mandatory, False + otherwise. + + """ + if value is self._is_mandatory: + return + self._is_mandatory = value + if value is True: + self["active"] = True + self._create_context.instance_requirement_changed(self.id) + def changes(self): """Calculate and return changes.""" diff --git a/client/ayon_core/tools/loader/control.py b/client/ayon_core/tools/loader/control.py index 95f48b3519..7ba42a0981 100644 --- a/client/ayon_core/tools/loader/control.py +++ b/client/ayon_core/tools/loader/control.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import logging import uuid diff --git a/client/ayon_core/tools/loader/models/actions.py b/client/ayon_core/tools/loader/models/actions.py index 40331d73a4..b792f92dfd 100644 --- a/client/ayon_core/tools/loader/models/actions.py +++ b/client/ayon_core/tools/loader/models/actions.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import sys import traceback import inspect diff --git a/client/ayon_core/tools/loader/models/products.py b/client/ayon_core/tools/loader/models/products.py index edf8efc3b3..87e2406c81 100644 --- a/client/ayon_core/tools/loader/models/products.py +++ b/client/ayon_core/tools/loader/models/products.py @@ -1,5 +1,6 @@ """Products model for loader tools.""" from __future__ import annotations + import collections import contextlib from typing import TYPE_CHECKING, Iterable, Optional diff --git a/client/ayon_core/tools/loader/models/selection.py b/client/ayon_core/tools/loader/models/selection.py index 04add26f86..f2148352cd 100644 --- a/client/ayon_core/tools/loader/models/selection.py +++ b/client/ayon_core/tools/loader/models/selection.py @@ -1,3 +1,6 @@ +from __future__ import annotations + + class SelectionModel(object): """Model handling selection changes. diff --git a/client/ayon_core/tools/loader/models/sitesync.py b/client/ayon_core/tools/loader/models/sitesync.py index c7f0038df4..3a54a1b5f8 100644 --- a/client/ayon_core/tools/loader/models/sitesync.py +++ b/client/ayon_core/tools/loader/models/sitesync.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import collections from ayon_api import ( diff --git a/client/ayon_core/tools/loader/ui/_multicombobox.py b/client/ayon_core/tools/loader/ui/_multicombobox.py index 393272fdf9..b4e1707242 100644 --- a/client/ayon_core/tools/loader/ui/_multicombobox.py +++ b/client/ayon_core/tools/loader/ui/_multicombobox.py @@ -1,4 +1,5 @@ from __future__ import annotations + import typing from typing import List, Tuple, Optional, Iterable, Any diff --git a/client/ayon_core/tools/loader/ui/products_delegates.py b/client/ayon_core/tools/loader/ui/products_delegates.py index e78b32ceb1..b500b86b97 100644 --- a/client/ayon_core/tools/loader/ui/products_delegates.py +++ b/client/ayon_core/tools/loader/ui/products_delegates.py @@ -2,7 +2,6 @@ from __future__ import annotations import numbers import uuid -from typing import Dict from qtpy import QtWidgets, QtCore, QtGui @@ -249,7 +248,7 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self._editor_by_id: Dict[str, VersionComboBox] = {} + self._editor_by_id: dict[str, VersionComboBox] = {} self._task_ids_filter = None self._statuses_filter = None self._version_tags_filter = None diff --git a/client/ayon_core/tools/loader/ui/products_widget.py b/client/ayon_core/tools/loader/ui/products_widget.py index f1c82ee83d..e5bb75a208 100644 --- a/client/ayon_core/tools/loader/ui/products_widget.py +++ b/client/ayon_core/tools/loader/ui/products_widget.py @@ -1,4 +1,5 @@ from __future__ import annotations + import collections from typing import Optional diff --git a/client/ayon_core/tools/loader/ui/search_bar.py b/client/ayon_core/tools/loader/ui/search_bar.py index ab673df1ac..a855a3c452 100644 --- a/client/ayon_core/tools/loader/ui/search_bar.py +++ b/client/ayon_core/tools/loader/ui/search_bar.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import copy import uuid from dataclasses import dataclass diff --git a/client/ayon_core/tools/loader/ui/window.py b/client/ayon_core/tools/loader/ui/window.py index d056b62b13..df5beb708f 100644 --- a/client/ayon_core/tools/loader/ui/window.py +++ b/client/ayon_core/tools/loader/ui/window.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from qtpy import QtWidgets, QtCore, QtGui from ayon_core.resources import get_ayon_icon_filepath diff --git a/client/ayon_core/tools/publisher/control.py b/client/ayon_core/tools/publisher/control.py index ef2e122692..038816c6fc 100644 --- a/client/ayon_core/tools/publisher/control.py +++ b/client/ayon_core/tools/publisher/control.py @@ -53,6 +53,8 @@ class PublisherController( changed. "create.context.create.attrs.changed" - Create attributes changed. "create.context.publish.attrs.changed" - Publish attributes changed. + "create.context.instance.requirement.changed" - Instance requirement + changed. "create.context.removed.instance" - Instance removed from context. "create.model.instances.context.changed" - Instances changed context. like folder, task or variant. diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index 900168eaef..75ed2c73fe 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -217,6 +217,7 @@ class InstanceItem: folder_path: Optional[str], task_name: Optional[str], is_active: bool, + is_mandatory: bool, has_promised_context: bool, ): self._instance_id: str = instance_id @@ -229,6 +230,7 @@ class InstanceItem: self._folder_path: Optional[str] = folder_path self._task_name: Optional[str] = task_name self._is_active: bool = is_active + self._is_mandatory: bool = is_mandatory self._has_promised_context: bool = has_promised_context @property @@ -251,6 +253,10 @@ class InstanceItem: def product_type(self): return self._product_type + @property + def is_mandatory(self): + return self._is_mandatory + @property def has_promised_context(self): return self._has_promised_context @@ -304,6 +310,7 @@ class InstanceItem: instance["folderPath"], instance["task"], instance["active"], + instance.is_mandatory, instance.has_promised_context, ) @@ -476,6 +483,9 @@ class CreateModel: self._create_context.add_publish_attr_defs_change_callback( self._cc_publish_attr_changed ) + self._create_context.add_instance_requirement_change_callback( + self._cc_instance_requirement_changed + ) self._create_context.reset_finalization() @@ -1171,6 +1181,16 @@ class CreateModel: event_data, ) + def _cc_instance_requirement_changed(self, event): + instance_ids = { + instance.id + for instance in event.data["instances"] + } + self._emit_event( + "create.model.instance.requirement.changed", + {"instance_ids": instance_ids}, + ) + def _get_allowed_creators_pattern(self) -> Union[Pattern, None]: """Provide regex pattern for configured creator labels in this context diff --git a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py index 2f633b3149..8a4eddf058 100644 --- a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py @@ -482,6 +482,9 @@ class InstanceCardWidget(CardWidget): if checkbox_value != new_value: self._active_checkbox.setChecked(new_value) + def _set_is_mandatory(self, is_mandatory: bool) -> None: + self._active_checkbox.setVisible(not is_mandatory) + def update_instance(self, instance, context_info): """Update instance object and update UI.""" self.instance = instance @@ -525,6 +528,7 @@ class InstanceCardWidget(CardWidget): """Update instance data""" self._update_product_name() self._set_active(self.instance.is_active) + self._set_is_mandatory(self.instance.is_mandatory) self._validate_context(context_info) def _set_expanded(self, expanded=None): diff --git a/client/ayon_core/tools/publisher/widgets/list_view_widgets.py b/client/ayon_core/tools/publisher/widgets/list_view_widgets.py index bc3353ba5e..969bec11e5 100644 --- a/client/ayon_core/tools/publisher/widgets/list_view_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/list_view_widgets.py @@ -132,6 +132,7 @@ class InstanceListItemWidget(QtWidgets.QWidget): active_checkbox = NiceCheckbox(parent=self) active_checkbox.setChecked(instance.is_active) + active_checkbox.setVisible(not instance.is_mandatory) layout = QtWidgets.QHBoxLayout(self) content_margins = layout.contentsMargins() @@ -151,6 +152,8 @@ class InstanceListItemWidget(QtWidgets.QWidget): self._has_valid_context = None + self._checkbox_enabled = not instance.is_mandatory + self._set_valid_property(context_info.is_valid) def mouseDoubleClickEvent(self, event): @@ -184,6 +187,10 @@ class InstanceListItemWidget(QtWidgets.QWidget): self._active_checkbox.setChecked(new_value) self._active_checkbox.blockSignals(False) + def is_checkbox_enabled(self) -> bool: + """Checkbox can be changed by user.""" + return self._checkbox_enabled + def update_instance(self, instance, context_info): """Update instance object.""" # Check product name @@ -192,6 +199,7 @@ class InstanceListItemWidget(QtWidgets.QWidget): self._instance_label_widget.setText(html_escape(label)) # Check active state self.set_active(instance.is_active) + self._set_is_mandatory(instance.is_mandatory) # Check valid states self._set_valid_property(context_info.is_valid) @@ -203,6 +211,10 @@ class InstanceListItemWidget(QtWidgets.QWidget): def set_active_toggle_enabled(self, enabled): self._active_checkbox.setEnabled(enabled) + def _set_is_mandatory(self, is_mandatory: bool) -> None: + self._checkbox_enabled = not is_mandatory + self._active_checkbox.setVisible(not is_mandatory) + class ListContextWidget(QtWidgets.QFrame): """Context (or global attributes) widget.""" @@ -949,11 +961,17 @@ class InstanceListView(AbstractInstanceView): return active_by_id = {} + all_changed = True for row in range(group_item.rowCount()): item = group_item.child(row) instance_id = item.data(INSTANCE_ID_ROLE) - if instance_id is not None: + widget = self._widgets_by_id.get(instance_id) + if widget is None: + continue + if widget.is_checkbox_enabled(): active_by_id[instance_id] = active + else: + all_changed = False self._controller.set_instances_active_state(active_by_id) @@ -963,6 +981,10 @@ class InstanceListView(AbstractInstanceView): if not self._instance_view.isExpanded(proxy_index): self._instance_view.expand(proxy_index) + if not all_changed: + # If not all instances were changed, update group checkstate + self._update_group_checkstate(group_name) + def has_items(self): if self._convertor_group_widget is not None: return True diff --git a/client/ayon_core/tools/publisher/widgets/overview_widget.py b/client/ayon_core/tools/publisher/widgets/overview_widget.py index c6c3b774f0..46395328e0 100644 --- a/client/ayon_core/tools/publisher/widgets/overview_widget.py +++ b/client/ayon_core/tools/publisher/widgets/overview_widget.py @@ -155,6 +155,10 @@ class OverviewWidget(QtWidgets.QFrame): "create.model.instances.context.changed", self._on_instance_context_change ) + controller.register_event_callback( + "create.model.instance.requirement.changed", + self._on_instance_requirement_changed + ) self._product_content_widget = product_content_widget self._product_content_layout = product_content_layout @@ -352,6 +356,12 @@ class OverviewWidget(QtWidgets.QFrame): ) def _on_instance_context_change(self, event): + self._refresh_instance_states(event["instance_ids"]) + + def _on_instance_requirement_changed(self, event): + self._refresh_instance_states(event["instance_ids"]) + + def _refresh_instance_states(self, instance_ids): current_idx = self._product_views_layout.currentIndex() for idx in range(self._product_views_layout.count()): if idx == current_idx: @@ -361,7 +371,7 @@ class OverviewWidget(QtWidgets.QFrame): widget.set_refreshed(False) current_widget = self._product_views_layout.widget(current_idx) - current_widget.refresh_instance_states(event["instance_ids"]) + current_widget.refresh_instance_states(instance_ids) def _on_convert_requested(self): self.convert_requested.emit() diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index df92396802..509c4a8d14 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.4.0+dev" +__version__ = "1.4.1+dev" diff --git a/package.py b/package.py index efed91b6cf..039bf0379c 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.4.0+dev" +version = "1.4.1+dev" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 91579f04fb..9609729420 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.4.0+dev" +version = "1.4.1+dev" description = "" authors = ["Ynput Team "] readme = "README.md"