mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge branch 'develop' into enhancement/remove_env_groups_from_settings
This commit is contained in:
commit
8ae3ccdccb
19 changed files with 653 additions and 100 deletions
|
|
@ -79,6 +79,7 @@ IMAGE_PREFIXES = {
|
|||
"redshift": "defaultRenderGlobals.imageFilePrefix",
|
||||
}
|
||||
|
||||
RENDERMAN_IMAGE_DIR = "maya/<scene>/<layer>"
|
||||
|
||||
@attr.s
|
||||
class LayerMetadata(object):
|
||||
|
|
@ -1054,6 +1055,8 @@ class RenderProductsRenderman(ARenderProducts):
|
|||
:func:`ARenderProducts.get_render_products()`
|
||||
|
||||
"""
|
||||
from rfm2.api.displays import get_displays # noqa
|
||||
|
||||
cameras = [
|
||||
self.sanitize_camera_name(c)
|
||||
for c in self.get_renderable_cameras()
|
||||
|
|
@ -1066,42 +1069,56 @@ class RenderProductsRenderman(ARenderProducts):
|
|||
]
|
||||
products = []
|
||||
|
||||
default_ext = "exr"
|
||||
displays = cmds.listConnections("rmanGlobals.displays")
|
||||
for aov in displays:
|
||||
enabled = self._get_attr(aov, "enabled")
|
||||
# NOTE: This is guessing extensions from renderman display types.
|
||||
# Some of them are just framebuffers, d_texture format can be
|
||||
# set in display setting. We set those now to None, but it
|
||||
# should be handled more gracefully.
|
||||
display_types = {
|
||||
"d_deepexr": "exr",
|
||||
"d_it": None,
|
||||
"d_null": None,
|
||||
"d_openexr": "exr",
|
||||
"d_png": "png",
|
||||
"d_pointcloud": "ptc",
|
||||
"d_targa": "tga",
|
||||
"d_texture": None,
|
||||
"d_tiff": "tif"
|
||||
}
|
||||
|
||||
displays = get_displays()["displays"]
|
||||
for name, display in displays.items():
|
||||
enabled = display["params"]["enable"]["value"]
|
||||
if not enabled:
|
||||
continue
|
||||
|
||||
aov_name = str(aov)
|
||||
aov_name = name
|
||||
if aov_name == "rmanDefaultDisplay":
|
||||
aov_name = "beauty"
|
||||
|
||||
extensions = display_types.get(
|
||||
display["driverNode"]["type"], "exr")
|
||||
|
||||
for camera in cameras:
|
||||
product = RenderProduct(productName=aov_name,
|
||||
ext=default_ext,
|
||||
ext=extensions,
|
||||
camera=camera)
|
||||
products.append(product)
|
||||
|
||||
return products
|
||||
|
||||
def get_files(self, product, camera):
|
||||
def get_files(self, product):
|
||||
"""Get expected files.
|
||||
|
||||
In renderman we hack it with prepending path. This path would
|
||||
normally be translated from `rmanGlobals.imageOutputDir`. We skip
|
||||
this and hardcode prepend path we expect. There is no place for user
|
||||
to mess around with this settings anyway and it is enforced in
|
||||
render settings validator.
|
||||
"""
|
||||
files = super(RenderProductsRenderman, self).get_files(product, camera)
|
||||
files = super(RenderProductsRenderman, self).get_files(product)
|
||||
|
||||
layer_data = self.layer_data
|
||||
new_files = []
|
||||
|
||||
resolved_image_dir = re.sub("<scene>", layer_data.sceneName, RENDERMAN_IMAGE_DIR, flags=re.IGNORECASE) # noqa: E501
|
||||
resolved_image_dir = re.sub("<layer>", layer_data.layerName, resolved_image_dir, flags=re.IGNORECASE) # noqa: E501
|
||||
for file in files:
|
||||
new_file = "{}/{}/{}".format(
|
||||
layer_data["sceneName"], layer_data["layerName"], file
|
||||
)
|
||||
new_file = "{}/{}".format(resolved_image_dir, file)
|
||||
new_files.append(new_file)
|
||||
|
||||
return new_files
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ class CreateRender(plugin.Creator):
|
|||
'mentalray': 'defaultRenderGlobals.imageFilePrefix',
|
||||
'vray': 'vraySettings.fileNamePrefix',
|
||||
'arnold': 'defaultRenderGlobals.imageFilePrefix',
|
||||
'renderman': 'defaultRenderGlobals.imageFilePrefix',
|
||||
'renderman': 'rmanGlobals.imageFileFormat',
|
||||
'redshift': 'defaultRenderGlobals.imageFilePrefix'
|
||||
}
|
||||
|
||||
|
|
@ -84,7 +84,9 @@ class CreateRender(plugin.Creator):
|
|||
'mentalray': 'maya/<Scene>/<RenderLayer>/<RenderLayer>{aov_separator}<RenderPass>', # noqa
|
||||
'vray': 'maya/<scene>/<Layer>/<Layer>',
|
||||
'arnold': 'maya/<Scene>/<RenderLayer>/<RenderLayer>{aov_separator}<RenderPass>', # noqa
|
||||
'renderman': 'maya/<Scene>/<layer>/<layer>{aov_separator}<aov>',
|
||||
# this needs `imageOutputDir`
|
||||
# (<ws>/renders/maya/<scene>) set separately
|
||||
'renderman': '<layer>_<aov>.<f4>.<ext>',
|
||||
'redshift': 'maya/<Scene>/<RenderLayer>/<RenderLayer>' # noqa
|
||||
}
|
||||
|
||||
|
|
@ -440,6 +442,10 @@ class CreateRender(plugin.Creator):
|
|||
|
||||
self._set_global_output_settings()
|
||||
|
||||
if renderer == "renderman":
|
||||
cmds.setAttr("rmanGlobals.imageOutputDir",
|
||||
"maya/<scene>/<layer>", type="string")
|
||||
|
||||
def _set_vray_settings(self, asset):
|
||||
# type: (dict) -> None
|
||||
"""Sets important settings for Vray."""
|
||||
|
|
|
|||
|
|
@ -194,13 +194,11 @@ class CollectMayaRender(pyblish.api.ContextPlugin):
|
|||
assert render_products, "no render products generated"
|
||||
exp_files = []
|
||||
multipart = False
|
||||
render_cameras = []
|
||||
for product in render_products:
|
||||
if product.multipart:
|
||||
multipart = True
|
||||
product_name = product.productName
|
||||
if product.camera and layer_render_products.has_camera_token():
|
||||
render_cameras.append(product.camera)
|
||||
product_name = "{}{}".format(
|
||||
product.camera,
|
||||
"_" + product_name if product_name else "")
|
||||
|
|
@ -210,7 +208,8 @@ class CollectMayaRender(pyblish.api.ContextPlugin):
|
|||
product)
|
||||
})
|
||||
|
||||
assert render_cameras, "No render cameras found."
|
||||
has_cameras = any(product.camera for product in render_products)
|
||||
assert has_cameras, "No render cameras found."
|
||||
|
||||
self.log.info("multipart: {}".format(
|
||||
multipart))
|
||||
|
|
|
|||
|
|
@ -69,14 +69,7 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin):
|
|||
|
||||
redshift_AOV_prefix = "<BeautyPath>/<BeautyFile>{aov_separator}<RenderPass>" # noqa: E501
|
||||
|
||||
# WARNING: There is bug? in renderman, translating <scene> token
|
||||
# to something left behind mayas default image prefix. So instead
|
||||
# `SceneName_v01` it translates to:
|
||||
# `SceneName_v01/<RenderLayer>/<RenderLayers_<RenderPass>` that means
|
||||
# for example:
|
||||
# `SceneName_v01/Main/Main_<RenderPass>`. Possible solution is to define
|
||||
# custom token like <scene_name> to point to determined scene name.
|
||||
RendermanDirPrefix = "<ws>/renders/maya/<scene>/<layer>"
|
||||
renderman_dir_prefix = "maya/<scene>/<layer>"
|
||||
|
||||
R_AOV_TOKEN = re.compile(
|
||||
r'%a|<aov>|<renderpass>', re.IGNORECASE)
|
||||
|
|
@ -116,15 +109,22 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin):
|
|||
|
||||
prefix = prefix.replace(
|
||||
"{aov_separator}", instance.data.get("aovSeparator", "_"))
|
||||
|
||||
required_prefix = "maya/<scene>"
|
||||
|
||||
if not anim_override:
|
||||
invalid = True
|
||||
cls.log.error("Animation needs to be enabled. Use the same "
|
||||
"frame for start and end to render single frame")
|
||||
|
||||
if not prefix.lower().startswith("maya/<scene>"):
|
||||
if renderer != "renderman" and not prefix.lower().startswith(
|
||||
required_prefix):
|
||||
invalid = True
|
||||
cls.log.error("Wrong image prefix [ {} ] - "
|
||||
"doesn't start with: 'maya/<scene>'".format(prefix))
|
||||
cls.log.error(
|
||||
("Wrong image prefix [ {} ] "
|
||||
" - doesn't start with: '{}'").format(
|
||||
prefix, required_prefix)
|
||||
)
|
||||
|
||||
if not re.search(cls.R_LAYER_TOKEN, prefix):
|
||||
invalid = True
|
||||
|
|
@ -198,7 +198,7 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin):
|
|||
invalid = True
|
||||
cls.log.error("Wrong image prefix [ {} ]".format(file_prefix))
|
||||
|
||||
if dir_prefix.lower() != cls.RendermanDirPrefix.lower():
|
||||
if dir_prefix.lower() != cls.renderman_dir_prefix.lower():
|
||||
invalid = True
|
||||
cls.log.error("Wrong directory prefix [ {} ]".format(
|
||||
dir_prefix))
|
||||
|
|
@ -304,7 +304,7 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin):
|
|||
default_prefix,
|
||||
type="string")
|
||||
cmds.setAttr("rmanGlobals.imageOutputDir",
|
||||
cls.RendermanDirPrefix,
|
||||
cls.renderman_dir_prefix,
|
||||
type="string")
|
||||
|
||||
if renderer == "vray":
|
||||
|
|
|
|||
|
|
@ -1532,13 +1532,13 @@ class BuildWorkfile:
|
|||
|
||||
subsets = list(legacy_io.find({
|
||||
"type": "subset",
|
||||
"parent": {"$in": asset_entity_by_ids.keys()}
|
||||
"parent": {"$in": list(asset_entity_by_ids.keys())}
|
||||
}))
|
||||
subset_entity_by_ids = {subset["_id"]: subset for subset in subsets}
|
||||
|
||||
sorted_versions = list(legacy_io.find({
|
||||
"type": "version",
|
||||
"parent": {"$in": subset_entity_by_ids.keys()}
|
||||
"parent": {"$in": list(subset_entity_by_ids.keys())}
|
||||
}).sort("name", -1))
|
||||
|
||||
subset_id_with_latest_version = []
|
||||
|
|
@ -1552,7 +1552,7 @@ class BuildWorkfile:
|
|||
|
||||
repres = legacy_io.find({
|
||||
"type": "representation",
|
||||
"parent": {"$in": last_versions_by_id.keys()}
|
||||
"parent": {"$in": list(last_versions_by_id.keys())}
|
||||
})
|
||||
|
||||
output = {}
|
||||
|
|
|
|||
|
|
@ -365,6 +365,7 @@ class TemplateResult(str):
|
|||
when value of key in data is dictionary but template expect string
|
||||
of number.
|
||||
"""
|
||||
|
||||
used_values = None
|
||||
solved = None
|
||||
template = None
|
||||
|
|
@ -383,6 +384,12 @@ class TemplateResult(str):
|
|||
new_obj.invalid_types = invalid_types
|
||||
return new_obj
|
||||
|
||||
def __copy__(self, *args, **kwargs):
|
||||
return self.copy()
|
||||
|
||||
def __deepcopy__(self, *args, **kwargs):
|
||||
return self.copy()
|
||||
|
||||
def validate(self):
|
||||
if not self.solved:
|
||||
raise TemplateUnsolved(
|
||||
|
|
@ -391,6 +398,17 @@ class TemplateResult(str):
|
|||
self.invalid_types
|
||||
)
|
||||
|
||||
def copy(self):
|
||||
cls = self.__class__
|
||||
return cls(
|
||||
str(self),
|
||||
self.template,
|
||||
self.solved,
|
||||
self.used_values,
|
||||
self.missing_keys,
|
||||
self.invalid_types
|
||||
)
|
||||
|
||||
|
||||
class TemplatesResultDict(dict):
|
||||
"""Holds and wrap TemplateResults for easy bug report."""
|
||||
|
|
|
|||
|
|
@ -727,9 +727,9 @@ def get_ffmpeg_format_args(ffprobe_data, source_ffmpeg_cmd=None):
|
|||
def _ffmpeg_mxf_format_args(ffprobe_data, source_ffmpeg_cmd):
|
||||
input_format = ffprobe_data["format"]
|
||||
format_tags = input_format.get("tags") or {}
|
||||
product_name = format_tags.get("product_name") or ""
|
||||
operational_pattern_ul = format_tags.get("operational_pattern_ul") or ""
|
||||
output = []
|
||||
if "opatom" in product_name.lower():
|
||||
if operational_pattern_ul == "060e2b34.04010102.0d010201.10030000":
|
||||
output.extend(["-f", "mxf_opatom"])
|
||||
return output
|
||||
|
||||
|
|
|
|||
|
|
@ -188,6 +188,10 @@ def get_renderer_variables(renderlayer, root):
|
|||
filename_0 = re.sub('_<RenderPass>', '_beauty',
|
||||
filename_0, flags=re.IGNORECASE)
|
||||
prefix_attr = "defaultRenderGlobals.imageFilePrefix"
|
||||
|
||||
scene = cmds.file(query=True, sceneName=True)
|
||||
scene, _ = os.path.splitext(os.path.basename(scene))
|
||||
|
||||
if renderer == "vray":
|
||||
renderlayer = renderlayer.split("_")[-1]
|
||||
# Maya's renderSettings function does not return V-Ray file extension
|
||||
|
|
@ -207,8 +211,7 @@ def get_renderer_variables(renderlayer, root):
|
|||
filename_prefix = cmds.getAttr(prefix_attr)
|
||||
# we need to determine path for vray as maya `renderSettings` query
|
||||
# does not work for vray.
|
||||
scene = cmds.file(query=True, sceneName=True)
|
||||
scene, _ = os.path.splitext(os.path.basename(scene))
|
||||
|
||||
filename_0 = re.sub('<Scene>', scene, filename_prefix, flags=re.IGNORECASE) # noqa: E501
|
||||
filename_0 = re.sub('<Layer>', renderlayer, filename_0, flags=re.IGNORECASE) # noqa: E501
|
||||
filename_0 = "{}.{}.{}".format(
|
||||
|
|
@ -216,6 +219,39 @@ def get_renderer_variables(renderlayer, root):
|
|||
filename_0 = os.path.normpath(os.path.join(root, filename_0))
|
||||
elif renderer == "renderman":
|
||||
prefix_attr = "rmanGlobals.imageFileFormat"
|
||||
# NOTE: This is guessing extensions from renderman display types.
|
||||
# Some of them are just framebuffers, d_texture format can be
|
||||
# set in display setting. We set those now to None, but it
|
||||
# should be handled more gracefully.
|
||||
display_types = {
|
||||
"d_deepexr": "exr",
|
||||
"d_it": None,
|
||||
"d_null": None,
|
||||
"d_openexr": "exr",
|
||||
"d_png": "png",
|
||||
"d_pointcloud": "ptc",
|
||||
"d_targa": "tga",
|
||||
"d_texture": None,
|
||||
"d_tiff": "tif"
|
||||
}
|
||||
|
||||
extension = display_types.get(
|
||||
cmds.listConnections("rmanDefaultDisplay.displayType")[0],
|
||||
"exr"
|
||||
) or "exr"
|
||||
|
||||
filename_prefix = "{}/{}".format(
|
||||
cmds.getAttr("rmanGlobals.imageOutputDir"),
|
||||
cmds.getAttr("rmanGlobals.imageFileFormat")
|
||||
)
|
||||
|
||||
renderlayer = renderlayer.split("_")[-1]
|
||||
|
||||
filename_0 = re.sub('<scene>', scene, filename_prefix, flags=re.IGNORECASE) # noqa: E501
|
||||
filename_0 = re.sub('<layer>', renderlayer, filename_0, flags=re.IGNORECASE) # noqa: E501
|
||||
filename_0 = re.sub('<f[\\d+]>', "#" * int(padding), filename_0, flags=re.IGNORECASE) # noqa: E501
|
||||
filename_0 = re.sub('<ext>', extension, filename_0, flags=re.IGNORECASE) # noqa: E501
|
||||
filename_0 = os.path.normpath(os.path.join(root, filename_0))
|
||||
elif renderer == "redshift":
|
||||
# mapping redshift extension dropdown values to strings
|
||||
ext_mapping = ["iff", "exr", "tif", "png", "tga", "jpg"]
|
||||
|
|
@ -404,6 +440,8 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin):
|
|||
|
||||
output_filename_0 = filename_0
|
||||
|
||||
dirname = os.path.dirname(output_filename_0)
|
||||
|
||||
# Create render folder ----------------------------------------------
|
||||
try:
|
||||
# Ensure render folder exists
|
||||
|
|
@ -799,6 +837,23 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin):
|
|||
"AssetDependency0": data["filepath"],
|
||||
}
|
||||
|
||||
renderer = self._instance.data["renderer"]
|
||||
|
||||
# This hack is here because of how Deadline handles Renderman version.
|
||||
# it considers everything with `renderman` set as version older than
|
||||
# Renderman 22, and so if we are using renderman > 21 we need to set
|
||||
# renderer string on the job to `renderman22`. We will have to change
|
||||
# this when Deadline releases new version handling this.
|
||||
if self._instance.data["renderer"] == "renderman":
|
||||
try:
|
||||
from rfm2.config import cfg # noqa
|
||||
except ImportError:
|
||||
raise Exception("Cannot determine renderman version")
|
||||
|
||||
rman_version = cfg().build_info.version() # type: str
|
||||
if int(rman_version.split(".")[0]) > 22:
|
||||
renderer = "renderman22"
|
||||
|
||||
plugin_info = {
|
||||
"SceneFile": data["filepath"],
|
||||
# Output directory and filename
|
||||
|
|
@ -812,7 +867,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin):
|
|||
"RenderLayer": data["renderlayer"],
|
||||
|
||||
# Determine which renderer to use from the file itself
|
||||
"Renderer": self._instance.data["renderer"],
|
||||
"Renderer": renderer,
|
||||
|
||||
# Resolve relative references
|
||||
"ProjectPath": data["workspace"],
|
||||
|
|
|
|||
|
|
@ -24,48 +24,6 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin):
|
|||
label = "Integrate Ftrack Api"
|
||||
families = ["ftrack"]
|
||||
|
||||
def query(self, entitytype, data):
|
||||
""" Generate a query expression from data supplied.
|
||||
|
||||
If a value is not a string, we'll add the id of the entity to the
|
||||
query.
|
||||
|
||||
Args:
|
||||
entitytype (str): The type of entity to query.
|
||||
data (dict): The data to identify the entity.
|
||||
exclusions (list): All keys to exclude from the query.
|
||||
|
||||
Returns:
|
||||
str: String query to use with "session.query"
|
||||
"""
|
||||
queries = []
|
||||
if sys.version_info[0] < 3:
|
||||
for key, value in data.iteritems():
|
||||
if not isinstance(value, (basestring, int)):
|
||||
self.log.info("value: {}".format(value))
|
||||
if "id" in value.keys():
|
||||
queries.append(
|
||||
"{0}.id is \"{1}\"".format(key, value["id"])
|
||||
)
|
||||
else:
|
||||
queries.append("{0} is \"{1}\"".format(key, value))
|
||||
else:
|
||||
for key, value in data.items():
|
||||
if not isinstance(value, (str, int)):
|
||||
self.log.info("value: {}".format(value))
|
||||
if "id" in value.keys():
|
||||
queries.append(
|
||||
"{0}.id is \"{1}\"".format(key, value["id"])
|
||||
)
|
||||
else:
|
||||
queries.append("{0} is \"{1}\"".format(key, value))
|
||||
|
||||
query = (
|
||||
"select id from " + entitytype + " where " + " and ".join(queries)
|
||||
)
|
||||
self.log.debug(query)
|
||||
return query
|
||||
|
||||
def process(self, instance):
|
||||
session = instance.context.data["ftrackSession"]
|
||||
context = instance.context
|
||||
|
|
@ -108,7 +66,19 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin):
|
|||
default_asset_name = parent_entity["name"]
|
||||
|
||||
# Change status on task
|
||||
self._set_task_status(instance, task_entity, session)
|
||||
asset_version_status_ids_by_name = {}
|
||||
project_entity = instance.context.data.get("ftrackProject")
|
||||
if project_entity:
|
||||
project_schema = project_entity["project_schema"]
|
||||
asset_version_statuses = (
|
||||
project_schema.get_statuses("AssetVersion")
|
||||
)
|
||||
asset_version_status_ids_by_name = {
|
||||
status["name"].lower(): status["id"]
|
||||
for status in asset_version_statuses
|
||||
}
|
||||
|
||||
self._set_task_status(instance, project_entity, task_entity, session)
|
||||
|
||||
# Prepare AssetTypes
|
||||
asset_types_by_short = self._ensure_asset_types_exists(
|
||||
|
|
@ -139,7 +109,11 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin):
|
|||
# Asset Version
|
||||
asset_version_data = data.get("assetversion_data") or {}
|
||||
asset_version_entity = self._ensure_asset_version_exists(
|
||||
session, asset_version_data, asset_entity["id"], task_entity
|
||||
session,
|
||||
asset_version_data,
|
||||
asset_entity["id"],
|
||||
task_entity,
|
||||
asset_version_status_ids_by_name
|
||||
)
|
||||
|
||||
# Component
|
||||
|
|
@ -174,8 +148,7 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin):
|
|||
if asset_version not in instance.data[asset_versions_key]:
|
||||
instance.data[asset_versions_key].append(asset_version)
|
||||
|
||||
def _set_task_status(self, instance, task_entity, session):
|
||||
project_entity = instance.context.data.get("ftrackProject")
|
||||
def _set_task_status(self, instance, project_entity, task_entity, session):
|
||||
if not project_entity:
|
||||
self.log.info("Task status won't be set, project is not known.")
|
||||
return
|
||||
|
|
@ -319,12 +292,19 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin):
|
|||
).first()
|
||||
|
||||
def _ensure_asset_version_exists(
|
||||
self, session, asset_version_data, asset_id, task_entity
|
||||
self,
|
||||
session,
|
||||
asset_version_data,
|
||||
asset_id,
|
||||
task_entity,
|
||||
status_ids_by_name
|
||||
):
|
||||
task_id = None
|
||||
if task_entity:
|
||||
task_id = task_entity["id"]
|
||||
|
||||
status_name = asset_version_data.pop("status_name", None)
|
||||
|
||||
# Try query asset version by criteria (asset id and version)
|
||||
version = asset_version_data.get("version") or 0
|
||||
asset_version_entity = self._query_asset_version(
|
||||
|
|
@ -366,6 +346,18 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin):
|
|||
session, version, asset_id
|
||||
)
|
||||
|
||||
if status_name:
|
||||
status_id = status_ids_by_name.get(status_name.lower())
|
||||
if not status_id:
|
||||
self.log.info((
|
||||
"Ftrack status with name \"{}\""
|
||||
" for AssetVersion was not found."
|
||||
).format(status_name))
|
||||
|
||||
elif asset_version_entity["status_id"] != status_id:
|
||||
asset_version_entity["status_id"] = status_id
|
||||
session.commit()
|
||||
|
||||
# Set custom attributes if there were any set
|
||||
custom_attrs = asset_version_data.get("custom_attributes") or {}
|
||||
for attr_key, attr_value in custom_attrs.items():
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import json
|
|||
import copy
|
||||
import pyblish.api
|
||||
|
||||
from openpype.lib.profiles_filtering import filter_profiles
|
||||
|
||||
|
||||
class IntegrateFtrackInstance(pyblish.api.InstancePlugin):
|
||||
"""Collect ftrack component data (not integrate yet).
|
||||
|
|
@ -36,6 +38,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin):
|
|||
"reference": "reference"
|
||||
}
|
||||
keep_first_subset_name_for_review = True
|
||||
asset_versions_status_profiles = {}
|
||||
|
||||
def process(self, instance):
|
||||
self.log.debug("instance {}".format(instance))
|
||||
|
|
@ -80,6 +83,8 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin):
|
|||
if instance_fps is None:
|
||||
instance_fps = instance.context.data["fps"]
|
||||
|
||||
status_name = self._get_asset_version_status_name(instance)
|
||||
|
||||
# Base of component item data
|
||||
# - create a copy of this object when want to use it
|
||||
base_component_item = {
|
||||
|
|
@ -91,7 +96,8 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin):
|
|||
},
|
||||
"assetversion_data": {
|
||||
"version": version_number,
|
||||
"comment": instance.context.data.get("comment") or ""
|
||||
"comment": instance.context.data.get("comment") or "",
|
||||
"status_name": status_name
|
||||
},
|
||||
"component_overwrite": False,
|
||||
# This can be change optionally
|
||||
|
|
@ -317,3 +323,24 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin):
|
|||
)
|
||||
))
|
||||
instance.data["ftrackComponentsList"] = component_list
|
||||
|
||||
def _get_asset_version_status_name(self, instance):
|
||||
if not self.asset_versions_status_profiles:
|
||||
return None
|
||||
|
||||
# Prepare filtering data for new asset version status
|
||||
anatomy_data = instance.data["anatomyData"]
|
||||
task_type = anatomy_data.get("task", {}).get("type")
|
||||
filtering_criteria = {
|
||||
"families": instance.data["family"],
|
||||
"hosts": instance.context.data["hostName"],
|
||||
"task_types": task_type
|
||||
}
|
||||
matching_profile = filter_profiles(
|
||||
self.asset_versions_status_profiles,
|
||||
filtering_criteria
|
||||
)
|
||||
if not matching_profile:
|
||||
return None
|
||||
|
||||
return matching_profile["status"] or None
|
||||
|
|
|
|||
|
|
@ -41,21 +41,33 @@ class CollectSceneLoadedVersions(pyblish.api.ContextPlugin):
|
|||
loaded_versions = []
|
||||
_containers = list(host.ls())
|
||||
_repr_ids = [ObjectId(c["representation"]) for c in _containers]
|
||||
repre_docs = legacy_io.find(
|
||||
{"_id": {"$in": _repr_ids}},
|
||||
projection={"_id": 1, "parent": 1}
|
||||
)
|
||||
version_by_repr = {
|
||||
str(doc["_id"]): doc["parent"] for doc in
|
||||
legacy_io.find(
|
||||
{"_id": {"$in": _repr_ids}},
|
||||
projection={"parent": 1}
|
||||
)
|
||||
str(doc["_id"]): doc["parent"]
|
||||
for doc in repre_docs
|
||||
}
|
||||
|
||||
# QUESTION should we add same representation id when loaded multiple
|
||||
# times?
|
||||
for con in _containers:
|
||||
repre_id = con["representation"]
|
||||
version_id = version_by_repr.get(repre_id)
|
||||
if version_id is None:
|
||||
self.log.warning((
|
||||
"Skipping container,"
|
||||
" did not find representation document. {}"
|
||||
).format(str(con)))
|
||||
continue
|
||||
|
||||
# NOTE:
|
||||
# may have more then one representation that are same version
|
||||
version = {
|
||||
"subsetName": con["name"],
|
||||
"representation": ObjectId(con["representation"]),
|
||||
"version": version_by_repr[con["representation"]], # _id
|
||||
"representation": ObjectId(repre_id),
|
||||
"version": version_id,
|
||||
}
|
||||
loaded_versions.append(version)
|
||||
|
||||
|
|
|
|||
|
|
@ -418,7 +418,8 @@
|
|||
"redshiftproxy": "cache",
|
||||
"usd": "usd"
|
||||
},
|
||||
"keep_first_subset_name_for_review": true
|
||||
"keep_first_subset_name_for_review": true,
|
||||
"asset_versions_status_profiles": []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -52,10 +52,39 @@
|
|||
"environment": {},
|
||||
"variants": {}
|
||||
},
|
||||
"renderman": {
|
||||
"environment": {},
|
||||
"variants": {
|
||||
"24-3-maya": {
|
||||
"host_names": [
|
||||
"maya"
|
||||
],
|
||||
"app_variants": [
|
||||
"maya/2022"
|
||||
],
|
||||
"environment": {
|
||||
"RFMTREE": {
|
||||
"windows": "C:\\Program Files\\Pixar\\RenderManForMaya-24.3",
|
||||
"darwin": "/Applications/Pixar/RenderManForMaya-24.3",
|
||||
"linux": "/opt/pixar/RenderManForMaya-24.3"
|
||||
},
|
||||
"RMANTREE": {
|
||||
"windows": "C:\\Program Files\\Pixar\\RenderManProServer-24.3",
|
||||
"darwin": "/Applications/Pixar/RenderManProServer-24.3",
|
||||
"linux": "/opt/pixar/RenderManProServer-24.3"
|
||||
}
|
||||
}
|
||||
},
|
||||
"__dynamic_keys_labels__": {
|
||||
"24-3-maya": "24.3 RFM"
|
||||
}
|
||||
}
|
||||
},
|
||||
"__dynamic_keys_labels__": {
|
||||
"mtoa": "Autodesk Arnold",
|
||||
"vray": "Chaos Group Vray",
|
||||
"yeti": "Pergrine Labs Yeti"
|
||||
"yeti": "Peregrine Labs Yeti",
|
||||
"renderman": "Pixar Renderman"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -858,6 +858,43 @@
|
|||
"key": "keep_first_subset_name_for_review",
|
||||
"label": "Make subset name as first asset name",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"collapsible": true,
|
||||
"key": "asset_versions_status_profiles",
|
||||
"label": "AssetVersion status on publish",
|
||||
"use_label_wrap": true,
|
||||
"object_type": {
|
||||
"type": "dict",
|
||||
"children": [
|
||||
{
|
||||
"key": "hosts",
|
||||
"label": "Host names",
|
||||
"type": "hosts-enum",
|
||||
"multiselection": true
|
||||
},
|
||||
{
|
||||
"key": "task_types",
|
||||
"label": "Task types",
|
||||
"type": "task-types-enum"
|
||||
},
|
||||
{
|
||||
"key": "family",
|
||||
"label": "Family",
|
||||
"type": "list",
|
||||
"object_type": "text"
|
||||
},
|
||||
{
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"key": "status",
|
||||
"label": "Status name",
|
||||
"type": "text"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,7 +61,11 @@
|
|||
"icon-entity-default": "#bfccd6",
|
||||
"icon-entity-disabled": "#808080",
|
||||
"font-entity-deprecated": "#666666",
|
||||
|
||||
"overlay-messages": {
|
||||
"close-btn": "#D3D8DE",
|
||||
"bg-success": "#458056",
|
||||
"bg-success-hover": "#55a066"
|
||||
},
|
||||
"tab-widget": {
|
||||
"bg": "#21252B",
|
||||
"bg-selected": "#434a56",
|
||||
|
|
|
|||
|
|
@ -687,6 +687,26 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
|
|||
background: none;
|
||||
}
|
||||
|
||||
/* Messages overlay */
|
||||
#OverlayMessageWidget {
|
||||
border-radius: 0.2em;
|
||||
background: {color:bg-buttons};
|
||||
}
|
||||
|
||||
#OverlayMessageWidget:hover {
|
||||
background: {color:bg-button-hover};
|
||||
}
|
||||
#OverlayMessageWidget {
|
||||
background: {color:overlay-messages:bg-success};
|
||||
}
|
||||
#OverlayMessageWidget:hover {
|
||||
background: {color:overlay-messages:bg-success-hover};
|
||||
}
|
||||
|
||||
#OverlayMessageWidget QWidget {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Password dialog*/
|
||||
#PasswordBtn {
|
||||
border: none;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from openpype.settings.lib import (
|
|||
save_local_settings
|
||||
)
|
||||
from openpype.tools.settings import CHILD_OFFSET
|
||||
from openpype.tools.utils import MessageOverlayObject
|
||||
from openpype.api import (
|
||||
Logger,
|
||||
SystemSettings,
|
||||
|
|
@ -221,6 +222,8 @@ class LocalSettingsWindow(QtWidgets.QWidget):
|
|||
|
||||
self.setWindowTitle("OpenPype Local settings")
|
||||
|
||||
overlay_object = MessageOverlayObject(self)
|
||||
|
||||
stylesheet = style.load_stylesheet()
|
||||
self.setStyleSheet(stylesheet)
|
||||
self.setWindowIcon(QtGui.QIcon(style.app_icon_path()))
|
||||
|
|
@ -247,6 +250,7 @@ class LocalSettingsWindow(QtWidgets.QWidget):
|
|||
save_btn.clicked.connect(self._on_save_clicked)
|
||||
reset_btn.clicked.connect(self._on_reset_clicked)
|
||||
|
||||
self._overlay_object = overlay_object
|
||||
# Do not create local settings widget in init phase as it's using
|
||||
# settings objects that must be OK to be able create this widget
|
||||
# - we want to show dialog if anything goes wrong
|
||||
|
|
@ -312,8 +316,10 @@ class LocalSettingsWindow(QtWidgets.QWidget):
|
|||
|
||||
def _on_reset_clicked(self):
|
||||
self.reset()
|
||||
self._overlay_object.add_message("Refreshed...")
|
||||
|
||||
def _on_save_clicked(self):
|
||||
value = self._settings_widget.settings_value()
|
||||
save_local_settings(value)
|
||||
self._overlay_object.add_message("Saved...", message_type="success")
|
||||
self.reset()
|
||||
|
|
|
|||
|
|
@ -22,6 +22,10 @@ from .lib import (
|
|||
from .models import (
|
||||
RecursiveSortFilterProxyModel,
|
||||
)
|
||||
from .overlay_messages import (
|
||||
MessageOverlayObject,
|
||||
)
|
||||
|
||||
|
||||
__all__ = (
|
||||
"PlaceholderLineEdit",
|
||||
|
|
@ -45,4 +49,6 @@ __all__ = (
|
|||
"get_asset_icon",
|
||||
|
||||
"RecursiveSortFilterProxyModel",
|
||||
|
||||
"MessageOverlayObject",
|
||||
)
|
||||
|
|
|
|||
324
openpype/tools/utils/overlay_messages.py
Normal file
324
openpype/tools/utils/overlay_messages.py
Normal file
|
|
@ -0,0 +1,324 @@
|
|||
import uuid
|
||||
|
||||
from Qt import QtWidgets, QtCore, QtGui
|
||||
|
||||
from openpype.style import get_objected_colors
|
||||
|
||||
from .lib import set_style_property
|
||||
|
||||
|
||||
class CloseButton(QtWidgets.QFrame):
|
||||
"""Close button drawed manually."""
|
||||
|
||||
clicked = QtCore.Signal()
|
||||
|
||||
def __init__(self, parent):
|
||||
super(CloseButton, self).__init__(parent)
|
||||
colors = get_objected_colors()
|
||||
close_btn_color = colors["overlay-messages"]["close-btn"]
|
||||
self._color = close_btn_color.get_qcolor()
|
||||
self._mouse_pressed = False
|
||||
policy = QtWidgets.QSizePolicy(
|
||||
QtWidgets.QSizePolicy.Fixed,
|
||||
QtWidgets.QSizePolicy.Fixed
|
||||
)
|
||||
self.setSizePolicy(policy)
|
||||
|
||||
def sizeHint(self):
|
||||
size = self.fontMetrics().height()
|
||||
return QtCore.QSize(size, size)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if event.button() == QtCore.Qt.LeftButton:
|
||||
self._mouse_pressed = True
|
||||
super(CloseButton, self).mousePressEvent(event)
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
if self._mouse_pressed:
|
||||
self._mouse_pressed = False
|
||||
if self.rect().contains(event.pos()):
|
||||
self.clicked.emit()
|
||||
|
||||
super(CloseButton, self).mouseReleaseEvent(event)
|
||||
|
||||
def paintEvent(self, event):
|
||||
rect = self.rect()
|
||||
painter = QtGui.QPainter(self)
|
||||
painter.setClipRect(event.rect())
|
||||
pen = QtGui.QPen()
|
||||
pen.setWidth(2)
|
||||
pen.setColor(self._color)
|
||||
pen.setStyle(QtCore.Qt.SolidLine)
|
||||
pen.setCapStyle(QtCore.Qt.RoundCap)
|
||||
painter.setPen(pen)
|
||||
offset = int(rect.height() / 4)
|
||||
top = rect.top() + offset
|
||||
left = rect.left() + offset
|
||||
right = rect.right() - offset
|
||||
bottom = rect.bottom() - offset
|
||||
painter.drawLine(
|
||||
left, top,
|
||||
right, bottom
|
||||
)
|
||||
painter.drawLine(
|
||||
left, bottom,
|
||||
right, top
|
||||
)
|
||||
|
||||
|
||||
class OverlayMessageWidget(QtWidgets.QFrame):
|
||||
"""Message widget showed as overlay.
|
||||
|
||||
Message is hidden after timeout but can be overriden by mouse hover.
|
||||
Mouse hover can add additional 2 seconds of widget's visibility.
|
||||
|
||||
Args:
|
||||
message_id (str): Unique identifier of message widget for
|
||||
'MessageOverlayObject'.
|
||||
message (str): Text shown in message.
|
||||
parent (QWidget): Parent widget where message is visible.
|
||||
timeout (int): Timeout of message's visibility (default 5000).
|
||||
message_type (str): Property which can be used in styles for specific
|
||||
kid of message.
|
||||
"""
|
||||
|
||||
close_requested = QtCore.Signal(str)
|
||||
_default_timeout = 5000
|
||||
|
||||
def __init__(
|
||||
self, message_id, message, parent, message_type=None, timeout=None
|
||||
):
|
||||
super(OverlayMessageWidget, self).__init__(parent)
|
||||
self.setObjectName("OverlayMessageWidget")
|
||||
|
||||
if message_type:
|
||||
set_style_property(self, "type", message_type)
|
||||
|
||||
if not timeout:
|
||||
timeout = self._default_timeout
|
||||
timeout_timer = QtCore.QTimer()
|
||||
timeout_timer.setInterval(timeout)
|
||||
timeout_timer.setSingleShot(True)
|
||||
|
||||
hover_timer = QtCore.QTimer()
|
||||
hover_timer.setInterval(2000)
|
||||
hover_timer.setSingleShot(True)
|
||||
|
||||
label_widget = QtWidgets.QLabel(message, self)
|
||||
label_widget.setAlignment(QtCore.Qt.AlignCenter)
|
||||
label_widget.setWordWrap(True)
|
||||
close_btn = CloseButton(self)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.setContentsMargins(5, 5, 0, 5)
|
||||
layout.addWidget(label_widget, 1)
|
||||
layout.addWidget(close_btn, 0)
|
||||
|
||||
close_btn.clicked.connect(self._on_close_clicked)
|
||||
timeout_timer.timeout.connect(self._on_timer_timeout)
|
||||
hover_timer.timeout.connect(self._on_hover_timeout)
|
||||
|
||||
self._label_widget = label_widget
|
||||
self._message_id = message_id
|
||||
self._timeout_timer = timeout_timer
|
||||
self._hover_timer = hover_timer
|
||||
|
||||
def size_hint_without_word_wrap(self):
|
||||
"""Size hint in cases that word wrap of label is disabled."""
|
||||
self._label_widget.setWordWrap(False)
|
||||
size_hint = self.sizeHint()
|
||||
self._label_widget.setWordWrap(True)
|
||||
return size_hint
|
||||
|
||||
def showEvent(self, event):
|
||||
"""Start timeout on show."""
|
||||
super(OverlayMessageWidget, self).showEvent(event)
|
||||
self._timeout_timer.start()
|
||||
|
||||
def _on_timer_timeout(self):
|
||||
"""On message timeout."""
|
||||
# Skip closing if hover timer is active
|
||||
if not self._hover_timer.isActive():
|
||||
self._close_message()
|
||||
|
||||
def _on_hover_timeout(self):
|
||||
"""Hover timer timed out."""
|
||||
# Check if is still under widget
|
||||
if self.underMouse():
|
||||
self._hover_timer.start()
|
||||
else:
|
||||
self._close_message()
|
||||
|
||||
def _on_close_clicked(self):
|
||||
self._close_message()
|
||||
|
||||
def _close_message(self):
|
||||
"""Emmit close request to 'MessageOverlayObject'."""
|
||||
self.close_requested.emit(self._message_id)
|
||||
|
||||
def enterEvent(self, event):
|
||||
"""Start hover timer on hover."""
|
||||
super(OverlayMessageWidget, self).enterEvent(event)
|
||||
self._hover_timer.start()
|
||||
|
||||
def leaveEvent(self, event):
|
||||
"""Start hover timer on hover leave."""
|
||||
super(OverlayMessageWidget, self).leaveEvent(event)
|
||||
self._hover_timer.start()
|
||||
|
||||
|
||||
class MessageOverlayObject(QtCore.QObject):
|
||||
"""Object that can be used to add overlay messages.
|
||||
|
||||
Args:
|
||||
widget (QWidget):
|
||||
"""
|
||||
|
||||
def __init__(self, widget, default_timeout=None):
|
||||
super(MessageOverlayObject, self).__init__()
|
||||
|
||||
widget.installEventFilter(self)
|
||||
|
||||
# Timer which triggers recalculation of message positions
|
||||
recalculate_timer = QtCore.QTimer()
|
||||
recalculate_timer.setInterval(10)
|
||||
|
||||
recalculate_timer.timeout.connect(self._recalculate_positions)
|
||||
|
||||
self._widget = widget
|
||||
self._recalculate_timer = recalculate_timer
|
||||
|
||||
self._messages_order = []
|
||||
self._closing_messages = set()
|
||||
self._messages = {}
|
||||
self._spacing = 5
|
||||
self._move_size = 4
|
||||
self._move_size_remove = 8
|
||||
self._default_timeout = default_timeout
|
||||
|
||||
def add_message(self, message, message_type=None, timeout=None):
|
||||
"""Add single message into overlay.
|
||||
|
||||
Args:
|
||||
message (str): Message that will be shown.
|
||||
timeout (int): Message timeout.
|
||||
message_type (str): Message type can be used as property in
|
||||
stylesheets.
|
||||
"""
|
||||
# Skip empty messages
|
||||
if not message:
|
||||
return
|
||||
|
||||
if timeout is None:
|
||||
timeout = self._default_timeout
|
||||
|
||||
# Create unique id of message
|
||||
label_id = str(uuid.uuid4())
|
||||
# Create message widget
|
||||
widget = OverlayMessageWidget(
|
||||
label_id, message, self._widget, message_type, timeout
|
||||
)
|
||||
widget.close_requested.connect(self._on_message_close_request)
|
||||
widget.show()
|
||||
|
||||
# Move widget outside of window
|
||||
pos = widget.pos()
|
||||
pos.setY(pos.y() - widget.height())
|
||||
widget.move(pos)
|
||||
# Store message
|
||||
self._messages[label_id] = widget
|
||||
self._messages_order.append(label_id)
|
||||
# Trigger recalculation timer
|
||||
self._recalculate_timer.start()
|
||||
|
||||
def _on_message_close_request(self, label_id):
|
||||
"""Message widget requested removement."""
|
||||
|
||||
widget = self._messages.get(label_id)
|
||||
if widget is not None:
|
||||
# Add message to closing messages and start recalculation
|
||||
self._closing_messages.add(label_id)
|
||||
self._recalculate_timer.start()
|
||||
|
||||
def _recalculate_positions(self):
|
||||
"""Recalculate positions of widgets."""
|
||||
|
||||
# Skip if there are no messages to process
|
||||
if not self._messages_order:
|
||||
self._recalculate_timer.stop()
|
||||
return
|
||||
|
||||
# All message widgets are in expected positions
|
||||
all_at_place = True
|
||||
# Starting y position
|
||||
pos_y = self._spacing
|
||||
# Current widget width
|
||||
widget_width = self._widget.width()
|
||||
max_width = widget_width - (2 * self._spacing)
|
||||
widget_half_width = widget_width / 2
|
||||
|
||||
# Store message ids that should be removed
|
||||
message_ids_to_remove = set()
|
||||
for message_id in reversed(self._messages_order):
|
||||
widget = self._messages[message_id]
|
||||
pos = widget.pos()
|
||||
# Messages to remove are moved upwards
|
||||
if message_id in self._closing_messages:
|
||||
bottom = pos.y() + widget.height()
|
||||
# Add message to remove if is not visible
|
||||
if bottom < 0 or self._move_size_remove < 1:
|
||||
message_ids_to_remove.add(message_id)
|
||||
continue
|
||||
|
||||
# Calculate new y position of message
|
||||
dst_pos_y = pos.y() - self._move_size_remove
|
||||
|
||||
else:
|
||||
# Calculate y position of message
|
||||
# - use y position of previous message widget and add
|
||||
# move size if is not in final destination yet
|
||||
if widget.underMouse():
|
||||
dst_pos_y = pos.y()
|
||||
elif pos.y() == pos_y or self._move_size < 1:
|
||||
dst_pos_y = pos_y
|
||||
elif pos.y() < pos_y:
|
||||
dst_pos_y = min(pos_y, pos.y() + self._move_size)
|
||||
else:
|
||||
dst_pos_y = max(pos_y, pos.y() - self._move_size)
|
||||
|
||||
# Store if widget is in place where should be
|
||||
if all_at_place and dst_pos_y != pos_y:
|
||||
all_at_place = False
|
||||
|
||||
# Calculate ideal width and height of message widget
|
||||
height = widget.heightForWidth(max_width)
|
||||
w_size_hint = widget.size_hint_without_word_wrap()
|
||||
widget.resize(min(max_width, w_size_hint.width()), height)
|
||||
|
||||
# Center message widget
|
||||
size = widget.size()
|
||||
pos_x = widget_half_width - (size.width() / 2)
|
||||
# Move widget to destination position
|
||||
widget.move(pos_x, dst_pos_y)
|
||||
|
||||
# Add message widget height and spacing for next message widget
|
||||
pos_y += size.height() + self._spacing
|
||||
|
||||
# Remove widgets to remove
|
||||
for message_id in message_ids_to_remove:
|
||||
self._messages_order.remove(message_id)
|
||||
self._closing_messages.remove(message_id)
|
||||
widget = self._messages.pop(message_id)
|
||||
widget.hide()
|
||||
widget.deleteLater()
|
||||
|
||||
# Stop recalculation timer if all widgets are where should be
|
||||
if all_at_place:
|
||||
self._recalculate_timer.stop()
|
||||
|
||||
def eventFilter(self, source, event):
|
||||
# Trigger recalculation of timer on resize of widget
|
||||
if source is self._widget and event.type() == QtCore.QEvent.Resize:
|
||||
self._recalculate_timer.start()
|
||||
|
||||
return super(MessageOverlayObject, self).eventFilter(source, event)
|
||||
Loading…
Add table
Add a link
Reference in a new issue