mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 12:54:40 +01:00
Merge branch 'release/3.15.x' into enhancement/remove-staging-logic-on-version
This commit is contained in:
commit
7c1c276f4d
31 changed files with 995 additions and 253 deletions
53
CHANGELOG.md
53
CHANGELOG.md
|
|
@ -1,8 +1,40 @@
|
|||
# Changelog
|
||||
|
||||
## [3.14.4](https://github.com/pypeclub/OpenPype/tree/HEAD)
|
||||
## [3.14.5](https://github.com/pypeclub/OpenPype/tree/HEAD)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.3...HEAD)
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.4...HEAD)
|
||||
|
||||
**🚀 Enhancements**
|
||||
|
||||
- Maya: add OBJ extractor to model family [\#4021](https://github.com/pypeclub/OpenPype/pull/4021)
|
||||
- Publish report viewer tool [\#4010](https://github.com/pypeclub/OpenPype/pull/4010)
|
||||
- Nuke | Global: adding custom tags representation filtering [\#4009](https://github.com/pypeclub/OpenPype/pull/4009)
|
||||
- Publisher: Create context has shared data for collection phase [\#3995](https://github.com/pypeclub/OpenPype/pull/3995)
|
||||
- Resolve: updating to v18 compatibility [\#3986](https://github.com/pypeclub/OpenPype/pull/3986)
|
||||
|
||||
**🐛 Bug fixes**
|
||||
|
||||
- TrayPublisher: Fix missing argument [\#4019](https://github.com/pypeclub/OpenPype/pull/4019)
|
||||
- General: Fix python 2 compatibility of ffmpeg and oiio tools discovery [\#4011](https://github.com/pypeclub/OpenPype/pull/4011)
|
||||
|
||||
**🔀 Refactored code**
|
||||
|
||||
- Maya: Removed unused imports [\#4008](https://github.com/pypeclub/OpenPype/pull/4008)
|
||||
- Unreal: Fix import of moved function [\#4007](https://github.com/pypeclub/OpenPype/pull/4007)
|
||||
- Houdini: Change import of RepairAction [\#4005](https://github.com/pypeclub/OpenPype/pull/4005)
|
||||
- Nuke/Hiero: Refactor openpype.api imports [\#4000](https://github.com/pypeclub/OpenPype/pull/4000)
|
||||
- TVPaint: Defined with HostBase [\#3994](https://github.com/pypeclub/OpenPype/pull/3994)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Unreal: Remove redundant Creator stub [\#4012](https://github.com/pypeclub/OpenPype/pull/4012)
|
||||
- Unreal: add `uproject` extension to Unreal project template [\#4004](https://github.com/pypeclub/OpenPype/pull/4004)
|
||||
- Unreal: fix order of includes [\#4002](https://github.com/pypeclub/OpenPype/pull/4002)
|
||||
- Fusion: Implement backwards compatibility \(+/- Fusion 17.2\) [\#3958](https://github.com/pypeclub/OpenPype/pull/3958)
|
||||
|
||||
## [3.14.4](https://github.com/pypeclub/OpenPype/tree/3.14.4) (2022-10-19)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.3...3.14.4)
|
||||
|
||||
**🆕 New features**
|
||||
|
||||
|
|
@ -27,7 +59,6 @@
|
|||
- Maya: Moved plugin from global to maya [\#3939](https://github.com/pypeclub/OpenPype/pull/3939)
|
||||
- Publisher: Create dialog is part of main window [\#3936](https://github.com/pypeclub/OpenPype/pull/3936)
|
||||
- Fusion: Implement Alembic and FBX mesh loader [\#3927](https://github.com/pypeclub/OpenPype/pull/3927)
|
||||
- Maya: Remove hardcoded requirement for maya/ start for image file prefix [\#3873](https://github.com/pypeclub/OpenPype/pull/3873)
|
||||
|
||||
**🐛 Bug fixes**
|
||||
|
||||
|
|
@ -71,14 +102,6 @@
|
|||
**🚀 Enhancements**
|
||||
|
||||
- Publisher: Enhancement proposals [\#3897](https://github.com/pypeclub/OpenPype/pull/3897)
|
||||
- Maya: better logging in Maketx [\#3886](https://github.com/pypeclub/OpenPype/pull/3886)
|
||||
- Photoshop: review can be turned off [\#3885](https://github.com/pypeclub/OpenPype/pull/3885)
|
||||
- TrayPublisher: added persisting of last selected project [\#3871](https://github.com/pypeclub/OpenPype/pull/3871)
|
||||
- TrayPublisher: added text filter on project name to Tray Publisher [\#3867](https://github.com/pypeclub/OpenPype/pull/3867)
|
||||
- Github issues adding `running version` section [\#3864](https://github.com/pypeclub/OpenPype/pull/3864)
|
||||
- Publisher: Increase size of main window [\#3862](https://github.com/pypeclub/OpenPype/pull/3862)
|
||||
- Flame: make migratable projects after creation [\#3860](https://github.com/pypeclub/OpenPype/pull/3860)
|
||||
- Photoshop: synchronize image version with workfile [\#3854](https://github.com/pypeclub/OpenPype/pull/3854)
|
||||
|
||||
**🐛 Bug fixes**
|
||||
|
||||
|
|
@ -86,12 +109,6 @@
|
|||
- Flame: loading multilayer exr to batch/reel is working [\#3901](https://github.com/pypeclub/OpenPype/pull/3901)
|
||||
- Hiero: Fix inventory check on launch [\#3895](https://github.com/pypeclub/OpenPype/pull/3895)
|
||||
- WebPublisher: Fix import after refactor [\#3891](https://github.com/pypeclub/OpenPype/pull/3891)
|
||||
- TVPaint: Fix renaming of rendered files [\#3882](https://github.com/pypeclub/OpenPype/pull/3882)
|
||||
- Publisher: Nice checkbox visible in Python 2 [\#3877](https://github.com/pypeclub/OpenPype/pull/3877)
|
||||
- Settings: Add missing default settings [\#3870](https://github.com/pypeclub/OpenPype/pull/3870)
|
||||
- General: Copy of workfile does not use 'copy' function but 'copyfile' [\#3869](https://github.com/pypeclub/OpenPype/pull/3869)
|
||||
- Tray Publisher: skip plugin if otioTimeline is missing [\#3856](https://github.com/pypeclub/OpenPype/pull/3856)
|
||||
- Flame: retimed attributes are integrated with settings [\#3855](https://github.com/pypeclub/OpenPype/pull/3855)
|
||||
|
||||
**🔀 Refactored code**
|
||||
|
||||
|
|
@ -105,8 +122,6 @@
|
|||
**Merged pull requests:**
|
||||
|
||||
- Maya: Fix Scene Inventory possibly starting off-screen due to maya preferences [\#3923](https://github.com/pypeclub/OpenPype/pull/3923)
|
||||
- Maya: RenderSettings set default image format for V-Ray+Redshift to exr [\#3879](https://github.com/pypeclub/OpenPype/pull/3879)
|
||||
- Remove lockfile during publish [\#3874](https://github.com/pypeclub/OpenPype/pull/3874)
|
||||
|
||||
## [3.14.2](https://github.com/pypeclub/OpenPype/tree/3.14.2) (2022-09-12)
|
||||
|
||||
|
|
|
|||
|
|
@ -251,7 +251,6 @@ def reload_config():
|
|||
import importlib
|
||||
|
||||
for module in (
|
||||
"openpype.api",
|
||||
"openpype.hosts.hiero.lib",
|
||||
"openpype.hosts.hiero.menu",
|
||||
"openpype.hosts.hiero.tags"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from pyblish import api
|
||||
import openpype.api as pype
|
||||
|
||||
from openpype.lib import version_up
|
||||
|
||||
|
||||
class IntegrateVersionUpWorkfile(api.ContextPlugin):
|
||||
|
|
@ -15,7 +16,7 @@ class IntegrateVersionUpWorkfile(api.ContextPlugin):
|
|||
def process(self, context):
|
||||
project = context.data["activeProject"]
|
||||
path = context.data.get("currentFile")
|
||||
new_path = pype.version_up(path)
|
||||
new_path = version_up(path)
|
||||
|
||||
if project:
|
||||
project.saveAs(new_path)
|
||||
|
|
|
|||
0
openpype/hosts/maya/api/obj.py
Normal file
0
openpype/hosts/maya/api/obj.py
Normal file
|
|
@ -90,7 +90,7 @@ class ImportMayaLoader(load.LoaderPlugin):
|
|||
so you could also use it as a new base.
|
||||
|
||||
"""
|
||||
representations = ["ma", "mb"]
|
||||
representations = ["ma", "mb", "obj"]
|
||||
families = ["*"]
|
||||
|
||||
label = "Import"
|
||||
|
|
|
|||
78
openpype/hosts/maya/plugins/publish/extract_obj.py
Normal file
78
openpype/hosts/maya/plugins/publish/extract_obj.py
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
|
||||
from maya import cmds
|
||||
# import maya.mel as mel
|
||||
import pyblish.api
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.maya.api import lib
|
||||
|
||||
|
||||
class ExtractObj(publish.Extractor):
|
||||
"""Extract OBJ from Maya.
|
||||
|
||||
This extracts reproducible OBJ exports ignoring any of the settings
|
||||
set on the local machine in the OBJ export options window.
|
||||
|
||||
"""
|
||||
order = pyblish.api.ExtractorOrder
|
||||
hosts = ["maya"]
|
||||
label = "Extract OBJ"
|
||||
families = ["model"]
|
||||
|
||||
def process(self, instance):
|
||||
|
||||
# Define output path
|
||||
|
||||
staging_dir = self.staging_dir(instance)
|
||||
filename = "{0}.obj".format(instance.name)
|
||||
path = os.path.join(staging_dir, filename)
|
||||
|
||||
# The export requires forward slashes because we need to
|
||||
# format it into a string in a mel expression
|
||||
|
||||
self.log.info("Extracting OBJ to: {0}".format(path))
|
||||
|
||||
members = instance.data("setMembers")
|
||||
members = cmds.ls(members,
|
||||
dag=True,
|
||||
shapes=True,
|
||||
type=("mesh", "nurbsCurve"),
|
||||
noIntermediate=True,
|
||||
long=True)
|
||||
self.log.info("Members: {0}".format(members))
|
||||
self.log.info("Instance: {0}".format(instance[:]))
|
||||
|
||||
if not cmds.pluginInfo('objExport', query=True, loaded=True):
|
||||
cmds.loadPlugin('objExport')
|
||||
|
||||
# Export
|
||||
with lib.no_display_layers(instance):
|
||||
with lib.displaySmoothness(members,
|
||||
divisionsU=0,
|
||||
divisionsV=0,
|
||||
pointsWire=4,
|
||||
pointsShaded=1,
|
||||
polygonObject=1):
|
||||
with lib.shader(members,
|
||||
shadingEngine="initialShadingGroup"):
|
||||
with lib.maintained_selection():
|
||||
cmds.select(members, noExpand=True)
|
||||
cmds.file(path,
|
||||
exportSelected=True,
|
||||
type='OBJexport',
|
||||
preserveReferences=True,
|
||||
force=True)
|
||||
|
||||
if "representation" not in instance.data:
|
||||
instance.data["representation"] = []
|
||||
|
||||
representation = {
|
||||
'name': 'obj',
|
||||
'ext': 'obj',
|
||||
'files': filename,
|
||||
"stagingDir": staging_dir,
|
||||
}
|
||||
instance.data["representations"].append(representation)
|
||||
|
||||
self.log.info("Extract OBJ successful to: {0}".format(path))
|
||||
|
|
@ -2930,3 +2930,47 @@ def get_nodes_by_names(names):
|
|||
nuke.toNode(name)
|
||||
for name in names
|
||||
]
|
||||
|
||||
|
||||
def get_viewer_config_from_string(input_string):
|
||||
"""Convert string to display and viewer string
|
||||
|
||||
Args:
|
||||
input_string (str): string with viewer
|
||||
|
||||
Raises:
|
||||
IndexError: if more then one slash in input string
|
||||
IndexError: if missing closing bracket
|
||||
|
||||
Returns:
|
||||
tuple[str]: display, viewer
|
||||
"""
|
||||
display = None
|
||||
viewer = input_string
|
||||
# check if () or / or \ in name
|
||||
if "/" in viewer:
|
||||
split = viewer.split("/")
|
||||
|
||||
# rise if more then one column
|
||||
if len(split) > 2:
|
||||
raise IndexError((
|
||||
"Viewer Input string is not correct. "
|
||||
"more then two `/` slashes! {}"
|
||||
).format(input_string))
|
||||
|
||||
viewer = split[1]
|
||||
display = split[0]
|
||||
elif "(" in viewer:
|
||||
pattern = r"([\w\d\s]+).*[(](.*)[)]"
|
||||
result = re.findall(pattern, viewer)
|
||||
try:
|
||||
result = result.pop()
|
||||
display = str(result[1]).rstrip()
|
||||
viewer = str(result[0]).rstrip()
|
||||
except IndexError:
|
||||
raise IndexError((
|
||||
"Viewer Input string is not correct. "
|
||||
"Missing bracket! {}"
|
||||
).format(input_string))
|
||||
|
||||
return (display, viewer)
|
||||
|
|
|
|||
|
|
@ -66,7 +66,6 @@ def reload_config():
|
|||
"""
|
||||
|
||||
for module in (
|
||||
"openpype.api",
|
||||
"openpype.hosts.nuke.api.actions",
|
||||
"openpype.hosts.nuke.api.menu",
|
||||
"openpype.hosts.nuke.api.plugin",
|
||||
|
|
|
|||
|
|
@ -19,7 +19,8 @@ from .lib import (
|
|||
add_publish_knob,
|
||||
get_nuke_imageio_settings,
|
||||
set_node_knobs_from_settings,
|
||||
get_view_process_node
|
||||
get_view_process_node,
|
||||
get_viewer_config_from_string
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -190,7 +191,20 @@ class ExporterReview(object):
|
|||
if "#" in self.fhead:
|
||||
self.fhead = self.fhead.replace("#", "")[:-1]
|
||||
|
||||
def get_representation_data(self, tags=None, range=False):
|
||||
def get_representation_data(
|
||||
self, tags=None, range=False,
|
||||
custom_tags=None
|
||||
):
|
||||
""" Add representation data to self.data
|
||||
|
||||
Args:
|
||||
tags (list[str], optional): list of defined tags.
|
||||
Defaults to None.
|
||||
range (bool, optional): flag for adding ranges.
|
||||
Defaults to False.
|
||||
custom_tags (list[str], optional): user inputed custom tags.
|
||||
Defaults to None.
|
||||
"""
|
||||
add_tags = tags or []
|
||||
repre = {
|
||||
"name": self.name,
|
||||
|
|
@ -200,6 +214,9 @@ class ExporterReview(object):
|
|||
"tags": [self.name.replace("_", "-")] + add_tags
|
||||
}
|
||||
|
||||
if custom_tags:
|
||||
repre["custom_tags"] = custom_tags
|
||||
|
||||
if range:
|
||||
repre.update({
|
||||
"frameStart": self.first_frame,
|
||||
|
|
@ -312,7 +329,8 @@ class ExporterReviewLut(ExporterReview):
|
|||
dag_node.setInput(0, self.previous_node)
|
||||
self._temp_nodes.append(dag_node)
|
||||
self.previous_node = dag_node
|
||||
self.log.debug("OCIODisplay... `{}`".format(self._temp_nodes))
|
||||
self.log.debug(
|
||||
"OCIODisplay... `{}`".format(self._temp_nodes))
|
||||
|
||||
# GenerateLUT
|
||||
gen_lut_node = nuke.createNode("GenerateLUT")
|
||||
|
|
@ -415,6 +433,7 @@ class ExporterReviewMov(ExporterReview):
|
|||
return path
|
||||
|
||||
def generate_mov(self, farm=False, **kwargs):
|
||||
add_tags = []
|
||||
self.publish_on_farm = farm
|
||||
read_raw = kwargs["read_raw"]
|
||||
reformat_node_add = kwargs["reformat_node_add"]
|
||||
|
|
@ -433,10 +452,10 @@ class ExporterReviewMov(ExporterReview):
|
|||
self.log.debug(">> baking_view_profile `{}`".format(
|
||||
baking_view_profile))
|
||||
|
||||
add_tags = kwargs.get("add_tags", [])
|
||||
add_custom_tags = kwargs.get("add_custom_tags", [])
|
||||
|
||||
self.log.info(
|
||||
"__ add_tags: `{0}`".format(add_tags))
|
||||
"__ add_custom_tags: `{0}`".format(add_custom_tags))
|
||||
|
||||
subset = self.instance.data["subset"]
|
||||
self._temp_nodes[subset] = []
|
||||
|
|
@ -491,7 +510,15 @@ class ExporterReviewMov(ExporterReview):
|
|||
if not self.viewer_lut_raw:
|
||||
# OCIODisplay
|
||||
dag_node = nuke.createNode("OCIODisplay")
|
||||
dag_node["view"].setValue(str(baking_view_profile))
|
||||
|
||||
display, viewer = get_viewer_config_from_string(
|
||||
str(baking_view_profile)
|
||||
)
|
||||
if display:
|
||||
dag_node["display"].setValue(display)
|
||||
|
||||
# assign viewer
|
||||
dag_node["view"].setValue(viewer)
|
||||
|
||||
# connect
|
||||
dag_node.setInput(0, self.previous_node)
|
||||
|
|
@ -542,6 +569,7 @@ class ExporterReviewMov(ExporterReview):
|
|||
# ---------- generate representation data
|
||||
self.get_representation_data(
|
||||
tags=["review", "delete"] + add_tags,
|
||||
custom_tags=add_custom_tags,
|
||||
range=True
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ import os
|
|||
import nuke
|
||||
|
||||
import pyblish.api
|
||||
import openpype.api as pype
|
||||
|
||||
from openpype.lib import get_version_from_path
|
||||
from openpype.hosts.nuke.api.lib import (
|
||||
add_publish_knob,
|
||||
get_avalon_knob_data
|
||||
|
|
@ -74,7 +75,7 @@ class CollectWorkfile(pyblish.api.ContextPlugin):
|
|||
"fps": root['fps'].value(),
|
||||
|
||||
"currentFile": current_file,
|
||||
"version": int(pype.get_version_from_path(current_file)),
|
||||
"version": int(get_version_from_path(current_file)),
|
||||
|
||||
"host": pyblish.api.current_host(),
|
||||
"hostVersion": nuke.NUKE_VERSION_STRING
|
||||
|
|
|
|||
|
|
@ -17,11 +17,27 @@ from openpype.lib.transcoding import IMAGE_EXTENSIONS, VIDEO_EXTENSIONS
|
|||
REVIEW_EXTENSIONS = IMAGE_EXTENSIONS + VIDEO_EXTENSIONS
|
||||
|
||||
|
||||
def _cache_and_get_instances(creator):
|
||||
"""Cache instances in shared data.
|
||||
|
||||
Args:
|
||||
creator (Creator): Plugin which would like to get instances from host.
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: Cached instances list from host implementation.
|
||||
"""
|
||||
|
||||
shared_key = "openpype.traypublisher.instances"
|
||||
if shared_key not in creator.collection_shared_data:
|
||||
creator.collection_shared_data[shared_key] = list_instances()
|
||||
return creator.collection_shared_data[shared_key]
|
||||
|
||||
|
||||
class HiddenTrayPublishCreator(HiddenCreator):
|
||||
host_name = "traypublisher"
|
||||
|
||||
def collect_instances(self):
|
||||
for instance_data in list_instances():
|
||||
for instance_data in _cache_and_get_instances(self):
|
||||
creator_id = instance_data.get("creator_identifier")
|
||||
if creator_id == self.identifier:
|
||||
instance = CreatedInstance.from_existing(
|
||||
|
|
@ -58,7 +74,7 @@ class TrayPublishCreator(Creator):
|
|||
host_name = "traypublisher"
|
||||
|
||||
def collect_instances(self):
|
||||
for instance_data in list_instances():
|
||||
for instance_data in _cache_and_get_instances(self):
|
||||
creator_id = instance_data.get("creator_identifier")
|
||||
if creator_id == self.identifier:
|
||||
instance = CreatedInstance.from_existing(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import os
|
||||
import sys
|
||||
import copy
|
||||
import logging
|
||||
import traceback
|
||||
import collections
|
||||
import inspect
|
||||
from uuid import uuid4
|
||||
|
|
@ -22,11 +24,17 @@ from .creator_plugins import (
|
|||
Creator,
|
||||
AutoCreator,
|
||||
discover_creator_plugins,
|
||||
CreatorError,
|
||||
)
|
||||
|
||||
UpdateData = collections.namedtuple("UpdateData", ["instance", "changes"])
|
||||
|
||||
|
||||
class UnavailableSharedData(Exception):
|
||||
"""Shared data are not available at the moment when are accessed."""
|
||||
pass
|
||||
|
||||
|
||||
class ImmutableKeyError(TypeError):
|
||||
"""Accessed key is immutable so does not allow changes or removements."""
|
||||
|
||||
|
|
@ -62,6 +70,77 @@ class HostMissRequiredMethod(Exception):
|
|||
super(HostMissRequiredMethod, self).__init__(msg)
|
||||
|
||||
|
||||
class CreatorsOperationFailed(Exception):
|
||||
"""Raised when a creator process crashes in 'CreateContext'.
|
||||
|
||||
The exception contains information about the creator and error. The data
|
||||
are prepared using 'prepare_failed_creator_operation_info' and can be
|
||||
serialized using json.
|
||||
|
||||
Usage is for UI purposes which may not have access to exceptions directly
|
||||
and would not have ability to catch exceptions 'per creator'.
|
||||
|
||||
Args:
|
||||
msg (str): General error message.
|
||||
failed_info (list[dict[str, Any]]): List of failed creators with
|
||||
exception message and optionally formatted traceback.
|
||||
"""
|
||||
|
||||
def __init__(self, msg, failed_info):
|
||||
super(CreatorsOperationFailed, self).__init__(msg)
|
||||
self.failed_info = failed_info
|
||||
|
||||
|
||||
class CreatorsCollectionFailed(CreatorsOperationFailed):
|
||||
def __init__(self, failed_info):
|
||||
msg = "Failed to collect instances"
|
||||
super(CreatorsCollectionFailed, self).__init__(
|
||||
msg, failed_info
|
||||
)
|
||||
|
||||
|
||||
class CreatorsSaveFailed(CreatorsOperationFailed):
|
||||
def __init__(self, failed_info):
|
||||
msg = "Failed update instance changes"
|
||||
super(CreatorsSaveFailed, self).__init__(
|
||||
msg, failed_info
|
||||
)
|
||||
|
||||
|
||||
class CreatorsRemoveFailed(CreatorsOperationFailed):
|
||||
def __init__(self, failed_info):
|
||||
msg = "Failed to remove instances"
|
||||
super(CreatorsRemoveFailed, self).__init__(
|
||||
msg, failed_info
|
||||
)
|
||||
|
||||
|
||||
class CreatorsCreateFailed(CreatorsOperationFailed):
|
||||
def __init__(self, failed_info):
|
||||
msg = "Faled to create instances"
|
||||
super(CreatorsCreateFailed, self).__init__(
|
||||
msg, failed_info
|
||||
)
|
||||
|
||||
|
||||
def prepare_failed_creator_operation_info(
|
||||
identifier, label, exc_info, add_traceback=True
|
||||
):
|
||||
formatted_traceback = None
|
||||
exc_type, exc_value, exc_traceback = exc_info
|
||||
if add_traceback:
|
||||
formatted_traceback = "".join(traceback.format_exception(
|
||||
exc_type, exc_value, exc_traceback
|
||||
))
|
||||
|
||||
return {
|
||||
"creator_identifier": identifier,
|
||||
"creator_label": label,
|
||||
"message": str(exc_value),
|
||||
"traceback": formatted_traceback
|
||||
}
|
||||
|
||||
|
||||
class InstanceMember:
|
||||
"""Representation of instance member.
|
||||
|
||||
|
|
@ -925,6 +1004,9 @@ class CreateContext:
|
|||
self._bulk_counter = 0
|
||||
self._bulk_instances_to_process = []
|
||||
|
||||
# Shared data across creators during collection phase
|
||||
self._collection_shared_data = None
|
||||
|
||||
# Trigger reset if was enabled
|
||||
if reset:
|
||||
self.reset(discover_publish_plugins)
|
||||
|
|
@ -980,6 +1062,9 @@ class CreateContext:
|
|||
|
||||
All changes will be lost if were not saved explicitely.
|
||||
"""
|
||||
|
||||
self.reset_preparation()
|
||||
|
||||
self.reset_avalon_context()
|
||||
self.reset_plugins(discover_publish_plugins)
|
||||
self.reset_context_data()
|
||||
|
|
@ -988,6 +1073,20 @@ class CreateContext:
|
|||
self.reset_instances()
|
||||
self.execute_autocreators()
|
||||
|
||||
self.reset_finalization()
|
||||
|
||||
def reset_preparation(self):
|
||||
"""Prepare attributes that must be prepared/cleaned before reset."""
|
||||
|
||||
# Give ability to store shared data for collection phase
|
||||
self._collection_shared_data = {}
|
||||
|
||||
def reset_finalization(self):
|
||||
"""Cleanup of attributes after reset."""
|
||||
|
||||
# Stop access to collection shared data
|
||||
self._collection_shared_data = None
|
||||
|
||||
def reset_avalon_context(self):
|
||||
"""Give ability to reset avalon context.
|
||||
|
||||
|
|
@ -1186,7 +1285,65 @@ class CreateContext:
|
|||
with self.bulk_instances_collection():
|
||||
self._bulk_instances_to_process.append(instance)
|
||||
|
||||
def create(self, identifier, *args, **kwargs):
|
||||
"""Wrapper for creators to trigger created.
|
||||
|
||||
Different types of creators may expect different arguments thus the
|
||||
hints for args are blind.
|
||||
|
||||
Args:
|
||||
identifier (str): Creator's identifier.
|
||||
*args (Tuple[Any]): Arguments for create method.
|
||||
**kwargs (Dict[Any, Any]): Keyword argument for create method.
|
||||
"""
|
||||
|
||||
error_message = "Failed to run Creator with identifier \"{}\". {}"
|
||||
creator = self.creators.get(identifier)
|
||||
label = getattr(creator, "label", None)
|
||||
failed = False
|
||||
add_traceback = False
|
||||
exc_info = None
|
||||
try:
|
||||
# Fake CreatorError (Could be maybe specific exception?)
|
||||
if creator is None:
|
||||
raise CreatorError(
|
||||
"Creator {} was not found".format(identifier)
|
||||
)
|
||||
|
||||
creator.create(*args, **kwargs)
|
||||
|
||||
except CreatorError:
|
||||
failed = True
|
||||
exc_info = sys.exc_info()
|
||||
self.log.warning(error_message.format(identifier, exc_info[1]))
|
||||
|
||||
except:
|
||||
failed = True
|
||||
add_traceback = True
|
||||
exc_info = sys.exc_info()
|
||||
self.log.warning(
|
||||
error_message.format(identifier, ""),
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
if failed:
|
||||
raise CreatorsCreateFailed([
|
||||
prepare_failed_creator_operation_info(
|
||||
identifier, label, exc_info, add_traceback
|
||||
)
|
||||
])
|
||||
|
||||
def creator_removed_instance(self, instance):
|
||||
"""When creator removes instance context should be acknowledged.
|
||||
|
||||
If creator removes instance conext should know about it to avoid
|
||||
possible issues in the session.
|
||||
|
||||
Args:
|
||||
instance (CreatedInstance): Object of instance which was removed
|
||||
from scene metadata.
|
||||
"""
|
||||
|
||||
self._instances_by_id.pop(instance.id, None)
|
||||
|
||||
@contextmanager
|
||||
|
|
@ -1221,24 +1378,81 @@ class CreateContext:
|
|||
self._instances_by_id = {}
|
||||
|
||||
# Collect instances
|
||||
error_message = "Collection of instances for creator {} failed. {}"
|
||||
failed_info = []
|
||||
for creator in self.creators.values():
|
||||
creator.collect_instances()
|
||||
label = creator.label
|
||||
identifier = creator.identifier
|
||||
failed = False
|
||||
add_traceback = False
|
||||
exc_info = None
|
||||
try:
|
||||
creator.collect_instances()
|
||||
|
||||
except CreatorError:
|
||||
failed = True
|
||||
exc_info = sys.exc_info()
|
||||
self.log.warning(error_message.format(identifier, exc_info[1]))
|
||||
|
||||
except:
|
||||
failed = True
|
||||
add_traceback = True
|
||||
exc_info = sys.exc_info()
|
||||
self.log.warning(
|
||||
error_message.format(identifier, ""),
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
if failed:
|
||||
failed_info.append(
|
||||
prepare_failed_creator_operation_info(
|
||||
identifier, label, exc_info, add_traceback
|
||||
)
|
||||
)
|
||||
|
||||
if failed_info:
|
||||
raise CreatorsCollectionFailed(failed_info)
|
||||
|
||||
def execute_autocreators(self):
|
||||
"""Execute discovered AutoCreator plugins.
|
||||
|
||||
Reset instances if any autocreator executed properly.
|
||||
"""
|
||||
|
||||
error_message = "Failed to run AutoCreator with identifier \"{}\". {}"
|
||||
failed_info = []
|
||||
for identifier, creator in self.autocreators.items():
|
||||
label = creator.label
|
||||
failed = False
|
||||
add_traceback = False
|
||||
try:
|
||||
creator.create()
|
||||
|
||||
except Exception:
|
||||
# TODO raise report exception if any crashed
|
||||
msg = (
|
||||
"Failed to run AutoCreator with identifier \"{}\" ({})."
|
||||
).format(identifier, inspect.getfile(creator.__class__))
|
||||
self.log.warning(msg, exc_info=True)
|
||||
except CreatorError:
|
||||
failed = True
|
||||
exc_info = sys.exc_info()
|
||||
self.log.warning(error_message.format(identifier, exc_info[1]))
|
||||
|
||||
# Use bare except because some hosts raise their exceptions that
|
||||
# do not inherit from python's `BaseException`
|
||||
except:
|
||||
failed = True
|
||||
add_traceback = True
|
||||
exc_info = sys.exc_info()
|
||||
self.log.warning(
|
||||
error_message.format(identifier, ""),
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
if failed:
|
||||
failed_info.append(
|
||||
prepare_failed_creator_operation_info(
|
||||
identifier, label, exc_info, add_traceback
|
||||
)
|
||||
)
|
||||
|
||||
if failed_info:
|
||||
raise CreatorsCreateFailed(failed_info)
|
||||
|
||||
def validate_instances_context(self, instances=None):
|
||||
"""Validate 'asset' and 'task' instance context."""
|
||||
|
|
@ -1315,17 +1529,48 @@ class CreateContext:
|
|||
identifier = instance.creator_identifier
|
||||
instances_by_identifier[identifier].append(instance)
|
||||
|
||||
for identifier, cretor_instances in instances_by_identifier.items():
|
||||
error_message = "Instances update of creator \"{}\" failed. {}"
|
||||
failed_info = []
|
||||
for identifier, creator_instances in instances_by_identifier.items():
|
||||
update_list = []
|
||||
for instance in cretor_instances:
|
||||
for instance in creator_instances:
|
||||
instance_changes = instance.changes()
|
||||
if instance_changes:
|
||||
update_list.append(UpdateData(instance, instance_changes))
|
||||
|
||||
creator = self.creators[identifier]
|
||||
if update_list:
|
||||
if not update_list:
|
||||
continue
|
||||
|
||||
label = creator.label
|
||||
failed = False
|
||||
add_traceback = False
|
||||
exc_info = None
|
||||
try:
|
||||
creator.update_instances(update_list)
|
||||
|
||||
except CreatorError:
|
||||
failed = True
|
||||
exc_info = sys.exc_info()
|
||||
self.log.warning(error_message.format(identifier, exc_info[1]))
|
||||
|
||||
except:
|
||||
failed = True
|
||||
add_traceback = True
|
||||
exc_info = sys.exc_info()
|
||||
self.log.warning(
|
||||
error_message.format(identifier, ""), exc_info=True)
|
||||
|
||||
if failed:
|
||||
failed_info.append(
|
||||
prepare_failed_creator_operation_info(
|
||||
identifier, label, exc_info, add_traceback
|
||||
)
|
||||
)
|
||||
|
||||
if failed_info:
|
||||
raise CreatorsSaveFailed(failed_info)
|
||||
|
||||
def remove_instances(self, instances):
|
||||
"""Remove instances from context.
|
||||
|
||||
|
|
@ -1333,14 +1578,48 @@ class CreateContext:
|
|||
instances(list<CreatedInstance>): Instances that should be removed
|
||||
from context.
|
||||
"""
|
||||
|
||||
instances_by_identifier = collections.defaultdict(list)
|
||||
for instance in instances:
|
||||
identifier = instance.creator_identifier
|
||||
instances_by_identifier[identifier].append(instance)
|
||||
|
||||
error_message = "Instances removement of creator \"{}\" failed. {}"
|
||||
failed_info = []
|
||||
for identifier, creator_instances in instances_by_identifier.items():
|
||||
creator = self.creators.get(identifier)
|
||||
creator.remove_instances(creator_instances)
|
||||
label = creator.label
|
||||
failed = False
|
||||
add_traceback = False
|
||||
exc_info = None
|
||||
try:
|
||||
creator.remove_instances(creator_instances)
|
||||
|
||||
except CreatorError:
|
||||
failed = True
|
||||
exc_info = sys.exc_info()
|
||||
self.log.warning(
|
||||
error_message.format(identifier, exc_info[1])
|
||||
)
|
||||
|
||||
except:
|
||||
failed = True
|
||||
add_traceback = True
|
||||
exc_info = sys.exc_info()
|
||||
self.log.warning(
|
||||
error_message.format(identifier, ""),
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
if failed:
|
||||
failed_info.append(
|
||||
prepare_failed_creator_operation_info(
|
||||
identifier, label, exc_info, add_traceback
|
||||
)
|
||||
)
|
||||
|
||||
if failed_info:
|
||||
raise CreatorsRemoveFailed(failed_info)
|
||||
|
||||
def _get_publish_plugins_with_attr_for_family(self, family):
|
||||
"""Publish plugin attributes for passed family.
|
||||
|
|
@ -1372,3 +1651,20 @@ class CreateContext:
|
|||
if not plugin.__instanceEnabled__:
|
||||
plugins.append(plugin)
|
||||
return plugins
|
||||
|
||||
@property
|
||||
def collection_shared_data(self):
|
||||
"""Access to shared data that can be used during creator's collection.
|
||||
|
||||
Retruns:
|
||||
Dict[str, Any]: Shared data.
|
||||
|
||||
Raises:
|
||||
UnavailableSharedData: When called out of collection phase.
|
||||
"""
|
||||
|
||||
if self._collection_shared_data is None:
|
||||
raise UnavailableSharedData(
|
||||
"Accessed Collection shared data out of collection phase"
|
||||
)
|
||||
return self._collection_shared_data
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from abc import (
|
|||
abstractmethod,
|
||||
abstractproperty
|
||||
)
|
||||
|
||||
import six
|
||||
|
||||
from openpype.settings import get_system_settings, get_project_settings
|
||||
|
|
@ -323,6 +324,19 @@ class BaseCreator:
|
|||
|
||||
return self.instance_attr_defs
|
||||
|
||||
@property
|
||||
def collection_shared_data(self):
|
||||
"""Access to shared data that can be used during creator's collection.
|
||||
|
||||
Retruns:
|
||||
Dict[str, Any]: Shared data.
|
||||
|
||||
Raises:
|
||||
UnavailableSharedData: When called out of collection phase.
|
||||
"""
|
||||
|
||||
return self.create_context.collection_shared_data
|
||||
|
||||
|
||||
class Creator(BaseCreator):
|
||||
"""Creator that has more information for artist to show in UI.
|
||||
|
|
|
|||
|
|
@ -128,6 +128,7 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
for repre in instance.data["representations"]:
|
||||
repre_name = str(repre.get("name"))
|
||||
tags = repre.get("tags") or []
|
||||
custom_tags = repre.get("custom_tags")
|
||||
if "review" not in tags:
|
||||
self.log.debug((
|
||||
"Repre: {} - Didn't found \"review\" in tags. Skipping"
|
||||
|
|
@ -158,15 +159,18 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
)
|
||||
continue
|
||||
|
||||
# Filter output definition by representation tags (optional)
|
||||
outputs = self.filter_outputs_by_tags(profile_outputs, tags)
|
||||
# Filter output definition by representation's
|
||||
# custom tags (optional)
|
||||
outputs = self.filter_outputs_by_custom_tags(
|
||||
profile_outputs, custom_tags)
|
||||
if not outputs:
|
||||
self.log.info((
|
||||
"Skipped representation. All output definitions from"
|
||||
" selected profile does not match to representation's"
|
||||
" tags. \"{}\""
|
||||
" custom tags. \"{}\""
|
||||
).format(str(tags)))
|
||||
continue
|
||||
|
||||
outputs_per_representations.append((repre, outputs))
|
||||
return outputs_per_representations
|
||||
|
||||
|
|
@ -1656,7 +1660,9 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
return True
|
||||
return False
|
||||
|
||||
def filter_output_defs(self, profile, subset_name, families):
|
||||
def filter_output_defs(
|
||||
self, profile, subset_name, families
|
||||
):
|
||||
"""Return outputs matching input instance families.
|
||||
|
||||
Output definitions without families filter are marked as valid.
|
||||
|
|
@ -1664,6 +1670,7 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
Args:
|
||||
profile (dict): Profile from presets matching current context.
|
||||
families (list): All families of current instance.
|
||||
subset_name (str): name of subset
|
||||
|
||||
Returns:
|
||||
list: Containg all output definitions matching entered families.
|
||||
|
|
@ -1711,40 +1718,51 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
|
||||
return filtered_outputs
|
||||
|
||||
def filter_outputs_by_tags(self, outputs, tags):
|
||||
"""Filter output definitions by entered representation tags.
|
||||
def filter_outputs_by_custom_tags(self, outputs, custom_tags):
|
||||
"""Filter output definitions by entered representation custom_tags.
|
||||
|
||||
Output definitions without tags filter are marked as valid.
|
||||
Output definitions without custom_tags filter are marked as invalid,
|
||||
only in case representation is having any custom_tags defined.
|
||||
|
||||
Args:
|
||||
outputs (list): Contain list of output definitions from presets.
|
||||
tags (list): Tags of processed representation.
|
||||
custom_tags (list): Custom Tags of processed representation.
|
||||
|
||||
Returns:
|
||||
list: Containg all output definitions matching entered tags.
|
||||
"""
|
||||
filtered_outputs = []
|
||||
repre_tags_low = [tag.lower() for tag in tags]
|
||||
for output_def in outputs:
|
||||
valid = True
|
||||
output_filters = output_def.get("filter")
|
||||
if output_filters:
|
||||
# Check tag filters
|
||||
tag_filters = output_filters.get("tags")
|
||||
if tag_filters:
|
||||
tag_filters_low = [tag.lower() for tag in tag_filters]
|
||||
valid = False
|
||||
for tag in repre_tags_low:
|
||||
if tag in tag_filters_low:
|
||||
valid = True
|
||||
break
|
||||
|
||||
if not valid:
|
||||
continue
|
||||
filtered_outputs = []
|
||||
repre_c_tags_low = [tag.lower() for tag in (custom_tags or [])]
|
||||
for output_def in outputs:
|
||||
tag_filters = output_def.get("filter", {}).get("custom_tags")
|
||||
|
||||
if not custom_tags and not tag_filters:
|
||||
# Definition is valid if both tags are empty
|
||||
valid = True
|
||||
|
||||
elif not custom_tags or not tag_filters:
|
||||
# Invalid if one is empty
|
||||
valid = False
|
||||
|
||||
else:
|
||||
# Check if output definition tags are in representation tags
|
||||
valid = False
|
||||
# lower all filter tags
|
||||
tag_filters_low = [tag.lower() for tag in tag_filters]
|
||||
# check if any repre tag is not in filter tags
|
||||
for tag in repre_c_tags_low:
|
||||
if tag in tag_filters_low:
|
||||
valid = True
|
||||
break
|
||||
|
||||
if valid:
|
||||
filtered_outputs.append(output_def)
|
||||
|
||||
self.log.debug("__ filtered_outputs: {}".format(
|
||||
[_o["filename_suffix"] for _o in filtered_outputs]
|
||||
))
|
||||
|
||||
return filtered_outputs
|
||||
|
||||
def add_video_filter_args(self, args, inserting_arg):
|
||||
|
|
|
|||
|
|
@ -22,10 +22,6 @@ FFMPEG = (
|
|||
'"{}"%(input_args)s -i "%(input)s" %(filters)s %(args)s%(output)s'
|
||||
).format(ffmpeg_path)
|
||||
|
||||
FFPROBE = (
|
||||
'"{}" -v quiet -print_format json -show_format -show_streams "%(source)s"'
|
||||
).format(ffprobe_path)
|
||||
|
||||
DRAWTEXT = (
|
||||
"drawtext=fontfile='%(font)s':text=\\'%(text)s\\':"
|
||||
"x=%(x)s:y=%(y)s:fontcolor=%(color)s@%(opacity).1f:fontsize=%(size)d"
|
||||
|
|
@ -48,8 +44,15 @@ def _get_ffprobe_data(source):
|
|||
:param str source: source media file
|
||||
:rtype: [{}, ...]
|
||||
"""
|
||||
command = FFPROBE % {'source': source}
|
||||
proc = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE)
|
||||
command = [
|
||||
ffprobe_path,
|
||||
"-v", "quiet",
|
||||
"-print_format", "json",
|
||||
"-show_format",
|
||||
"-show_streams",
|
||||
source
|
||||
]
|
||||
proc = subprocess.Popen(command, stdout=subprocess.PIPE)
|
||||
out = proc.communicate()[0]
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError("Failed to run: %s" % command)
|
||||
|
|
@ -113,11 +116,20 @@ class ModifiedBurnins(ffmpeg_burnins.Burnins):
|
|||
if not ffprobe_data:
|
||||
ffprobe_data = _get_ffprobe_data(source)
|
||||
|
||||
# Validate 'streams' before calling super to raise more specific
|
||||
# error
|
||||
source_streams = ffprobe_data.get("streams")
|
||||
if not source_streams:
|
||||
raise ValueError((
|
||||
"Input file \"{}\" does not contain any streams"
|
||||
" with image/video content."
|
||||
).format(source))
|
||||
|
||||
self.ffprobe_data = ffprobe_data
|
||||
self.first_frame = first_frame
|
||||
self.input_args = []
|
||||
|
||||
super().__init__(source, ffprobe_data["streams"])
|
||||
super().__init__(source, source_streams)
|
||||
|
||||
if options_init:
|
||||
self.options_init.update(options_init)
|
||||
|
|
|
|||
|
|
@ -78,7 +78,8 @@
|
|||
"review",
|
||||
"ftrack"
|
||||
],
|
||||
"subsets": []
|
||||
"subsets": [],
|
||||
"custom_tags": []
|
||||
},
|
||||
"overscan_crop": "",
|
||||
"overscan_color": [
|
||||
|
|
|
|||
|
|
@ -131,6 +131,16 @@
|
|||
"Main"
|
||||
]
|
||||
},
|
||||
"CreateModel": {
|
||||
"enabled": true,
|
||||
"write_color_sets": false,
|
||||
"write_face_sets": false,
|
||||
"defaults": [
|
||||
"Main",
|
||||
"Proxy",
|
||||
"Sculpt"
|
||||
]
|
||||
},
|
||||
"CreatePointCache": {
|
||||
"enabled": true,
|
||||
"write_color_sets": false,
|
||||
|
|
@ -187,16 +197,6 @@
|
|||
"Main"
|
||||
]
|
||||
},
|
||||
"CreateModel": {
|
||||
"enabled": true,
|
||||
"write_color_sets": false,
|
||||
"write_face_sets": false,
|
||||
"defaults": [
|
||||
"Main",
|
||||
"Proxy",
|
||||
"Sculpt"
|
||||
]
|
||||
},
|
||||
"CreateRenderSetup": {
|
||||
"enabled": true,
|
||||
"defaults": [
|
||||
|
|
@ -577,6 +577,10 @@
|
|||
"vrayproxy"
|
||||
]
|
||||
},
|
||||
"ExtractObj": {
|
||||
"enabled": false,
|
||||
"optional": true
|
||||
},
|
||||
"ValidateRigContents": {
|
||||
"enabled": false,
|
||||
"optional": true,
|
||||
|
|
|
|||
|
|
@ -434,7 +434,7 @@
|
|||
}
|
||||
],
|
||||
"extension": "mov",
|
||||
"add_tags": []
|
||||
"add_custom_tags": []
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -295,6 +295,15 @@
|
|||
"label": "Subsets",
|
||||
"type": "list",
|
||||
"object_type": "text"
|
||||
},
|
||||
{
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"key": "custom_tags",
|
||||
"label": "Custom Tags",
|
||||
"type": "list",
|
||||
"object_type": "text"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -657,6 +657,25 @@
|
|||
"object_type": "text"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"key": "ExtractObj",
|
||||
"label": "Extract OBJ",
|
||||
"checkbox_key": "enabled",
|
||||
"children": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "enabled",
|
||||
"label": "Enabled"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "optional",
|
||||
"label": "Optional"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -296,8 +296,8 @@
|
|||
"label": "Write node file type"
|
||||
},
|
||||
{
|
||||
"key": "add_tags",
|
||||
"label": "Add additional tags to representations",
|
||||
"key": "add_custom_tags",
|
||||
"label": "Add custom tags",
|
||||
"type": "list",
|
||||
"object_type": "text"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,9 @@ from openpype.pipeline.create import (
|
|||
HiddenCreator,
|
||||
Creator,
|
||||
)
|
||||
from openpype.pipeline.create.context import (
|
||||
CreatorsOperationFailed,
|
||||
)
|
||||
|
||||
# Define constant for plugin orders offset
|
||||
PLUGIN_ORDER_OFFSET = 0.5
|
||||
|
|
@ -299,8 +302,11 @@ class PublishReport:
|
|||
}
|
||||
|
||||
def _extract_context_data(self, context):
|
||||
context_label = "Context"
|
||||
if context is not None:
|
||||
context_label = context.data.get("label")
|
||||
return {
|
||||
"label": context.data.get("label")
|
||||
"label": context_label
|
||||
}
|
||||
|
||||
def _extract_instance_data(self, instance, exists):
|
||||
|
|
@ -1101,6 +1107,8 @@ class AbstractPublisherController(object):
|
|||
options (Dict[str, Any]): Data from pre-create attributes.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
def save_changes(self):
|
||||
"""Save changes in create context."""
|
||||
|
||||
|
|
@ -1662,13 +1670,12 @@ class PublisherController(BasePublisherController):
|
|||
|
||||
def reset(self):
|
||||
"""Reset everything related to creation and publishing."""
|
||||
# Stop publishing
|
||||
self.stop_publish()
|
||||
|
||||
self.save_changes()
|
||||
|
||||
self.host_is_valid = self._create_context.host_is_valid
|
||||
|
||||
self._create_context.reset_preparation()
|
||||
|
||||
# Reset avalon context
|
||||
self._create_context.reset_avalon_context()
|
||||
|
||||
|
|
@ -1679,6 +1686,8 @@ class PublisherController(BasePublisherController):
|
|||
self._reset_publish()
|
||||
self._reset_instances()
|
||||
|
||||
self._create_context.reset_finalization()
|
||||
|
||||
self._emit_event("controller.reset.finished")
|
||||
|
||||
self.emit_card_message("Refreshed..")
|
||||
|
|
@ -1711,8 +1720,28 @@ class PublisherController(BasePublisherController):
|
|||
|
||||
self._create_context.reset_context_data()
|
||||
with self._create_context.bulk_instances_collection():
|
||||
self._create_context.reset_instances()
|
||||
self._create_context.execute_autocreators()
|
||||
try:
|
||||
self._create_context.reset_instances()
|
||||
except CreatorsOperationFailed as exc:
|
||||
self._emit_event(
|
||||
"instances.collection.failed",
|
||||
{
|
||||
"title": "Instance collection failed",
|
||||
"failed_info": exc.failed_info
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
self._create_context.execute_autocreators()
|
||||
|
||||
except CreatorsOperationFailed as exc:
|
||||
self._emit_event(
|
||||
"instances.create.failed",
|
||||
{
|
||||
"title": "AutoCreation failed",
|
||||
"failed_info": exc.failed_info
|
||||
}
|
||||
)
|
||||
|
||||
self._resetting_instances = False
|
||||
|
||||
|
|
@ -1841,16 +1870,42 @@ class PublisherController(BasePublisherController):
|
|||
self, creator_identifier, subset_name, instance_data, options
|
||||
):
|
||||
"""Trigger creation and refresh of instances in UI."""
|
||||
creator = self._creators[creator_identifier]
|
||||
creator.create(subset_name, instance_data, options)
|
||||
|
||||
success = True
|
||||
try:
|
||||
self._create_context.create(
|
||||
creator_identifier, subset_name, instance_data, options
|
||||
)
|
||||
except CreatorsOperationFailed as exc:
|
||||
success = False
|
||||
self._emit_event(
|
||||
"instances.create.failed",
|
||||
{
|
||||
"title": "Creation failed",
|
||||
"failed_info": exc.failed_info
|
||||
}
|
||||
)
|
||||
|
||||
self._on_create_instance_change()
|
||||
return success
|
||||
|
||||
def save_changes(self):
|
||||
"""Save changes happened during creation."""
|
||||
if self._create_context.host_is_valid:
|
||||
if not self._create_context.host_is_valid:
|
||||
return
|
||||
|
||||
try:
|
||||
self._create_context.save_changes()
|
||||
|
||||
except CreatorsOperationFailed as exc:
|
||||
self._emit_event(
|
||||
"instances.save.failed",
|
||||
{
|
||||
"title": "Instances save failed",
|
||||
"failed_info": exc.failed_info
|
||||
}
|
||||
)
|
||||
|
||||
def remove_instances(self, instance_ids):
|
||||
"""Remove instances based on instance ids.
|
||||
|
||||
|
|
@ -1872,7 +1927,16 @@ class PublisherController(BasePublisherController):
|
|||
instances_by_id[instance_id]
|
||||
for instance_id in instance_ids
|
||||
]
|
||||
self._create_context.remove_instances(instances)
|
||||
try:
|
||||
self._create_context.remove_instances(instances)
|
||||
except CreatorsOperationFailed as exc:
|
||||
self._emit_event(
|
||||
"instances.remove.failed",
|
||||
{
|
||||
"title": "Instance removement failed",
|
||||
"failed_info": exc.failed_info
|
||||
}
|
||||
)
|
||||
|
||||
def _on_create_instance_change(self):
|
||||
self._emit_event("instances.refresh.finished")
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ from openpype.pipeline.create import (
|
|||
SUBSET_NAME_ALLOWED_SYMBOLS,
|
||||
TaskNotSetError,
|
||||
)
|
||||
from openpype.tools.utils import ErrorMessageBox
|
||||
|
||||
from .widgets import (
|
||||
IconValuePixmapLabel,
|
||||
|
|
@ -35,79 +34,6 @@ class VariantInputsWidget(QtWidgets.QWidget):
|
|||
self.resized.emit()
|
||||
|
||||
|
||||
class CreateErrorMessageBox(ErrorMessageBox):
|
||||
def __init__(
|
||||
self,
|
||||
creator_label,
|
||||
subset_name,
|
||||
asset_name,
|
||||
exc_msg,
|
||||
formatted_traceback,
|
||||
parent
|
||||
):
|
||||
self._creator_label = creator_label
|
||||
self._subset_name = subset_name
|
||||
self._asset_name = asset_name
|
||||
self._exc_msg = exc_msg
|
||||
self._formatted_traceback = formatted_traceback
|
||||
super(CreateErrorMessageBox, self).__init__("Creation failed", parent)
|
||||
|
||||
def _create_top_widget(self, parent_widget):
|
||||
label_widget = QtWidgets.QLabel(parent_widget)
|
||||
label_widget.setText(
|
||||
"<span style='font-size:18pt;'>Failed to create</span>"
|
||||
)
|
||||
return label_widget
|
||||
|
||||
def _get_report_data(self):
|
||||
report_message = (
|
||||
"{creator}: Failed to create Subset: \"{subset}\""
|
||||
" in Asset: \"{asset}\""
|
||||
"\n\nError: {message}"
|
||||
).format(
|
||||
creator=self._creator_label,
|
||||
subset=self._subset_name,
|
||||
asset=self._asset_name,
|
||||
message=self._exc_msg,
|
||||
)
|
||||
if self._formatted_traceback:
|
||||
report_message += "\n\n{}".format(self._formatted_traceback)
|
||||
return [report_message]
|
||||
|
||||
def _create_content(self, content_layout):
|
||||
item_name_template = (
|
||||
"<span style='font-weight:bold;'>Creator:</span> {}<br>"
|
||||
"<span style='font-weight:bold;'>Subset:</span> {}<br>"
|
||||
"<span style='font-weight:bold;'>Asset:</span> {}<br>"
|
||||
)
|
||||
exc_msg_template = "<span style='font-weight:bold'>{}</span>"
|
||||
|
||||
line = self._create_line()
|
||||
content_layout.addWidget(line)
|
||||
|
||||
item_name_widget = QtWidgets.QLabel(self)
|
||||
item_name_widget.setText(
|
||||
item_name_template.format(
|
||||
self._creator_label, self._subset_name, self._asset_name
|
||||
)
|
||||
)
|
||||
content_layout.addWidget(item_name_widget)
|
||||
|
||||
message_label_widget = QtWidgets.QLabel(self)
|
||||
message_label_widget.setText(
|
||||
exc_msg_template.format(self.convert_text_for_html(self._exc_msg))
|
||||
)
|
||||
content_layout.addWidget(message_label_widget)
|
||||
|
||||
if self._formatted_traceback:
|
||||
line_widget = self._create_line()
|
||||
tb_widget = self._create_traceback_widget(
|
||||
self._formatted_traceback
|
||||
)
|
||||
content_layout.addWidget(line_widget)
|
||||
content_layout.addWidget(tb_widget)
|
||||
|
||||
|
||||
# TODO add creator identifier/label to details
|
||||
class CreatorShortDescWidget(QtWidgets.QWidget):
|
||||
def __init__(self, parent=None):
|
||||
|
|
@ -178,8 +104,6 @@ class CreateWidget(QtWidgets.QWidget):
|
|||
|
||||
self._prereq_available = False
|
||||
|
||||
self._message_dialog = None
|
||||
|
||||
name_pattern = "^[{}]*$".format(SUBSET_NAME_ALLOWED_SYMBOLS)
|
||||
self._name_pattern = name_pattern
|
||||
self._compiled_name_pattern = re.compile(name_pattern)
|
||||
|
|
@ -769,7 +693,6 @@ class CreateWidget(QtWidgets.QWidget):
|
|||
return
|
||||
|
||||
index = indexes[0]
|
||||
creator_label = index.data(QtCore.Qt.DisplayRole)
|
||||
creator_identifier = index.data(CREATOR_IDENTIFIER_ROLE)
|
||||
family = index.data(FAMILY_ROLE)
|
||||
variant = self.variant_input.text()
|
||||
|
|
@ -792,40 +715,13 @@ class CreateWidget(QtWidgets.QWidget):
|
|||
"family": family
|
||||
}
|
||||
|
||||
error_msg = None
|
||||
formatted_traceback = None
|
||||
try:
|
||||
self._controller.create(
|
||||
creator_identifier,
|
||||
subset_name,
|
||||
instance_data,
|
||||
pre_create_data
|
||||
)
|
||||
success = self._controller.create(
|
||||
creator_identifier,
|
||||
subset_name,
|
||||
instance_data,
|
||||
pre_create_data
|
||||
)
|
||||
|
||||
except CreatorError as exc:
|
||||
error_msg = str(exc)
|
||||
|
||||
# Use bare except because some hosts raise their exceptions that
|
||||
# do not inherit from python's `BaseException`
|
||||
except:
|
||||
exc_type, exc_value, exc_traceback = sys.exc_info()
|
||||
formatted_traceback = "".join(traceback.format_exception(
|
||||
exc_type, exc_value, exc_traceback
|
||||
))
|
||||
error_msg = str(exc_value)
|
||||
|
||||
if error_msg is None:
|
||||
if success:
|
||||
self._set_creator(self._selected_creator)
|
||||
self._controller.emit_card_message("Creation finished...")
|
||||
else:
|
||||
box = CreateErrorMessageBox(
|
||||
creator_label,
|
||||
subset_name,
|
||||
asset_name,
|
||||
error_msg,
|
||||
formatted_traceback,
|
||||
parent=self
|
||||
)
|
||||
box.show()
|
||||
# Store dialog so is not garbage collected before is shown
|
||||
self._message_dialog = box
|
||||
|
|
|
|||
|
|
@ -93,8 +93,8 @@ class OverviewWidget(QtWidgets.QFrame):
|
|||
main_layout.addWidget(subset_content_widget, 1)
|
||||
|
||||
change_anim = QtCore.QVariantAnimation()
|
||||
change_anim.setStartValue(0)
|
||||
change_anim.setEndValue(self.anim_end_value)
|
||||
change_anim.setStartValue(float(0))
|
||||
change_anim.setEndValue(float(self.anim_end_value))
|
||||
change_anim.setDuration(self.anim_duration)
|
||||
change_anim.setEasingCurve(QtCore.QEasingCurve.InOutQuad)
|
||||
|
||||
|
|
@ -264,9 +264,10 @@ class OverviewWidget(QtWidgets.QFrame):
|
|||
+ (self._subset_content_layout.spacing() * 2)
|
||||
)
|
||||
)
|
||||
subset_attrs_width = int(float(width) / self.anim_end_value) * value
|
||||
subset_attrs_width = int((float(width) / self.anim_end_value) * value)
|
||||
if subset_attrs_width > width:
|
||||
subset_attrs_width = width
|
||||
|
||||
create_width = width - subset_attrs_width
|
||||
|
||||
self._create_widget.setMinimumWidth(create_width)
|
||||
|
|
|
|||
|
|
@ -248,13 +248,13 @@ class PublishFrame(QtWidgets.QWidget):
|
|||
hint = self._top_content_widget.minimumSizeHint()
|
||||
end = hint.height()
|
||||
|
||||
self._shrunk_anim.setStartValue(start)
|
||||
self._shrunk_anim.setEndValue(end)
|
||||
self._shrunk_anim.setStartValue(float(start))
|
||||
self._shrunk_anim.setEndValue(float(end))
|
||||
if not anim_is_running:
|
||||
self._shrunk_anim.start()
|
||||
|
||||
def _on_shrunk_anim(self, value):
|
||||
diff = self._top_content_widget.height() - value
|
||||
diff = self._top_content_widget.height() - int(value)
|
||||
if not self._top_content_widget.isVisible():
|
||||
diff -= self._content_layout.spacing()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import collections
|
||||
from Qt import QtWidgets, QtCore, QtGui
|
||||
|
||||
from openpype import (
|
||||
|
|
@ -5,6 +6,7 @@ from openpype import (
|
|||
style
|
||||
)
|
||||
from openpype.tools.utils import (
|
||||
ErrorMessageBox,
|
||||
PlaceholderLineEdit,
|
||||
MessageOverlayObject,
|
||||
PixmapLabel,
|
||||
|
|
@ -222,6 +224,12 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
# Floating publish frame
|
||||
publish_frame = PublishFrame(controller, self.footer_border, self)
|
||||
|
||||
creators_dialog_message_timer = QtCore.QTimer()
|
||||
creators_dialog_message_timer.setInterval(100)
|
||||
creators_dialog_message_timer.timeout.connect(
|
||||
self._on_creators_message_timeout
|
||||
)
|
||||
|
||||
help_btn.clicked.connect(self._on_help_click)
|
||||
tabs_widget.tab_changed.connect(self._on_tab_change)
|
||||
overview_widget.active_changed.connect(
|
||||
|
|
@ -259,6 +267,18 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
controller.event_system.add_callback(
|
||||
"show.card.message", self._on_overlay_message
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
"instances.collection.failed", self._instance_collection_failed
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
"instances.save.failed", self._instance_save_failed
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
"instances.remove.failed", self._instance_remove_failed
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
"instances.create.failed", self._instance_create_failed
|
||||
)
|
||||
|
||||
# Store extra header widget for TrayPublisher
|
||||
# - can be used to add additional widgets to header between context
|
||||
|
|
@ -298,10 +318,16 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
self._controller = controller
|
||||
|
||||
self._first_show = True
|
||||
self._reset_on_show = reset_on_show
|
||||
# This is a little bit confusing but 'reset_on_first_show' is too long
|
||||
# forin init
|
||||
self._reset_on_first_show = reset_on_show
|
||||
self._reset_on_show = True
|
||||
self._restart_timer = None
|
||||
self._publish_frame_visible = None
|
||||
|
||||
self._creators_messages_to_show = collections.deque()
|
||||
self._creators_dialog_message_timer = creators_dialog_message_timer
|
||||
|
||||
self._set_publish_visibility(False)
|
||||
|
||||
@property
|
||||
|
|
@ -314,6 +340,18 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
self._first_show = False
|
||||
self._on_first_show()
|
||||
|
||||
if not self._reset_on_show:
|
||||
return
|
||||
|
||||
self._reset_on_show = False
|
||||
# Detach showing - give OS chance to draw the window
|
||||
timer = QtCore.QTimer()
|
||||
timer.setSingleShot(True)
|
||||
timer.setInterval(1)
|
||||
timer.timeout.connect(self._on_show_restart_timer)
|
||||
self._restart_timer = timer
|
||||
timer.start()
|
||||
|
||||
def resizeEvent(self, event):
|
||||
super(PublisherWindow, self).resizeEvent(event)
|
||||
self._update_publish_frame_rect()
|
||||
|
|
@ -324,16 +362,7 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
def _on_first_show(self):
|
||||
self.resize(self.default_width, self.default_height)
|
||||
self.setStyleSheet(style.load_stylesheet())
|
||||
if not self._reset_on_show:
|
||||
return
|
||||
|
||||
# Detach showing - give OS chance to draw the window
|
||||
timer = QtCore.QTimer()
|
||||
timer.setSingleShot(True)
|
||||
timer.setInterval(1)
|
||||
timer.timeout.connect(self._on_show_restart_timer)
|
||||
self._restart_timer = timer
|
||||
timer.start()
|
||||
self._reset_on_show = self._reset_on_first_show
|
||||
|
||||
def _on_show_restart_timer(self):
|
||||
"""Callback for '_restart_timer' timer."""
|
||||
|
|
@ -342,9 +371,13 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
self.reset()
|
||||
|
||||
def closeEvent(self, event):
|
||||
self._controller.save_changes()
|
||||
self.save_changes()
|
||||
self._reset_on_show = True
|
||||
super(PublisherWindow, self).closeEvent(event)
|
||||
|
||||
def save_changes(self):
|
||||
self._controller.save_changes()
|
||||
|
||||
def reset(self):
|
||||
self._controller.reset()
|
||||
|
||||
|
|
@ -436,7 +469,8 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
self._update_publish_frame_rect()
|
||||
|
||||
def _on_reset_clicked(self):
|
||||
self._controller.reset()
|
||||
self.save_changes()
|
||||
self.reset()
|
||||
|
||||
def _on_stop_clicked(self):
|
||||
self._controller.stop_publish()
|
||||
|
|
@ -472,7 +506,7 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
self._update_publish_details_widget()
|
||||
if (
|
||||
not self._tabs_widget.is_current_tab("create")
|
||||
or not self._tabs_widget.is_current_tab("publish")
|
||||
and not self._tabs_widget.is_current_tab("publish")
|
||||
):
|
||||
self._tabs_widget.set_current_tab("publish")
|
||||
|
||||
|
|
@ -569,3 +603,129 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
self._publish_frame.move(
|
||||
0, window_size.height() - height
|
||||
)
|
||||
|
||||
def add_message_dialog(self, title, failed_info):
|
||||
self._creators_messages_to_show.append((title, failed_info))
|
||||
self._creators_dialog_message_timer.start()
|
||||
|
||||
def _on_creators_message_timeout(self):
|
||||
if not self._creators_messages_to_show:
|
||||
self._creators_dialog_message_timer.stop()
|
||||
return
|
||||
|
||||
item = self._creators_messages_to_show.popleft()
|
||||
title, failed_info = item
|
||||
dialog = CreatorsErrorMessageBox(title, failed_info, self)
|
||||
dialog.exec_()
|
||||
dialog.deleteLater()
|
||||
|
||||
def _instance_collection_failed(self, event):
|
||||
self.add_message_dialog(event["title"], event["failed_info"])
|
||||
|
||||
def _instance_save_failed(self, event):
|
||||
self.add_message_dialog(event["title"], event["failed_info"])
|
||||
|
||||
def _instance_remove_failed(self, event):
|
||||
self.add_message_dialog(event["title"], event["failed_info"])
|
||||
|
||||
def _instance_create_failed(self, event):
|
||||
self.add_message_dialog(event["title"], event["failed_info"])
|
||||
|
||||
|
||||
class CreatorsErrorMessageBox(ErrorMessageBox):
|
||||
def __init__(self, error_title, failed_info, parent):
|
||||
self._failed_info = failed_info
|
||||
self._info_with_id = [
|
||||
# Id must be string when used in tab widget
|
||||
{"id": str(idx), "info": info}
|
||||
for idx, info in enumerate(failed_info)
|
||||
]
|
||||
self._widgets_by_id = {}
|
||||
self._tabs_widget = None
|
||||
self._stack_layout = None
|
||||
|
||||
super(CreatorsErrorMessageBox, self).__init__(error_title, parent)
|
||||
|
||||
layout = self.layout()
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
|
||||
footer_layout = self._footer_widget.layout()
|
||||
footer_layout.setContentsMargins(5, 5, 5, 5)
|
||||
|
||||
def _create_top_widget(self, parent_widget):
|
||||
return None
|
||||
|
||||
def _get_report_data(self):
|
||||
output = []
|
||||
for info in self._failed_info:
|
||||
creator_label = info["creator_label"]
|
||||
creator_identifier = info["creator_identifier"]
|
||||
report_message = "Creator:"
|
||||
if creator_label:
|
||||
report_message += " {} ({})".format(
|
||||
creator_label, creator_identifier)
|
||||
else:
|
||||
report_message += " {}".format(creator_identifier)
|
||||
|
||||
report_message += "\n\nError: {}".format(info["message"])
|
||||
formatted_traceback = info["traceback"]
|
||||
if formatted_traceback:
|
||||
report_message += "\n\n{}".format(formatted_traceback)
|
||||
output.append(report_message)
|
||||
return output
|
||||
|
||||
def _create_content(self, content_layout):
|
||||
tabs_widget = PublisherTabsWidget(self)
|
||||
|
||||
stack_widget = QtWidgets.QFrame(self._content_widget)
|
||||
stack_layout = QtWidgets.QStackedLayout(stack_widget)
|
||||
|
||||
first = True
|
||||
for item in self._info_with_id:
|
||||
item_id = item["id"]
|
||||
info = item["info"]
|
||||
message = info["message"]
|
||||
formatted_traceback = info["traceback"]
|
||||
creator_label = info["creator_label"]
|
||||
creator_identifier = info["creator_identifier"]
|
||||
if not creator_label:
|
||||
creator_label = creator_identifier
|
||||
|
||||
msg_widget = QtWidgets.QWidget(stack_widget)
|
||||
msg_layout = QtWidgets.QVBoxLayout(msg_widget)
|
||||
|
||||
exc_msg_template = "<span style='font-weight:bold'>{}</span>"
|
||||
message_label_widget = QtWidgets.QLabel(msg_widget)
|
||||
message_label_widget.setText(
|
||||
exc_msg_template.format(self.convert_text_for_html(message))
|
||||
)
|
||||
msg_layout.addWidget(message_label_widget, 0)
|
||||
|
||||
if formatted_traceback:
|
||||
line_widget = self._create_line(msg_widget)
|
||||
tb_widget = self._create_traceback_widget(formatted_traceback)
|
||||
msg_layout.addWidget(line_widget, 0)
|
||||
msg_layout.addWidget(tb_widget, 0)
|
||||
|
||||
msg_layout.addStretch(1)
|
||||
|
||||
tabs_widget.add_tab(creator_label, item_id)
|
||||
stack_layout.addWidget(msg_widget)
|
||||
if first:
|
||||
first = False
|
||||
stack_layout.setCurrentWidget(msg_widget)
|
||||
|
||||
self._widgets_by_id[item_id] = msg_widget
|
||||
|
||||
content_layout.addWidget(tabs_widget, 0)
|
||||
content_layout.addWidget(stack_widget, 1)
|
||||
|
||||
tabs_widget.tab_changed.connect(self._on_tab_change)
|
||||
|
||||
self._tabs_widget = tabs_widget
|
||||
self._stack_layout = stack_layout
|
||||
|
||||
def _on_tab_change(self, old_identifier, identifier):
|
||||
widget = self._widgets_by_id[identifier]
|
||||
self._stack_layout.setCurrentWidget(widget)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from .widgets import (
|
|||
ExpandBtn,
|
||||
PixmapLabel,
|
||||
IconButton,
|
||||
SeparatorWidget,
|
||||
)
|
||||
from .views import DeselectableTreeView
|
||||
from .error_dialog import ErrorMessageBox
|
||||
|
|
@ -37,6 +38,7 @@ __all__ = (
|
|||
"ExpandBtn",
|
||||
"PixmapLabel",
|
||||
"IconButton",
|
||||
"SeparatorWidget",
|
||||
|
||||
"DeselectableTreeView",
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
from Qt import QtWidgets, QtCore
|
||||
|
||||
from .widgets import ClickableFrame, ExpandBtn
|
||||
from .widgets import ClickableFrame, ExpandBtn, SeparatorWidget
|
||||
|
||||
|
||||
def convert_text_for_html(text):
|
||||
def escape_text_for_html(text):
|
||||
return (
|
||||
text
|
||||
.replace("<", "<")
|
||||
|
|
@ -19,7 +19,7 @@ class TracebackWidget(QtWidgets.QWidget):
|
|||
|
||||
# Modify text to match html
|
||||
# - add more replacements when needed
|
||||
tb_text = convert_text_for_html(tb_text)
|
||||
tb_text = escape_text_for_html(tb_text)
|
||||
expand_btn = ExpandBtn(self)
|
||||
|
||||
clickable_frame = ClickableFrame(self)
|
||||
|
|
@ -85,17 +85,20 @@ class ErrorMessageBox(QtWidgets.QDialog):
|
|||
copy_report_btn = QtWidgets.QPushButton("Copy report", self)
|
||||
ok_btn = QtWidgets.QPushButton("OK", self)
|
||||
|
||||
footer_layout = QtWidgets.QHBoxLayout()
|
||||
footer_widget = QtWidgets.QWidget(self)
|
||||
footer_layout = QtWidgets.QHBoxLayout(footer_widget)
|
||||
footer_layout.setContentsMargins(0, 0, 0, 0)
|
||||
footer_layout.addWidget(copy_report_btn, 0)
|
||||
footer_layout.addStretch(1)
|
||||
footer_layout.addWidget(ok_btn, 0)
|
||||
|
||||
bottom_line = self._create_line()
|
||||
body_layout = QtWidgets.QVBoxLayout(self)
|
||||
body_layout.addWidget(top_widget, 0)
|
||||
body_layout.addWidget(content_scroll, 1)
|
||||
body_layout.addWidget(bottom_line, 0)
|
||||
body_layout.addLayout(footer_layout, 0)
|
||||
main_layout = QtWidgets.QVBoxLayout(self)
|
||||
if top_widget is not None:
|
||||
main_layout.addWidget(top_widget, 0)
|
||||
main_layout.addWidget(content_scroll, 1)
|
||||
main_layout.addWidget(bottom_line, 0)
|
||||
main_layout.addWidget(footer_widget, 0)
|
||||
|
||||
copy_report_btn.clicked.connect(self._on_copy_report)
|
||||
ok_btn.clicked.connect(self._on_ok_clicked)
|
||||
|
|
@ -106,11 +109,13 @@ class ErrorMessageBox(QtWidgets.QDialog):
|
|||
if not report_data:
|
||||
copy_report_btn.setVisible(False)
|
||||
|
||||
self._content_scroll = content_scroll
|
||||
self._footer_widget = footer_widget
|
||||
self._report_data = report_data
|
||||
|
||||
@staticmethod
|
||||
def convert_text_for_html(text):
|
||||
return convert_text_for_html(text)
|
||||
return escape_text_for_html(text)
|
||||
|
||||
def _create_top_widget(self, parent_widget):
|
||||
label_widget = QtWidgets.QLabel(parent_widget)
|
||||
|
|
@ -131,7 +136,8 @@ class ErrorMessageBox(QtWidgets.QDialog):
|
|||
self.close()
|
||||
|
||||
def _on_copy_report(self):
|
||||
report_text = (10 * "*").join(self._report_data)
|
||||
sep = "\n{}\n".format(10 * "*")
|
||||
report_text = sep.join(self._report_data)
|
||||
|
||||
mime_data = QtCore.QMimeData()
|
||||
mime_data.setText(report_text)
|
||||
|
|
@ -139,12 +145,10 @@ class ErrorMessageBox(QtWidgets.QDialog):
|
|||
mime_data
|
||||
)
|
||||
|
||||
def _create_line(self):
|
||||
line = QtWidgets.QFrame(self)
|
||||
line.setObjectName("Separator")
|
||||
line.setMinimumHeight(2)
|
||||
line.setMaximumHeight(2)
|
||||
return line
|
||||
def _create_line(self, parent=None):
|
||||
if parent is None:
|
||||
parent = self
|
||||
return SeparatorWidget(2, parent=parent)
|
||||
|
||||
def _create_traceback_widget(self, traceback_text, parent=None):
|
||||
if parent is None:
|
||||
|
|
|
|||
|
|
@ -448,3 +448,57 @@ class OptionDialog(QtWidgets.QDialog):
|
|||
|
||||
def parse(self):
|
||||
return self._options.copy()
|
||||
|
||||
|
||||
class SeparatorWidget(QtWidgets.QFrame):
|
||||
"""Prepared widget that can be used as separator with predefined color.
|
||||
|
||||
Args:
|
||||
size (int): Size of separator (width or height).
|
||||
orientation (Qt.Horizontal|Qt.Vertical): Orintation of widget.
|
||||
parent (QtWidgets.QWidget): Parent widget.
|
||||
"""
|
||||
|
||||
def __init__(self, size=2, orientation=QtCore.Qt.Horizontal, parent=None):
|
||||
super(SeparatorWidget, self).__init__(parent)
|
||||
|
||||
self.setObjectName("Separator")
|
||||
|
||||
maximum_width = self.maximumWidth()
|
||||
maximum_height = self.maximumHeight()
|
||||
|
||||
self._size = None
|
||||
self._orientation = orientation
|
||||
self._maximum_width = maximum_width
|
||||
self._maximum_height = maximum_height
|
||||
self.set_size(size)
|
||||
|
||||
def set_size(self, size):
|
||||
if size == self._size:
|
||||
return
|
||||
if self._orientation == QtCore.Qt.Vertical:
|
||||
self.setMinimumWidth(size)
|
||||
self.setMaximumWidth(size)
|
||||
else:
|
||||
self.setMinimumHeight(size)
|
||||
self.setMaximumHeight(size)
|
||||
|
||||
self._size = size
|
||||
|
||||
def set_orientation(self, orientation):
|
||||
if self._orientation == orientation:
|
||||
return
|
||||
|
||||
# Reset min/max sizes in opossite direction
|
||||
if self._orientation == QtCore.Qt.Vertical:
|
||||
self.setMinimumHeight(0)
|
||||
self.setMaximumHeight(self._maximum_height)
|
||||
else:
|
||||
self.setMinimumWidth(0)
|
||||
self.setMaximumWidth(self._maximum_width)
|
||||
|
||||
self._orientation = orientation
|
||||
|
||||
size = self._size
|
||||
self._size = None
|
||||
self.set_size(size)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring Pype version."""
|
||||
__version__ = "3.14.4"
|
||||
__version__ = "3.14.5"
|
||||
|
|
|
|||
|
|
@ -47,10 +47,14 @@ Context discovers creator and publish plugins. Trigger collections of existing i
|
|||
|
||||
Creator plugins can call **creator_adds_instance** or **creator_removed_instance** to add/remove instances but these methods are not meant to be called directly out of the creator. The reason is that it is the creator's responsibility to remove metadata or decide if it should remove the instance.
|
||||
|
||||
#### Required functions in host implementation
|
||||
Host implementation **must** implement **get_context_data** and **update_context_data**. These two functions are needed to store metadata that are not related to any instance but are needed for Creating and publishing process. Right now only data about enabled/disabled optional publish plugins is stored there. When data is not stored and loaded properly, reset of publishing will cause that they will be set to default value. Context data also parsed to json string similarly as instance data.
|
||||
During reset are re-cached Creator plugins, re-collected instances, refreshed host context and more. Object of `CreateContext` supply shared data during the reset. They can be used by creators to share same data needed during collection phase or during creation for autocreators.
|
||||
|
||||
There are also few optional functions. For UI purposes it is possible to implement **get_context_title** which can return a string shown in UI as a title. Output string may contain html tags. It is recommended to return context path (it will be created function this purposes) in this order `"{project name}/{asset hierarchy}/<b>{asset name}</b>/{task name}"`.
|
||||
#### Required functions in host implementation
|
||||
It is recommended to use `HostBase` class (`from openpype.host import HostBase`) as base for host implementation with combination of `IPublishHost` interface (`from openpype.host import IPublishHost`). These abstract classes should guide you to fill missing attributes and methods.
|
||||
|
||||
To sum them and in case host implementation is inheriting `HostBase` the implementation **must** implement **get_context_data** and **update_context_data**. These two functions are needed to store metadata that are not related to any instance but are needed for Creating and publishing process. Right now only data about enabled/disabled optional publish plugins is stored there. When data is not stored and loaded properly, reset of publishing will cause that they will be set to default value. Context data also parsed to json string similarly as instance data.
|
||||
|
||||
There are also few optional functions. For UI purposes it is possible to implement **get_context_title** which can return a string shown in UI as a title. Output string may contain html tags. It is recommended to return context path (it will be created function this purposes) in this order `"{project name}/{asset hierarchy}/<b>{asset name}</b>/{task name}"` (this is default implementation in `HostBase`).
|
||||
|
||||
Another optional function is **get_current_context**. This function is handy in hosts where it is possible to open multiple workfiles in one process so using global context variables is not relevant because artists can switch between opened workfiles without being acknowledged. When a function is not implemented or won't return the right keys the global context is used.
|
||||
```json
|
||||
|
|
@ -68,6 +72,9 @@ Main responsibility of create plugin is to create, update, collect and remove in
|
|||
#### *BaseCreator*
|
||||
Base implementation of creator plugin. It is not recommended to use this class as base for production plugins but rather use one of **HiddenCreator**, **AutoCreator** and **Creator** variants.
|
||||
|
||||
**Access to shared data**
|
||||
Functions to work with "Collection shared data" can be used during reset phase of `CreateContext`. Creators can cache there data that are common for them. For example list of nodes in scene. Methods are implemented on `CreateContext` but their usage is primarily for Create plugins as nothing else should use it. Each creator can access `collection_shared_data` attribute which is a dictionary where shared data can be stored.
|
||||
|
||||
**Abstractions**
|
||||
- **`family`** (class attr) - Tells what kind of instance will be created.
|
||||
```python
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue