mirror of
https://github.com/ynput/ayon-core.git
synced 2026-01-01 16:34:53 +01:00
Merge branch 'develop' of github.com:pypeclub/OpenPype into feature/OP-3446_tray-publish-batch-mov
This commit is contained in:
commit
57e6ac0c1d
17 changed files with 563 additions and 344 deletions
|
|
@ -6,8 +6,8 @@ import attr
|
|||
import pyblish.api
|
||||
|
||||
from openpype.settings import get_project_settings
|
||||
from openpype.lib import abstract_collect_render
|
||||
from openpype.lib.abstract_collect_render import RenderInstance
|
||||
from openpype.pipeline import publish
|
||||
from openpype.pipeline.publish import RenderInstance
|
||||
|
||||
from openpype.hosts.aftereffects.api import get_stub
|
||||
|
||||
|
|
@ -25,7 +25,7 @@ class AERenderInstance(RenderInstance):
|
|||
file_name = attr.ib(default=None)
|
||||
|
||||
|
||||
class CollectAERender(abstract_collect_render.AbstractCollectRender):
|
||||
class CollectAERender(publish.AbstractCollectRender):
|
||||
|
||||
order = pyblish.api.CollectorOrder + 0.405
|
||||
label = "Collect After Effects Render Layers"
|
||||
|
|
|
|||
|
|
@ -4,11 +4,10 @@ from pathlib import Path
|
|||
|
||||
import attr
|
||||
|
||||
import openpype.lib
|
||||
import openpype.lib.abstract_collect_render
|
||||
from openpype.lib.abstract_collect_render import RenderInstance
|
||||
from openpype.lib import get_formatted_current_time
|
||||
from openpype.pipeline import legacy_io
|
||||
from openpype.pipeline import publish
|
||||
from openpype.pipeline.publish import RenderInstance
|
||||
import openpype.hosts.harmony.api as harmony
|
||||
|
||||
|
||||
|
|
@ -20,8 +19,7 @@ class HarmonyRenderInstance(RenderInstance):
|
|||
leadingZeros = attr.ib(default=3)
|
||||
|
||||
|
||||
class CollectFarmRender(openpype.lib.abstract_collect_render.
|
||||
AbstractCollectRender):
|
||||
class CollectFarmRender(publish.AbstractCollectRender):
|
||||
"""Gather all publishable renders."""
|
||||
|
||||
# https://docs.toonboom.com/help/harmony-17/premium/reference/node/output/write-node-image-formats.html
|
||||
|
|
|
|||
|
|
@ -1,269 +1,33 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Collect render template.
|
||||
"""Content was moved to 'openpype.pipeline.publish.abstract_collect_render'.
|
||||
|
||||
TODO: use @dataclass when times come.
|
||||
Please change your imports as soon as possible.
|
||||
|
||||
File will be probably removed in OpenPype 3.14.*
|
||||
"""
|
||||
from abc import abstractmethod
|
||||
|
||||
import attr
|
||||
import six
|
||||
|
||||
import pyblish.api
|
||||
|
||||
from openpype.pipeline import legacy_io
|
||||
|
||||
from .abstract_metaplugins import AbstractMetaContextPlugin
|
||||
import warnings
|
||||
from openpype.pipeline.publish import AbstractCollectRender, RenderInstance
|
||||
|
||||
|
||||
@attr.s
|
||||
class RenderInstance(object):
|
||||
"""Data collected by collectors.
|
||||
|
||||
This data class later on passed to collected instances.
|
||||
Those attributes are required later on.
|
||||
|
||||
"""
|
||||
|
||||
# metadata
|
||||
version = attr.ib() # instance version
|
||||
time = attr.ib() # time of instance creation (get_formatted_current_time)
|
||||
source = attr.ib() # path to source scene file
|
||||
label = attr.ib() # label to show in GUI
|
||||
subset = attr.ib() # subset name
|
||||
task = attr.ib() # task name
|
||||
asset = attr.ib() # asset name (AVALON_ASSET)
|
||||
attachTo = attr.ib() # subset name to attach render to
|
||||
setMembers = attr.ib() # list of nodes/members producing render output
|
||||
publish = attr.ib() # bool, True to publish instance
|
||||
name = attr.ib() # instance name
|
||||
|
||||
# format settings
|
||||
resolutionWidth = attr.ib() # resolution width (1920)
|
||||
resolutionHeight = attr.ib() # resolution height (1080)
|
||||
pixelAspect = attr.ib() # pixel aspect (1.0)
|
||||
|
||||
# time settings
|
||||
frameStart = attr.ib() # start frame
|
||||
frameEnd = attr.ib() # start end
|
||||
frameStep = attr.ib() # frame step
|
||||
|
||||
handleStart = attr.ib(default=None) # start frame
|
||||
handleEnd = attr.ib(default=None) # start frame
|
||||
|
||||
# for software (like Harmony) where frame range cannot be set by DB
|
||||
# handles need to be propagated if exist
|
||||
ignoreFrameHandleCheck = attr.ib(default=False)
|
||||
|
||||
# --------------------
|
||||
# With default values
|
||||
# metadata
|
||||
renderer = attr.ib(default="") # renderer - can be used in Deadline
|
||||
review = attr.ib(default=False) # generate review from instance (bool)
|
||||
priority = attr.ib(default=50) # job priority on farm
|
||||
|
||||
family = attr.ib(default="renderlayer")
|
||||
families = attr.ib(default=["renderlayer"]) # list of families
|
||||
|
||||
# format settings
|
||||
multipartExr = attr.ib(default=False) # flag for multipart exrs
|
||||
convertToScanline = attr.ib(default=False) # flag for exr conversion
|
||||
|
||||
tileRendering = attr.ib(default=False) # bool: treat render as tiles
|
||||
tilesX = attr.ib(default=0) # number of tiles in X
|
||||
tilesY = attr.ib(default=0) # number of tiles in Y
|
||||
|
||||
# submit_publish_job
|
||||
toBeRenderedOn = attr.ib(default=None)
|
||||
deadlineSubmissionJob = attr.ib(default=None)
|
||||
anatomyData = attr.ib(default=None)
|
||||
outputDir = attr.ib(default=None)
|
||||
context = attr.ib(default=None)
|
||||
|
||||
@frameStart.validator
|
||||
def check_frame_start(self, _, value):
|
||||
"""Validate if frame start is not larger then end."""
|
||||
if value > self.frameEnd:
|
||||
raise ValueError("frameStart must be smaller "
|
||||
"or equal then frameEnd")
|
||||
|
||||
@frameEnd.validator
|
||||
def check_frame_end(self, _, value):
|
||||
"""Validate if frame end is not less then start."""
|
||||
if value < self.frameStart:
|
||||
raise ValueError("frameEnd must be smaller "
|
||||
"or equal then frameStart")
|
||||
|
||||
@tilesX.validator
|
||||
def check_tiles_x(self, _, value):
|
||||
"""Validate if tile x isn't less then 1."""
|
||||
if not self.tileRendering:
|
||||
return
|
||||
if value < 1:
|
||||
raise ValueError("tile X size cannot be less then 1")
|
||||
|
||||
if value == 1 and self.tilesY == 1:
|
||||
raise ValueError("both tiles X a Y sizes are set to 1")
|
||||
|
||||
@tilesY.validator
|
||||
def check_tiles_y(self, _, value):
|
||||
"""Validate if tile y isn't less then 1."""
|
||||
if not self.tileRendering:
|
||||
return
|
||||
if value < 1:
|
||||
raise ValueError("tile Y size cannot be less then 1")
|
||||
|
||||
if value == 1 and self.tilesX == 1:
|
||||
raise ValueError("both tiles X a Y sizes are set to 1")
|
||||
class CollectRenderDeprecated(DeprecationWarning):
|
||||
pass
|
||||
|
||||
|
||||
@six.add_metaclass(AbstractMetaContextPlugin)
|
||||
class AbstractCollectRender(pyblish.api.ContextPlugin):
|
||||
"""Gather all publishable render layers from renderSetup."""
|
||||
warnings.simplefilter("always", CollectRenderDeprecated)
|
||||
warnings.warn(
|
||||
(
|
||||
"Content of 'abstract_collect_render' was moved."
|
||||
"\nUsing deprecated source of 'abstract_collect_render'. Content was"
|
||||
" move to 'openpype.pipeline.publish.abstract_collect_render'."
|
||||
" Please change your imports as soon as possible."
|
||||
),
|
||||
category=CollectRenderDeprecated,
|
||||
stacklevel=4
|
||||
)
|
||||
|
||||
order = pyblish.api.CollectorOrder + 0.01
|
||||
label = "Collect Render"
|
||||
sync_workfile_version = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Constructor."""
|
||||
super(AbstractCollectRender, self).__init__(*args, **kwargs)
|
||||
self._file_path = None
|
||||
self._asset = legacy_io.Session["AVALON_ASSET"]
|
||||
self._context = None
|
||||
|
||||
def process(self, context):
|
||||
"""Entry point to collector."""
|
||||
self._context = context
|
||||
for instance in context:
|
||||
# make sure workfile instance publishing is enabled
|
||||
try:
|
||||
if "workfile" in instance.data["families"]:
|
||||
instance.data["publish"] = True
|
||||
# TODO merge renderFarm and render.farm
|
||||
if ("renderFarm" in instance.data["families"] or
|
||||
"render.farm" in instance.data["families"]):
|
||||
instance.data["remove"] = True
|
||||
except KeyError:
|
||||
# be tolerant if 'families' is missing.
|
||||
pass
|
||||
|
||||
self._file_path = context.data["currentFile"].replace("\\", "/")
|
||||
|
||||
render_instances = self.get_instances(context)
|
||||
for render_instance in render_instances:
|
||||
exp_files = self.get_expected_files(render_instance)
|
||||
assert exp_files, "no file names were generated, this is bug"
|
||||
|
||||
# if we want to attach render to subset, check if we have AOV's
|
||||
# in expectedFiles. If so, raise error as we cannot attach AOV
|
||||
# (considered to be subset on its own) to another subset
|
||||
if render_instance.attachTo:
|
||||
assert isinstance(exp_files, list), (
|
||||
"attaching multiple AOVs or renderable cameras to "
|
||||
"subset is not supported"
|
||||
)
|
||||
|
||||
frame_start_render = int(render_instance.frameStart)
|
||||
frame_end_render = int(render_instance.frameEnd)
|
||||
if (render_instance.ignoreFrameHandleCheck or
|
||||
int(context.data['frameStartHandle']) == frame_start_render
|
||||
and int(context.data['frameEndHandle']) == frame_end_render): # noqa: W503, E501
|
||||
|
||||
handle_start = context.data['handleStart']
|
||||
handle_end = context.data['handleEnd']
|
||||
frame_start = context.data['frameStart']
|
||||
frame_end = context.data['frameEnd']
|
||||
frame_start_handle = context.data['frameStartHandle']
|
||||
frame_end_handle = context.data['frameEndHandle']
|
||||
else:
|
||||
handle_start = 0
|
||||
handle_end = 0
|
||||
frame_start = frame_start_render
|
||||
frame_end = frame_end_render
|
||||
frame_start_handle = frame_start_render
|
||||
frame_end_handle = frame_end_render
|
||||
|
||||
data = {
|
||||
"handleStart": handle_start,
|
||||
"handleEnd": handle_end,
|
||||
"frameStart": frame_start,
|
||||
"frameEnd": frame_end,
|
||||
"frameStartHandle": frame_start_handle,
|
||||
"frameEndHandle": frame_end_handle,
|
||||
"byFrameStep": int(render_instance.frameStep),
|
||||
|
||||
"author": context.data["user"],
|
||||
# Add source to allow tracing back to the scene from
|
||||
# which was submitted originally
|
||||
"expectedFiles": exp_files,
|
||||
}
|
||||
if self.sync_workfile_version:
|
||||
data["version"] = context.data["version"]
|
||||
|
||||
# add additional data
|
||||
data = self.add_additional_data(data)
|
||||
render_instance_dict = attr.asdict(render_instance)
|
||||
|
||||
instance = context.create_instance(render_instance.name)
|
||||
instance.data["label"] = render_instance.label
|
||||
instance.data.update(render_instance_dict)
|
||||
instance.data.update(data)
|
||||
|
||||
self.post_collecting_action()
|
||||
|
||||
@abstractmethod
|
||||
def get_instances(self, context):
|
||||
"""Get all renderable instances and their data.
|
||||
|
||||
Args:
|
||||
context (pyblish.api.Context): Context object.
|
||||
|
||||
Returns:
|
||||
list of :class:`RenderInstance`: All collected renderable instances
|
||||
(like render layers, write nodes, etc.)
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_expected_files(self, render_instance):
|
||||
"""Get list of expected files.
|
||||
|
||||
Returns:
|
||||
list: expected files. This can be either simple list of files with
|
||||
their paths, or list of dictionaries, where key is name of AOV
|
||||
for example and value is list of files for that AOV.
|
||||
|
||||
Example::
|
||||
|
||||
['/path/to/file.001.exr', '/path/to/file.002.exr']
|
||||
|
||||
or as dictionary:
|
||||
|
||||
[
|
||||
{
|
||||
"beauty": ['/path/to/beauty.001.exr', ...],
|
||||
"mask": ['/path/to/mask.001.exr']
|
||||
}
|
||||
]
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
def add_additional_data(self, data):
|
||||
"""Add additional data to collected instance.
|
||||
|
||||
This can be overridden by host implementation to add custom
|
||||
additional data.
|
||||
|
||||
"""
|
||||
return data
|
||||
|
||||
def post_collecting_action(self):
|
||||
"""Execute some code after collection is done.
|
||||
|
||||
This is useful for example for restoring current render layer.
|
||||
|
||||
"""
|
||||
pass
|
||||
__all__ = (
|
||||
"AbstractCollectRender",
|
||||
"RenderInstance"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,53 +1,32 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Abstract ExpectedFile class definition."""
|
||||
from abc import ABCMeta, abstractmethod
|
||||
import six
|
||||
"""Content was moved to 'openpype.pipeline.publish.abstract_expected_files'.
|
||||
|
||||
Please change your imports as soon as possible.
|
||||
|
||||
File will be probably removed in OpenPype 3.14.*
|
||||
"""
|
||||
|
||||
import warnings
|
||||
from openpype.pipeline.publish import ExpectedFiles
|
||||
|
||||
|
||||
@six.add_metaclass(ABCMeta)
|
||||
class ExpectedFiles:
|
||||
"""Class grouping functionality for all supported renderers.
|
||||
|
||||
Attributes:
|
||||
multipart (bool): Flag if multipart exrs are used.
|
||||
|
||||
"""
|
||||
|
||||
multipart = False
|
||||
|
||||
@abstractmethod
|
||||
def get(self, render_instance):
|
||||
"""Get expected files for given renderer and render layer.
|
||||
|
||||
This method should return dictionary of all files we are expecting
|
||||
to be rendered from the host. Usually `render_instance` corresponds
|
||||
to *render layer*. Result can be either flat list with the file
|
||||
paths or it can be list of dictionaries. Each key corresponds to
|
||||
for example AOV name or channel, etc.
|
||||
|
||||
Example::
|
||||
|
||||
['/path/to/file.001.exr', '/path/to/file.002.exr']
|
||||
|
||||
or as dictionary:
|
||||
|
||||
[
|
||||
{
|
||||
"beauty": ['/path/to/beauty.001.exr', ...],
|
||||
"mask": ['/path/to/mask.001.exr']
|
||||
}
|
||||
]
|
||||
class ExpectedFilesDeprecated(DeprecationWarning):
|
||||
pass
|
||||
|
||||
|
||||
Args:
|
||||
render_instance (:class:`RenderInstance`): Data passed from
|
||||
collector to determine files. This should be instance of
|
||||
:class:`abstract_collect_render.RenderInstance`
|
||||
warnings.simplefilter("always", ExpectedFilesDeprecated)
|
||||
warnings.warn(
|
||||
(
|
||||
"Content of 'abstract_expected_files' was moved."
|
||||
"\nUsing deprecated source of 'abstract_expected_files'. Content was"
|
||||
" move to 'openpype.pipeline.publish.abstract_expected_files'."
|
||||
" Please change your imports as soon as possible."
|
||||
),
|
||||
category=ExpectedFilesDeprecated,
|
||||
stacklevel=4
|
||||
)
|
||||
|
||||
Returns:
|
||||
list: Full paths to expected rendered files.
|
||||
list of dict: Path to expected rendered files categorized by
|
||||
AOVs, etc.
|
||||
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
__all__ = (
|
||||
"ExpectedFiles",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,35 @@
|
|||
from abc import ABCMeta
|
||||
from pyblish.plugin import MetaPlugin, ExplicitMetaPlugin
|
||||
"""Content was moved to 'openpype.pipeline.publish.publish_plugins'.
|
||||
|
||||
Please change your imports as soon as possible.
|
||||
|
||||
File will be probably removed in OpenPype 3.14.*
|
||||
"""
|
||||
|
||||
import warnings
|
||||
from openpype.pipeline.publish import (
|
||||
AbstractMetaInstancePlugin,
|
||||
AbstractMetaContextPlugin
|
||||
)
|
||||
|
||||
|
||||
class AbstractMetaInstancePlugin(ABCMeta, MetaPlugin):
|
||||
class MetaPluginsDeprecated(DeprecationWarning):
|
||||
pass
|
||||
|
||||
|
||||
class AbstractMetaContextPlugin(ABCMeta, ExplicitMetaPlugin):
|
||||
pass
|
||||
warnings.simplefilter("always", MetaPluginsDeprecated)
|
||||
warnings.warn(
|
||||
(
|
||||
"Content of 'abstract_metaplugins' was moved."
|
||||
"\nUsing deprecated source of 'abstract_metaplugins'. Content was"
|
||||
" moved to 'openpype.pipeline.publish.publish_plugins'."
|
||||
" Please change your imports as soon as possible."
|
||||
),
|
||||
category=MetaPluginsDeprecated,
|
||||
stacklevel=4
|
||||
)
|
||||
|
||||
|
||||
__all__ = (
|
||||
"AbstractMetaInstancePlugin",
|
||||
"AbstractMetaContextPlugin",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -665,7 +665,11 @@ class ApplicationExecutable:
|
|||
if os.path.exists(plist_filepath):
|
||||
import plistlib
|
||||
|
||||
parsed_plist = plistlib.readPlist(plist_filepath)
|
||||
if hasattr(plistlib, "load"):
|
||||
with open(plist_filepath, "rb") as stream:
|
||||
parsed_plist = plistlib.load(stream)
|
||||
else:
|
||||
parsed_plist = plistlib.readPlist(plist_filepath)
|
||||
executable_filename = parsed_plist.get("CFBundleExecutable")
|
||||
|
||||
if executable_filename:
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import attr
|
|||
import requests
|
||||
|
||||
import pyblish.api
|
||||
from openpype.lib.abstract_metaplugins import AbstractMetaInstancePlugin
|
||||
from openpype.pipeline.publish import AbstractMetaInstancePlugin
|
||||
|
||||
|
||||
def requests_post(*args, **kwargs):
|
||||
|
|
|
|||
|
|
@ -496,6 +496,20 @@ class CreatedInstance:
|
|||
def subset_name(self):
|
||||
return self._data["subset"]
|
||||
|
||||
@property
|
||||
def label(self):
|
||||
label = self._data.get("label")
|
||||
if not label:
|
||||
label = self.subset_name
|
||||
return label
|
||||
|
||||
@property
|
||||
def group_label(self):
|
||||
label = self._data.get("group")
|
||||
if label:
|
||||
return label
|
||||
return self.creator.get_group_label()
|
||||
|
||||
@property
|
||||
def creator_identifier(self):
|
||||
return self.creator.identifier
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import copy
|
||||
import logging
|
||||
|
||||
from abc import (
|
||||
ABCMeta,
|
||||
|
|
@ -47,6 +46,9 @@ class BaseCreator:
|
|||
|
||||
# Label shown in UI
|
||||
label = None
|
||||
group_label = None
|
||||
# Cached group label after first call 'get_group_label'
|
||||
_cached_group_label = None
|
||||
|
||||
# Variable to store logger
|
||||
_log = None
|
||||
|
|
@ -85,11 +87,13 @@ class BaseCreator:
|
|||
|
||||
Default implementation returns plugin's family.
|
||||
"""
|
||||
|
||||
return self.family
|
||||
|
||||
@abstractproperty
|
||||
def family(self):
|
||||
"""Family that plugin represents."""
|
||||
|
||||
pass
|
||||
|
||||
@property
|
||||
|
|
@ -98,8 +102,35 @@ class BaseCreator:
|
|||
|
||||
return self.create_context.project_name
|
||||
|
||||
def get_group_label(self):
|
||||
"""Group label under which are instances grouped in UI.
|
||||
|
||||
Default implementation use attributes in this order:
|
||||
- 'group_label' -> 'label' -> 'identifier'
|
||||
Keep in mind that 'identifier' use 'family' by default.
|
||||
|
||||
Returns:
|
||||
str: Group label that can be used for grouping of instances in UI.
|
||||
Group label can be overriden by instance itself.
|
||||
"""
|
||||
|
||||
if self._cached_group_label is None:
|
||||
label = self.identifier
|
||||
if self.group_label:
|
||||
label = self.group_label
|
||||
elif self.label:
|
||||
label = self.label
|
||||
self._cached_group_label = label
|
||||
return self._cached_group_label
|
||||
|
||||
@property
|
||||
def log(self):
|
||||
"""Logger of the plugin.
|
||||
|
||||
Returns:
|
||||
logging.Logger: Logger with name of the plugin.
|
||||
"""
|
||||
|
||||
if self._log is None:
|
||||
from openpype.api import Logger
|
||||
|
||||
|
|
@ -107,10 +138,30 @@ class BaseCreator:
|
|||
return self._log
|
||||
|
||||
def _add_instance_to_context(self, instance):
|
||||
"""Helper method to ad d"""
|
||||
"""Helper method to add instance to create context.
|
||||
|
||||
Instances should be stored to DCC workfile metadata to be able reload
|
||||
them and also stored to CreateContext in which is creator plugin
|
||||
existing at the moment to be able use it without refresh of
|
||||
CreateContext.
|
||||
|
||||
Args:
|
||||
instance (CreatedInstance): New created instance.
|
||||
"""
|
||||
|
||||
self.create_context.creator_adds_instance(instance)
|
||||
|
||||
def _remove_instance_from_context(self, instance):
|
||||
"""Helper method to remove instance from create context.
|
||||
|
||||
Instances must be removed from DCC workfile metadat aand from create
|
||||
context in which plugin is existing at the moment of removement to
|
||||
propagate the change without restarting create context.
|
||||
|
||||
Args:
|
||||
instance (CreatedInstance): Instance which should be removed.
|
||||
"""
|
||||
|
||||
self.create_context.creator_removed_instance(instance)
|
||||
|
||||
@abstractmethod
|
||||
|
|
@ -121,6 +172,7 @@ class BaseCreator:
|
|||
- must expect all data that were passed to init in previous
|
||||
implementation
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
|
|
@ -147,6 +199,7 @@ class BaseCreator:
|
|||
self._add_instance_to_context(instance)
|
||||
```
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
|
|
@ -154,9 +207,10 @@ class BaseCreator:
|
|||
"""Store changes of existing instances so they can be recollected.
|
||||
|
||||
Args:
|
||||
update_list(list<UpdateData>): Gets list of tuples. Each item
|
||||
update_list(List[UpdateData]): Gets list of tuples. Each item
|
||||
contain changed instance and it's changes.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
|
|
@ -167,9 +221,10 @@ class BaseCreator:
|
|||
'True' if did so.
|
||||
|
||||
Args:
|
||||
instance(list<CreatedInstance>): Instance objects which should be
|
||||
instance(List[CreatedInstance]): Instance objects which should be
|
||||
removed.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
def get_icon(self):
|
||||
|
|
@ -177,6 +232,7 @@ class BaseCreator:
|
|||
|
||||
Can return path to image file or awesome icon name.
|
||||
"""
|
||||
|
||||
return self.icon
|
||||
|
||||
def get_dynamic_data(
|
||||
|
|
@ -187,6 +243,7 @@ class BaseCreator:
|
|||
These may be get dynamically created based on current context of
|
||||
workfile.
|
||||
"""
|
||||
|
||||
return {}
|
||||
|
||||
def get_subset_name(
|
||||
|
|
@ -211,6 +268,7 @@ class BaseCreator:
|
|||
project_name(str): Project name.
|
||||
host_name(str): Which host creates subset.
|
||||
"""
|
||||
|
||||
dynamic_data = self.get_dynamic_data(
|
||||
variant, task_name, asset_doc, project_name, host_name
|
||||
)
|
||||
|
|
@ -237,9 +295,10 @@ class BaseCreator:
|
|||
keys/values when plugin attributes change.
|
||||
|
||||
Returns:
|
||||
list<AbtractAttrDef>: Attribute definitions that can be tweaked for
|
||||
List[AbtractAttrDef]: Attribute definitions that can be tweaked for
|
||||
created instance.
|
||||
"""
|
||||
|
||||
return self.instance_attr_defs
|
||||
|
||||
|
||||
|
|
@ -297,6 +356,7 @@ class Creator(BaseCreator):
|
|||
Returns:
|
||||
str: Short description of family.
|
||||
"""
|
||||
|
||||
return self.description
|
||||
|
||||
def get_detail_description(self):
|
||||
|
|
@ -307,6 +367,7 @@ class Creator(BaseCreator):
|
|||
Returns:
|
||||
str: Detailed description of family for artist.
|
||||
"""
|
||||
|
||||
return self.detailed_description
|
||||
|
||||
def get_default_variants(self):
|
||||
|
|
@ -318,8 +379,9 @@ class Creator(BaseCreator):
|
|||
By default returns `default_variants` value.
|
||||
|
||||
Returns:
|
||||
list<str>: Whisper variants for user input.
|
||||
List[str]: Whisper variants for user input.
|
||||
"""
|
||||
|
||||
return copy.deepcopy(self.default_variants)
|
||||
|
||||
def get_default_variant(self):
|
||||
|
|
@ -338,11 +400,13 @@ class Creator(BaseCreator):
|
|||
"""Plugin attribute definitions needed for creation.
|
||||
Attribute definitions of plugin that define how creation will work.
|
||||
Values of these definitions are passed to `create` method.
|
||||
NOTE:
|
||||
Convert method should be implemented which should care about updating
|
||||
keys/values when plugin attributes change.
|
||||
|
||||
Note:
|
||||
Convert method should be implemented which should care about
|
||||
updating keys/values when plugin attributes change.
|
||||
|
||||
Returns:
|
||||
list<AbtractAttrDef>: Attribute definitions that can be tweaked for
|
||||
List[AbtractAttrDef]: Attribute definitions that can be tweaked for
|
||||
created instance.
|
||||
"""
|
||||
return self.pre_create_attr_defs
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
from .publish_plugins import (
|
||||
AbstractMetaInstancePlugin,
|
||||
AbstractMetaContextPlugin,
|
||||
|
||||
PublishValidationError,
|
||||
PublishXmlValidationError,
|
||||
KnownPublishError,
|
||||
|
|
@ -13,8 +16,17 @@ from .lib import (
|
|||
load_help_content_from_filepath,
|
||||
)
|
||||
|
||||
from .abstract_expected_files import ExpectedFiles
|
||||
from .abstract_collect_render import (
|
||||
RenderInstance,
|
||||
AbstractCollectRender,
|
||||
)
|
||||
|
||||
|
||||
__all__ = (
|
||||
"AbstractMetaInstancePlugin",
|
||||
"AbstractMetaContextPlugin",
|
||||
|
||||
"PublishValidationError",
|
||||
"PublishXmlValidationError",
|
||||
"KnownPublishError",
|
||||
|
|
@ -25,4 +37,9 @@ __all__ = (
|
|||
"publish_plugins_discover",
|
||||
"load_help_content_from_plugin",
|
||||
"load_help_content_from_filepath",
|
||||
|
||||
"ExpectedFiles",
|
||||
|
||||
"RenderInstance",
|
||||
"AbstractCollectRender",
|
||||
)
|
||||
|
|
|
|||
268
openpype/pipeline/publish/abstract_collect_render.py
Normal file
268
openpype/pipeline/publish/abstract_collect_render.py
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Collect render template.
|
||||
|
||||
TODO: use @dataclass when times come.
|
||||
|
||||
"""
|
||||
from abc import abstractmethod
|
||||
|
||||
import attr
|
||||
import six
|
||||
|
||||
import pyblish.api
|
||||
|
||||
from openpype.pipeline import legacy_io
|
||||
from .publish_plugins import AbstractMetaContextPlugin
|
||||
|
||||
|
||||
@attr.s
|
||||
class RenderInstance(object):
|
||||
"""Data collected by collectors.
|
||||
|
||||
This data class later on passed to collected instances.
|
||||
Those attributes are required later on.
|
||||
|
||||
"""
|
||||
|
||||
# metadata
|
||||
version = attr.ib() # instance version
|
||||
time = attr.ib() # time of instance creation (get_formatted_current_time)
|
||||
source = attr.ib() # path to source scene file
|
||||
label = attr.ib() # label to show in GUI
|
||||
subset = attr.ib() # subset name
|
||||
task = attr.ib() # task name
|
||||
asset = attr.ib() # asset name (AVALON_ASSET)
|
||||
attachTo = attr.ib() # subset name to attach render to
|
||||
setMembers = attr.ib() # list of nodes/members producing render output
|
||||
publish = attr.ib() # bool, True to publish instance
|
||||
name = attr.ib() # instance name
|
||||
|
||||
# format settings
|
||||
resolutionWidth = attr.ib() # resolution width (1920)
|
||||
resolutionHeight = attr.ib() # resolution height (1080)
|
||||
pixelAspect = attr.ib() # pixel aspect (1.0)
|
||||
|
||||
# time settings
|
||||
frameStart = attr.ib() # start frame
|
||||
frameEnd = attr.ib() # start end
|
||||
frameStep = attr.ib() # frame step
|
||||
|
||||
handleStart = attr.ib(default=None) # start frame
|
||||
handleEnd = attr.ib(default=None) # start frame
|
||||
|
||||
# for software (like Harmony) where frame range cannot be set by DB
|
||||
# handles need to be propagated if exist
|
||||
ignoreFrameHandleCheck = attr.ib(default=False)
|
||||
|
||||
# --------------------
|
||||
# With default values
|
||||
# metadata
|
||||
renderer = attr.ib(default="") # renderer - can be used in Deadline
|
||||
review = attr.ib(default=False) # generate review from instance (bool)
|
||||
priority = attr.ib(default=50) # job priority on farm
|
||||
|
||||
family = attr.ib(default="renderlayer")
|
||||
families = attr.ib(default=["renderlayer"]) # list of families
|
||||
|
||||
# format settings
|
||||
multipartExr = attr.ib(default=False) # flag for multipart exrs
|
||||
convertToScanline = attr.ib(default=False) # flag for exr conversion
|
||||
|
||||
tileRendering = attr.ib(default=False) # bool: treat render as tiles
|
||||
tilesX = attr.ib(default=0) # number of tiles in X
|
||||
tilesY = attr.ib(default=0) # number of tiles in Y
|
||||
|
||||
# submit_publish_job
|
||||
toBeRenderedOn = attr.ib(default=None)
|
||||
deadlineSubmissionJob = attr.ib(default=None)
|
||||
anatomyData = attr.ib(default=None)
|
||||
outputDir = attr.ib(default=None)
|
||||
context = attr.ib(default=None)
|
||||
|
||||
@frameStart.validator
|
||||
def check_frame_start(self, _, value):
|
||||
"""Validate if frame start is not larger then end."""
|
||||
if value > self.frameEnd:
|
||||
raise ValueError("frameStart must be smaller "
|
||||
"or equal then frameEnd")
|
||||
|
||||
@frameEnd.validator
|
||||
def check_frame_end(self, _, value):
|
||||
"""Validate if frame end is not less then start."""
|
||||
if value < self.frameStart:
|
||||
raise ValueError("frameEnd must be smaller "
|
||||
"or equal then frameStart")
|
||||
|
||||
@tilesX.validator
|
||||
def check_tiles_x(self, _, value):
|
||||
"""Validate if tile x isn't less then 1."""
|
||||
if not self.tileRendering:
|
||||
return
|
||||
if value < 1:
|
||||
raise ValueError("tile X size cannot be less then 1")
|
||||
|
||||
if value == 1 and self.tilesY == 1:
|
||||
raise ValueError("both tiles X a Y sizes are set to 1")
|
||||
|
||||
@tilesY.validator
|
||||
def check_tiles_y(self, _, value):
|
||||
"""Validate if tile y isn't less then 1."""
|
||||
if not self.tileRendering:
|
||||
return
|
||||
if value < 1:
|
||||
raise ValueError("tile Y size cannot be less then 1")
|
||||
|
||||
if value == 1 and self.tilesX == 1:
|
||||
raise ValueError("both tiles X a Y sizes are set to 1")
|
||||
|
||||
|
||||
@six.add_metaclass(AbstractMetaContextPlugin)
|
||||
class AbstractCollectRender(pyblish.api.ContextPlugin):
|
||||
"""Gather all publishable render layers from renderSetup."""
|
||||
|
||||
order = pyblish.api.CollectorOrder + 0.01
|
||||
label = "Collect Render"
|
||||
sync_workfile_version = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Constructor."""
|
||||
super(AbstractCollectRender, self).__init__(*args, **kwargs)
|
||||
self._file_path = None
|
||||
self._asset = legacy_io.Session["AVALON_ASSET"]
|
||||
self._context = None
|
||||
|
||||
def process(self, context):
|
||||
"""Entry point to collector."""
|
||||
self._context = context
|
||||
for instance in context:
|
||||
# make sure workfile instance publishing is enabled
|
||||
try:
|
||||
if "workfile" in instance.data["families"]:
|
||||
instance.data["publish"] = True
|
||||
# TODO merge renderFarm and render.farm
|
||||
if ("renderFarm" in instance.data["families"] or
|
||||
"render.farm" in instance.data["families"]):
|
||||
instance.data["remove"] = True
|
||||
except KeyError:
|
||||
# be tolerant if 'families' is missing.
|
||||
pass
|
||||
|
||||
self._file_path = context.data["currentFile"].replace("\\", "/")
|
||||
|
||||
render_instances = self.get_instances(context)
|
||||
for render_instance in render_instances:
|
||||
exp_files = self.get_expected_files(render_instance)
|
||||
assert exp_files, "no file names were generated, this is bug"
|
||||
|
||||
# if we want to attach render to subset, check if we have AOV's
|
||||
# in expectedFiles. If so, raise error as we cannot attach AOV
|
||||
# (considered to be subset on its own) to another subset
|
||||
if render_instance.attachTo:
|
||||
assert isinstance(exp_files, list), (
|
||||
"attaching multiple AOVs or renderable cameras to "
|
||||
"subset is not supported"
|
||||
)
|
||||
|
||||
frame_start_render = int(render_instance.frameStart)
|
||||
frame_end_render = int(render_instance.frameEnd)
|
||||
if (render_instance.ignoreFrameHandleCheck or
|
||||
int(context.data['frameStartHandle']) == frame_start_render
|
||||
and int(context.data['frameEndHandle']) == frame_end_render): # noqa: W503, E501
|
||||
|
||||
handle_start = context.data['handleStart']
|
||||
handle_end = context.data['handleEnd']
|
||||
frame_start = context.data['frameStart']
|
||||
frame_end = context.data['frameEnd']
|
||||
frame_start_handle = context.data['frameStartHandle']
|
||||
frame_end_handle = context.data['frameEndHandle']
|
||||
else:
|
||||
handle_start = 0
|
||||
handle_end = 0
|
||||
frame_start = frame_start_render
|
||||
frame_end = frame_end_render
|
||||
frame_start_handle = frame_start_render
|
||||
frame_end_handle = frame_end_render
|
||||
|
||||
data = {
|
||||
"handleStart": handle_start,
|
||||
"handleEnd": handle_end,
|
||||
"frameStart": frame_start,
|
||||
"frameEnd": frame_end,
|
||||
"frameStartHandle": frame_start_handle,
|
||||
"frameEndHandle": frame_end_handle,
|
||||
"byFrameStep": int(render_instance.frameStep),
|
||||
|
||||
"author": context.data["user"],
|
||||
# Add source to allow tracing back to the scene from
|
||||
# which was submitted originally
|
||||
"expectedFiles": exp_files,
|
||||
}
|
||||
if self.sync_workfile_version:
|
||||
data["version"] = context.data["version"]
|
||||
|
||||
# add additional data
|
||||
data = self.add_additional_data(data)
|
||||
render_instance_dict = attr.asdict(render_instance)
|
||||
|
||||
instance = context.create_instance(render_instance.name)
|
||||
instance.data["label"] = render_instance.label
|
||||
instance.data.update(render_instance_dict)
|
||||
instance.data.update(data)
|
||||
|
||||
self.post_collecting_action()
|
||||
|
||||
@abstractmethod
|
||||
def get_instances(self, context):
|
||||
"""Get all renderable instances and their data.
|
||||
|
||||
Args:
|
||||
context (pyblish.api.Context): Context object.
|
||||
|
||||
Returns:
|
||||
list of :class:`RenderInstance`: All collected renderable instances
|
||||
(like render layers, write nodes, etc.)
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_expected_files(self, render_instance):
|
||||
"""Get list of expected files.
|
||||
|
||||
Returns:
|
||||
list: expected files. This can be either simple list of files with
|
||||
their paths, or list of dictionaries, where key is name of AOV
|
||||
for example and value is list of files for that AOV.
|
||||
|
||||
Example::
|
||||
|
||||
['/path/to/file.001.exr', '/path/to/file.002.exr']
|
||||
|
||||
or as dictionary:
|
||||
|
||||
[
|
||||
{
|
||||
"beauty": ['/path/to/beauty.001.exr', ...],
|
||||
"mask": ['/path/to/mask.001.exr']
|
||||
}
|
||||
]
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
def add_additional_data(self, data):
|
||||
"""Add additional data to collected instance.
|
||||
|
||||
This can be overridden by host implementation to add custom
|
||||
additional data.
|
||||
|
||||
"""
|
||||
return data
|
||||
|
||||
def post_collecting_action(self):
|
||||
"""Execute some code after collection is done.
|
||||
|
||||
This is useful for example for restoring current render layer.
|
||||
|
||||
"""
|
||||
pass
|
||||
53
openpype/pipeline/publish/abstract_expected_files.py
Normal file
53
openpype/pipeline/publish/abstract_expected_files.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Abstract ExpectedFile class definition."""
|
||||
from abc import ABCMeta, abstractmethod
|
||||
import six
|
||||
|
||||
|
||||
@six.add_metaclass(ABCMeta)
|
||||
class ExpectedFiles:
|
||||
"""Class grouping functionality for all supported renderers.
|
||||
|
||||
Attributes:
|
||||
multipart (bool): Flag if multipart exrs are used.
|
||||
|
||||
"""
|
||||
|
||||
multipart = False
|
||||
|
||||
@abstractmethod
|
||||
def get(self, render_instance):
|
||||
"""Get expected files for given renderer and render layer.
|
||||
|
||||
This method should return dictionary of all files we are expecting
|
||||
to be rendered from the host. Usually `render_instance` corresponds
|
||||
to *render layer*. Result can be either flat list with the file
|
||||
paths or it can be list of dictionaries. Each key corresponds to
|
||||
for example AOV name or channel, etc.
|
||||
|
||||
Example::
|
||||
|
||||
['/path/to/file.001.exr', '/path/to/file.002.exr']
|
||||
|
||||
or as dictionary:
|
||||
|
||||
[
|
||||
{
|
||||
"beauty": ['/path/to/beauty.001.exr', ...],
|
||||
"mask": ['/path/to/mask.001.exr']
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
Args:
|
||||
render_instance (:class:`RenderInstance`): Data passed from
|
||||
collector to determine files. This should be instance of
|
||||
:class:`abstract_collect_render.RenderInstance`
|
||||
|
||||
Returns:
|
||||
list: Full paths to expected rendered files.
|
||||
list of dict: Path to expected rendered files categorized by
|
||||
AOVs, etc.
|
||||
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
|
@ -1,7 +1,17 @@
|
|||
from abc import ABCMeta
|
||||
from pyblish.plugin import MetaPlugin, ExplicitMetaPlugin
|
||||
from openpype.lib import BoolDef
|
||||
from .lib import load_help_content_from_plugin
|
||||
|
||||
|
||||
class AbstractMetaInstancePlugin(ABCMeta, MetaPlugin):
|
||||
pass
|
||||
|
||||
|
||||
class AbstractMetaContextPlugin(ABCMeta, ExplicitMetaPlugin):
|
||||
pass
|
||||
|
||||
|
||||
class PublishValidationError(Exception):
|
||||
"""Validation error happened during publishing.
|
||||
|
||||
|
|
@ -16,6 +26,7 @@ class PublishValidationError(Exception):
|
|||
description(str): Detailed description of an error. It is possible
|
||||
to use Markdown syntax.
|
||||
"""
|
||||
|
||||
def __init__(self, message, title=None, description=None, detail=None):
|
||||
self.message = message
|
||||
self.title = title or "< Missing title >"
|
||||
|
|
@ -49,6 +60,7 @@ class KnownPublishError(Exception):
|
|||
|
||||
Message will be shown in UI for artist.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
|
|
@ -92,6 +104,7 @@ class OpenPypePyblishPluginMixin:
|
|||
Returns:
|
||||
list<AbtractAttrDef>: Attribute definitions for plugin.
|
||||
"""
|
||||
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
|
|
@ -116,6 +129,7 @@ class OpenPypePyblishPluginMixin:
|
|||
Args:
|
||||
data(dict): Data from instance or context.
|
||||
"""
|
||||
|
||||
return (
|
||||
data
|
||||
.get("publish_attributes", {})
|
||||
|
|
|
|||
|
|
@ -447,7 +447,22 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
|
||||
input_is_sequence = self.input_is_sequence(repre)
|
||||
input_allow_bg = False
|
||||
first_sequence_frame = None
|
||||
if input_is_sequence and repre["files"]:
|
||||
# Calculate first frame that should be used
|
||||
cols, _ = clique.assemble(repre["files"])
|
||||
input_frames = list(sorted(cols[0].indexes))
|
||||
first_sequence_frame = input_frames[0]
|
||||
# WARNING: This is an issue as we don't know if first frame
|
||||
# is with or without handles!
|
||||
# - handle start is added but how do not know if we should
|
||||
output_duration = (output_frame_end - output_frame_start) + 1
|
||||
if (
|
||||
without_handles
|
||||
and len(input_frames) - handle_start >= output_duration
|
||||
):
|
||||
first_sequence_frame += handle_start
|
||||
|
||||
ext = os.path.splitext(repre["files"][0])[1].replace(".", "")
|
||||
if ext in self.alpha_exts:
|
||||
input_allow_bg = True
|
||||
|
|
@ -467,6 +482,7 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
"resolution_height": instance.data.get("resolutionHeight"),
|
||||
"origin_repre": repre,
|
||||
"input_is_sequence": input_is_sequence,
|
||||
"first_sequence_frame": first_sequence_frame,
|
||||
"input_allow_bg": input_allow_bg,
|
||||
"with_audio": with_audio,
|
||||
"without_handles": without_handles,
|
||||
|
|
@ -545,9 +561,9 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
if temp_data["input_is_sequence"]:
|
||||
# Set start frame of input sequence (just frame in filename)
|
||||
# - definition of input filepath
|
||||
ffmpeg_input_args.append(
|
||||
"-start_number {}".format(temp_data["output_frame_start"])
|
||||
)
|
||||
ffmpeg_input_args.extend([
|
||||
"-start_number", str(temp_data["first_sequence_frame"])
|
||||
])
|
||||
|
||||
# TODO add fps mapping `{fps: fraction}` ?
|
||||
# - e.g.: {
|
||||
|
|
|
|||
|
|
@ -303,13 +303,14 @@ class InstanceCardWidget(CardWidget):
|
|||
self._last_variant = variant
|
||||
self._last_subset_name = subset_name
|
||||
# Make `variant` bold
|
||||
found_parts = set(re.findall(variant, subset_name, re.IGNORECASE))
|
||||
label = self.instance.label
|
||||
found_parts = set(re.findall(variant, label, re.IGNORECASE))
|
||||
if found_parts:
|
||||
for part in found_parts:
|
||||
replacement = "<b>{}</b>".format(part)
|
||||
subset_name = subset_name.replace(part, replacement)
|
||||
label = label.replace(part, replacement)
|
||||
|
||||
self._label_widget.setText(subset_name)
|
||||
self._label_widget.setText(label)
|
||||
# HTML text will cause that label start catch mouse clicks
|
||||
# - disabling with changing interaction flag
|
||||
self._label_widget.setTextInteractionFlags(
|
||||
|
|
@ -435,7 +436,7 @@ class InstanceCardView(AbstractInstanceView):
|
|||
instances_by_group = collections.defaultdict(list)
|
||||
identifiers_by_group = collections.defaultdict(set)
|
||||
for instance in self.controller.instances:
|
||||
group_name = instance.creator_label
|
||||
group_name = instance.group_label
|
||||
instances_by_group[group_name].append(instance)
|
||||
identifiers_by_group[group_name].add(
|
||||
instance.creator_identifier
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ class InstanceListItemWidget(QtWidgets.QWidget):
|
|||
|
||||
self.instance = instance
|
||||
|
||||
subset_name_label = QtWidgets.QLabel(instance["subset"], self)
|
||||
subset_name_label = QtWidgets.QLabel(instance.label, self)
|
||||
subset_name_label.setObjectName("ListViewSubsetName")
|
||||
|
||||
active_checkbox = NiceCheckbox(parent=self)
|
||||
|
|
@ -132,7 +132,7 @@ class InstanceListItemWidget(QtWidgets.QWidget):
|
|||
|
||||
active_checkbox.stateChanged.connect(self._on_active_change)
|
||||
|
||||
self._subset_name_label = subset_name_label
|
||||
self._instance_label_widget = subset_name_label
|
||||
self._active_checkbox = active_checkbox
|
||||
|
||||
self._has_valid_context = None
|
||||
|
|
@ -146,8 +146,8 @@ class InstanceListItemWidget(QtWidgets.QWidget):
|
|||
state = ""
|
||||
if not valid:
|
||||
state = "invalid"
|
||||
self._subset_name_label.setProperty("state", state)
|
||||
self._subset_name_label.style().polish(self._subset_name_label)
|
||||
self._instance_label_widget.setProperty("state", state)
|
||||
self._instance_label_widget.style().polish(self._instance_label_widget)
|
||||
|
||||
def is_active(self):
|
||||
"""Instance is activated."""
|
||||
|
|
@ -176,9 +176,9 @@ class InstanceListItemWidget(QtWidgets.QWidget):
|
|||
def update_instance_values(self):
|
||||
"""Update instance data propagated to widgets."""
|
||||
# Check subset name
|
||||
subset_name = self.instance["subset"]
|
||||
if subset_name != self._subset_name_label.text():
|
||||
self._subset_name_label.setText(subset_name)
|
||||
label = self.instance.label
|
||||
if label != self._instance_label_widget.text():
|
||||
self._instance_label_widget.setText(label)
|
||||
# Check active state
|
||||
self.set_active(self.instance["active"])
|
||||
# Check valid states
|
||||
|
|
@ -519,7 +519,7 @@ class InstanceListView(AbstractInstanceView):
|
|||
instances_by_group_name = collections.defaultdict(list)
|
||||
group_names = set()
|
||||
for instance in self.controller.instances:
|
||||
group_label = instance.creator_label
|
||||
group_label = instance.group_label
|
||||
group_names.add(group_label)
|
||||
instances_by_group_name[group_label].append(instance)
|
||||
|
||||
|
|
|
|||
|
|
@ -1225,6 +1225,7 @@ class CreatorAttrsWidget(QtWidgets.QWidget):
|
|||
different creators. If creator have same (similar) definitions their
|
||||
widgets are merged into one (different label does not count).
|
||||
"""
|
||||
|
||||
def __init__(self, controller, parent):
|
||||
super(CreatorAttrsWidget, self).__init__(parent)
|
||||
|
||||
|
|
@ -1275,6 +1276,7 @@ class CreatorAttrsWidget(QtWidgets.QWidget):
|
|||
content_layout = QtWidgets.QGridLayout(content_widget)
|
||||
content_layout.setColumnStretch(0, 0)
|
||||
content_layout.setColumnStretch(1, 1)
|
||||
content_layout.setAlignment(QtCore.Qt.AlignTop)
|
||||
|
||||
row = 0
|
||||
for attr_def, attr_instances, values in result:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue