mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge branch 'develop' into enhancement/exception-for-artist-error
This commit is contained in:
commit
89d2f145dd
13 changed files with 285 additions and 145 deletions
|
|
@ -4,7 +4,7 @@ import collections
|
|||
import uuid
|
||||
import json
|
||||
import copy
|
||||
from abc import ABCMeta, abstractmethod, abstractproperty
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
import clique
|
||||
|
||||
|
|
@ -16,7 +16,7 @@ _attr_defs_by_type = {}
|
|||
def register_attr_def_class(cls):
|
||||
"""Register attribute definition.
|
||||
|
||||
Currently are registered definitions used to deserialize data to objects.
|
||||
Currently registered definitions are used to deserialize data to objects.
|
||||
|
||||
Attrs:
|
||||
cls (AbstractAttrDef): Non-abstract class to be registered with unique
|
||||
|
|
@ -60,7 +60,7 @@ def get_default_values(attribute_definitions):
|
|||
for which default values should be collected.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Default values for passet attribute definitions.
|
||||
Dict[str, Any]: Default values for passed attribute definitions.
|
||||
"""
|
||||
|
||||
output = {}
|
||||
|
|
@ -75,13 +75,13 @@ def get_default_values(attribute_definitions):
|
|||
|
||||
|
||||
class AbstractAttrDefMeta(ABCMeta):
|
||||
"""Metaclass to validate existence of 'key' attribute.
|
||||
"""Metaclass to validate the existence of 'key' attribute.
|
||||
|
||||
Each object of `AbstractAttrDef` mus have defined 'key' attribute.
|
||||
Each object of `AbstractAttrDef` must have defined 'key' attribute.
|
||||
"""
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
obj = super(AbstractAttrDefMeta, self).__call__(*args, **kwargs)
|
||||
def __call__(cls, *args, **kwargs):
|
||||
obj = super(AbstractAttrDefMeta, cls).__call__(*args, **kwargs)
|
||||
init_class = getattr(obj, "__init__class__", None)
|
||||
if init_class is not AbstractAttrDef:
|
||||
raise TypeError("{} super was not called in __init__.".format(
|
||||
|
|
@ -162,7 +162,8 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta):
|
|||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
@abstractproperty
|
||||
@property
|
||||
@abstractmethod
|
||||
def type(self):
|
||||
"""Attribute definition type also used as identifier of class.
|
||||
|
||||
|
|
@ -215,7 +216,7 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta):
|
|||
|
||||
|
||||
# -----------------------------------------
|
||||
# UI attribute definitoins won't hold value
|
||||
# UI attribute definitions won't hold value
|
||||
# -----------------------------------------
|
||||
|
||||
class UIDef(AbstractAttrDef):
|
||||
|
|
@ -245,7 +246,7 @@ class UILabelDef(UIDef):
|
|||
|
||||
|
||||
# ---------------------------------------
|
||||
# Attribute defintioins should hold value
|
||||
# Attribute definitions should hold value
|
||||
# ---------------------------------------
|
||||
|
||||
class UnknownDef(AbstractAttrDef):
|
||||
|
|
@ -311,7 +312,7 @@ class NumberDef(AbstractAttrDef):
|
|||
):
|
||||
minimum = 0 if minimum is None else minimum
|
||||
maximum = 999999 if maximum is None else maximum
|
||||
# Swap min/max when are passed in opposited order
|
||||
# Swap min/max when are passed in opposite order
|
||||
if minimum > maximum:
|
||||
maximum, minimum = minimum, maximum
|
||||
|
||||
|
|
@ -364,10 +365,10 @@ class NumberDef(AbstractAttrDef):
|
|||
class TextDef(AbstractAttrDef):
|
||||
"""Text definition.
|
||||
|
||||
Text can have multiline option so endline characters are allowed regex
|
||||
Text can have multiline option so end-line characters are allowed regex
|
||||
validation can be applied placeholder for UI purposes and default value.
|
||||
|
||||
Regex validation is not part of attribute implemntentation.
|
||||
Regex validation is not part of attribute implementation.
|
||||
|
||||
Args:
|
||||
multiline(bool): Text has single or multiline support.
|
||||
|
|
@ -949,7 +950,8 @@ def deserialize_attr_def(attr_def_data):
|
|||
"""Deserialize attribute definition from data.
|
||||
|
||||
Args:
|
||||
attr_def (Dict[str, Any]): Attribute definition data to deserialize.
|
||||
attr_def_data (Dict[str, Any]): Attribute definition data to
|
||||
deserialize.
|
||||
"""
|
||||
|
||||
attr_type = attr_def_data.pop("type")
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ import traceback
|
|||
import collections
|
||||
import inspect
|
||||
from contextlib import contextmanager
|
||||
from typing import Optional
|
||||
import typing
|
||||
from typing import Optional, Iterable, Dict
|
||||
|
||||
import pyblish.logic
|
||||
import pyblish.api
|
||||
|
|
@ -31,13 +32,15 @@ from .exceptions import (
|
|||
HostMissRequiredMethod,
|
||||
)
|
||||
from .changes import TrackChangesItem
|
||||
from .structures import PublishAttributes, ConvertorItem
|
||||
from .structures import PublishAttributes, ConvertorItem, InstanceContextInfo
|
||||
from .creator_plugins import (
|
||||
Creator,
|
||||
AutoCreator,
|
||||
discover_creator_plugins,
|
||||
discover_convertor_plugins,
|
||||
)
|
||||
if typing.TYPE_CHECKING:
|
||||
from .structures import CreatedInstance
|
||||
|
||||
# Import of functions and classes that were moved to different file
|
||||
# TODO Should be removed in future release - Added 24/08/28, 0.4.3-dev.1
|
||||
|
|
@ -183,6 +186,10 @@ class CreateContext:
|
|||
# Shared data across creators during collection phase
|
||||
self._collection_shared_data = None
|
||||
|
||||
# Context validation cache
|
||||
self._folder_id_by_folder_path = {}
|
||||
self._task_names_by_folder_path = {}
|
||||
|
||||
self.thumbnail_paths_by_instance_id = {}
|
||||
|
||||
# Trigger reset if was enabled
|
||||
|
|
@ -202,17 +209,19 @@ class CreateContext:
|
|||
"""Access to global publish attributes."""
|
||||
return self._publish_attributes
|
||||
|
||||
def get_instance_by_id(self, instance_id):
|
||||
def get_instance_by_id(
|
||||
self, instance_id: str
|
||||
) -> Optional["CreatedInstance"]:
|
||||
"""Receive instance by id.
|
||||
|
||||
Args:
|
||||
instance_id (str): Instance id.
|
||||
|
||||
Returns:
|
||||
Union[CreatedInstance, None]: Instance or None if instance with
|
||||
Optional[CreatedInstance]: Instance or None if instance with
|
||||
given id is not available.
|
||||
"""
|
||||
|
||||
"""
|
||||
return self._instances_by_id.get(instance_id)
|
||||
|
||||
def get_sorted_creators(self, identifiers=None):
|
||||
|
|
@ -224,8 +233,8 @@ class CreateContext:
|
|||
|
||||
Returns:
|
||||
List[BaseCreator]: Sorted creator plugins by 'order' value.
|
||||
"""
|
||||
|
||||
"""
|
||||
if identifiers is not None:
|
||||
identifiers = set(identifiers)
|
||||
creators = [
|
||||
|
|
@ -491,6 +500,8 @@ class CreateContext:
|
|||
|
||||
# Give ability to store shared data for collection phase
|
||||
self._collection_shared_data = {}
|
||||
self._folder_id_by_folder_path = {}
|
||||
self._task_names_by_folder_path = {}
|
||||
|
||||
def reset_finalization(self):
|
||||
"""Cleanup of attributes after reset."""
|
||||
|
|
@ -715,7 +726,7 @@ class CreateContext:
|
|||
self._original_context_data, self.context_data_to_store()
|
||||
)
|
||||
|
||||
def creator_adds_instance(self, instance):
|
||||
def creator_adds_instance(self, instance: "CreatedInstance"):
|
||||
"""Creator adds new instance to context.
|
||||
|
||||
Instances should be added only from creators.
|
||||
|
|
@ -942,7 +953,7 @@ class CreateContext:
|
|||
def _remove_instance(self, instance):
|
||||
self._instances_by_id.pop(instance.id, None)
|
||||
|
||||
def creator_removed_instance(self, instance):
|
||||
def creator_removed_instance(self, instance: "CreatedInstance"):
|
||||
"""When creator removes instance context should be acknowledged.
|
||||
|
||||
If creator removes instance conext should know about it to avoid
|
||||
|
|
@ -990,7 +1001,7 @@ class CreateContext:
|
|||
[],
|
||||
self._bulk_instances_to_process
|
||||
)
|
||||
self.validate_instances_context(instances_to_validate)
|
||||
self.get_instances_context_info(instances_to_validate)
|
||||
|
||||
def reset_instances(self):
|
||||
"""Reload instances"""
|
||||
|
|
@ -1079,26 +1090,70 @@ class CreateContext:
|
|||
if failed_info:
|
||||
raise CreatorsCreateFailed(failed_info)
|
||||
|
||||
def validate_instances_context(self, instances=None):
|
||||
"""Validate 'folder' and 'task' instance context."""
|
||||
def get_instances_context_info(
|
||||
self, instances: Optional[Iterable["CreatedInstance"]] = None
|
||||
) -> Dict[str, InstanceContextInfo]:
|
||||
"""Validate 'folder' and 'task' instance context.
|
||||
|
||||
Args:
|
||||
instances (Optional[Iterable[CreatedInstance]]): Instances to
|
||||
validate. If not provided all instances are validated.
|
||||
|
||||
Returns:
|
||||
Dict[str, InstanceContextInfo]: Validation results by instance id.
|
||||
|
||||
"""
|
||||
# Use all instances from context if 'instances' are not passed
|
||||
if instances is None:
|
||||
instances = tuple(self._instances_by_id.values())
|
||||
instances = self._instances_by_id.values()
|
||||
instances = tuple(instances)
|
||||
info_by_instance_id = {
|
||||
instance.id: InstanceContextInfo(
|
||||
instance.get("folderPath"),
|
||||
instance.get("task"),
|
||||
False,
|
||||
False,
|
||||
)
|
||||
for instance in instances
|
||||
}
|
||||
|
||||
# Skip if instances are empty
|
||||
if not instances:
|
||||
return
|
||||
if not info_by_instance_id:
|
||||
return info_by_instance_id
|
||||
|
||||
project_name = self.project_name
|
||||
|
||||
task_names_by_folder_path = {}
|
||||
to_validate = []
|
||||
task_names_by_folder_path = collections.defaultdict(set)
|
||||
for instance in instances:
|
||||
folder_path = instance.get("folderPath")
|
||||
task_name = instance.get("task")
|
||||
if folder_path:
|
||||
task_names_by_folder_path[folder_path] = set()
|
||||
if task_name:
|
||||
task_names_by_folder_path[folder_path].add(task_name)
|
||||
context_info = info_by_instance_id[instance.id]
|
||||
if instance.has_promised_context:
|
||||
context_info.folder_is_valid = True
|
||||
context_info.task_is_valid = True
|
||||
continue
|
||||
# TODO allow context promise
|
||||
folder_path = context_info.folder_path
|
||||
if not folder_path:
|
||||
continue
|
||||
|
||||
if folder_path in self._folder_id_by_folder_path:
|
||||
folder_id = self._folder_id_by_folder_path[folder_path]
|
||||
if folder_id is None:
|
||||
continue
|
||||
context_info.folder_is_valid = True
|
||||
|
||||
task_name = context_info.task_name
|
||||
if task_name is not None:
|
||||
tasks_cache = self._task_names_by_folder_path.get(folder_path)
|
||||
if tasks_cache is not None:
|
||||
context_info.task_is_valid = task_name in tasks_cache
|
||||
continue
|
||||
|
||||
to_validate.append(instance)
|
||||
task_names_by_folder_path[folder_path].add(task_name)
|
||||
|
||||
if not to_validate:
|
||||
return info_by_instance_id
|
||||
|
||||
# Backwards compatibility for cases where folder name is set instead
|
||||
# of folder path
|
||||
|
|
@ -1120,7 +1175,9 @@ class CreateContext:
|
|||
fields={"id", "path"}
|
||||
):
|
||||
folder_id = folder_entity["id"]
|
||||
folder_paths_by_id[folder_id] = folder_entity["path"]
|
||||
folder_path = folder_entity["path"]
|
||||
folder_paths_by_id[folder_id] = folder_path
|
||||
self._folder_id_by_folder_path[folder_path] = folder_id
|
||||
|
||||
folder_entities_by_name = collections.defaultdict(list)
|
||||
if folder_names:
|
||||
|
|
@ -1131,8 +1188,10 @@ class CreateContext:
|
|||
):
|
||||
folder_id = folder_entity["id"]
|
||||
folder_name = folder_entity["name"]
|
||||
folder_paths_by_id[folder_id] = folder_entity["path"]
|
||||
folder_path = folder_entity["path"]
|
||||
folder_paths_by_id[folder_id] = folder_path
|
||||
folder_entities_by_name[folder_name].append(folder_entity)
|
||||
self._folder_id_by_folder_path[folder_path] = folder_id
|
||||
|
||||
tasks_entities = ayon_api.get_tasks(
|
||||
project_name,
|
||||
|
|
@ -1145,12 +1204,11 @@ class CreateContext:
|
|||
folder_id = task_entity["folderId"]
|
||||
folder_path = folder_paths_by_id[folder_id]
|
||||
task_names_by_folder_path[folder_path].add(task_entity["name"])
|
||||
self._task_names_by_folder_path.update(task_names_by_folder_path)
|
||||
|
||||
for instance in instances:
|
||||
if not instance.has_valid_folder or not instance.has_valid_task:
|
||||
continue
|
||||
|
||||
for instance in to_validate:
|
||||
folder_path = instance["folderPath"]
|
||||
task_name = instance.get("task")
|
||||
if folder_path and "/" not in folder_path:
|
||||
folder_entities = folder_entities_by_name.get(folder_path)
|
||||
if len(folder_entities) == 1:
|
||||
|
|
@ -1158,15 +1216,16 @@ class CreateContext:
|
|||
instance["folderPath"] = folder_path
|
||||
|
||||
if folder_path not in task_names_by_folder_path:
|
||||
instance.set_folder_invalid(True)
|
||||
continue
|
||||
context_info = info_by_instance_id[instance.id]
|
||||
context_info.folder_is_valid = True
|
||||
|
||||
task_name = instance["task"]
|
||||
if not task_name:
|
||||
continue
|
||||
|
||||
if task_name not in task_names_by_folder_path[folder_path]:
|
||||
instance.set_task_invalid(True)
|
||||
if (
|
||||
not task_name
|
||||
or task_name in task_names_by_folder_path[folder_path]
|
||||
):
|
||||
context_info.task_is_valid = True
|
||||
return info_by_instance_id
|
||||
|
||||
def save_changes(self):
|
||||
"""Save changes. Update all changed values."""
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import copy
|
||||
import collections
|
||||
from uuid import uuid4
|
||||
from typing import Optional
|
||||
|
||||
from ayon_core.lib.attribute_definitions import (
|
||||
UnknownDef,
|
||||
|
|
@ -396,6 +397,24 @@ class PublishAttributes:
|
|||
)
|
||||
|
||||
|
||||
class InstanceContextInfo:
|
||||
def __init__(
|
||||
self,
|
||||
folder_path: Optional[str],
|
||||
task_name: Optional[str],
|
||||
folder_is_valid: bool,
|
||||
task_is_valid: bool,
|
||||
):
|
||||
self.folder_path: Optional[str] = folder_path
|
||||
self.task_name: Optional[str] = task_name
|
||||
self.folder_is_valid: bool = folder_is_valid
|
||||
self.task_is_valid: bool = task_is_valid
|
||||
|
||||
@property
|
||||
def is_valid(self) -> bool:
|
||||
return self.folder_is_valid and self.task_is_valid
|
||||
|
||||
|
||||
class CreatedInstance:
|
||||
"""Instance entity with data that will be stored to workfile.
|
||||
|
||||
|
|
@ -528,9 +547,6 @@ class CreatedInstance:
|
|||
if not self._data.get("instance_id"):
|
||||
self._data["instance_id"] = str(uuid4())
|
||||
|
||||
self._folder_is_valid = self.has_set_folder
|
||||
self._task_is_valid = self.has_set_task
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
"<CreatedInstance {product[name]}"
|
||||
|
|
@ -699,6 +715,17 @@ class CreatedInstance:
|
|||
def publish_attributes(self):
|
||||
return self._data["publish_attributes"]
|
||||
|
||||
@property
|
||||
def has_promised_context(self) -> bool:
|
||||
"""Get context data that are promised to be set by creator.
|
||||
|
||||
Returns:
|
||||
bool: Has context that won't bo validated. Artist can't change
|
||||
value when set to True.
|
||||
|
||||
"""
|
||||
return self._data.get("has_promised_context", False)
|
||||
|
||||
def data_to_store(self):
|
||||
"""Collect data that contain json parsable types.
|
||||
|
||||
|
|
@ -826,46 +853,3 @@ class CreatedInstance:
|
|||
obj.publish_attributes.deserialize_attributes(publish_attributes)
|
||||
|
||||
return obj
|
||||
|
||||
# Context validation related methods/properties
|
||||
@property
|
||||
def has_set_folder(self):
|
||||
"""Folder path is set in data."""
|
||||
|
||||
return "folderPath" in self._data
|
||||
|
||||
@property
|
||||
def has_set_task(self):
|
||||
"""Task name is set in data."""
|
||||
|
||||
return "task" in self._data
|
||||
|
||||
@property
|
||||
def has_valid_context(self):
|
||||
"""Context data are valid for publishing."""
|
||||
|
||||
return self.has_valid_folder and self.has_valid_task
|
||||
|
||||
@property
|
||||
def has_valid_folder(self):
|
||||
"""Folder set in context exists in project."""
|
||||
|
||||
if not self.has_set_folder:
|
||||
return False
|
||||
return self._folder_is_valid
|
||||
|
||||
@property
|
||||
def has_valid_task(self):
|
||||
"""Task set in context exists in project."""
|
||||
|
||||
if not self.has_set_task:
|
||||
return False
|
||||
return self._task_is_valid
|
||||
|
||||
def set_folder_invalid(self, invalid):
|
||||
# TODO replace with `set_folder_path`
|
||||
self._folder_is_valid = not invalid
|
||||
|
||||
def set_task_invalid(self, invalid):
|
||||
# TODO replace with `set_task_name`
|
||||
self._task_is_valid = not invalid
|
||||
|
|
|
|||
|
|
@ -859,7 +859,7 @@ class AbstractTemplateBuilder(ABC):
|
|||
"Settings\\Profiles"
|
||||
).format(host_name.title()))
|
||||
|
||||
# Try fill path with environments and anatomy roots
|
||||
# Try to fill path with environments and anatomy roots
|
||||
anatomy = Anatomy(project_name)
|
||||
fill_data = {
|
||||
key: value
|
||||
|
|
@ -872,9 +872,7 @@ class AbstractTemplateBuilder(ABC):
|
|||
"code": anatomy.project_code,
|
||||
}
|
||||
|
||||
result = StringTemplate.format_template(path, fill_data)
|
||||
if result.solved:
|
||||
path = result.normalized()
|
||||
path = self.resolve_template_path(path, fill_data)
|
||||
|
||||
if path and os.path.exists(path):
|
||||
self.log.info("Found template at: '{}'".format(path))
|
||||
|
|
@ -914,6 +912,27 @@ class AbstractTemplateBuilder(ABC):
|
|||
"create_first_version": create_first_version
|
||||
}
|
||||
|
||||
def resolve_template_path(self, path, fill_data) -> str:
|
||||
"""Resolve the template path.
|
||||
|
||||
By default, this does nothing except returning the path directly.
|
||||
|
||||
This can be overridden in host integrations to perform additional
|
||||
resolving over the template. Like, `hou.text.expandString` in Houdini.
|
||||
|
||||
Arguments:
|
||||
path (str): The input path.
|
||||
fill_data (dict[str, str]): Data to use for template formatting.
|
||||
|
||||
Returns:
|
||||
str: The resolved path.
|
||||
|
||||
"""
|
||||
result = StringTemplate.format_template(path, fill_data)
|
||||
if result.solved:
|
||||
path = result.normalized()
|
||||
return path
|
||||
|
||||
def emit_event(self, topic, data=None, source=None) -> Event:
|
||||
return self._event_system.emit(topic, data, source)
|
||||
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
]
|
||||
|
||||
# Supported extensions
|
||||
image_exts = ["exr", "jpg", "jpeg", "png", "dpx", "tga"]
|
||||
image_exts = ["exr", "jpg", "jpeg", "png", "dpx", "tga", "tiff", "tif"]
|
||||
video_exts = ["mov", "mp4"]
|
||||
supported_exts = image_exts + video_exts
|
||||
|
||||
|
|
|
|||
|
|
@ -486,11 +486,11 @@ class TableField(BaseItem):
|
|||
line = self.ellide_text
|
||||
break
|
||||
|
||||
for idx, char in enumerate(_word):
|
||||
for char_index, char in enumerate(_word):
|
||||
_line = line + char + self.ellide_text
|
||||
_line_width = font.getsize(_line)[0]
|
||||
if _line_width > max_width:
|
||||
if idx == 0:
|
||||
if char_index == 0:
|
||||
line = _line
|
||||
break
|
||||
line = line + char
|
||||
|
|
|
|||
|
|
@ -322,6 +322,12 @@ class AbstractPublisherFrontend(AbstractPublisherCommon):
|
|||
) -> Dict[str, Union[CreatedInstance, None]]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_instances_context_info(
|
||||
self, instance_ids: Optional[Iterable[str]] = None
|
||||
):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_existing_product_names(self, folder_path: str) -> List[str]:
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -190,6 +190,9 @@ class PublisherController(
|
|||
def get_instances_by_id(self, instance_ids=None):
|
||||
return self._create_model.get_instances_by_id(instance_ids)
|
||||
|
||||
def get_instances_context_info(self, instance_ids=None):
|
||||
return self._create_model.get_instances_context_info(instance_ids)
|
||||
|
||||
def get_convertor_items(self):
|
||||
return self._create_model.get_convertor_items()
|
||||
|
||||
|
|
|
|||
|
|
@ -306,6 +306,14 @@ class CreateModel:
|
|||
for instance_id in instance_ids
|
||||
}
|
||||
|
||||
def get_instances_context_info(
|
||||
self, instance_ids: Optional[Iterable[str]] = None
|
||||
):
|
||||
instances = self.get_instances_by_id(instance_ids).values()
|
||||
return self._create_context.get_instances_context_info(
|
||||
instances
|
||||
)
|
||||
|
||||
def get_convertor_items(self) -> Dict[str, ConvertorItem]:
|
||||
return self._create_context.convertor_items_by_id
|
||||
|
||||
|
|
|
|||
|
|
@ -217,20 +217,22 @@ class InstanceGroupWidget(BaseGroupWidget):
|
|||
def update_icons(self, group_icons):
|
||||
self._group_icons = group_icons
|
||||
|
||||
def update_instance_values(self):
|
||||
def update_instance_values(self, context_info_by_id):
|
||||
"""Trigger update on instance widgets."""
|
||||
|
||||
for widget in self._widgets_by_id.values():
|
||||
widget.update_instance_values()
|
||||
for instance_id, widget in self._widgets_by_id.items():
|
||||
widget.update_instance_values(context_info_by_id[instance_id])
|
||||
|
||||
def update_instances(self, instances):
|
||||
def update_instances(self, instances, context_info_by_id):
|
||||
"""Update instances for the group.
|
||||
|
||||
Args:
|
||||
instances(list<CreatedInstance>): List of instances in
|
||||
instances (list[CreatedInstance]): List of instances in
|
||||
CreateContext.
|
||||
"""
|
||||
context_info_by_id (Dict[str, InstanceContextInfo]): Instance
|
||||
context info by instance id.
|
||||
|
||||
"""
|
||||
# Store instances by id and by product name
|
||||
instances_by_id = {}
|
||||
instances_by_product_name = collections.defaultdict(list)
|
||||
|
|
@ -249,13 +251,14 @@ class InstanceGroupWidget(BaseGroupWidget):
|
|||
widget_idx = 1
|
||||
for product_names in sorted_product_names:
|
||||
for instance in instances_by_product_name[product_names]:
|
||||
context_info = context_info_by_id[instance.id]
|
||||
if instance.id in self._widgets_by_id:
|
||||
widget = self._widgets_by_id[instance.id]
|
||||
widget.update_instance(instance)
|
||||
widget.update_instance(instance, context_info)
|
||||
else:
|
||||
group_icon = self._group_icons[instance.creator_identifier]
|
||||
widget = InstanceCardWidget(
|
||||
instance, group_icon, self
|
||||
instance, context_info, group_icon, self
|
||||
)
|
||||
widget.selected.connect(self._on_widget_selection)
|
||||
widget.active_changed.connect(self._on_active_changed)
|
||||
|
|
@ -388,7 +391,7 @@ class ConvertorItemCardWidget(CardWidget):
|
|||
self._icon_widget = icon_widget
|
||||
self._label_widget = label_widget
|
||||
|
||||
def update_instance_values(self):
|
||||
def update_instance_values(self, context_info):
|
||||
pass
|
||||
|
||||
|
||||
|
|
@ -397,7 +400,7 @@ class InstanceCardWidget(CardWidget):
|
|||
|
||||
active_changed = QtCore.Signal(str, bool)
|
||||
|
||||
def __init__(self, instance, group_icon, parent):
|
||||
def __init__(self, instance, context_info, group_icon, parent):
|
||||
super().__init__(parent)
|
||||
|
||||
self._id = instance.id
|
||||
|
|
@ -458,7 +461,7 @@ class InstanceCardWidget(CardWidget):
|
|||
self._active_checkbox = active_checkbox
|
||||
self._expand_btn = expand_btn
|
||||
|
||||
self.update_instance_values()
|
||||
self.update_instance_values(context_info)
|
||||
|
||||
def set_active_toggle_enabled(self, enabled):
|
||||
self._active_checkbox.setEnabled(enabled)
|
||||
|
|
@ -480,13 +483,13 @@ class InstanceCardWidget(CardWidget):
|
|||
if checkbox_value != new_value:
|
||||
self._active_checkbox.setChecked(new_value)
|
||||
|
||||
def update_instance(self, instance):
|
||||
def update_instance(self, instance, context_info):
|
||||
"""Update instance object and update UI."""
|
||||
self.instance = instance
|
||||
self.update_instance_values()
|
||||
self.update_instance_values(context_info)
|
||||
|
||||
def _validate_context(self):
|
||||
valid = self.instance.has_valid_context
|
||||
def _validate_context(self, context_info):
|
||||
valid = context_info.is_valid
|
||||
self._icon_widget.setVisible(valid)
|
||||
self._context_warning.setVisible(not valid)
|
||||
|
||||
|
|
@ -519,11 +522,11 @@ class InstanceCardWidget(CardWidget):
|
|||
QtCore.Qt.NoTextInteraction
|
||||
)
|
||||
|
||||
def update_instance_values(self):
|
||||
def update_instance_values(self, context_info):
|
||||
"""Update instance data"""
|
||||
self._update_product_name()
|
||||
self.set_active(self.instance["active"])
|
||||
self._validate_context()
|
||||
self._validate_context(context_info)
|
||||
|
||||
def _set_expanded(self, expanded=None):
|
||||
if expanded is None:
|
||||
|
|
@ -694,6 +697,8 @@ class InstanceCardView(AbstractInstanceView):
|
|||
|
||||
self._update_convertor_items_group()
|
||||
|
||||
context_info_by_id = self._controller.get_instances_context_info()
|
||||
|
||||
# Prepare instances by group and identifiers by group
|
||||
instances_by_group = collections.defaultdict(list)
|
||||
identifiers_by_group = collections.defaultdict(set)
|
||||
|
|
@ -747,7 +752,7 @@ class InstanceCardView(AbstractInstanceView):
|
|||
|
||||
widget_idx += 1
|
||||
group_widget.update_instances(
|
||||
instances_by_group[group_name]
|
||||
instances_by_group[group_name], context_info_by_id
|
||||
)
|
||||
group_widget.set_active_toggle_enabled(
|
||||
self._active_toggle_enabled
|
||||
|
|
@ -814,8 +819,9 @@ class InstanceCardView(AbstractInstanceView):
|
|||
|
||||
def refresh_instance_states(self):
|
||||
"""Trigger update of instances on group widgets."""
|
||||
context_info_by_id = self._controller.get_instances_context_info()
|
||||
for widget in self._widgets_by_group.values():
|
||||
widget.update_instance_values()
|
||||
widget.update_instance_values(context_info_by_id)
|
||||
|
||||
def _on_active_changed(self, group_name, instance_id, value):
|
||||
group_widget = self._widgets_by_group[group_name]
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ class InstanceListItemWidget(QtWidgets.QWidget):
|
|||
active_changed = QtCore.Signal(str, bool)
|
||||
double_clicked = QtCore.Signal()
|
||||
|
||||
def __init__(self, instance, parent):
|
||||
def __init__(self, instance, context_info, parent):
|
||||
super().__init__(parent)
|
||||
|
||||
self.instance = instance
|
||||
|
|
@ -151,7 +151,7 @@ class InstanceListItemWidget(QtWidgets.QWidget):
|
|||
|
||||
self._has_valid_context = None
|
||||
|
||||
self._set_valid_property(instance.has_valid_context)
|
||||
self._set_valid_property(context_info.is_valid)
|
||||
|
||||
def mouseDoubleClickEvent(self, event):
|
||||
widget = self.childAt(event.pos())
|
||||
|
|
@ -188,12 +188,12 @@ class InstanceListItemWidget(QtWidgets.QWidget):
|
|||
if checkbox_value != new_value:
|
||||
self._active_checkbox.setChecked(new_value)
|
||||
|
||||
def update_instance(self, instance):
|
||||
def update_instance(self, instance, context_info):
|
||||
"""Update instance object."""
|
||||
self.instance = instance
|
||||
self.update_instance_values()
|
||||
self.update_instance_values(context_info)
|
||||
|
||||
def update_instance_values(self):
|
||||
def update_instance_values(self, context_info):
|
||||
"""Update instance data propagated to widgets."""
|
||||
# Check product name
|
||||
label = self.instance.label
|
||||
|
|
@ -202,7 +202,7 @@ class InstanceListItemWidget(QtWidgets.QWidget):
|
|||
# Check active state
|
||||
self.set_active(self.instance["active"])
|
||||
# Check valid states
|
||||
self._set_valid_property(self.instance.has_valid_context)
|
||||
self._set_valid_property(context_info.is_valid)
|
||||
|
||||
def _on_active_change(self):
|
||||
new_value = self._active_checkbox.isChecked()
|
||||
|
|
@ -583,6 +583,8 @@ class InstanceListView(AbstractInstanceView):
|
|||
|
||||
self._update_convertor_items_group()
|
||||
|
||||
context_info_by_id = self._controller.get_instances_context_info()
|
||||
|
||||
# Prepare instances by their groups
|
||||
instances_by_group_name = collections.defaultdict(list)
|
||||
group_names = set()
|
||||
|
|
@ -643,13 +645,15 @@ class InstanceListView(AbstractInstanceView):
|
|||
elif activity != instance["active"]:
|
||||
activity = -1
|
||||
|
||||
context_info = context_info_by_id[instance_id]
|
||||
|
||||
self._group_by_instance_id[instance_id] = group_name
|
||||
# Remove instance id from `to_remove` if already exists and
|
||||
# trigger update of widget
|
||||
if instance_id in to_remove:
|
||||
to_remove.remove(instance_id)
|
||||
widget = self._widgets_by_id[instance_id]
|
||||
widget.update_instance(instance)
|
||||
widget.update_instance(instance, context_info)
|
||||
continue
|
||||
|
||||
# Create new item and store it as new
|
||||
|
|
@ -695,7 +699,8 @@ class InstanceListView(AbstractInstanceView):
|
|||
group_item.appendRows(new_items)
|
||||
|
||||
for item, instance in new_items_with_instance:
|
||||
if not instance.has_valid_context:
|
||||
context_info = context_info_by_id[instance.id]
|
||||
if not context_info.is_valid:
|
||||
expand_groups.add(group_name)
|
||||
item_index = self._instance_model.index(
|
||||
item.row(),
|
||||
|
|
@ -704,7 +709,7 @@ class InstanceListView(AbstractInstanceView):
|
|||
)
|
||||
proxy_index = self._proxy_model.mapFromSource(item_index)
|
||||
widget = InstanceListItemWidget(
|
||||
instance, self._instance_view
|
||||
instance, context_info, self._instance_view
|
||||
)
|
||||
widget.set_active_toggle_enabled(
|
||||
self._active_toggle_enabled
|
||||
|
|
@ -870,8 +875,10 @@ class InstanceListView(AbstractInstanceView):
|
|||
|
||||
def refresh_instance_states(self):
|
||||
"""Trigger update of all instances."""
|
||||
for widget in self._widgets_by_id.values():
|
||||
widget.update_instance_values()
|
||||
context_info_by_id = self._controller.get_instances_context_info()
|
||||
for instance_id, widget in self._widgets_by_id.items():
|
||||
context_info = context_info_by_id[instance_id]
|
||||
widget.update_instance_values(context_info)
|
||||
|
||||
def _on_active_changed(self, changed_instance_id, new_value):
|
||||
selected_instance_ids, _, _ = self.get_selected_items()
|
||||
|
|
|
|||
|
|
@ -1182,6 +1182,10 @@ class GlobalAttrsWidget(QtWidgets.QWidget):
|
|||
invalid_tasks = False
|
||||
folder_paths = []
|
||||
for instance in self._current_instances:
|
||||
# Ignore instances that have promised context
|
||||
if instance.has_promised_context:
|
||||
continue
|
||||
|
||||
new_variant_value = instance.get("variant")
|
||||
new_folder_path = instance.get("folderPath")
|
||||
new_task_name = instance.get("task")
|
||||
|
|
@ -1206,7 +1210,6 @@ class GlobalAttrsWidget(QtWidgets.QWidget):
|
|||
|
||||
except TaskNotSetError:
|
||||
invalid_tasks = True
|
||||
instance.set_task_invalid(True)
|
||||
product_names.add(instance["productName"])
|
||||
continue
|
||||
|
||||
|
|
@ -1216,11 +1219,9 @@ class GlobalAttrsWidget(QtWidgets.QWidget):
|
|||
|
||||
if folder_path is not None:
|
||||
instance["folderPath"] = folder_path
|
||||
instance.set_folder_invalid(False)
|
||||
|
||||
if task_name is not None:
|
||||
instance["task"] = task_name or None
|
||||
instance.set_task_invalid(False)
|
||||
|
||||
instance["productName"] = new_product_name
|
||||
|
||||
|
|
@ -1306,7 +1307,13 @@ class GlobalAttrsWidget(QtWidgets.QWidget):
|
|||
editable = False
|
||||
|
||||
folder_task_combinations = []
|
||||
context_editable = None
|
||||
for instance in instances:
|
||||
if not instance.has_promised_context:
|
||||
context_editable = True
|
||||
elif context_editable is None:
|
||||
context_editable = False
|
||||
|
||||
# NOTE I'm not sure how this can even happen?
|
||||
if instance.creator_identifier is None:
|
||||
editable = False
|
||||
|
|
@ -1319,6 +1326,11 @@ class GlobalAttrsWidget(QtWidgets.QWidget):
|
|||
folder_task_combinations.append((folder_path, task_name))
|
||||
product_names.add(instance.get("productName") or self.unknown_value)
|
||||
|
||||
if not editable:
|
||||
context_editable = False
|
||||
elif context_editable is None:
|
||||
context_editable = True
|
||||
|
||||
self.variant_input.set_value(variants)
|
||||
|
||||
# Set context of folder widget
|
||||
|
|
@ -1329,8 +1341,21 @@ class GlobalAttrsWidget(QtWidgets.QWidget):
|
|||
self.product_value_widget.set_value(product_names)
|
||||
|
||||
self.variant_input.setEnabled(editable)
|
||||
self.folder_value_widget.setEnabled(editable)
|
||||
self.task_value_widget.setEnabled(editable)
|
||||
self.folder_value_widget.setEnabled(context_editable)
|
||||
self.task_value_widget.setEnabled(context_editable)
|
||||
|
||||
if not editable:
|
||||
folder_tooltip = "Select instances to change folder path."
|
||||
task_tooltip = "Select instances to change task name."
|
||||
elif not context_editable:
|
||||
folder_tooltip = "Folder path is defined by Create plugin."
|
||||
task_tooltip = "Task is defined by Create plugin."
|
||||
else:
|
||||
folder_tooltip = "Change folder path of selected instances."
|
||||
task_tooltip = "Change task of selected instances."
|
||||
|
||||
self.folder_value_widget.setToolTip(folder_tooltip)
|
||||
self.task_value_widget.setToolTip(task_tooltip)
|
||||
|
||||
|
||||
class CreatorAttrsWidget(QtWidgets.QWidget):
|
||||
|
|
@ -1768,9 +1793,16 @@ class ProductAttributesWidget(QtWidgets.QWidget):
|
|||
self.bottom_separator = bottom_separator
|
||||
|
||||
def _on_instance_context_changed(self):
|
||||
instance_ids = {
|
||||
instance.id
|
||||
for instance in self._current_instances
|
||||
}
|
||||
context_info_by_id = self._controller.get_instances_context_info(
|
||||
instance_ids
|
||||
)
|
||||
all_valid = True
|
||||
for instance in self._current_instances:
|
||||
if not instance.has_valid_context:
|
||||
for instance_id, context_info in context_info_by_id.items():
|
||||
if not context_info.is_valid:
|
||||
all_valid = False
|
||||
break
|
||||
|
||||
|
|
@ -1795,9 +1827,17 @@ class ProductAttributesWidget(QtWidgets.QWidget):
|
|||
convertor_identifiers(List[str]): Identifiers of convert items.
|
||||
"""
|
||||
|
||||
instance_ids = {
|
||||
instance.id
|
||||
for instance in instances
|
||||
}
|
||||
context_info_by_id = self._controller.get_instances_context_info(
|
||||
instance_ids
|
||||
)
|
||||
|
||||
all_valid = True
|
||||
for instance in instances:
|
||||
if not instance.has_valid_context:
|
||||
for context_info in context_info_by_id.values():
|
||||
if not context_info.is_valid:
|
||||
all_valid = False
|
||||
break
|
||||
|
||||
|
|
|
|||
|
|
@ -913,12 +913,18 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
self._set_footer_enabled(True)
|
||||
return
|
||||
|
||||
active_instances_by_id = {
|
||||
instance.id: instance
|
||||
for instance in self._controller.get_instances()
|
||||
if instance["active"]
|
||||
}
|
||||
context_info_by_id = self._controller.get_instances_context_info(
|
||||
active_instances_by_id.keys()
|
||||
)
|
||||
all_valid = None
|
||||
for instance in self._controller.get_instances():
|
||||
if not instance["active"]:
|
||||
continue
|
||||
|
||||
if not instance.has_valid_context:
|
||||
for instance_id, instance in active_instances_by_id.items():
|
||||
context_info = context_info_by_id[instance_id]
|
||||
if not context_info.is_valid:
|
||||
all_valid = False
|
||||
break
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue