Merge branch 'develop' into bugfix/ocio-v2-aces1.3-display-resolving-error

This commit is contained in:
Jakub Ježek 2025-07-14 14:40:33 +02:00 committed by GitHub
commit 0146b5e709
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 226 additions and 14 deletions

View file

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

View file

@ -2,6 +2,7 @@
"""Package for handling AYON command line arguments."""
import os
import sys
import logging
import code
import traceback
from pathlib import Path
@ -306,6 +307,8 @@ def _add_addons(addons_manager):
def main(*args, **kwargs):
logging.basicConfig()
initialize_ayon_connection()
python_path = os.getenv("PYTHONPATH", "")
split_paths = python_path.split(os.pathsep)

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

@ -37,7 +37,7 @@ class ValidateCurrentSaveFile(pyblish.api.ContextPlugin):
label = "Validate File Saved"
order = pyblish.api.ValidatorOrder - 0.1
hosts = ["fusion", "houdini", "max", "maya", "nuke", "substancepainter",
"cinema4d", "silhouette", "gaffer"]
"cinema4d", "silhouette", "gaffer", "blender"]
actions = [SaveByVersionUpAction, ShowWorkfilesAction]
def process(self, context):

View file

@ -892,6 +892,29 @@ class FilesWidget(QtWidgets.QFrame):
self._add_filepaths(new_items)
self._remove_item_by_ids(item_ids)
def _on_merge_request(self):
if self._multivalue:
return
item_ids = self._files_view.get_selected_item_ids()
if not item_ids:
return
all_paths = set()
merged_item_ids = set()
for item_id in item_ids:
file_item = self._files_model.get_file_item_by_id(item_id)
if file_item is None:
continue
merged_item_ids.add(item_id)
all_paths |= {
os.path.join(file_item.directory, filename)
for filename in file_item.filenames
}
self._remove_item_by_ids(merged_item_ids)
new_items = FileDefItem.from_value(list(all_paths), True)
self._add_filepaths(new_items)
def _on_remove_requested(self):
if self._multivalue:
return
@ -911,6 +934,9 @@ class FilesWidget(QtWidgets.QFrame):
split_action.triggered.connect(self._on_split_request)
menu.addAction(split_action)
merge_action = QtWidgets.QAction("Merge sequence", menu)
merge_action.triggered.connect(self._on_merge_request)
menu.addAction(merge_action)
remove_action = QtWidgets.QAction("Remove", menu)
remove_action.triggered.connect(self._on_remove_requested)
menu.addAction(remove_action)

View file

@ -1,3 +1,5 @@
from __future__ import annotations
import logging
import uuid

View file

@ -1,3 +1,5 @@
from __future__ import annotations
import sys
import traceback
import inspect

View file

@ -1,5 +1,6 @@
"""Products model for loader tools."""
from __future__ import annotations
import collections
import contextlib
from typing import TYPE_CHECKING, Iterable, Optional

View file

@ -1,3 +1,6 @@
from __future__ import annotations
class SelectionModel(object):
"""Model handling selection changes.

View file

@ -1,3 +1,5 @@
from __future__ import annotations
import collections
from ayon_api import (

View file

@ -1,4 +1,5 @@
from __future__ import annotations
import typing
from typing import List, Tuple, Optional, Iterable, Any

View file

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

View file

@ -1,4 +1,5 @@
from __future__ import annotations
import collections
from typing import Optional

View file

@ -1,3 +1,5 @@
from __future__ import annotations
import copy
import uuid
from dataclasses import dataclass

View file

@ -1,3 +1,5 @@
from __future__ import annotations
from qtpy import QtWidgets, QtCore, QtGui
from ayon_core.resources import get_ayon_icon_filepath

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

View file

@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring AYON addon 'core' version."""
__version__ = "1.4.0+dev"
__version__ = "1.4.1+dev"

View file

@ -1,6 +1,6 @@
name = "core"
title = "Core"
version = "1.4.0+dev"
version = "1.4.1+dev"
client_dir = "ayon_core"

View file

@ -5,7 +5,7 @@
[tool.poetry]
name = "ayon-core"
version = "1.4.0+dev"
version = "1.4.1+dev"
description = ""
authors = ["Ynput Team <team@ynput.io>"]
readme = "README.md"