Merge pull request #1360 from ynput/enhancement/1176-ay-7575_make-instances-in-publisher-mandatory

Publisher: Mandatory instances
This commit is contained in:
Jakub Trllo 2025-07-10 08:33:51 +02:00 committed by GitHub
commit 0f022786fb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 175 additions and 8 deletions

View file

@ -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,
)

View file

@ -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."""

View file

@ -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.

View file

@ -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

View file

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

View file

@ -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

View file

@ -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()